Compare commits
165 Commits
feat/apple
...
feat/botto
| Author | SHA1 | Date | |
|---|---|---|---|
| 2665a27803 | |||
| 74b96225c6 | |||
| f180b7f39b | |||
| a2fbfcb13c | |||
| d640f7f882 | |||
| d43c12b103 | |||
| 38c3792675 | |||
| ac2785abd5 | |||
| 1ff6a0e831 | |||
| 7a3b709404 | |||
|
|
d63cb4ac52 | ||
| b6ee1cf906 | |||
| 60201b1b67 | |||
| a8b8603649 | |||
| e193528fe9 | |||
| 73afb34964 | |||
| 65bbc453e6 | |||
|
|
188477ab64 | ||
|
|
a31bfb6b39 | ||
| 681ed69ef0 | |||
| b771428b4d | |||
| fc0103ee5d | |||
| 55067b81b8 | |||
| dfe2b5df09 | |||
| dc0c435163 | |||
| 9d1ac56b9a | |||
| fc2c3664d9 | |||
| 0bd45ed777 | |||
| 3912766982 | |||
| 3becce2a6c | |||
| 20b8692c91 | |||
| 14ac780aa5 | |||
| d836870612 | |||
| bc6f706e4a | |||
| 6ac6a9b039 | |||
| 85be80d712 | |||
| 105be1e411 | |||
| 010830243e | |||
| 923dc46dc7 | |||
| f2ef5366f5 | |||
| 20380a4587 | |||
| 069ef2c458 | |||
| 2f430b2d8f | |||
| f7a579a438 | |||
| b9ddd998bc | |||
| ae59d02df2 | |||
| ec205bab0c | |||
| ed49d825b8 | |||
| a9db8be46a | |||
| 1caa3c7fae | |||
| 2ea4bffd49 | |||
| 5ae52f59fc | |||
| a7e6d25d3f | |||
| 83751a4e3e | |||
| 0e9daab187 | |||
| 4390491873 | |||
| d620a4cc2e | |||
| 32669e5bef | |||
| 6edad3991f | |||
| 50acf0bedc | |||
| d4369ec7a4 | |||
| c16543099e | |||
| f2a65d755c | |||
| 1fd48edd42 | |||
| 68e45303c6 | |||
| 532993e9dd | |||
| d19d72ce0c | |||
| d1820a08cf | |||
| bc73665b12 | |||
| 9edb19569a | |||
| 7802a89d15 | |||
| 915260f41b | |||
| 0d57e9a03b | |||
| 582207d453 | |||
| b1b08bfa04 | |||
| 14e883672d | |||
| 7a405140db | |||
| 35497f5bd2 | |||
| 91b19785d6 | |||
| a301d21cc2 | |||
| a2a4b9a553 | |||
| 45f45559fd | |||
| 458256132a | |||
| 0f2c166e1c | |||
| 1c7a688cb8 | |||
| 6269f178e9 | |||
| 3e7527ee19 | |||
| 2236316863 | |||
| cc2fded193 | |||
| f32e0a8ab0 | |||
| ec6e6d2ba0 | |||
| ca85635b03 | |||
| 32257dc64e | |||
| 6bba319735 | |||
| dcce972fdc | |||
| 32e25fb983 | |||
| e7882869e6 | |||
| d0a251f69a | |||
| 9bc7f29162 | |||
| 3ff963f007 | |||
| bcfce66ec0 | |||
| 33e3ee3489 | |||
| e3502a7690 | |||
| 8d09ba4d07 | |||
| ba670d06aa | |||
| a11ad2f651 | |||
| 755bd116d5 | |||
| 9e33784781 | |||
| 470bcdd72e | |||
| d56a7d4dfe | |||
| b46e586c92 | |||
| 563eb3f1ef | |||
| 98644513ad | |||
| 3033db02b8 | |||
| 70a6ed189b | |||
| d7e4d2095c | |||
| 1c0799a30a | |||
| 2b3955060f | |||
| 4ac4d642e7 | |||
| 3d12cd2735 | |||
| 4a44924f56 | |||
| 3910b5d7b2 | |||
| 4a32fe5255 | |||
| 8b9b2be891 | |||
| 96321831d1 | |||
| 39cd5ce04a | |||
| 4a46bbd2be | |||
| f45dcc560c | |||
| 1a014bea15 | |||
| 6d6f1ffd06 | |||
| 4528b240e1 | |||
| c454d9c9e0 | |||
| f8c284cd71 | |||
| 46daff2ddb | |||
| 9bb98ce569 | |||
| 001c243f95 | |||
| 0fdaf5bd4e | |||
| 931918c60b | |||
| a9d3246b97 | |||
| cde119592d | |||
| 031127fb1f | |||
| fa50dd3455 | |||
| 49c418c3f1 | |||
| 8e7aa77ee3 | |||
| 4b0fcca5d2 | |||
| 585fa5afcf | |||
| 38cec8c31a | |||
| 431cb7c034 | |||
| 91a92a30ad | |||
| 9d819e9a14 | |||
| ca910089c5 | |||
| 6270206812 | |||
| 1d1a78608e | |||
| 2e8795a317 | |||
| f39560e041 | |||
| 6f74a5bff4 | |||
| c339045a0e | |||
| 9e38b67857 | |||
| a6f72c8f6b | |||
| c8f9cb7e22 | |||
| 7bb624b942 | |||
| b11d2f752b | |||
| 1a82b751ea | |||
|
|
45bc0389ac | ||
| 67d3af0ed0 |
44
.drone.yml
Normal file
44
.drone.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: seasoned build
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: frontend_install
|
||||
image: node:13.6.0
|
||||
commands:
|
||||
- node -v
|
||||
- yarn --version
|
||||
- name: deploy
|
||||
image: appleboy/drone-ssh
|
||||
pull: true
|
||||
secrets:
|
||||
- ssh_key
|
||||
when:
|
||||
event:
|
||||
- push
|
||||
branch:
|
||||
- master
|
||||
- drone-test
|
||||
status: success
|
||||
settings:
|
||||
host: 10.0.0.114
|
||||
username: root
|
||||
key:
|
||||
from_secret: ssh_key
|
||||
command_timeout: 600s
|
||||
script:
|
||||
- /home/kevin/deploy/seasoned.sh
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
event:
|
||||
include:
|
||||
- pull_request
|
||||
- push
|
||||
|
||||
10
.prettierrc
Normal file
10
.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
/*!
|
||||
* AnchorJS - v4.0.0 - 2017-06-02
|
||||
* https://github.com/bryanbraun/anchorjs
|
||||
* Copyright (c) 2017 Bryan Braun; Licensed MIT
|
||||
*/
|
||||
/* eslint-env amd, node */
|
||||
|
||||
// https://github.com/umdjs/umd/blob/master/templates/returnExports.js
|
||||
(function(root, factory) {
|
||||
'use strict';
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define([], factory);
|
||||
} else if (typeof module === 'object' && module.exports) {
|
||||
// Node. Does not work with strict CommonJS, but
|
||||
// only CommonJS-like environments that support module.exports,
|
||||
// like Node.
|
||||
module.exports = factory();
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
root.AnchorJS = factory();
|
||||
root.anchors = new root.AnchorJS();
|
||||
}
|
||||
})(this, function() {
|
||||
'use strict';
|
||||
function AnchorJS(options) {
|
||||
this.options = options || {};
|
||||
this.elements = [];
|
||||
|
||||
/**
|
||||
* Assigns options to the internal options object, and provides defaults.
|
||||
* @param {Object} opts - Options object
|
||||
*/
|
||||
function _applyRemainingDefaultOptions(opts) {
|
||||
opts.icon = opts.hasOwnProperty('icon') ? opts.icon : '\ue9cb'; // Accepts characters (and also URLs?), like '#', '¶', '❡', or '§'.
|
||||
opts.visible = opts.hasOwnProperty('visible') ? opts.visible : 'hover'; // Also accepts 'always' & 'touch'
|
||||
opts.placement = opts.hasOwnProperty('placement')
|
||||
? opts.placement
|
||||
: 'right'; // Also accepts 'left'
|
||||
opts.class = opts.hasOwnProperty('class') ? opts.class : ''; // Accepts any class name.
|
||||
// Using Math.floor here will ensure the value is Number-cast and an integer.
|
||||
opts.truncate = opts.hasOwnProperty('truncate')
|
||||
? Math.floor(opts.truncate)
|
||||
: 64; // Accepts any value that can be typecast to a number.
|
||||
}
|
||||
|
||||
_applyRemainingDefaultOptions(this.options);
|
||||
|
||||
/**
|
||||
* Checks to see if this device supports touch. Uses criteria pulled from Modernizr:
|
||||
* https://github.com/Modernizr/Modernizr/blob/da22eb27631fc4957f67607fe6042e85c0a84656/feature-detects/touchevents.js#L40
|
||||
* @returns {Boolean} - true if the current device supports touch.
|
||||
*/
|
||||
this.isTouchDevice = function() {
|
||||
return !!(
|
||||
'ontouchstart' in window ||
|
||||
(window.DocumentTouch && document instanceof DocumentTouch)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add anchor links to page elements.
|
||||
* @param {String|Array|Nodelist} selector - A CSS selector for targeting the elements you wish to add anchor links
|
||||
* to. Also accepts an array or nodeList containing the relavant elements.
|
||||
* @returns {this} - The AnchorJS object
|
||||
*/
|
||||
this.add = function(selector) {
|
||||
var elements,
|
||||
elsWithIds,
|
||||
idList,
|
||||
elementID,
|
||||
i,
|
||||
index,
|
||||
count,
|
||||
tidyText,
|
||||
newTidyText,
|
||||
readableID,
|
||||
anchor,
|
||||
visibleOptionToUse,
|
||||
indexesToDrop = [];
|
||||
|
||||
// We reapply options here because somebody may have overwritten the default options object when setting options.
|
||||
// For example, this overwrites all options but visible:
|
||||
//
|
||||
// anchors.options = { visible: 'always'; }
|
||||
_applyRemainingDefaultOptions(this.options);
|
||||
|
||||
visibleOptionToUse = this.options.visible;
|
||||
if (visibleOptionToUse === 'touch') {
|
||||
visibleOptionToUse = this.isTouchDevice() ? 'always' : 'hover';
|
||||
}
|
||||
|
||||
// Provide a sensible default selector, if none is given.
|
||||
if (!selector) {
|
||||
selector = 'h2, h3, h4, h5, h6';
|
||||
}
|
||||
|
||||
elements = _getElements(selector);
|
||||
|
||||
if (elements.length === 0) {
|
||||
return this;
|
||||
}
|
||||
|
||||
_addBaselineStyles();
|
||||
|
||||
// We produce a list of existing IDs so we don't generate a duplicate.
|
||||
elsWithIds = document.querySelectorAll('[id]');
|
||||
idList = [].map.call(elsWithIds, function assign(el) {
|
||||
return el.id;
|
||||
});
|
||||
|
||||
for (i = 0; i < elements.length; i++) {
|
||||
if (this.hasAnchorJSLink(elements[i])) {
|
||||
indexesToDrop.push(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (elements[i].hasAttribute('id')) {
|
||||
elementID = elements[i].getAttribute('id');
|
||||
} else if (elements[i].hasAttribute('data-anchor-id')) {
|
||||
elementID = elements[i].getAttribute('data-anchor-id');
|
||||
} else {
|
||||
tidyText = this.urlify(elements[i].textContent);
|
||||
|
||||
// Compare our generated ID to existing IDs (and increment it if needed)
|
||||
// before we add it to the page.
|
||||
newTidyText = tidyText;
|
||||
count = 0;
|
||||
do {
|
||||
if (index !== undefined) {
|
||||
newTidyText = tidyText + '-' + count;
|
||||
}
|
||||
|
||||
index = idList.indexOf(newTidyText);
|
||||
count += 1;
|
||||
} while (index !== -1);
|
||||
index = undefined;
|
||||
idList.push(newTidyText);
|
||||
|
||||
elements[i].setAttribute('id', newTidyText);
|
||||
elementID = newTidyText;
|
||||
}
|
||||
|
||||
readableID = elementID.replace(/-/g, ' ');
|
||||
|
||||
// The following code builds the following DOM structure in a more effiecient (albeit opaque) way.
|
||||
// '<a class="anchorjs-link ' + this.options.class + '" href="#' + elementID + '" aria-label="Anchor link for: ' + readableID + '" data-anchorjs-icon="' + this.options.icon + '"></a>';
|
||||
anchor = document.createElement('a');
|
||||
anchor.className = 'anchorjs-link ' + this.options.class;
|
||||
anchor.href = '#' + elementID;
|
||||
anchor.setAttribute('aria-label', 'Anchor link for: ' + readableID);
|
||||
anchor.setAttribute('data-anchorjs-icon', this.options.icon);
|
||||
|
||||
if (visibleOptionToUse === 'always') {
|
||||
anchor.style.opacity = '1';
|
||||
}
|
||||
|
||||
if (this.options.icon === '\ue9cb') {
|
||||
anchor.style.font = '1em/1 anchorjs-icons';
|
||||
|
||||
// We set lineHeight = 1 here because the `anchorjs-icons` font family could otherwise affect the
|
||||
// height of the heading. This isn't the case for icons with `placement: left`, so we restore
|
||||
// line-height: inherit in that case, ensuring they remain positioned correctly. For more info,
|
||||
// see https://github.com/bryanbraun/anchorjs/issues/39.
|
||||
if (this.options.placement === 'left') {
|
||||
anchor.style.lineHeight = 'inherit';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.options.placement === 'left') {
|
||||
anchor.style.position = 'absolute';
|
||||
anchor.style.marginLeft = '-1em';
|
||||
anchor.style.paddingRight = '0.5em';
|
||||
elements[i].insertBefore(anchor, elements[i].firstChild);
|
||||
} else {
|
||||
// if the option provided is `right` (or anything else).
|
||||
anchor.style.paddingLeft = '0.375em';
|
||||
elements[i].appendChild(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < indexesToDrop.length; i++) {
|
||||
elements.splice(indexesToDrop[i] - i, 1);
|
||||
}
|
||||
this.elements = this.elements.concat(elements);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all anchorjs-links from elements targed by the selector.
|
||||
* @param {String|Array|Nodelist} selector - A CSS selector string targeting elements with anchor links,
|
||||
* OR a nodeList / array containing the DOM elements.
|
||||
* @returns {this} - The AnchorJS object
|
||||
*/
|
||||
this.remove = function(selector) {
|
||||
var index,
|
||||
domAnchor,
|
||||
elements = _getElements(selector);
|
||||
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
domAnchor = elements[i].querySelector('.anchorjs-link');
|
||||
if (domAnchor) {
|
||||
// Drop the element from our main list, if it's in there.
|
||||
index = this.elements.indexOf(elements[i]);
|
||||
if (index !== -1) {
|
||||
this.elements.splice(index, 1);
|
||||
}
|
||||
// Remove the anchor from the DOM.
|
||||
elements[i].removeChild(domAnchor);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes all anchorjs links. Mostly used for tests.
|
||||
*/
|
||||
this.removeAll = function() {
|
||||
this.remove(this.elements);
|
||||
};
|
||||
|
||||
/**
|
||||
* Urlify - Refine text so it makes a good ID.
|
||||
*
|
||||
* To do this, we remove apostrophes, replace nonsafe characters with hyphens,
|
||||
* remove extra hyphens, truncate, trim hyphens, and make lowercase.
|
||||
*
|
||||
* @param {String} text - Any text. Usually pulled from the webpage element we are linking to.
|
||||
* @returns {String} - hyphen-delimited text for use in IDs and URLs.
|
||||
*/
|
||||
this.urlify = function(text) {
|
||||
// Regex for finding the nonsafe URL characters (many need escaping): & +$,:;=?@"#{}|^~[`%!'<>]./()*\
|
||||
var nonsafeChars = /[& +$,:;=?@"#{}|^~[`%!'<>\]\.\/\(\)\*\\]/g,
|
||||
urlText;
|
||||
|
||||
// The reason we include this _applyRemainingDefaultOptions is so urlify can be called independently,
|
||||
// even after setting options. This can be useful for tests or other applications.
|
||||
if (!this.options.truncate) {
|
||||
_applyRemainingDefaultOptions(this.options);
|
||||
}
|
||||
|
||||
// Note: we trim hyphens after truncating because truncating can cause dangling hyphens.
|
||||
// Example string: // " ⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
|
||||
urlText = text
|
||||
.trim() // "⚡⚡ Don't forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
|
||||
.replace(/\'/gi, '') // "⚡⚡ Dont forget: URL fragments should be i18n-friendly, hyphenated, short, and clean."
|
||||
.replace(nonsafeChars, '-') // "⚡⚡-Dont-forget--URL-fragments-should-be-i18n-friendly--hyphenated--short--and-clean-"
|
||||
.replace(/-{2,}/g, '-') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-short-and-clean-"
|
||||
.substring(0, this.options.truncate) // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated-"
|
||||
.replace(/^-+|-+$/gm, '') // "⚡⚡-Dont-forget-URL-fragments-should-be-i18n-friendly-hyphenated"
|
||||
.toLowerCase(); // "⚡⚡-dont-forget-url-fragments-should-be-i18n-friendly-hyphenated"
|
||||
|
||||
return urlText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if this element already has an AnchorJS link on it.
|
||||
* Uses this technique: http://stackoverflow.com/a/5898748/1154642
|
||||
* @param {HTMLElemnt} el - a DOM node
|
||||
* @returns {Boolean} true/false
|
||||
*/
|
||||
this.hasAnchorJSLink = function(el) {
|
||||
var hasLeftAnchor =
|
||||
el.firstChild &&
|
||||
(' ' + el.firstChild.className + ' ').indexOf(' anchorjs-link ') > -1,
|
||||
hasRightAnchor =
|
||||
el.lastChild &&
|
||||
(' ' + el.lastChild.className + ' ').indexOf(' anchorjs-link ') > -1;
|
||||
|
||||
return hasLeftAnchor || hasRightAnchor || false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Turns a selector, nodeList, or array of elements into an array of elements (so we can use array methods).
|
||||
* It also throws errors on any other inputs. Used to handle inputs to .add and .remove.
|
||||
* @param {String|Array|Nodelist} input - A CSS selector string targeting elements with anchor links,
|
||||
* OR a nodeList / array containing the DOM elements.
|
||||
* @returns {Array} - An array containing the elements we want.
|
||||
*/
|
||||
function _getElements(input) {
|
||||
var elements;
|
||||
if (typeof input === 'string' || input instanceof String) {
|
||||
// See https://davidwalsh.name/nodelist-array for the technique transforming nodeList -> Array.
|
||||
elements = [].slice.call(document.querySelectorAll(input));
|
||||
// I checked the 'input instanceof NodeList' test in IE9 and modern browsers and it worked for me.
|
||||
} else if (Array.isArray(input) || input instanceof NodeList) {
|
||||
elements = [].slice.call(input);
|
||||
} else {
|
||||
throw new Error('The selector provided to AnchorJS was invalid.');
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* _addBaselineStyles
|
||||
* Adds baseline styles to the page, used by all AnchorJS links irregardless of configuration.
|
||||
*/
|
||||
function _addBaselineStyles() {
|
||||
// We don't want to add global baseline styles if they've been added before.
|
||||
if (document.head.querySelector('style.anchorjs') !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var style = document.createElement('style'),
|
||||
linkRule =
|
||||
' .anchorjs-link {' +
|
||||
' opacity: 0;' +
|
||||
' text-decoration: none;' +
|
||||
' -webkit-font-smoothing: antialiased;' +
|
||||
' -moz-osx-font-smoothing: grayscale;' +
|
||||
' }',
|
||||
hoverRule =
|
||||
' *:hover > .anchorjs-link,' +
|
||||
' .anchorjs-link:focus {' +
|
||||
' opacity: 1;' +
|
||||
' }',
|
||||
anchorjsLinkFontFace =
|
||||
' @font-face {' +
|
||||
' font-family: "anchorjs-icons";' + // Icon from icomoon; 10px wide & 10px tall; 2 empty below & 4 above
|
||||
' src: url(data:n/a;base64,AAEAAAALAIAAAwAwT1MvMg8yG2cAAAE4AAAAYGNtYXDp3gC3AAABpAAAAExnYXNwAAAAEAAAA9wAAAAIZ2x5ZlQCcfwAAAH4AAABCGhlYWQHFvHyAAAAvAAAADZoaGVhBnACFwAAAPQAAAAkaG10eASAADEAAAGYAAAADGxvY2EACACEAAAB8AAAAAhtYXhwAAYAVwAAARgAAAAgbmFtZQGOH9cAAAMAAAAAunBvc3QAAwAAAAADvAAAACAAAQAAAAEAAHzE2p9fDzz1AAkEAAAAAADRecUWAAAAANQA6R8AAAAAAoACwAAAAAgAAgAAAAAAAAABAAADwP/AAAACgAAA/9MCrQABAAAAAAAAAAAAAAAAAAAAAwABAAAAAwBVAAIAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAMCQAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAg//0DwP/AAEADwABAAAAAAQAAAAAAAAAAAAAAIAAAAAAAAAIAAAACgAAxAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEADAAAAAIAAgAAgAAACDpy//9//8AAAAg6cv//f///+EWNwADAAEAAAAAAAAAAAAAAAAACACEAAEAAAAAAAAAAAAAAAAxAAACAAQARAKAAsAAKwBUAAABIiYnJjQ3NzY2MzIWFxYUBwcGIicmNDc3NjQnJiYjIgYHBwYUFxYUBwYGIwciJicmNDc3NjIXFhQHBwYUFxYWMzI2Nzc2NCcmNDc2MhcWFAcHBgYjARQGDAUtLXoWOR8fORYtLTgKGwoKCjgaGg0gEhIgDXoaGgkJBQwHdR85Fi0tOAobCgoKOBoaDSASEiANehoaCQkKGwotLXoWOR8BMwUFLYEuehYXFxYugC44CQkKGwo4GkoaDQ0NDXoaShoKGwoFBe8XFi6ALjgJCQobCjgaShoNDQ0NehpKGgobCgoKLYEuehYXAAAADACWAAEAAAAAAAEACAAAAAEAAAAAAAIAAwAIAAEAAAAAAAMACAAAAAEAAAAAAAQACAAAAAEAAAAAAAUAAQALAAEAAAAAAAYACAAAAAMAAQQJAAEAEAAMAAMAAQQJAAIABgAcAAMAAQQJAAMAEAAMAAMAAQQJAAQAEAAMAAMAAQQJAAUAAgAiAAMAAQQJAAYAEAAMYW5jaG9yanM0MDBAAGEAbgBjAGgAbwByAGoAcwA0ADAAMABAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAH//wAP) format("truetype");' +
|
||||
' }',
|
||||
pseudoElContent =
|
||||
' [data-anchorjs-icon]::after {' +
|
||||
' content: attr(data-anchorjs-icon);' +
|
||||
' }',
|
||||
firstStyleEl;
|
||||
|
||||
style.className = 'anchorjs';
|
||||
style.appendChild(document.createTextNode('')); // Necessary for Webkit.
|
||||
|
||||
// We place it in the head with the other style tags, if possible, so as to
|
||||
// not look out of place. We insert before the others so these styles can be
|
||||
// overridden if necessary.
|
||||
firstStyleEl = document.head.querySelector('[rel="stylesheet"], style');
|
||||
if (firstStyleEl === undefined) {
|
||||
document.head.appendChild(style);
|
||||
} else {
|
||||
document.head.insertBefore(style, firstStyleEl);
|
||||
}
|
||||
|
||||
style.sheet.insertRule(linkRule, style.sheet.cssRules.length);
|
||||
style.sheet.insertRule(hoverRule, style.sheet.cssRules.length);
|
||||
style.sheet.insertRule(pseudoElContent, style.sheet.cssRules.length);
|
||||
style.sheet.insertRule(anchorjsLinkFontFace, style.sheet.cssRules.length);
|
||||
}
|
||||
}
|
||||
|
||||
return AnchorJS;
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
.input {
|
||||
font-family: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
padding: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
font-size: .875rem;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -1,544 +0,0 @@
|
||||
/*! Basscss | http://basscss.com | MIT License */
|
||||
|
||||
.h1{ font-size: 2rem }
|
||||
.h2{ font-size: 1.5rem }
|
||||
.h3{ font-size: 1.25rem }
|
||||
.h4{ font-size: 1rem }
|
||||
.h5{ font-size: .875rem }
|
||||
.h6{ font-size: .75rem }
|
||||
|
||||
.font-family-inherit{ font-family:inherit }
|
||||
.font-size-inherit{ font-size:inherit }
|
||||
.text-decoration-none{ text-decoration:none }
|
||||
|
||||
.bold{ font-weight: bold; font-weight: bold }
|
||||
.regular{ font-weight:normal }
|
||||
.italic{ font-style:italic }
|
||||
.caps{ text-transform:uppercase; letter-spacing: .2em; }
|
||||
|
||||
.left-align{ text-align:left }
|
||||
.center{ text-align:center }
|
||||
.right-align{ text-align:right }
|
||||
.justify{ text-align:justify }
|
||||
|
||||
.nowrap{ white-space:nowrap }
|
||||
.break-word{ word-wrap:break-word }
|
||||
|
||||
.line-height-1{ line-height: 1 }
|
||||
.line-height-2{ line-height: 1.125 }
|
||||
.line-height-3{ line-height: 1.25 }
|
||||
.line-height-4{ line-height: 1.5 }
|
||||
|
||||
.list-style-none{ list-style:none }
|
||||
.underline{ text-decoration:underline }
|
||||
|
||||
.truncate{
|
||||
max-width:100%;
|
||||
overflow:hidden;
|
||||
text-overflow:ellipsis;
|
||||
white-space:nowrap;
|
||||
}
|
||||
|
||||
.list-reset{
|
||||
list-style:none;
|
||||
padding-left:0;
|
||||
}
|
||||
|
||||
.inline{ display:inline }
|
||||
.block{ display:block }
|
||||
.inline-block{ display:inline-block }
|
||||
.table{ display:table }
|
||||
.table-cell{ display:table-cell }
|
||||
|
||||
.overflow-hidden{ overflow:hidden }
|
||||
.overflow-scroll{ overflow:scroll }
|
||||
.overflow-auto{ overflow:auto }
|
||||
|
||||
.clearfix:before,
|
||||
.clearfix:after{
|
||||
content:" ";
|
||||
display:table
|
||||
}
|
||||
.clearfix:after{ clear:both }
|
||||
|
||||
.left{ float:left }
|
||||
.right{ float:right }
|
||||
|
||||
.fit{ max-width:100% }
|
||||
|
||||
.max-width-1{ max-width: 24rem }
|
||||
.max-width-2{ max-width: 32rem }
|
||||
.max-width-3{ max-width: 48rem }
|
||||
.max-width-4{ max-width: 64rem }
|
||||
|
||||
.border-box{ box-sizing:border-box }
|
||||
|
||||
.align-baseline{ vertical-align:baseline }
|
||||
.align-top{ vertical-align:top }
|
||||
.align-middle{ vertical-align:middle }
|
||||
.align-bottom{ vertical-align:bottom }
|
||||
|
||||
.m0{ margin:0 }
|
||||
.mt0{ margin-top:0 }
|
||||
.mr0{ margin-right:0 }
|
||||
.mb0{ margin-bottom:0 }
|
||||
.ml0{ margin-left:0 }
|
||||
.mx0{ margin-left:0; margin-right:0 }
|
||||
.my0{ margin-top:0; margin-bottom:0 }
|
||||
|
||||
.m1{ margin: .5rem }
|
||||
.mt1{ margin-top: .5rem }
|
||||
.mr1{ margin-right: .5rem }
|
||||
.mb1{ margin-bottom: .5rem }
|
||||
.ml1{ margin-left: .5rem }
|
||||
.mx1{ margin-left: .5rem; margin-right: .5rem }
|
||||
.my1{ margin-top: .5rem; margin-bottom: .5rem }
|
||||
|
||||
.m2{ margin: 1rem }
|
||||
.mt2{ margin-top: 1rem }
|
||||
.mr2{ margin-right: 1rem }
|
||||
.mb2{ margin-bottom: 1rem }
|
||||
.ml2{ margin-left: 1rem }
|
||||
.mx2{ margin-left: 1rem; margin-right: 1rem }
|
||||
.my2{ margin-top: 1rem; margin-bottom: 1rem }
|
||||
|
||||
.m3{ margin: 2rem }
|
||||
.mt3{ margin-top: 2rem }
|
||||
.mr3{ margin-right: 2rem }
|
||||
.mb3{ margin-bottom: 2rem }
|
||||
.ml3{ margin-left: 2rem }
|
||||
.mx3{ margin-left: 2rem; margin-right: 2rem }
|
||||
.my3{ margin-top: 2rem; margin-bottom: 2rem }
|
||||
|
||||
.m4{ margin: 4rem }
|
||||
.mt4{ margin-top: 4rem }
|
||||
.mr4{ margin-right: 4rem }
|
||||
.mb4{ margin-bottom: 4rem }
|
||||
.ml4{ margin-left: 4rem }
|
||||
.mx4{ margin-left: 4rem; margin-right: 4rem }
|
||||
.my4{ margin-top: 4rem; margin-bottom: 4rem }
|
||||
|
||||
.mxn1{ margin-left: -.5rem; margin-right: -.5rem; }
|
||||
.mxn2{ margin-left: -1rem; margin-right: -1rem; }
|
||||
.mxn3{ margin-left: -2rem; margin-right: -2rem; }
|
||||
.mxn4{ margin-left: -4rem; margin-right: -4rem; }
|
||||
|
||||
.ml-auto{ margin-left:auto }
|
||||
.mr-auto{ margin-right:auto }
|
||||
.mx-auto{ margin-left:auto; margin-right:auto; }
|
||||
|
||||
.p0{ padding:0 }
|
||||
.pt0{ padding-top:0 }
|
||||
.pr0{ padding-right:0 }
|
||||
.pb0{ padding-bottom:0 }
|
||||
.pl0{ padding-left:0 }
|
||||
.px0{ padding-left:0; padding-right:0 }
|
||||
.py0{ padding-top:0; padding-bottom:0 }
|
||||
|
||||
.p1{ padding: .5rem }
|
||||
.pt1{ padding-top: .5rem }
|
||||
.pr1{ padding-right: .5rem }
|
||||
.pb1{ padding-bottom: .5rem }
|
||||
.pl1{ padding-left: .5rem }
|
||||
.py1{ padding-top: .5rem; padding-bottom: .5rem }
|
||||
.px1{ padding-left: .5rem; padding-right: .5rem }
|
||||
|
||||
.p2{ padding: 1rem }
|
||||
.pt2{ padding-top: 1rem }
|
||||
.pr2{ padding-right: 1rem }
|
||||
.pb2{ padding-bottom: 1rem }
|
||||
.pl2{ padding-left: 1rem }
|
||||
.py2{ padding-top: 1rem; padding-bottom: 1rem }
|
||||
.px2{ padding-left: 1rem; padding-right: 1rem }
|
||||
|
||||
.p3{ padding: 2rem }
|
||||
.pt3{ padding-top: 2rem }
|
||||
.pr3{ padding-right: 2rem }
|
||||
.pb3{ padding-bottom: 2rem }
|
||||
.pl3{ padding-left: 2rem }
|
||||
.py3{ padding-top: 2rem; padding-bottom: 2rem }
|
||||
.px3{ padding-left: 2rem; padding-right: 2rem }
|
||||
|
||||
.p4{ padding: 4rem }
|
||||
.pt4{ padding-top: 4rem }
|
||||
.pr4{ padding-right: 4rem }
|
||||
.pb4{ padding-bottom: 4rem }
|
||||
.pl4{ padding-left: 4rem }
|
||||
.py4{ padding-top: 4rem; padding-bottom: 4rem }
|
||||
.px4{ padding-left: 4rem; padding-right: 4rem }
|
||||
|
||||
.col{
|
||||
float:left;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.col-right{
|
||||
float:right;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.col-1{
|
||||
width:8.33333%;
|
||||
}
|
||||
|
||||
.col-2{
|
||||
width:16.66667%;
|
||||
}
|
||||
|
||||
.col-3{
|
||||
width:25%;
|
||||
}
|
||||
|
||||
.col-4{
|
||||
width:33.33333%;
|
||||
}
|
||||
|
||||
.col-5{
|
||||
width:41.66667%;
|
||||
}
|
||||
|
||||
.col-6{
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.col-7{
|
||||
width:58.33333%;
|
||||
}
|
||||
|
||||
.col-8{
|
||||
width:66.66667%;
|
||||
}
|
||||
|
||||
.col-9{
|
||||
width:75%;
|
||||
}
|
||||
|
||||
.col-10{
|
||||
width:83.33333%;
|
||||
}
|
||||
|
||||
.col-11{
|
||||
width:91.66667%;
|
||||
}
|
||||
|
||||
.col-12{
|
||||
width:100%;
|
||||
}
|
||||
@media (min-width: 40em){
|
||||
|
||||
.sm-col{
|
||||
float:left;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.sm-col-right{
|
||||
float:right;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.sm-col-1{
|
||||
width:8.33333%;
|
||||
}
|
||||
|
||||
.sm-col-2{
|
||||
width:16.66667%;
|
||||
}
|
||||
|
||||
.sm-col-3{
|
||||
width:25%;
|
||||
}
|
||||
|
||||
.sm-col-4{
|
||||
width:33.33333%;
|
||||
}
|
||||
|
||||
.sm-col-5{
|
||||
width:41.66667%;
|
||||
}
|
||||
|
||||
.sm-col-6{
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.sm-col-7{
|
||||
width:58.33333%;
|
||||
}
|
||||
|
||||
.sm-col-8{
|
||||
width:66.66667%;
|
||||
}
|
||||
|
||||
.sm-col-9{
|
||||
width:75%;
|
||||
}
|
||||
|
||||
.sm-col-10{
|
||||
width:83.33333%;
|
||||
}
|
||||
|
||||
.sm-col-11{
|
||||
width:91.66667%;
|
||||
}
|
||||
|
||||
.sm-col-12{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
}
|
||||
@media (min-width: 52em){
|
||||
|
||||
.md-col{
|
||||
float:left;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.md-col-right{
|
||||
float:right;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.md-col-1{
|
||||
width:8.33333%;
|
||||
}
|
||||
|
||||
.md-col-2{
|
||||
width:16.66667%;
|
||||
}
|
||||
|
||||
.md-col-3{
|
||||
width:25%;
|
||||
}
|
||||
|
||||
.md-col-4{
|
||||
width:33.33333%;
|
||||
}
|
||||
|
||||
.md-col-5{
|
||||
width:41.66667%;
|
||||
}
|
||||
|
||||
.md-col-6{
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.md-col-7{
|
||||
width:58.33333%;
|
||||
}
|
||||
|
||||
.md-col-8{
|
||||
width:66.66667%;
|
||||
}
|
||||
|
||||
.md-col-9{
|
||||
width:75%;
|
||||
}
|
||||
|
||||
.md-col-10{
|
||||
width:83.33333%;
|
||||
}
|
||||
|
||||
.md-col-11{
|
||||
width:91.66667%;
|
||||
}
|
||||
|
||||
.md-col-12{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
}
|
||||
@media (min-width: 64em){
|
||||
|
||||
.lg-col{
|
||||
float:left;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.lg-col-right{
|
||||
float:right;
|
||||
box-sizing:border-box;
|
||||
}
|
||||
|
||||
.lg-col-1{
|
||||
width:8.33333%;
|
||||
}
|
||||
|
||||
.lg-col-2{
|
||||
width:16.66667%;
|
||||
}
|
||||
|
||||
.lg-col-3{
|
||||
width:25%;
|
||||
}
|
||||
|
||||
.lg-col-4{
|
||||
width:33.33333%;
|
||||
}
|
||||
|
||||
.lg-col-5{
|
||||
width:41.66667%;
|
||||
}
|
||||
|
||||
.lg-col-6{
|
||||
width:50%;
|
||||
}
|
||||
|
||||
.lg-col-7{
|
||||
width:58.33333%;
|
||||
}
|
||||
|
||||
.lg-col-8{
|
||||
width:66.66667%;
|
||||
}
|
||||
|
||||
.lg-col-9{
|
||||
width:75%;
|
||||
}
|
||||
|
||||
.lg-col-10{
|
||||
width:83.33333%;
|
||||
}
|
||||
|
||||
.lg-col-11{
|
||||
width:91.66667%;
|
||||
}
|
||||
|
||||
.lg-col-12{
|
||||
width:100%;
|
||||
}
|
||||
|
||||
}
|
||||
.flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
|
||||
|
||||
@media (min-width: 40em){
|
||||
.sm-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
|
||||
}
|
||||
|
||||
@media (min-width: 52em){
|
||||
.md-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
|
||||
}
|
||||
|
||||
@media (min-width: 64em){
|
||||
.lg-flex{ display:-webkit-box; display:-webkit-flex; display:-ms-flexbox; display:flex }
|
||||
}
|
||||
|
||||
.flex-column{ -webkit-box-orient:vertical; -webkit-box-direction:normal; -webkit-flex-direction:column; -ms-flex-direction:column; flex-direction:column }
|
||||
.flex-wrap{ -webkit-flex-wrap:wrap; -ms-flex-wrap:wrap; flex-wrap:wrap }
|
||||
|
||||
.items-start{ -webkit-box-align:start; -webkit-align-items:flex-start; -ms-flex-align:start; -ms-grid-row-align:flex-start; align-items:flex-start }
|
||||
.items-end{ -webkit-box-align:end; -webkit-align-items:flex-end; -ms-flex-align:end; -ms-grid-row-align:flex-end; align-items:flex-end }
|
||||
.items-center{ -webkit-box-align:center; -webkit-align-items:center; -ms-flex-align:center; -ms-grid-row-align:center; align-items:center }
|
||||
.items-baseline{ -webkit-box-align:baseline; -webkit-align-items:baseline; -ms-flex-align:baseline; -ms-grid-row-align:baseline; align-items:baseline }
|
||||
.items-stretch{ -webkit-box-align:stretch; -webkit-align-items:stretch; -ms-flex-align:stretch; -ms-grid-row-align:stretch; align-items:stretch }
|
||||
|
||||
.self-start{ -webkit-align-self:flex-start; -ms-flex-item-align:start; align-self:flex-start }
|
||||
.self-end{ -webkit-align-self:flex-end; -ms-flex-item-align:end; align-self:flex-end }
|
||||
.self-center{ -webkit-align-self:center; -ms-flex-item-align:center; align-self:center }
|
||||
.self-baseline{ -webkit-align-self:baseline; -ms-flex-item-align:baseline; align-self:baseline }
|
||||
.self-stretch{ -webkit-align-self:stretch; -ms-flex-item-align:stretch; align-self:stretch }
|
||||
|
||||
.justify-start{ -webkit-box-pack:start; -webkit-justify-content:flex-start; -ms-flex-pack:start; justify-content:flex-start }
|
||||
.justify-end{ -webkit-box-pack:end; -webkit-justify-content:flex-end; -ms-flex-pack:end; justify-content:flex-end }
|
||||
.justify-center{ -webkit-box-pack:center; -webkit-justify-content:center; -ms-flex-pack:center; justify-content:center }
|
||||
.justify-between{ -webkit-box-pack:justify; -webkit-justify-content:space-between; -ms-flex-pack:justify; justify-content:space-between }
|
||||
.justify-around{ -webkit-justify-content:space-around; -ms-flex-pack:distribute; justify-content:space-around }
|
||||
|
||||
.content-start{ -webkit-align-content:flex-start; -ms-flex-line-pack:start; align-content:flex-start }
|
||||
.content-end{ -webkit-align-content:flex-end; -ms-flex-line-pack:end; align-content:flex-end }
|
||||
.content-center{ -webkit-align-content:center; -ms-flex-line-pack:center; align-content:center }
|
||||
.content-between{ -webkit-align-content:space-between; -ms-flex-line-pack:justify; align-content:space-between }
|
||||
.content-around{ -webkit-align-content:space-around; -ms-flex-line-pack:distribute; align-content:space-around }
|
||||
.content-stretch{ -webkit-align-content:stretch; -ms-flex-line-pack:stretch; align-content:stretch }
|
||||
.flex-auto{
|
||||
-webkit-box-flex:1;
|
||||
-webkit-flex:1 1 auto;
|
||||
-ms-flex:1 1 auto;
|
||||
flex:1 1 auto;
|
||||
min-width:0;
|
||||
min-height:0;
|
||||
}
|
||||
.flex-none{ -webkit-box-flex:0; -webkit-flex:none; -ms-flex:none; flex:none }
|
||||
.fs0{ flex-shrink: 0 }
|
||||
|
||||
.order-0{ -webkit-box-ordinal-group:1; -webkit-order:0; -ms-flex-order:0; order:0 }
|
||||
.order-1{ -webkit-box-ordinal-group:2; -webkit-order:1; -ms-flex-order:1; order:1 }
|
||||
.order-2{ -webkit-box-ordinal-group:3; -webkit-order:2; -ms-flex-order:2; order:2 }
|
||||
.order-3{ -webkit-box-ordinal-group:4; -webkit-order:3; -ms-flex-order:3; order:3 }
|
||||
.order-last{ -webkit-box-ordinal-group:100000; -webkit-order:99999; -ms-flex-order:99999; order:99999 }
|
||||
|
||||
.relative{ position:relative }
|
||||
.absolute{ position:absolute }
|
||||
.fixed{ position:fixed }
|
||||
|
||||
.top-0{ top:0 }
|
||||
.right-0{ right:0 }
|
||||
.bottom-0{ bottom:0 }
|
||||
.left-0{ left:0 }
|
||||
|
||||
.z1{ z-index: 1 }
|
||||
.z2{ z-index: 2 }
|
||||
.z3{ z-index: 3 }
|
||||
.z4{ z-index: 4 }
|
||||
|
||||
.border{
|
||||
border-style:solid;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-top{
|
||||
border-top-style:solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-right{
|
||||
border-right-style:solid;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.border-bottom{
|
||||
border-bottom-style:solid;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.border-left{
|
||||
border-left-style:solid;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
.border-none{ border:0 }
|
||||
|
||||
.rounded{ border-radius: 3px }
|
||||
.circle{ border-radius:50% }
|
||||
|
||||
.rounded-top{ border-radius: 3px 3px 0 0 }
|
||||
.rounded-right{ border-radius: 0 3px 3px 0 }
|
||||
.rounded-bottom{ border-radius: 0 0 3px 3px }
|
||||
.rounded-left{ border-radius: 3px 0 0 3px }
|
||||
|
||||
.not-rounded{ border-radius:0 }
|
||||
|
||||
.hide{
|
||||
position:absolute !important;
|
||||
height:1px;
|
||||
width:1px;
|
||||
overflow:hidden;
|
||||
clip:rect(1px, 1px, 1px, 1px);
|
||||
}
|
||||
|
||||
@media (max-width: 40em){
|
||||
.xs-hide{ display:none !important }
|
||||
}
|
||||
|
||||
@media (min-width: 40em) and (max-width: 52em){
|
||||
.sm-hide{ display:none !important }
|
||||
}
|
||||
|
||||
@media (min-width: 52em) and (max-width: 64em){
|
||||
.md-hide{ display:none !important }
|
||||
}
|
||||
|
||||
@media (min-width: 64em){
|
||||
.lg-hide{ display:none !important }
|
||||
}
|
||||
|
||||
.display-none{ display:none !important }
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,93 +0,0 @@
|
||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,23 +0,0 @@
|
||||
@font-face{
|
||||
font-family: 'Source Code Pro';
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
src: url('EOT/SourceCodePro-Regular.eot') format('embedded-opentype'),
|
||||
url('WOFF2/TTF/SourceCodePro-Regular.ttf.woff2') format('woff2'),
|
||||
url('WOFF/OTF/SourceCodePro-Regular.otf.woff') format('woff'),
|
||||
url('OTF/SourceCodePro-Regular.otf') format('opentype'),
|
||||
url('TTF/SourceCodePro-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
@font-face{
|
||||
font-family: 'Source Code Pro';
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-stretch: normal;
|
||||
src: url('EOT/SourceCodePro-Bold.eot') format('embedded-opentype'),
|
||||
url('WOFF2/TTF/SourceCodePro-Bold.ttf.woff2') format('woff2'),
|
||||
url('WOFF/OTF/SourceCodePro-Bold.otf.woff') format('woff'),
|
||||
url('OTF/SourceCodePro-Bold.otf') format('opentype'),
|
||||
url('TTF/SourceCodePro-Bold.ttf') format('truetype');
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
|
||||
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.diff .hljs-header,
|
||||
.hljs-javadoc {
|
||||
color: #998;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-keyword,
|
||||
.css .rule .hljs-keyword,
|
||||
.hljs-winutils,
|
||||
.nginx .hljs-title,
|
||||
.hljs-subst,
|
||||
.hljs-request,
|
||||
.hljs-status {
|
||||
color: #1184CE;
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-hexcolor,
|
||||
.ruby .hljs-constant {
|
||||
color: #ed225d;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-tag .hljs-value,
|
||||
.hljs-phpdoc,
|
||||
.hljs-dartdoc,
|
||||
.tex .hljs-formula {
|
||||
color: #ed225d;
|
||||
}
|
||||
|
||||
.hljs-title,
|
||||
.hljs-id,
|
||||
.scss .hljs-preprocessor {
|
||||
color: #900;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-list .hljs-keyword,
|
||||
.hljs-subst {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-class .hljs-title,
|
||||
.hljs-type,
|
||||
.vhdl .hljs-literal,
|
||||
.tex .hljs-command {
|
||||
color: #458;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title,
|
||||
.hljs-rules .hljs-property,
|
||||
.django .hljs-tag .hljs-keyword {
|
||||
color: #000080;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-attribute,
|
||||
.hljs-variable,
|
||||
.lisp .hljs-body {
|
||||
color: #008080;
|
||||
}
|
||||
|
||||
.hljs-regexp {
|
||||
color: #009926;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.ruby .hljs-symbol .hljs-string,
|
||||
.lisp .hljs-keyword,
|
||||
.clojure .hljs-keyword,
|
||||
.scheme .hljs-keyword,
|
||||
.tex .hljs-special,
|
||||
.hljs-prompt {
|
||||
color: #990073;
|
||||
}
|
||||
|
||||
.hljs-built_in {
|
||||
color: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-preprocessor,
|
||||
.hljs-pragma,
|
||||
.hljs-pi,
|
||||
.hljs-doctype,
|
||||
.hljs-shebang,
|
||||
.hljs-cdata {
|
||||
color: #999;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
background: #fdd;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
background: #dfd;
|
||||
}
|
||||
|
||||
.diff .hljs-change {
|
||||
background: #0086b3;
|
||||
}
|
||||
|
||||
.hljs-chunk {
|
||||
color: #aaa;
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
/* global anchors */
|
||||
|
||||
// add anchor links to headers
|
||||
anchors.options.placement = 'left';
|
||||
anchors.add('h3');
|
||||
|
||||
// Filter UI
|
||||
var tocElements = document.getElementById('toc').getElementsByTagName('li');
|
||||
|
||||
document.getElementById('filter-input').addEventListener('keyup', function(e) {
|
||||
var i, element, children;
|
||||
|
||||
// enter key
|
||||
if (e.keyCode === 13) {
|
||||
// go to the first displayed item in the toc
|
||||
for (i = 0; i < tocElements.length; i++) {
|
||||
element = tocElements[i];
|
||||
if (!element.classList.contains('display-none')) {
|
||||
location.replace(element.firstChild.href);
|
||||
return e.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var match = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
var value = this.value.toLowerCase();
|
||||
|
||||
if (!value.match(/^\s*$/)) {
|
||||
match = function(element) {
|
||||
var html = element.firstChild.innerHTML;
|
||||
return html && html.toLowerCase().indexOf(value) !== -1;
|
||||
};
|
||||
}
|
||||
|
||||
for (i = 0; i < tocElements.length; i++) {
|
||||
element = tocElements[i];
|
||||
children = Array.from(element.getElementsByTagName('li'));
|
||||
if (match(element) || children.some(match)) {
|
||||
element.classList.remove('display-none');
|
||||
} else {
|
||||
element.classList.add('display-none');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var items = document.getElementsByClassName('toggle-sibling');
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
items[j].addEventListener('click', toggleSibling);
|
||||
}
|
||||
|
||||
function toggleSibling() {
|
||||
var stepSibling = this.parentNode.getElementsByClassName('toggle-target')[0];
|
||||
var icon = this.getElementsByClassName('icon')[0];
|
||||
var klass = 'display-none';
|
||||
if (stepSibling.classList.contains(klass)) {
|
||||
stepSibling.classList.remove(klass);
|
||||
icon.innerHTML = '▾';
|
||||
} else {
|
||||
stepSibling.classList.add(klass);
|
||||
icon.innerHTML = '▸';
|
||||
}
|
||||
}
|
||||
|
||||
function showHashTarget(targetId) {
|
||||
if (targetId) {
|
||||
var hashTarget = document.getElementById(targetId);
|
||||
// new target is hidden
|
||||
if (
|
||||
hashTarget &&
|
||||
hashTarget.offsetHeight === 0 &&
|
||||
hashTarget.parentNode.parentNode.classList.contains('display-none')
|
||||
) {
|
||||
hashTarget.parentNode.parentNode.classList.remove('display-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scrollIntoView(targetId) {
|
||||
// Only scroll to element if we don't have a stored scroll position.
|
||||
if (targetId && !history.state) {
|
||||
var hashTarget = document.getElementById(targetId);
|
||||
if (hashTarget) {
|
||||
hashTarget.scrollIntoView();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gotoCurrentTarget() {
|
||||
showHashTarget(location.hash.substring(1));
|
||||
scrollIntoView(location.hash.substring(1));
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', gotoCurrentTarget);
|
||||
gotoCurrentTarget();
|
||||
|
||||
var toclinks = document.getElementsByClassName('pre-open');
|
||||
for (var k = 0; k < toclinks.length; k++) {
|
||||
toclinks[k].addEventListener('mousedown', preOpen, false);
|
||||
}
|
||||
|
||||
function preOpen() {
|
||||
showHashTarget(this.hash.substring(1));
|
||||
}
|
||||
|
||||
var split_left = document.querySelector('#split-left');
|
||||
var split_right = document.querySelector('#split-right');
|
||||
var split_parent = split_left.parentNode;
|
||||
var cw_with_sb = split_left.clientWidth;
|
||||
split_left.style.overflow = 'hidden';
|
||||
var cw_without_sb = split_left.clientWidth;
|
||||
split_left.style.overflow = '';
|
||||
|
||||
Split(['#split-left', '#split-right'], {
|
||||
elementStyle: function(dimension, size, gutterSize) {
|
||||
return {
|
||||
'flex-basis': 'calc(' + size + '% - ' + gutterSize + 'px)'
|
||||
};
|
||||
},
|
||||
gutterStyle: function(dimension, gutterSize) {
|
||||
return {
|
||||
'flex-basis': gutterSize + 'px'
|
||||
};
|
||||
},
|
||||
gutterSize: 20,
|
||||
sizes: [33, 67]
|
||||
});
|
||||
|
||||
// Chrome doesn't remember scroll position properly so do it ourselves.
|
||||
// Also works on Firefox and Edge.
|
||||
|
||||
function updateState() {
|
||||
history.replaceState(
|
||||
{
|
||||
left_top: split_left.scrollTop,
|
||||
right_top: split_right.scrollTop
|
||||
},
|
||||
document.title
|
||||
);
|
||||
}
|
||||
|
||||
function loadState(ev) {
|
||||
if (ev) {
|
||||
// Edge doesn't replace change history.state on popstate.
|
||||
history.replaceState(ev.state, document.title);
|
||||
}
|
||||
if (history.state) {
|
||||
split_left.scrollTop = history.state.left_top;
|
||||
split_right.scrollTop = history.state.right_top;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
// Restore after Firefox scrolls to hash.
|
||||
setTimeout(function() {
|
||||
loadState();
|
||||
// Update with initial scroll position.
|
||||
updateState();
|
||||
// Update scroll positions only after we've loaded because Firefox
|
||||
// emits an initial scroll event with 0.
|
||||
split_left.addEventListener('scroll', updateState);
|
||||
split_right.addEventListener('scroll', updateState);
|
||||
}, 1);
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', loadState);
|
||||
@@ -1,15 +0,0 @@
|
||||
.gutter {
|
||||
background-color: #f5f5f5;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
}
|
||||
|
||||
.gutter.gutter-vertical {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=');
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.gutter.gutter-horizontal {
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==');
|
||||
cursor: ew-resize;
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
/*! Split.js - v1.3.5 */
|
||||
// https://github.com/nathancahill/Split.js
|
||||
// Copyright (c) 2017 Nathan Cahill; Licensed MIT
|
||||
|
||||
(function(global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined'
|
||||
? (module.exports = factory())
|
||||
: typeof define === 'function' && define.amd
|
||||
? define(factory)
|
||||
: (global.Split = factory());
|
||||
})(this, function() {
|
||||
'use strict';
|
||||
// The programming goals of Split.js are to deliver readable, understandable and
|
||||
// maintainable code, while at the same time manually optimizing for tiny minified file size,
|
||||
// browser compatibility without additional requirements, graceful fallback (IE8 is supported)
|
||||
// and very few assumptions about the user's page layout.
|
||||
var global = window;
|
||||
var document = global.document;
|
||||
|
||||
// Save a couple long function names that are used frequently.
|
||||
// This optimization saves around 400 bytes.
|
||||
var addEventListener = 'addEventListener';
|
||||
var removeEventListener = 'removeEventListener';
|
||||
var getBoundingClientRect = 'getBoundingClientRect';
|
||||
var NOOP = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Figure out if we're in IE8 or not. IE8 will still render correctly,
|
||||
// but will be static instead of draggable.
|
||||
var isIE8 = global.attachEvent && !global[addEventListener];
|
||||
|
||||
// This library only needs two helper functions:
|
||||
//
|
||||
// The first determines which prefixes of CSS calc we need.
|
||||
// We only need to do this once on startup, when this anonymous function is called.
|
||||
//
|
||||
// Tests -webkit, -moz and -o prefixes. Modified from StackOverflow:
|
||||
// http://stackoverflow.com/questions/16625140/js-feature-detection-to-detect-the-usage-of-webkit-calc-over-calc/16625167#16625167
|
||||
var calc =
|
||||
['', '-webkit-', '-moz-', '-o-']
|
||||
.filter(function(prefix) {
|
||||
var el = document.createElement('div');
|
||||
el.style.cssText = 'width:' + prefix + 'calc(9px)';
|
||||
|
||||
return !!el.style.length;
|
||||
})
|
||||
.shift() + 'calc';
|
||||
|
||||
// The second helper function allows elements and string selectors to be used
|
||||
// interchangeably. In either case an element is returned. This allows us to
|
||||
// do `Split([elem1, elem2])` as well as `Split(['#id1', '#id2'])`.
|
||||
var elementOrSelector = function(el) {
|
||||
if (typeof el === 'string' || el instanceof String) {
|
||||
return document.querySelector(el);
|
||||
}
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
// The main function to initialize a split. Split.js thinks about each pair
|
||||
// of elements as an independant pair. Dragging the gutter between two elements
|
||||
// only changes the dimensions of elements in that pair. This is key to understanding
|
||||
// how the following functions operate, since each function is bound to a pair.
|
||||
//
|
||||
// A pair object is shaped like this:
|
||||
//
|
||||
// {
|
||||
// a: DOM element,
|
||||
// b: DOM element,
|
||||
// aMin: Number,
|
||||
// bMin: Number,
|
||||
// dragging: Boolean,
|
||||
// parent: DOM element,
|
||||
// isFirst: Boolean,
|
||||
// isLast: Boolean,
|
||||
// direction: 'horizontal' | 'vertical'
|
||||
// }
|
||||
//
|
||||
// The basic sequence:
|
||||
//
|
||||
// 1. Set defaults to something sane. `options` doesn't have to be passed at all.
|
||||
// 2. Initialize a bunch of strings based on the direction we're splitting.
|
||||
// A lot of the behavior in the rest of the library is paramatized down to
|
||||
// rely on CSS strings and classes.
|
||||
// 3. Define the dragging helper functions, and a few helpers to go with them.
|
||||
// 4. Loop through the elements while pairing them off. Every pair gets an
|
||||
// `pair` object, a gutter, and special isFirst/isLast properties.
|
||||
// 5. Actually size the pair elements, insert gutters and attach event listeners.
|
||||
var Split = function(ids, options) {
|
||||
if (options === void 0) options = {};
|
||||
|
||||
var dimension;
|
||||
var clientDimension;
|
||||
var clientAxis;
|
||||
var position;
|
||||
var paddingA;
|
||||
var paddingB;
|
||||
var elements;
|
||||
|
||||
// All DOM elements in the split should have a common parent. We can grab
|
||||
// the first elements parent and hope users read the docs because the
|
||||
// behavior will be whacky otherwise.
|
||||
var parent = elementOrSelector(ids[0]).parentNode;
|
||||
var parentFlexDirection = global.getComputedStyle(parent).flexDirection;
|
||||
|
||||
// Set default options.sizes to equal percentages of the parent element.
|
||||
var sizes =
|
||||
options.sizes ||
|
||||
ids.map(function() {
|
||||
return 100 / ids.length;
|
||||
});
|
||||
|
||||
// Standardize minSize to an array if it isn't already. This allows minSize
|
||||
// to be passed as a number.
|
||||
var minSize = options.minSize !== undefined ? options.minSize : 100;
|
||||
var minSizes = Array.isArray(minSize)
|
||||
? minSize
|
||||
: ids.map(function() {
|
||||
return minSize;
|
||||
});
|
||||
var gutterSize = options.gutterSize !== undefined ? options.gutterSize : 10;
|
||||
var snapOffset = options.snapOffset !== undefined ? options.snapOffset : 30;
|
||||
var direction = options.direction || 'horizontal';
|
||||
var cursor =
|
||||
options.cursor ||
|
||||
(direction === 'horizontal' ? 'ew-resize' : 'ns-resize');
|
||||
var gutter =
|
||||
options.gutter ||
|
||||
function(i, gutterDirection) {
|
||||
var gut = document.createElement('div');
|
||||
gut.className = 'gutter gutter-' + gutterDirection;
|
||||
return gut;
|
||||
};
|
||||
var elementStyle =
|
||||
options.elementStyle ||
|
||||
function(dim, size, gutSize) {
|
||||
var style = {};
|
||||
|
||||
if (typeof size !== 'string' && !(size instanceof String)) {
|
||||
if (!isIE8) {
|
||||
style[dim] = calc + '(' + size + '% - ' + gutSize + 'px)';
|
||||
} else {
|
||||
style[dim] = size + '%';
|
||||
}
|
||||
} else {
|
||||
style[dim] = size;
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
var gutterStyle =
|
||||
options.gutterStyle ||
|
||||
function(dim, gutSize) {
|
||||
return (obj = {}), (obj[dim] = gutSize + 'px'), obj;
|
||||
var obj;
|
||||
};
|
||||
|
||||
// 2. Initialize a bunch of strings based on the direction we're splitting.
|
||||
// A lot of the behavior in the rest of the library is paramatized down to
|
||||
// rely on CSS strings and classes.
|
||||
if (direction === 'horizontal') {
|
||||
dimension = 'width';
|
||||
clientDimension = 'clientWidth';
|
||||
clientAxis = 'clientX';
|
||||
position = 'left';
|
||||
paddingA = 'paddingLeft';
|
||||
paddingB = 'paddingRight';
|
||||
} else if (direction === 'vertical') {
|
||||
dimension = 'height';
|
||||
clientDimension = 'clientHeight';
|
||||
clientAxis = 'clientY';
|
||||
position = 'top';
|
||||
paddingA = 'paddingTop';
|
||||
paddingB = 'paddingBottom';
|
||||
}
|
||||
|
||||
// 3. Define the dragging helper functions, and a few helpers to go with them.
|
||||
// Each helper is bound to a pair object that contains it's metadata. This
|
||||
// also makes it easy to store references to listeners that that will be
|
||||
// added and removed.
|
||||
//
|
||||
// Even though there are no other functions contained in them, aliasing
|
||||
// this to self saves 50 bytes or so since it's used so frequently.
|
||||
//
|
||||
// The pair object saves metadata like dragging state, position and
|
||||
// event listener references.
|
||||
|
||||
function setElementSize(el, size, gutSize) {
|
||||
// Split.js allows setting sizes via numbers (ideally), or if you must,
|
||||
// by string, like '300px'. This is less than ideal, because it breaks
|
||||
// the fluid layout that `calc(% - px)` provides. You're on your own if you do that,
|
||||
// make sure you calculate the gutter size by hand.
|
||||
var style = elementStyle(dimension, size, gutSize);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
Object.keys(style).forEach(function(prop) {
|
||||
return (el.style[prop] = style[prop]);
|
||||
});
|
||||
}
|
||||
|
||||
function setGutterSize(gutterElement, gutSize) {
|
||||
var style = gutterStyle(dimension, gutSize);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
Object.keys(style).forEach(function(prop) {
|
||||
return (gutterElement.style[prop] = style[prop]);
|
||||
});
|
||||
}
|
||||
|
||||
// Actually adjust the size of elements `a` and `b` to `offset` while dragging.
|
||||
// calc is used to allow calc(percentage + gutterpx) on the whole split instance,
|
||||
// which allows the viewport to be resized without additional logic.
|
||||
// Element a's size is the same as offset. b's size is total size - a size.
|
||||
// Both sizes are calculated from the initial parent percentage,
|
||||
// then the gutter size is subtracted.
|
||||
function adjust(offset) {
|
||||
var a = elements[this.a];
|
||||
var b = elements[this.b];
|
||||
var percentage = a.size + b.size;
|
||||
|
||||
a.size = (offset / this.size) * percentage;
|
||||
b.size = percentage - (offset / this.size) * percentage;
|
||||
|
||||
setElementSize(a.element, a.size, this.aGutterSize);
|
||||
setElementSize(b.element, b.size, this.bGutterSize);
|
||||
}
|
||||
|
||||
// drag, where all the magic happens. The logic is really quite simple:
|
||||
//
|
||||
// 1. Ignore if the pair is not dragging.
|
||||
// 2. Get the offset of the event.
|
||||
// 3. Snap offset to min if within snappable range (within min + snapOffset).
|
||||
// 4. Actually adjust each element in the pair to offset.
|
||||
//
|
||||
// ---------------------------------------------------------------------
|
||||
// | | <- a.minSize || b.minSize -> | |
|
||||
// | | | <- this.snapOffset || this.snapOffset -> | | |
|
||||
// | | | || | | |
|
||||
// | | | || | | |
|
||||
// ---------------------------------------------------------------------
|
||||
// | <- this.start this.size -> |
|
||||
function drag(e) {
|
||||
var offset;
|
||||
|
||||
if (!this.dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the offset of the event from the first side of the
|
||||
// pair `this.start`. Supports touch events, but not multitouch, so only the first
|
||||
// finger `touches[0]` is counted.
|
||||
if ('touches' in e) {
|
||||
offset = e.touches[0][clientAxis] - this.start;
|
||||
} else {
|
||||
offset = e[clientAxis] - this.start;
|
||||
}
|
||||
|
||||
// If within snapOffset of min or max, set offset to min or max.
|
||||
// snapOffset buffers a.minSize and b.minSize, so logic is opposite for both.
|
||||
// Include the appropriate gutter sizes to prevent overflows.
|
||||
if (offset <= elements[this.a].minSize + snapOffset + this.aGutterSize) {
|
||||
offset = elements[this.a].minSize + this.aGutterSize;
|
||||
} else if (
|
||||
offset >=
|
||||
this.size - (elements[this.b].minSize + snapOffset + this.bGutterSize)
|
||||
) {
|
||||
offset = this.size - (elements[this.b].minSize + this.bGutterSize);
|
||||
}
|
||||
|
||||
// Actually adjust the size.
|
||||
adjust.call(this, offset);
|
||||
|
||||
// Call the drag callback continously. Don't do anything too intensive
|
||||
// in this callback.
|
||||
if (options.onDrag) {
|
||||
options.onDrag();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache some important sizes when drag starts, so we don't have to do that
|
||||
// continously:
|
||||
//
|
||||
// `size`: The total size of the pair. First + second + first gutter + second gutter.
|
||||
// `start`: The leading side of the first element.
|
||||
//
|
||||
// ------------------------------------------------
|
||||
// | aGutterSize -> ||| |
|
||||
// | ||| |
|
||||
// | ||| |
|
||||
// | ||| <- bGutterSize |
|
||||
// ------------------------------------------------
|
||||
// | <- start size -> |
|
||||
function calculateSizes() {
|
||||
// Figure out the parent size minus padding.
|
||||
var a = elements[this.a].element;
|
||||
var b = elements[this.b].element;
|
||||
|
||||
this.size =
|
||||
a[getBoundingClientRect]()[dimension] +
|
||||
b[getBoundingClientRect]()[dimension] +
|
||||
this.aGutterSize +
|
||||
this.bGutterSize;
|
||||
this.start = a[getBoundingClientRect]()[position];
|
||||
}
|
||||
|
||||
// stopDragging is very similar to startDragging in reverse.
|
||||
function stopDragging() {
|
||||
var self = this;
|
||||
var a = elements[self.a].element;
|
||||
var b = elements[self.b].element;
|
||||
|
||||
if (self.dragging && options.onDragEnd) {
|
||||
options.onDragEnd();
|
||||
}
|
||||
|
||||
self.dragging = false;
|
||||
|
||||
// Remove the stored event listeners. This is why we store them.
|
||||
global[removeEventListener]('mouseup', self.stop);
|
||||
global[removeEventListener]('touchend', self.stop);
|
||||
global[removeEventListener]('touchcancel', self.stop);
|
||||
|
||||
self.parent[removeEventListener]('mousemove', self.move);
|
||||
self.parent[removeEventListener]('touchmove', self.move);
|
||||
|
||||
// Delete them once they are removed. I think this makes a difference
|
||||
// in memory usage with a lot of splits on one page. But I don't know for sure.
|
||||
delete self.stop;
|
||||
delete self.move;
|
||||
|
||||
a[removeEventListener]('selectstart', NOOP);
|
||||
a[removeEventListener]('dragstart', NOOP);
|
||||
b[removeEventListener]('selectstart', NOOP);
|
||||
b[removeEventListener]('dragstart', NOOP);
|
||||
|
||||
a.style.userSelect = '';
|
||||
a.style.webkitUserSelect = '';
|
||||
a.style.MozUserSelect = '';
|
||||
a.style.pointerEvents = '';
|
||||
|
||||
b.style.userSelect = '';
|
||||
b.style.webkitUserSelect = '';
|
||||
b.style.MozUserSelect = '';
|
||||
b.style.pointerEvents = '';
|
||||
|
||||
self.gutter.style.cursor = '';
|
||||
self.parent.style.cursor = '';
|
||||
}
|
||||
|
||||
// startDragging calls `calculateSizes` to store the inital size in the pair object.
|
||||
// It also adds event listeners for mouse/touch events,
|
||||
// and prevents selection while dragging so avoid the selecting text.
|
||||
function startDragging(e) {
|
||||
// Alias frequently used variables to save space. 200 bytes.
|
||||
var self = this;
|
||||
var a = elements[self.a].element;
|
||||
var b = elements[self.b].element;
|
||||
|
||||
// Call the onDragStart callback.
|
||||
if (!self.dragging && options.onDragStart) {
|
||||
options.onDragStart();
|
||||
}
|
||||
|
||||
// Don't actually drag the element. We emulate that in the drag function.
|
||||
e.preventDefault();
|
||||
|
||||
// Set the dragging property of the pair object.
|
||||
self.dragging = true;
|
||||
|
||||
// Create two event listeners bound to the same pair object and store
|
||||
// them in the pair object.
|
||||
self.move = drag.bind(self);
|
||||
self.stop = stopDragging.bind(self);
|
||||
|
||||
// All the binding. `window` gets the stop events in case we drag out of the elements.
|
||||
global[addEventListener]('mouseup', self.stop);
|
||||
global[addEventListener]('touchend', self.stop);
|
||||
global[addEventListener]('touchcancel', self.stop);
|
||||
|
||||
self.parent[addEventListener]('mousemove', self.move);
|
||||
self.parent[addEventListener]('touchmove', self.move);
|
||||
|
||||
// Disable selection. Disable!
|
||||
a[addEventListener]('selectstart', NOOP);
|
||||
a[addEventListener]('dragstart', NOOP);
|
||||
b[addEventListener]('selectstart', NOOP);
|
||||
b[addEventListener]('dragstart', NOOP);
|
||||
|
||||
a.style.userSelect = 'none';
|
||||
a.style.webkitUserSelect = 'none';
|
||||
a.style.MozUserSelect = 'none';
|
||||
a.style.pointerEvents = 'none';
|
||||
|
||||
b.style.userSelect = 'none';
|
||||
b.style.webkitUserSelect = 'none';
|
||||
b.style.MozUserSelect = 'none';
|
||||
b.style.pointerEvents = 'none';
|
||||
|
||||
// Set the cursor, both on the gutter and the parent element.
|
||||
// Doing only a, b and gutter causes flickering.
|
||||
self.gutter.style.cursor = cursor;
|
||||
self.parent.style.cursor = cursor;
|
||||
|
||||
// Cache the initial sizes of the pair.
|
||||
calculateSizes.call(self);
|
||||
}
|
||||
|
||||
// 5. Create pair and element objects. Each pair has an index reference to
|
||||
// elements `a` and `b` of the pair (first and second elements).
|
||||
// Loop through the elements while pairing them off. Every pair gets a
|
||||
// `pair` object, a gutter, and isFirst/isLast properties.
|
||||
//
|
||||
// Basic logic:
|
||||
//
|
||||
// - Starting with the second element `i > 0`, create `pair` objects with
|
||||
// `a = i - 1` and `b = i`
|
||||
// - Set gutter sizes based on the _pair_ being first/last. The first and last
|
||||
// pair have gutterSize / 2, since they only have one half gutter, and not two.
|
||||
// - Create gutter elements and add event listeners.
|
||||
// - Set the size of the elements, minus the gutter sizes.
|
||||
//
|
||||
// -----------------------------------------------------------------------
|
||||
// | i=0 | i=1 | i=2 | i=3 |
|
||||
// | | isFirst | | isLast |
|
||||
// | pair 0 pair 1 pair 2 |
|
||||
// | | | | |
|
||||
// -----------------------------------------------------------------------
|
||||
var pairs = [];
|
||||
elements = ids.map(function(id, i) {
|
||||
// Create the element object.
|
||||
var element = {
|
||||
element: elementOrSelector(id),
|
||||
size: sizes[i],
|
||||
minSize: minSizes[i]
|
||||
};
|
||||
|
||||
var pair;
|
||||
|
||||
if (i > 0) {
|
||||
// Create the pair object with it's metadata.
|
||||
pair = {
|
||||
a: i - 1,
|
||||
b: i,
|
||||
dragging: false,
|
||||
isFirst: i === 1,
|
||||
isLast: i === ids.length - 1,
|
||||
direction: direction,
|
||||
parent: parent
|
||||
};
|
||||
|
||||
// For first and last pairs, first and last gutter width is half.
|
||||
pair.aGutterSize = gutterSize;
|
||||
pair.bGutterSize = gutterSize;
|
||||
|
||||
if (pair.isFirst) {
|
||||
pair.aGutterSize = gutterSize / 2;
|
||||
}
|
||||
|
||||
if (pair.isLast) {
|
||||
pair.bGutterSize = gutterSize / 2;
|
||||
}
|
||||
|
||||
// if the parent has a reverse flex-direction, switch the pair elements.
|
||||
if (
|
||||
parentFlexDirection === 'row-reverse' ||
|
||||
parentFlexDirection === 'column-reverse'
|
||||
) {
|
||||
var temp = pair.a;
|
||||
pair.a = pair.b;
|
||||
pair.b = temp;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the size of the current element. IE8 is supported by
|
||||
// staticly assigning sizes without draggable gutters. Assigns a string
|
||||
// to `size`.
|
||||
//
|
||||
// IE9 and above
|
||||
if (!isIE8) {
|
||||
// Create gutter elements for each pair.
|
||||
if (i > 0) {
|
||||
var gutterElement = gutter(i, direction);
|
||||
setGutterSize(gutterElement, gutterSize);
|
||||
|
||||
gutterElement[addEventListener](
|
||||
'mousedown',
|
||||
startDragging.bind(pair)
|
||||
);
|
||||
gutterElement[addEventListener](
|
||||
'touchstart',
|
||||
startDragging.bind(pair)
|
||||
);
|
||||
|
||||
parent.insertBefore(gutterElement, element.element);
|
||||
|
||||
pair.gutter = gutterElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the element size to our determined size.
|
||||
// Half-size gutters for first and last elements.
|
||||
if (i === 0 || i === ids.length - 1) {
|
||||
setElementSize(element.element, element.size, gutterSize / 2);
|
||||
} else {
|
||||
setElementSize(element.element, element.size, gutterSize);
|
||||
}
|
||||
|
||||
var computedSize = element.element[getBoundingClientRect]()[dimension];
|
||||
|
||||
if (computedSize < element.minSize) {
|
||||
element.minSize = computedSize;
|
||||
}
|
||||
|
||||
// After the first iteration, and we have a pair object, append it to the
|
||||
// list of pairs.
|
||||
if (i > 0) {
|
||||
pairs.push(pair);
|
||||
}
|
||||
|
||||
return element;
|
||||
});
|
||||
|
||||
function setSizes(newSizes) {
|
||||
newSizes.forEach(function(newSize, i) {
|
||||
if (i > 0) {
|
||||
var pair = pairs[i - 1];
|
||||
var a = elements[pair.a];
|
||||
var b = elements[pair.b];
|
||||
|
||||
a.size = newSizes[i - 1];
|
||||
b.size = newSize;
|
||||
|
||||
setElementSize(a.element, a.size, pair.aGutterSize);
|
||||
setElementSize(b.element, b.size, pair.bGutterSize);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
pairs.forEach(function(pair) {
|
||||
pair.parent.removeChild(pair.gutter);
|
||||
elements[pair.a].element.style[dimension] = '';
|
||||
elements[pair.b].element.style[dimension] = '';
|
||||
});
|
||||
}
|
||||
|
||||
if (isIE8) {
|
||||
return {
|
||||
setSizes: setSizes,
|
||||
destroy: destroy
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
setSizes: setSizes,
|
||||
getSizes: function getSizes() {
|
||||
return elements.map(function(element) {
|
||||
return element.size;
|
||||
});
|
||||
},
|
||||
collapse: function collapse(i) {
|
||||
if (i === pairs.length) {
|
||||
var pair = pairs[i - 1];
|
||||
|
||||
calculateSizes.call(pair);
|
||||
|
||||
if (!isIE8) {
|
||||
adjust.call(pair, pair.size - pair.bGutterSize);
|
||||
}
|
||||
} else {
|
||||
var pair$1 = pairs[i];
|
||||
|
||||
calculateSizes.call(pair$1);
|
||||
|
||||
if (!isIE8) {
|
||||
adjust.call(pair$1, pair$1.aGutterSize);
|
||||
}
|
||||
}
|
||||
},
|
||||
destroy: destroy
|
||||
};
|
||||
};
|
||||
|
||||
return Split;
|
||||
});
|
||||
@@ -1,140 +0,0 @@
|
||||
.documentation {
|
||||
font-family: Helvetica, sans-serif;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.black {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 20px 0 10px 0;
|
||||
}
|
||||
|
||||
.documentation h3 {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.border-bottom {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1184CE;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.documentation a[href]:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.py1-ul li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.max-height-100 {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.height-viewport-100 {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
section:target h3 {
|
||||
font-weight:700;
|
||||
}
|
||||
|
||||
.documentation td,
|
||||
.documentation th {
|
||||
padding: .25rem .25rem;
|
||||
}
|
||||
|
||||
h1:hover .anchorjs-link,
|
||||
h2:hover .anchorjs-link,
|
||||
h3:hover .anchorjs-link,
|
||||
h4:hover .anchorjs-link {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fix-3 {
|
||||
width: 25%;
|
||||
max-width: 244px;
|
||||
}
|
||||
|
||||
.fix-3 {
|
||||
width: 25%;
|
||||
max-width: 244px;
|
||||
}
|
||||
|
||||
@media (min-width: 52em) {
|
||||
.fix-margin-3 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
}
|
||||
|
||||
.pre, pre, code, .code {
|
||||
font-family: Source Code Pro,Menlo,Consolas,Liberation Mono,monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fill-light {
|
||||
background: #F9F9F9;
|
||||
}
|
||||
|
||||
.width2 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
font-family: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 2rem;
|
||||
padding: .5rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
font-size: .875rem;
|
||||
border-radius: 3px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.prose table th,
|
||||
.prose table td {
|
||||
text-align: left;
|
||||
padding:8px;
|
||||
border:1px solid #ddd;
|
||||
}
|
||||
|
||||
.prose table th:nth-child(1) { border-right: none; }
|
||||
.prose table th:nth-child(2) { border-left: none; }
|
||||
|
||||
.prose table {
|
||||
border:1px solid #ddd;
|
||||
}
|
||||
|
||||
.prose-big {
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.quiet {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.minishadow {
|
||||
box-shadow: 2px 2px 10px #f3f3f3;
|
||||
}
|
||||
@@ -1,770 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<title>seasoned-request 1.0.0 | Documentation</title>
|
||||
<meta name='description' content='seasoned request app'>
|
||||
<meta name='viewport' content='width=device-width,initial-scale=1'>
|
||||
<link href='assets/bass.css' rel='stylesheet' />
|
||||
<link href='assets/style.css' rel='stylesheet' />
|
||||
<link href='assets/github.css' rel='stylesheet' />
|
||||
<link href='assets/split.css' rel='stylesheet' />
|
||||
</head>
|
||||
<body class='documentation m0'>
|
||||
<div class='flex'>
|
||||
<div id='split-left' class='overflow-auto fs0 height-viewport-100'>
|
||||
<div class='py1 px2'>
|
||||
<h3 class='mb0 no-anchor'>seasoned-request</h3>
|
||||
<div class='mb1'><code>1.0.0</code></div>
|
||||
<input
|
||||
placeholder='Filter'
|
||||
id='filter-input'
|
||||
class='col12 block input'
|
||||
type='text' />
|
||||
<div id='toc'>
|
||||
<ul class='list-reset h5 py1-ul'>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#getmovie'
|
||||
class="">
|
||||
getMovie
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#getshow'
|
||||
class="">
|
||||
getShow
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#gettmdblistbypath'
|
||||
class="">
|
||||
getTmdbListByPath
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#searchtmdb'
|
||||
class="">
|
||||
searchTmdb
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#searchtorrents'
|
||||
class="">
|
||||
searchTorrents
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#addmagnet'
|
||||
class="">
|
||||
addMagnet
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#request'
|
||||
class="">
|
||||
request
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
<li><a
|
||||
href='#elasticsearchmoviesandshows'
|
||||
class="">
|
||||
elasticSearchMoviesAndShows
|
||||
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class='mt1 h6 quiet'>
|
||||
<a href='https://documentation.js.org/reading-documentation.html'>Need help reading this?</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='split-right' class='relative overflow-auto height-viewport-100'>
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='getmovie'>
|
||||
getMovie
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Fetches tmdb movie by id. Can optionally include cast credits in result object.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>getMovie</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>
|
||||
= <code>false</code>)</code>
|
||||
Include credits
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Tmdb response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='getshow'>
|
||||
getShow
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Fetches tmdb show by id. Can optionally include cast credits in result object.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>getShow</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>
|
||||
= <code>false</code>)</code>
|
||||
Include credits
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Tmdb response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='gettmdblistbypath'>
|
||||
getTmdbListByPath
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Fetches tmdb list by path.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>getTmdbListByPath</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>listPath</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
Path of list
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>page</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>
|
||||
= <code>1</code>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Tmdb list response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='searchtmdb'>
|
||||
searchTmdb
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Fetches tmdb movies and shows by query.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>searchTmdb</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>page</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>
|
||||
= <code>1</code>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Tmdb response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='searchtorrents'>
|
||||
searchTorrents
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Search for torrents by query</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>searchTorrents</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>authorization_token</span> <code class='quiet'>(any)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>credits</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code>
|
||||
Include credits
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Torrent response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='addmagnet'>
|
||||
addMagnet
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Add magnet to download queue.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>addMagnet</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>magnet</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
Magnet link
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>name</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code>
|
||||
Name of torrent
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>tmdb_id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean">boolean</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Success/Failure response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='request'>
|
||||
request
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Request a movie or show from id. If authorization token is included the user will be linked
|
||||
to the requested item.</p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>request</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>id</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number">number</a>)</code>
|
||||
Movie or show id
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>type</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
Movie or show type
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>authorization_token</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>?
|
||||
= <code>undefined</code>)</code>
|
||||
To identify the requesting user
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
Success/Failure response
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
|
||||
<section class='p2 mb2 clearfix bg-white minishadow'>
|
||||
|
||||
|
||||
<div class='clearfix'>
|
||||
|
||||
<h3 class='fl m0' id='elasticsearchmoviesandshows'>
|
||||
elasticSearchMoviesAndShows
|
||||
</h3>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<p>Search elastic indexes movies and shows by query. Doc includes Tmdb daily export of Movies and
|
||||
Tv Shows. See tmdb docs for more info: <a href="https://developers.themoviedb.org/3/getting-started/daily-file-exports">https://developers.themoviedb.org/3/getting-started/daily-file-exports</a></p>
|
||||
|
||||
<div class='pre p1 fill-light mt0'>elasticSearchMoviesAndShows</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Parameters</div>
|
||||
<div class='prose'>
|
||||
|
||||
<div class='space-bottom0'>
|
||||
<div>
|
||||
<span class='code bold'>query</span> <code class='quiet'>(<a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String">string</a>)</code>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class='py1 quiet mt1 prose-big'>Returns</div>
|
||||
<code><a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object">object</a></code>:
|
||||
List of movies and shows matching query
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src='assets/anchor.js'></script>
|
||||
<script src='assets/split.js'></script>
|
||||
<script src='assets/site.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
17
index.html
17
index.html
File diff suppressed because one or more lines are too long
@@ -11,8 +11,9 @@
|
||||
"docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.15.3",
|
||||
"axios": "^0.18.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"chart.js": "^2.9.2",
|
||||
"connect-history-api-fallback": "^1.3.0",
|
||||
"express": "^4.16.1",
|
||||
"vue": "^2.5.2",
|
||||
@@ -29,12 +30,14 @@
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"babel-loader": "^8.0.6",
|
||||
"cross-env": "^3.0.0",
|
||||
"css-loader": "^0.25.0",
|
||||
"css-loader": "^3.4.2",
|
||||
"documentation": "^11.0.0",
|
||||
"file-loader": "^0.9.0",
|
||||
"node-sass": "^4.5.0",
|
||||
"sass-loader": "^5.0.1",
|
||||
"schema-utils": "^2.4.1",
|
||||
"vue-loader": "^10.0.0",
|
||||
"vue-svg-inline-loader": "^1.3.1",
|
||||
"vue-template-compiler": "2.6.10",
|
||||
"webpack": "^2.2.0",
|
||||
"webpack-dev-server": "^2.2.0"
|
||||
|
||||
181
src/App.vue
181
src/App.vue
@@ -1,73 +1,78 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
|
||||
<!-- Header and hamburger navigation -->
|
||||
<navigation></navigation>
|
||||
|
||||
<!-- Header with search field -->
|
||||
<header class="header">
|
||||
<search-input v-model="query"></search-input>
|
||||
</header>
|
||||
<search-input v-model="query"></search-input>
|
||||
|
||||
<!-- Movie popup that will show above existing rendered content -->
|
||||
<movie-popup v-if="moviePopupIsVisible" :id="popupID" :type="popupType"></movie-popup>
|
||||
<movie-popup
|
||||
v-if="moviePopupIsVisible"
|
||||
:id="popupID"
|
||||
:type="popupType"
|
||||
></movie-popup>
|
||||
|
||||
<darkmode-toggle />
|
||||
|
||||
<!-- Display the component assigned to the given route (default: home) -->
|
||||
<router-view class="content"></router-view>
|
||||
|
||||
<router-view class="content" :key="$route.fullPath"></router-view>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import Navigation from '@/components/Navigation.vue'
|
||||
import MoviePopup from '@/components/MoviePopup.vue'
|
||||
import SearchInput from '@/components/SearchInput.vue'
|
||||
import Vue from "vue";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import MoviePopup from "@/components/MoviePopup";
|
||||
import SearchInput from "@/components/SearchInput";
|
||||
import DarkmodeToggle from "@/components/ui/darkmodeToggle";
|
||||
|
||||
export default {
|
||||
name: 'app',
|
||||
name: "app",
|
||||
components: {
|
||||
Navigation,
|
||||
MoviePopup,
|
||||
SearchInput
|
||||
SearchInput,
|
||||
DarkmodeToggle
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
query: '',
|
||||
query: "",
|
||||
moviePopupIsVisible: false,
|
||||
popupID: 0,
|
||||
popupType: 'movie'
|
||||
}
|
||||
popupType: "movie"
|
||||
};
|
||||
},
|
||||
created(){
|
||||
let that = this
|
||||
created() {
|
||||
let that = this;
|
||||
Vue.prototype.$popup = {
|
||||
get isOpen() {
|
||||
return that.moviePopupIsVisible
|
||||
return that.moviePopupIsVisible;
|
||||
},
|
||||
open: (id, type) => {
|
||||
this.popupID = id || this.popupID
|
||||
this.popupType = type || this.popupType
|
||||
this.moviePopupIsVisible = true
|
||||
console.log('opened')
|
||||
this.popupID = id || this.popupID;
|
||||
this.popupType = type || this.popupType;
|
||||
this.moviePopupIsVisible = true;
|
||||
console.log("opened");
|
||||
},
|
||||
close: () => {
|
||||
this.moviePopupIsVisible = false
|
||||
console.log('closed')
|
||||
this.moviePopupIsVisible = false;
|
||||
console.log("closed");
|
||||
}
|
||||
}
|
||||
console.log('MoviePopup registered at this.$popup and has state: ', this.$popup.isOpen)
|
||||
};
|
||||
console.log(
|
||||
"MoviePopup registered at this.$popup and has state: ",
|
||||
this.$popup.isOpen
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/variables";
|
||||
.content {
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
width: calc(100% - 95px);
|
||||
padding-top: $header-size;
|
||||
margin-top: $header-size;
|
||||
margin-left: 95px;
|
||||
position: relative;
|
||||
}
|
||||
@@ -75,120 +80,84 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./src/scss/main";
|
||||
// @import "./src/scss/main";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
*{
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body{
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: "Roboto", sans-serif;
|
||||
line-height: 1.6;
|
||||
background: $c-light;
|
||||
color: $c-dark;
|
||||
&.hidden{
|
||||
background: $background-color;
|
||||
color: $text-color;
|
||||
transition: background-color 0.5s ease, color 0.5s ease;
|
||||
&.hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
input, textarea, button{
|
||||
font-family: 'Roboto', sans-serif;
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
figure{
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
}
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
figure {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
img{
|
||||
img {
|
||||
display: block;
|
||||
// max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.wrapper{
|
||||
.no-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.header{
|
||||
.header {
|
||||
position: fixed;
|
||||
background: $c-white;
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
width: calc(100% - 170px);
|
||||
margin-left: 95px;
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
top: 0;
|
||||
}
|
||||
&__search{
|
||||
display: flex;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 55px;
|
||||
@include tablet-min{
|
||||
position: relative;
|
||||
height: 75px;
|
||||
right: 0;
|
||||
}
|
||||
&-input{
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 15px 20px 15px 45px;
|
||||
outline: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: $c-dark;
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
@include tablet-min{
|
||||
padding: 15px 30px 15px 60px;
|
||||
}
|
||||
@include tablet-landscape-min{
|
||||
padding: 15px 30px 15px 80px;
|
||||
}
|
||||
@include desktop-min{
|
||||
padding: 15px 30px 15px 90px;
|
||||
}
|
||||
}
|
||||
&-arrow {
|
||||
height: 19px;
|
||||
width: 30px;
|
||||
display: flex;
|
||||
align-self: center;
|
||||
margin-right: 30px;
|
||||
|
||||
-moz-transition: all 0.5s ease;
|
||||
-webkit-transition: all 0.5s ease;
|
||||
transition: all 0.5s ease;
|
||||
|
||||
&.down {
|
||||
-ms-transform: rotate(180deg);
|
||||
-moz-transform: rotate(180deg);
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
&-input:focus + &-icon{
|
||||
fill: $c-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// router view transition
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.25s;
|
||||
}
|
||||
.fade-enter-active {
|
||||
transition-delay: 0.25s;
|
||||
}
|
||||
.fade-enter, .fade-leave-active {
|
||||
opacity: 0
|
||||
.fade-enter,
|
||||
.fade-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
311
src/api.js
311
src/api.js
@@ -1,7 +1,8 @@
|
||||
import axios from 'axios'
|
||||
import storage from '@/storage.js'
|
||||
import storage from '@/storage'
|
||||
import config from '@/config.json'
|
||||
import path from 'path'
|
||||
import store from '@/store'
|
||||
|
||||
const SEASONED_URL = config.SEASONED_URL
|
||||
const ELASTIC_URL = config.ELASTIC_URL
|
||||
@@ -10,6 +11,13 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
|
||||
// TODO
|
||||
// - Move autorization token and errors here?
|
||||
|
||||
const checkStatusAndReturnJson = (response) => {
|
||||
if (!response.ok) {
|
||||
throw resp
|
||||
}
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// - - - TMDB - - -
|
||||
|
||||
/**
|
||||
@@ -18,14 +26,21 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getMovie = (id, credits=false) => {
|
||||
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => {
|
||||
const url = new URL('v2/movie', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (checkExistance) {
|
||||
url.searchParams.append('check_existance', true)
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
if(release_dates) {
|
||||
url.searchParams.append('release_dates', true)
|
||||
}
|
||||
|
||||
return axios.get(url.href)
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting movie: ${id}`); throw error })
|
||||
}
|
||||
|
||||
@@ -35,31 +50,79 @@ const getMovie = (id, credits=false) => {
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getShow = (id, credits=false) => {
|
||||
const getShow = (id, checkExistance=false, credits=false) => {
|
||||
const url = new URL('v2/show', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (checkExistance) {
|
||||
url.searchParams.append('check_existance', true)
|
||||
}
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting show: ${id}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tmdb person by id. Can optionally include cast credits in result object.
|
||||
* @param {number} id
|
||||
* @param {boolean} [credits=false] Include credits
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const getPerson = (id, credits=false) => {
|
||||
const url = new URL('v2/person', SEASONED_URL)
|
||||
url.pathname = path.join(url.pathname, id.toString())
|
||||
if (credits) {
|
||||
url.searchParams.append('credits', true)
|
||||
}
|
||||
|
||||
return axios.get(url.href)
|
||||
.catch(error => { console.error(`api error getting show: ${id}`); throw error })
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error getting person: ${id}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches tmdb list by path.
|
||||
* @param {string} listPath Path of list
|
||||
* Fetches tmdb list by name.
|
||||
* @param {string} name List the fetch
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Tmdb list response
|
||||
*/
|
||||
const getTmdbListByPath = (listPath, page=1) => {
|
||||
const url = new URL(listPath, SEASONED_URL)
|
||||
const getTmdbMovieListByName = (name, page=1) => {
|
||||
const url = new URL('v2/movie/' + name, SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
// TODO - remove. this is temporary fix for user-requests endpoint (also import)
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return axios.get(url.href, { headers: headers })
|
||||
.catch(error => { console.error(`api error getting list: ${listPath}, page: ${page}`); throw error })
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches requested items.
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Request response
|
||||
*/
|
||||
const getRequests = (page=1) => {
|
||||
const url = new URL('v2/request', SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
// .catch(error => { console.error(`api error getting list: ${name}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
|
||||
const getUserRequests = (page=1) => {
|
||||
const url = new URL('v1/user/requests', SEASONED_URL)
|
||||
url.searchParams.append('page', page)
|
||||
|
||||
const headers = { authorization: localStorage.getItem('token') }
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,12 +131,20 @@ const getTmdbListByPath = (listPath, page=1) => {
|
||||
* @param {number} [page=1]
|
||||
* @returns {object} Tmdb response
|
||||
*/
|
||||
const searchTmdb = (query, page=1) => {
|
||||
const searchTmdb = (query, page=1, adult=false, mediaType=null) => {
|
||||
const url = new URL('v2/search', SEASONED_URL)
|
||||
if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) {
|
||||
url.pathname += `/${mediaType}`
|
||||
}
|
||||
|
||||
url.searchParams.append('query', query)
|
||||
url.searchParams.append('page', page)
|
||||
url.searchParams.append('adult', adult)
|
||||
|
||||
return axios.get(url.href)
|
||||
const headers = { authorization: localStorage.getItem('token') }
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error })
|
||||
}
|
||||
|
||||
@@ -86,12 +157,13 @@ const searchTmdb = (query, page=1) => {
|
||||
* @returns {object} Torrent response
|
||||
*/
|
||||
const searchTorrents = (query, authorization_token) => {
|
||||
const url = new URL('v1/pirate/search', SEASONED_URL)
|
||||
const url = new URL('/api/v1/pirate/search', SEASONED_URL)
|
||||
url.searchParams.append('query', query)
|
||||
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return axios.get(url.href, { headers: headers })
|
||||
return fetch(url.href, { headers: headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error searching torrents: ${query}`); throw error })
|
||||
}
|
||||
|
||||
@@ -105,15 +177,23 @@ const searchTorrents = (query, authorization_token) => {
|
||||
const addMagnet = (magnet, name, tmdb_id) => {
|
||||
const url = new URL('v1/pirate/add', SEASONED_URL)
|
||||
|
||||
const body = {
|
||||
const body = JSON.stringify({
|
||||
magnet: magnet,
|
||||
name: name,
|
||||
tmdb_id: tmdb_id
|
||||
})
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
const headers = { authorization: storage.token }
|
||||
|
||||
return axios.post(url.href, body, { headers: headers })
|
||||
.catch(error => { console.error(`api error adding magnet: ${name}`); throw error })
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error adding magnet: ${name} ${error}`); throw error })
|
||||
}
|
||||
|
||||
// - - - Plex/Request - - -
|
||||
@@ -172,33 +252,166 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
|
||||
.catch(err => Promise.reject(err))
|
||||
}
|
||||
|
||||
// - - - Authenticate with plex - - -
|
||||
|
||||
const plexAuthenticate = (username, password) => {
|
||||
const url = new URL('https://plex.tv/users/sign_in.json')
|
||||
url.searchParams.append('user[login]', username)
|
||||
url.searchParams.append('user[password]', password)
|
||||
const watchLink = (title, year, authorization_token=undefined) => {
|
||||
const url = new URL('v1/plex/watch-link', SEASONED_URL)
|
||||
url.searchParams.append('title', title)
|
||||
url.searchParams.append('year', year)
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Plex-Platform': 'Linux',
|
||||
'X-Plex-Version': 'v2.0.24',
|
||||
'X-Plex-Platform-Version': '4.13.0-36-generic',
|
||||
'X-Plex-Device-Name': 'Tautulli',
|
||||
'X-Plex-Client-Identifier': '123'
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
return axios.post(url.href, { headers: headers })
|
||||
.catch(error => { console.error(`api error authentication plex: ${username}`); throw error })
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.then(response => response.link)
|
||||
}
|
||||
|
||||
// - - - Seasoned user endpoints - - -
|
||||
|
||||
const register = (username, password) => {
|
||||
const url = new URL('v1/user', SEASONED_URL)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => {
|
||||
console.error('Unexpected error occured before receiving response. Error:', error)
|
||||
// TODO log to sentry the issue here
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
const login = (username, password, throwError=false) => {
|
||||
const url = new URL('v1/user/login', SEASONED_URL)
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
}
|
||||
|
||||
return fetch(url.href, options)
|
||||
.then(resp => {
|
||||
if (resp.status == 200)
|
||||
return resp.json();
|
||||
|
||||
if (throwError)
|
||||
throw resp;
|
||||
else
|
||||
console.error("Error occured when trying to sign in.\nError:", resp);
|
||||
})
|
||||
}
|
||||
|
||||
const getSettings = () => {
|
||||
const settingsExists = (value) => {
|
||||
if (value instanceof Object && value.hasOwnProperty('settings'))
|
||||
return value;
|
||||
throw "Settings does not exist in response object.";
|
||||
}
|
||||
const commitSettingsToStore = (response) => {
|
||||
store.dispatch('userModule/setSettings', response.settings)
|
||||
return response
|
||||
}
|
||||
|
||||
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.then(settingsExists)
|
||||
.then(commitSettingsToStore)
|
||||
.then(response => response.settings)
|
||||
.catch(error => { console.log('api error getting user settings'); throw error })
|
||||
}
|
||||
|
||||
const updateSettings = (settings) => {
|
||||
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify(settings)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error updating user settings'); throw error })
|
||||
}
|
||||
|
||||
// - - - Authenticate with plex - - -
|
||||
|
||||
const linkPlexAccount = (username, password) => {
|
||||
const url = new URL('v1/user/link_plex', SEASONED_URL)
|
||||
const body = { username, password }
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error })
|
||||
}
|
||||
|
||||
const unlinkPlexAccount = (username, password) => {
|
||||
const url = new URL('v1/user/unlink_plex', SEASONED_URL)
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
authorization: storage.token
|
||||
}
|
||||
|
||||
return fetch(url.href, {
|
||||
method: 'POST',
|
||||
headers
|
||||
})
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error })
|
||||
}
|
||||
|
||||
|
||||
// - - - User graphs - - -
|
||||
|
||||
const fetchChart = (urlPath, days, chartType) => {
|
||||
const url = new URL('v1/user' + urlPath, SEASONED_URL)
|
||||
url.searchParams.append('days', days)
|
||||
url.searchParams.append('y_axis', chartType)
|
||||
|
||||
const authorization_token = localStorage.getItem('token')
|
||||
const headers = authorization_token ? {
|
||||
'Authorization': authorization_token,
|
||||
'Content-Type': 'application/json'
|
||||
} : {}
|
||||
|
||||
return fetch(url.href, { headers })
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error fetching chart'); throw error })
|
||||
}
|
||||
|
||||
|
||||
// - - - Random emoji - - -
|
||||
|
||||
const getEmoji = () => {
|
||||
const url = path.join(SEASONED_URL, 'v1/emoji')
|
||||
const url = new URL('v1/emoji', SEASONED_URL)
|
||||
|
||||
return axios.get(url)
|
||||
return fetch(url.href)
|
||||
.then(resp => resp.json())
|
||||
.catch(error => { console.log('api error getting emoji'); throw error })
|
||||
}
|
||||
|
||||
@@ -252,4 +465,26 @@ const elasticSearchMoviesAndShows = (query) => {
|
||||
|
||||
|
||||
|
||||
export { getMovie, getShow, getTmdbListByPath, searchTmdb, searchTorrents, addMagnet, request, getRequestStatus, plexAuthenticate, getEmoji, elasticSearchMoviesAndShows }
|
||||
export {
|
||||
getMovie,
|
||||
getShow,
|
||||
getPerson,
|
||||
getTmdbMovieListByName,
|
||||
searchTmdb,
|
||||
getUserRequests,
|
||||
getRequests,
|
||||
searchTorrents,
|
||||
addMagnet,
|
||||
request,
|
||||
watchLink,
|
||||
getRequestStatus,
|
||||
linkPlexAccount,
|
||||
unlinkPlexAccount,
|
||||
register,
|
||||
login,
|
||||
getSettings,
|
||||
updateSettings,
|
||||
fetchChart,
|
||||
getEmoji,
|
||||
elasticSearchMoviesAndShows
|
||||
}
|
||||
|
||||
@@ -1,72 +1,75 @@
|
||||
<template>
|
||||
<section class="not-found">
|
||||
<div class="not-found__content">
|
||||
<h2 class="not-found__title">Page Not Found</h2>
|
||||
</div>
|
||||
</section>
|
||||
<div>
|
||||
<section class="not-found">
|
||||
<h1 class="not-found__title">Page Not Found</h1>
|
||||
</section>
|
||||
<seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '../storage.js'
|
||||
import store from '@/store'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
|
||||
export default {
|
||||
created(){
|
||||
document.title = 'Page Not Found' + storage.pageTitlePostfix;
|
||||
components: { SeasonedButton },
|
||||
methods: {
|
||||
goBack() {
|
||||
this.$router.go(-1)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.$popup.isOpen == true)
|
||||
this.$popup.close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
.not-found{
|
||||
width: 100%;
|
||||
height: calc(100vh - 100px);
|
||||
|
||||
.button {
|
||||
font-size: 1.2rem;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: calc(50% + 46px);
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
@include mobile {
|
||||
top: 60%;
|
||||
left: 50%;
|
||||
font-size: 1rem;
|
||||
width: content;
|
||||
}
|
||||
}
|
||||
|
||||
.not-found {
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-size));
|
||||
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
|
||||
background-size: cover;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@include tablet-min{
|
||||
height: calc(100vh - 75px);
|
||||
}
|
||||
&:before{
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($c-light, 0.7);
|
||||
}
|
||||
&-shortList{
|
||||
width: 100%;
|
||||
}
|
||||
&__content{
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
text-align: center;
|
||||
@include tablet-min{
|
||||
padding: 20px 0 0 0;
|
||||
}
|
||||
&-shortList {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: calc(100vh - var(--header-size));
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
background: $background-40;
|
||||
}
|
||||
&__title {
|
||||
margin-top: 30vh;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
color: $text-color;
|
||||
position: relative;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
&__title{
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
color: $c-dark;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
@include tablet-min{
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
&__button{
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
316
src/components/ActivityPage.vue
Normal file
316
src/components/ActivityPage.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="wrapper" v-if="hasPlexUser">
|
||||
<h1>Your watch activity</h1>
|
||||
|
||||
<div class="filter">
|
||||
<h2>Filter</h2>
|
||||
|
||||
<div class="filter-item">
|
||||
<label class="desktop-only">Days:</label>
|
||||
<input class="dayinput"
|
||||
v-model="days"
|
||||
placeholder="number of days"
|
||||
type="number"
|
||||
pattern="[0-9]*"
|
||||
:style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/>
|
||||
<!-- <datalist id="days">
|
||||
<option v-for="index in 1500" :value="index" :key="index"></option>
|
||||
</datalist> -->
|
||||
</div>
|
||||
|
||||
<toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" />
|
||||
</div>
|
||||
|
||||
<div class="chart-section">
|
||||
<h3 class="chart-header">Activity per day:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="activityCanvas"></canvas>
|
||||
</div>
|
||||
|
||||
<h3 class="chart-header">Activity per day of week:</h3>
|
||||
<div class="chart">
|
||||
<canvas ref="playsByDayOfWeekCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1>Must be authenticated</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import ToggleButton from '@/components/ui/ToggleButton';
|
||||
import { fetchChart } from '@/api'
|
||||
|
||||
var Chart = require('chart.js');
|
||||
Chart.defaults.global.elements.point.radius = 0
|
||||
Chart.defaults.global.elements.point.hitRadius = 10
|
||||
Chart.defaults.global.elements.point.pointHoverRadius = 10
|
||||
Chart.defaults.global.elements.point.hoverBorderWidth = 4
|
||||
|
||||
export default {
|
||||
components: { ToggleButton },
|
||||
data() {
|
||||
return {
|
||||
days: 30,
|
||||
selectedChartDataType: 'plays',
|
||||
charts: [{
|
||||
name: 'Watch activity',
|
||||
ref: 'activityCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_day',
|
||||
graphType: 'line'
|
||||
}, {
|
||||
name: 'Plays by day of week',
|
||||
ref: 'playsByDayOfWeekCanvas',
|
||||
data: null,
|
||||
urlPath: '/plays_by_dayofweek',
|
||||
graphType: 'bar'
|
||||
}],
|
||||
chartData: [{
|
||||
type: 'plays',
|
||||
tooltipLabel: 'Play count',
|
||||
},{
|
||||
type: 'duration',
|
||||
tooltipLabel: 'Watched duration',
|
||||
valueConvertFunction: this.convertSecondsToHumanReadable
|
||||
}],
|
||||
gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser() {
|
||||
return store.getters['userModule/plex_userid'] != null ? true : false
|
||||
},
|
||||
chartTypes() {
|
||||
return this.chartData.map(chart => chart.type)
|
||||
},
|
||||
selectedChartType() {
|
||||
return this.chartData.filter(data => data.type == this.selectedChartDataType)[0]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hasPlexUser(newValue, oldValue) {
|
||||
if (newValue != oldValue && newValue == true) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
days(newValue) {
|
||||
if (newValue !== '') {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
selectedChartDataType(selectedChartDataType) {
|
||||
this.fetchChartData(this.charts)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (typeof(this.days) == 'number') {
|
||||
this.days = this.days.toString()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchChartData(charts) {
|
||||
if (this.hasPlexUser == false) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let chart of charts) {
|
||||
|
||||
|
||||
|
||||
fetchChart(chart.urlPath, this.days, this.selectedChartType.type)
|
||||
.then(data => {
|
||||
this.series = data.data.series.filter(group => group.name === 'TV')[0].data; // plays pr date in groups (movie/tv/music)
|
||||
this.categories = data.data.categories; // dates
|
||||
|
||||
const x_labels = data.data.categories.map(date => {
|
||||
if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
||||
const [year, month, day] = date.split('-')
|
||||
return `${day}.${month}`
|
||||
}
|
||||
|
||||
return date
|
||||
})
|
||||
let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data
|
||||
let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data
|
||||
|
||||
const datasets = [{
|
||||
label: `Movies watch last ${ this.days } days`,
|
||||
data: y_activityMovies,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
},
|
||||
{
|
||||
label: `Shows watch last ${ this.days } days`,
|
||||
data: y_activityTV,
|
||||
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||
borderColor: 'rgba(255, 159, 64, 1)',
|
||||
borderWidth: 1
|
||||
}
|
||||
]
|
||||
|
||||
if (chart.data == null) {
|
||||
this.generateChart(chart, x_labels, datasets)
|
||||
} else {
|
||||
chart.data.clear();
|
||||
chart.data.data.labels = x_labels;
|
||||
chart.data.data.datasets = datasets;
|
||||
chart.data.update();
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
generateChart(chart, labels, datasets) {
|
||||
const chartInstance = new Chart(this.$refs[chart.ref], {
|
||||
type: chart.graphType,
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: datasets
|
||||
},
|
||||
options: {
|
||||
// hitRadius: 8,
|
||||
maintainAspectRatio: false,
|
||||
tooltips: {
|
||||
callbacks: {
|
||||
title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`,
|
||||
label: (tooltipItem, data) => {
|
||||
let label = data.datasets[tooltipItem.datasetIndex].label
|
||||
let value = tooltipItem.value;
|
||||
let text = 'Duration watched'
|
||||
|
||||
const context = label.split(' ')[0]
|
||||
if (context) {
|
||||
text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}`
|
||||
}
|
||||
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
value = this.selectedChartType.valueConvertFunction(tooltipItem.value)
|
||||
}
|
||||
|
||||
return ` ${text}: ${value}`
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
yAxes: [{
|
||||
gridLines: {
|
||||
color: this.gridColor
|
||||
},
|
||||
stacked: chart.graphType === 'bar',
|
||||
ticks: {
|
||||
// suggestedMax: 10000,
|
||||
callback: (value, index, values) => {
|
||||
if (this.selectedChartType.valueConvertFunction) {
|
||||
return this.selectedChartType.valueConvertFunction(value, values)
|
||||
}
|
||||
return value
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
}],
|
||||
xAxes: [{
|
||||
stacked: chart.graphType === 'bar',
|
||||
gridLines: {
|
||||
display: false,
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
chart.data = chartInstance;
|
||||
},
|
||||
convertSecondsToHumanReadable(value, values=null) {
|
||||
const highestValue = values ? values[0] : value;
|
||||
|
||||
// minutes
|
||||
if (highestValue < 3600) {
|
||||
const minutes = Math.floor(value / 60);
|
||||
|
||||
value = `${minutes} m`
|
||||
}
|
||||
// hours and minutes
|
||||
else if (highestValue > 3600 && highestValue < 86400) {
|
||||
const hours = Math.floor(value / 3600);
|
||||
const minutes = Math.floor(value % 3600 / 60);
|
||||
|
||||
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`
|
||||
}
|
||||
// days and hours
|
||||
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||
const days = Math.floor(value / 86400);
|
||||
const hours = Math.floor(value % 86400 / 3600);
|
||||
|
||||
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`
|
||||
}
|
||||
// years and days
|
||||
else if (highestValue > 31557600) {
|
||||
const years = Math.floor(value / 31557600);
|
||||
const days = Math.floor(value % 31557600 / 86400);
|
||||
|
||||
value = years != 0 ? `${years} y ${days} d` : `${days} d`
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.wrapper {
|
||||
padding: 2rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 0 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
width: 100%;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
|
||||
&-item:not(:first-of-type) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.dayinput {
|
||||
font-size: 1.2rem;
|
||||
max-width: 3rem;
|
||||
background-color: $background-ui;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
height: 35vh;
|
||||
width: 90vw;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -2,27 +2,86 @@
|
||||
<section>
|
||||
<LandingBanner />
|
||||
|
||||
<movies-list v-for="item in homepageLists" :propList="item" :shortList="true"></movies-list>
|
||||
<div v-for="list in lists">
|
||||
<list-header :title="list.title" :link="'/list/' + list.route" />
|
||||
|
||||
<results-list :results="list.data" :shortList="true" />
|
||||
<loader v-if="!list.data.length" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '../storage.js'
|
||||
import LandingBanner from '@/components/LandingBanner.vue'
|
||||
import MoviesList from './MoviesList.vue'
|
||||
import LandingBanner from "@/components/LandingBanner";
|
||||
import ListHeader from "@/components/ListHeader";
|
||||
import ResultsList from "@/components/ResultsList";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
|
||||
import { getTmdbMovieListByName, getRequests } from "@/api";
|
||||
|
||||
export default {
|
||||
name: 'home',
|
||||
components: { LandingBanner, MoviesList },
|
||||
data(){
|
||||
name: "home",
|
||||
components: { LandingBanner, ResultsList, ListHeader, Loader },
|
||||
data() {
|
||||
return {
|
||||
homepageLists: storage.homepageLists,
|
||||
imageFile: 'dist/pulp-fiction.jpg'
|
||||
imageFile: "/pulp-fiction.jpg",
|
||||
requests: [],
|
||||
nowplaying: [],
|
||||
upcoming: [],
|
||||
popular: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lists() {
|
||||
return [
|
||||
{
|
||||
title: "Requests",
|
||||
route: "request",
|
||||
data: this.requests
|
||||
},
|
||||
{
|
||||
title: "Now playing",
|
||||
route: "now_playing",
|
||||
data: this.nowplaying
|
||||
},
|
||||
{
|
||||
title: "Upcoming",
|
||||
route: "upcoming",
|
||||
data: this.upcoming
|
||||
},
|
||||
{
|
||||
title: "Popular",
|
||||
route: "popular",
|
||||
data: this.popular
|
||||
}
|
||||
];
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'TMDb';
|
||||
storage.backTitle = document.title;
|
||||
methods: {
|
||||
fetchRequests() {
|
||||
getRequests().then(results => (this.requests = results.results));
|
||||
},
|
||||
fetchNowPlaying() {
|
||||
getTmdbMovieListByName("now_playing").then(
|
||||
results => (this.nowplaying = results.results)
|
||||
);
|
||||
},
|
||||
fetchUpcoming() {
|
||||
getTmdbMovieListByName("upcoming").then(
|
||||
results => (this.upcoming = results.results)
|
||||
);
|
||||
},
|
||||
fetchPopular() {
|
||||
getTmdbMovieListByName("popular").then(
|
||||
results => (this.popular = results.results)
|
||||
);
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.fetchRequests();
|
||||
this.fetchNowPlaying();
|
||||
this.fetchUpcoming();
|
||||
this.fetchPopular();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }">
|
||||
<div class="container">
|
||||
<h1 class="title">Request new movies or tv shows for plex</h1>
|
||||
<strong class="subtitle">Made with Vue.js</strong>
|
||||
<h1 class="title">Request movies or tv shows</h1>
|
||||
<strong class="subtitle"
|
||||
>Create a profile to track and view requests</strong
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -17,16 +19,15 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imageFile: 'dist/pulp-fiction.jpg'
|
||||
}
|
||||
imageFile: "/pulp-fiction.jpg"
|
||||
};
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.image && this.image.length > 0) {
|
||||
this.imageFile = this.image
|
||||
this.imageFile = this.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -43,10 +44,11 @@ header {
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
position: relative;
|
||||
background-color: $c-dark;
|
||||
|
||||
@include tablet-min {
|
||||
height: 284px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -54,23 +56,26 @@ header {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($c-light, 0.7);
|
||||
background-color: $background-70;
|
||||
transition: background-color 0.5s ease;
|
||||
}
|
||||
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
|
||||
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 22px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
margin: 0;
|
||||
@include tablet-min{
|
||||
font-size: 28px;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,35 +83,12 @@ header {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: $c-dark;
|
||||
color: $text-color-70;
|
||||
margin: 5px 0;
|
||||
@include tablet-min{
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
color: $c-dark;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.5s ease;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
&-icon {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 2px;
|
||||
width: 16px;
|
||||
height: 15px;
|
||||
fill: $c-dark;
|
||||
@include tablet-min {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
117
src/components/ListHeader.vue
Normal file
117
src/components/ListHeader.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<header :class="{ sticky: sticky }">
|
||||
<h2>{{ title }}</h2>
|
||||
|
||||
<div v-if="info instanceof Array" class="flex flex-direction-column">
|
||||
<span v-for="item in info" class="info">{{ item }}</span>
|
||||
</div>
|
||||
<span v-else class="info">{{ info }}</span>
|
||||
<router-link
|
||||
v-if="link"
|
||||
:to="link"
|
||||
class="view-more"
|
||||
:aria-label="`View all ${title}`"
|
||||
>
|
||||
View All
|
||||
</router-link>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sticky: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
info: {
|
||||
type: [String, Array],
|
||||
required: false
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
min-height: 45px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
|
||||
@include tablet-min {
|
||||
min-height: 65px;
|
||||
}
|
||||
|
||||
&.sticky {
|
||||
background-color: $background-color;
|
||||
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
|
||||
@include tablet-min {
|
||||
top: $header-size;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
text-transform: capitalize;
|
||||
line-height: 1.4rem;
|
||||
margin: 0;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-color-70;
|
||||
text-decoration: none;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
content: " →";
|
||||
}
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.5px;
|
||||
color: $text-color;
|
||||
text-decoration: none;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
@include desktop-lg-min {
|
||||
padding-left: 1.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
123
src/components/ListPage.vue
Normal file
123
src/components/ListPage.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<list-header :title="listTitle" :info="info" :sticky="true" />
|
||||
|
||||
<results-list :results="results" v-if="results" />
|
||||
|
||||
<loader v-if="!results.length" />
|
||||
|
||||
<div v-if="page < totalPages" class="fullwidth-button">
|
||||
<seasoned-button @click="loadMore">load more</seasoned-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ListHeader from "@/components/ListHeader";
|
||||
import ResultsList from "@/components/ResultsList";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
import { getTmdbMovieListByName, getRequests } from "@/api";
|
||||
import store from "@/store";
|
||||
|
||||
export default {
|
||||
components: { ListHeader, ResultsList, SeasonedButton, Loader },
|
||||
data() {
|
||||
return {
|
||||
legalTmdbLists: ["now_playing", "upcoming", "popular"],
|
||||
results: [],
|
||||
page: 1,
|
||||
totalPages: 0,
|
||||
totalResults: 0,
|
||||
loading: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
listTitle() {
|
||||
if (this.results.length === 0) return "";
|
||||
|
||||
const routeListName = this.$route.params.name;
|
||||
console.log("routelistname", routeListName);
|
||||
return routeListName.includes("_")
|
||||
? routeListName.split("_").join(" ")
|
||||
: routeListName;
|
||||
},
|
||||
info() {
|
||||
if (this.results.length === 0) return [null, null];
|
||||
return [this.pageCount, this.resultCount];
|
||||
},
|
||||
resultCount() {
|
||||
const loadedResults = this.results.length;
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞";
|
||||
return `${loadedResults} of ${totalResults} results`;
|
||||
},
|
||||
pageCount() {
|
||||
return `Page ${this.page} of ${this.totalPages}`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadMore() {
|
||||
console.log(this.$route);
|
||||
this.loading = true;
|
||||
this.page++;
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"search",
|
||||
`/#/${this.$route.fullPath}?page=${this.page}`
|
||||
);
|
||||
this.init();
|
||||
},
|
||||
init() {
|
||||
const routeListName = this.$route.params.name;
|
||||
|
||||
if (routeListName === "request") {
|
||||
getRequests(this.page).then(results => {
|
||||
this.results = this.results.concat(...results.results);
|
||||
this.page = results.page;
|
||||
this.totalPages = results.total_pages;
|
||||
this.totalResults = results.total_results;
|
||||
});
|
||||
} else if (this.legalTmdbLists.includes(routeListName)) {
|
||||
getTmdbMovieListByName(routeListName, this.page).then(results => {
|
||||
this.results = this.results.concat(...results.results);
|
||||
this.page = results.page;
|
||||
this.totalPages = results.total_pages;
|
||||
this.totalResults = results.total_results;
|
||||
});
|
||||
} else {
|
||||
// TODO handle if list is not found
|
||||
console.log("404 this is not a tmdb list");
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (this.results.length === 0) this.init();
|
||||
|
||||
store.dispatch(
|
||||
"documentTitle/updateTitle",
|
||||
`${this.$router.history.current.name} ${this.$route.params.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
@include mobile-only {
|
||||
.page-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fullwidth-button {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,134 +1,192 @@
|
||||
<template>
|
||||
<section class="movie">
|
||||
|
||||
<!-- HEADER w/ POSTER -->
|
||||
<header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }">
|
||||
<div class="movie__wrap movie__wrap--header">
|
||||
<figure class="movie__poster">
|
||||
<img v-if="movie && poster === null"
|
||||
class="movies-item__img is-loaded"
|
||||
alt="movie poster image"
|
||||
src="~assets/no-image.png">
|
||||
<img v-else-if="poster === undefined"
|
||||
class="movies-item__img grey"
|
||||
alt="movie poster image">
|
||||
<!-- src="~assets/placeholder.png"> -->
|
||||
<img v-else
|
||||
class="movies-item__img is-loaded"
|
||||
alt="movie poster image"
|
||||
:src="ASSET_URL + ASSET_SIZES[0] + poster">
|
||||
</figure>
|
||||
<header
|
||||
ref="header"
|
||||
:class="compact ? 'compact' : ''"
|
||||
@click="compact = !compact"
|
||||
>
|
||||
<figure class="movie__poster">
|
||||
<img
|
||||
class="movie-item__img is-loaded"
|
||||
ref="poster-image"
|
||||
src="~assets/placeholder.png"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<div class="movie__title">
|
||||
<h1>{{ title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="movie__title" v-if="movie">{{ movie.title }}</h1>
|
||||
<loading-placeholder v-else :count="1" />
|
||||
</header>
|
||||
|
||||
<!-- Siderbar and movie info -->
|
||||
<div class="movie__main">
|
||||
<div class="movie__wrap movie__wrap--main">
|
||||
|
||||
<!-- SIDEBAR ACTIONS -->
|
||||
<div class="movie__actions" v-if="movie">
|
||||
|
||||
<sidebar-action
|
||||
:text="'Not yet in plex'" :iconRef="'#iconNot_exsits'"
|
||||
:textActive="'Already in plex 🎉'" :iconRefActive="'#iconExists'"
|
||||
:active="matched"></sidebar-action>
|
||||
<sidebar-action
|
||||
<sidebar-list-element
|
||||
:iconRef="'#iconNot_exsits'"
|
||||
:active="matched"
|
||||
:iconRefActive="'#iconExists'"
|
||||
:textActive="'Already in plex 🎉'"
|
||||
>
|
||||
Not yet in plex
|
||||
</sidebar-list-element>
|
||||
<sidebar-list-element
|
||||
@click="sendRequest"
|
||||
:text="'Request to be downloaded?'" :iconRef="'#iconSent'"
|
||||
:iconRef="'#iconSent'"
|
||||
:active="requested"
|
||||
:textActive="'Requested to be downloaded'"
|
||||
:active="requested"></sidebar-action>
|
||||
<sidebar-action
|
||||
v-if="admin" @click="showTorrents=!showTorrents"
|
||||
:text="'Search for torrents'" :iconRef="'#icon_torrents'"
|
||||
:active="showTorrents"></sidebar-action>
|
||||
<sidebar-action
|
||||
@click="openTmdb()"
|
||||
:iconRef="'#icon_info'" :text="'See more info'"></sidebar-action>
|
||||
>
|
||||
Request to be downloaded?
|
||||
</sidebar-list-element>
|
||||
|
||||
<sidebar-list-element
|
||||
v-if="isPlexAuthenticated && matched"
|
||||
@click="openInPlex"
|
||||
:iconString="'⏯ '"
|
||||
>
|
||||
Watch in plex now!
|
||||
</sidebar-list-element>
|
||||
|
||||
<sidebar-list-element
|
||||
v-if="admin"
|
||||
@click="showTorrents = !showTorrents"
|
||||
:iconRef="'#icon_torrents'"
|
||||
:active="showTorrents"
|
||||
:supplementaryText="numberOfTorrentResults"
|
||||
>
|
||||
Search for torrents
|
||||
</sidebar-list-element>
|
||||
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
|
||||
See more info
|
||||
</sidebar-list-element>
|
||||
</div>
|
||||
|
||||
<!-- Loading placeholder -->
|
||||
<div class="movie__actions text-input__loading" v-else>
|
||||
<div class="movie__actions-link" v-for="_ in admin ? Array(4) : Array(3)">
|
||||
<div class="movie__actions-text text-input__loading--line" style="margin:9px; margin-left: -3px;"></div>
|
||||
<div
|
||||
class="movie__actions-link"
|
||||
v-for="_ in admin ? Array(4) : Array(3)"
|
||||
>
|
||||
<div
|
||||
class="movie__actions-text text-input__loading--line"
|
||||
style="margin: 9px; margin-left: -3px"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- MOVIE INFO -->
|
||||
<div class="movie__info">
|
||||
<div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
|
||||
|
||||
<!-- Loading placeholder -->
|
||||
<div
|
||||
class="movie__description noselect"
|
||||
@click="truncatedDescription = !truncatedDescription"
|
||||
v-if="!loading"
|
||||
>
|
||||
<span :class="truncatedDescription ? 'truncated' : null">{{
|
||||
movie.overview
|
||||
}}</span>
|
||||
<button class="truncate-toggle"><i>⬆</i></button>
|
||||
</div>
|
||||
<div v-else class="movie__description">
|
||||
<loading-placeholder :count="12" />
|
||||
<loading-placeholder :count="5" />
|
||||
</div>
|
||||
|
||||
<div class="movie__details" v-if="movie">
|
||||
<div v-if="movie.year" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Release Date</h2>
|
||||
<div class="movie__details-text">{{ movie.year }}</div>
|
||||
<div v-if="movie.year">
|
||||
<h2 class="title">Release Date</h2>
|
||||
<div class="text">{{ movie.year }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.rank" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Rating</h2>
|
||||
<div class="movie__details-text">{{ movie.rank }}</div>
|
||||
<div v-if="movie.rating">
|
||||
<h2 class="title">Rating</h2>
|
||||
<div class="text">{{ movie.rating }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Seasons</h2>
|
||||
<div class="movie__details-text">{{ movie.seasons }}</div>
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Seasons</h2>
|
||||
<div class="text">{{ movie.seasons }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.genres" class="movie__details-block">
|
||||
<h2 class="movie__details-title">Genres</h2>
|
||||
<div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div>
|
||||
<div v-if="movie.genres">
|
||||
<h2 class="title">Genres</h2>
|
||||
<div class="text">{{ movie.genres.join(", ") }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Production status</h2>
|
||||
<div class="text">{{ movie.production_status }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="movie.type == 'show'">
|
||||
<h2 class="title">Runtime</h2>
|
||||
<div class="text">{{ movie.runtime[0] }} minutes</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TODO: change this classname, this is general -->
|
||||
|
||||
<div class="movie__admin" v-if="movie && movie.credits">
|
||||
<h2 class="movie__details-title">Cast</h2>
|
||||
<div style="display: flex; flex-wrap: wrap;">
|
||||
<person v-for="cast in movie.credits.cast" :info="cast"
|
||||
style="flex-basis: 0;"></person>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap">
|
||||
<person
|
||||
v-for="cast in movie.credits.cast"
|
||||
:info="cast"
|
||||
style="flex-basis: 0"
|
||||
></person>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- TORRENT LIST -->
|
||||
<TorrentList v-if="movie" :show="showTorrents" :query="title" :tmdb_id="id"
|
||||
:admin="admin"></TorrentList>
|
||||
<TorrentList
|
||||
v-if="movie"
|
||||
:show="showTorrents"
|
||||
:query="title"
|
||||
:tmdb_id="id"
|
||||
:admin="admin"
|
||||
></TorrentList>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import img from '@/directives/v-image.js'
|
||||
import TorrentList from './TorrentList.vue'
|
||||
import Person from './Person.vue'
|
||||
import SidebarAction from './movie/SidebarAction.vue'
|
||||
import storage from "@/storage";
|
||||
import img from "@/directives/v-image";
|
||||
import TorrentList from "./TorrentList";
|
||||
import Person from "./Person";
|
||||
import SidebarListElement from "./ui/sidebarListElem";
|
||||
import store from "@/store";
|
||||
import LoadingPlaceholder from "./ui/LoadingPlaceholder";
|
||||
|
||||
import LoadingPlaceholder from './ui/LoadingPlaceholder.vue'
|
||||
|
||||
import { getMovie, getShow, request, getRequestStatus } from '@/api.js'
|
||||
import {
|
||||
getMovie,
|
||||
getPerson,
|
||||
getShow,
|
||||
request,
|
||||
getRequestStatus,
|
||||
watchLink
|
||||
} from "@/api";
|
||||
|
||||
export default {
|
||||
props: ['id', 'type'],
|
||||
components: { TorrentList, Person, LoadingPlaceholder, SidebarAction },
|
||||
// props: ['id', 'type'],
|
||||
props: {
|
||||
id: {
|
||||
required: true,
|
||||
type: Number
|
||||
},
|
||||
type: {
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
|
||||
directives: { img: img }, // TODO decide to remove or use
|
||||
data(){
|
||||
return{
|
||||
ASSET_URL: 'https://image.tmdb.org/t/p/',
|
||||
ASSET_SIZES: ['w500', 'w780', 'original'],
|
||||
data() {
|
||||
return {
|
||||
ASSET_URL: "https://image.tmdb.org/t/p/",
|
||||
ASSET_SIZES: ["w500", "w780", "original"],
|
||||
movie: undefined,
|
||||
title: undefined,
|
||||
poster: undefined,
|
||||
@@ -136,82 +194,201 @@ export default {
|
||||
matched: false,
|
||||
userLoggedIn: storage.sessionId ? true : false,
|
||||
requested: false,
|
||||
admin: localStorage.getItem('admin'),
|
||||
showTorrents: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseResponse(resp) {
|
||||
let movie = resp.data;
|
||||
this.movie = { ...movie }
|
||||
this.title = movie.title
|
||||
this.poster = movie.poster
|
||||
this.backdrop = movie.backdrop
|
||||
this.matched = movie.existsInPlex
|
||||
this.checkIfRequested(movie)
|
||||
.then(status => this.requested = status)
|
||||
|
||||
document.title = movie.title + storage.pageTitlePostfix
|
||||
},
|
||||
async checkIfRequested(movie) {
|
||||
return await getRequestStatus(movie.id, movie.type)
|
||||
},
|
||||
nestedDataToString(data) {
|
||||
let nestedArray = []
|
||||
data.forEach(item => nestedArray.push(item));
|
||||
return nestedArray.join(', ');
|
||||
},
|
||||
sendRequest(){
|
||||
request(this.id, this.type, storage.token)
|
||||
.then(resp => {
|
||||
if (resp.success) {
|
||||
this.requested = true
|
||||
}
|
||||
})
|
||||
},
|
||||
openTmdb(){
|
||||
const tmdbType = this.type === 'show' ? 'tv' : this.type
|
||||
window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
|
||||
},
|
||||
admin: localStorage.getItem("admin") == "true" ? true : false,
|
||||
showTorrents: false,
|
||||
compact: false,
|
||||
loading: true,
|
||||
truncatedDescription: true
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
id: function(val){
|
||||
if (this.type === 'movie') {
|
||||
id: function (val) {
|
||||
if (this.type === "movie") {
|
||||
this.fetchMovie(val);
|
||||
} else {
|
||||
this.fetchShow(val)
|
||||
this.fetchShow(val);
|
||||
}
|
||||
},
|
||||
backdrop: function (backdrop) {
|
||||
if (backdrop != null) {
|
||||
const style = {
|
||||
backgroundImage:
|
||||
"url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")"
|
||||
};
|
||||
|
||||
Object.assign(this.$refs.header.style, style);
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.title = this.prevDocumentTitle
|
||||
},
|
||||
created(){
|
||||
this.prevDocumentTitle = document.title
|
||||
|
||||
if (this.type === 'movie') {
|
||||
getMovie(this.id)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: '404' });
|
||||
})
|
||||
} else {
|
||||
getShow(this.id)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: '404' });
|
||||
})
|
||||
computed: {
|
||||
numberOfTorrentResults: () => {
|
||||
let numTorrents = store.getters["torrentModule/resultCount"];
|
||||
return numTorrents !== null ? numTorrents + " results" : null;
|
||||
},
|
||||
isPlexAuthenticated: () => {
|
||||
return store.getters["userModule/isPlexAuthenticated"];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseResponse(movie) {
|
||||
this.loading = false;
|
||||
this.movie = { ...movie };
|
||||
this.title = movie.title;
|
||||
this.poster = movie.poster;
|
||||
this.backdrop = movie.backdrop;
|
||||
this.matched = movie.exists_in_plex || false;
|
||||
this.checkIfRequested(movie).then(status => (this.requested = status));
|
||||
|
||||
console.log('admin: ', this.admin)
|
||||
store.dispatch("documentTitle/updateTitle", movie.title);
|
||||
this.setPosterSrc();
|
||||
},
|
||||
async checkIfRequested(movie) {
|
||||
return await getRequestStatus(movie.id, movie.type);
|
||||
},
|
||||
setPosterSrc() {
|
||||
const poster = this.$refs["poster-image"];
|
||||
if (this.poster == null) {
|
||||
poster.src = "/no-image.png";
|
||||
return;
|
||||
}
|
||||
|
||||
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`;
|
||||
},
|
||||
sendRequest() {
|
||||
request(this.id, this.type, storage.token).then(resp => {
|
||||
if (resp.success) {
|
||||
this.requested = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
openInPlex() {
|
||||
watchLink(this.title, this.movie.year, storage.token).then(
|
||||
watchLink => (window.location = watchLink)
|
||||
);
|
||||
},
|
||||
openTmdb() {
|
||||
const tmdbType = this.type === "show" ? "tv" : this.type;
|
||||
window.location.href =
|
||||
"https://www.themoviedb.org/" + tmdbType + "/" + this.id;
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.prevDocumentTitle = store.getters["documentTitle/title"];
|
||||
|
||||
if (this.type === "movie") {
|
||||
getMovie(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
} else if (this.type == "person") {
|
||||
getPerson(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
} else {
|
||||
getShow(this.id, true)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
this.$router.push({ name: "404" });
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/loading-placeholder";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/main";
|
||||
|
||||
header {
|
||||
$duration: 0.2s;
|
||||
height: 250px;
|
||||
transform: scaleY(1);
|
||||
transition: height $duration ease;
|
||||
transform-origin: top;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: $background-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@include tablet-min {
|
||||
height: 350px;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $background-dark-85;
|
||||
}
|
||||
@include mobile {
|
||||
&.compact {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.movie__poster {
|
||||
display: none;
|
||||
|
||||
@include desktop {
|
||||
background: $background-color;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: calc(45% - 40px);
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.truncate-toggle {
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: $text-color;
|
||||
|
||||
> i {
|
||||
font-style: unset;
|
||||
font-size: 0.7rem;
|
||||
transition: 0.3s ease all;
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
}
|
||||
&::before {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
&::after {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
&__wrap {
|
||||
@@ -224,45 +401,12 @@ export default {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__header {
|
||||
height: 250px;
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50% 50%;
|
||||
background-color: $c-dark;
|
||||
@include tablet-min {
|
||||
height: 350px;
|
||||
}
|
||||
&:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($c-dark, 0.85);
|
||||
}
|
||||
}
|
||||
|
||||
&__poster {
|
||||
display: none;
|
||||
@include tablet-min {
|
||||
background: $c-white;
|
||||
height: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: calc(45% - 40px);
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
|
||||
background-color: $background-color;
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +426,7 @@ export default {
|
||||
&__title {
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
color: $c-green;
|
||||
color: $green;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
@include tablet-min {
|
||||
@@ -299,16 +443,8 @@ export default {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
color: rgba($c-white, 0.7);
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
&__main {
|
||||
background: $c-light;
|
||||
min-height: calc(100vh - 250px);
|
||||
@include tablet-min {
|
||||
min-height: 0;
|
||||
@@ -316,141 +452,105 @@ export default {
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
&__actions {
|
||||
text-align: center;
|
||||
// min-height: 394px;
|
||||
width: 100%;
|
||||
order: 2;
|
||||
padding: 20px;
|
||||
border-top: 1px solid rgba($c-dark, 0.05);
|
||||
@include tablet-min {
|
||||
order: 1;
|
||||
width: 45%;
|
||||
padding: 185px 0 40px 40px;
|
||||
border-top: 0;
|
||||
}
|
||||
&-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: rgba($c-dark, 0.5);
|
||||
transition: color 0.5s ease;
|
||||
font-size: 11px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid rgba($c-dark, 0.05);
|
||||
&:hover {
|
||||
color: rgba($c-dark, 0.75);
|
||||
}
|
||||
&.active {
|
||||
color: $c-dark;
|
||||
}
|
||||
&.pending {
|
||||
color: #f8bd2d;
|
||||
}
|
||||
}
|
||||
&-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0 10px 0 0;
|
||||
fill: rgba($c-dark, 0.5);
|
||||
transition: fill 0.5s ease, transform 0.5s ease;
|
||||
&.waiting {
|
||||
transform: scale(0.8, 0.8);
|
||||
}
|
||||
&.pending {
|
||||
fill: #f8bd2d;
|
||||
}
|
||||
}
|
||||
&-link:hover &-icon {
|
||||
fill: rgba($c-dark, 0.75);
|
||||
cursor: pointer;
|
||||
}
|
||||
&-link.active &-icon {
|
||||
fill: $c-green;
|
||||
}
|
||||
&-text {
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
cursor: pointer;
|
||||
margin:4.4px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
}
|
||||
&__info {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
&__actions {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
order: 2;
|
||||
padding: 20px;
|
||||
border-top: 1px solid $text-color-5;
|
||||
@include tablet-min {
|
||||
order: 1;
|
||||
@include tablet-min {
|
||||
order: 2;
|
||||
padding: 40px;
|
||||
width: 55%;
|
||||
margin-left: 45%;
|
||||
width: 45%;
|
||||
padding: 185px 0 40px 40px;
|
||||
border-top: 0;
|
||||
}
|
||||
}
|
||||
&__info {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
order: 1;
|
||||
@include tablet-min {
|
||||
order: 2;
|
||||
padding: 40px;
|
||||
width: 55%;
|
||||
margin-left: 45%;
|
||||
}
|
||||
}
|
||||
&__info {
|
||||
margin-left: 0;
|
||||
}
|
||||
&__description {
|
||||
font-weight: 300;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
margin-bottom: 20px;
|
||||
|
||||
& .truncated {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
& + .truncate-toggle > i {
|
||||
transform: rotateY(0deg) rotateZ(180deg);
|
||||
}
|
||||
}
|
||||
&__actions + &__info {
|
||||
margin-left: 0;
|
||||
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&__description {
|
||||
font-weight: 300;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
&__details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div {
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
font-size: 14px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
&__details {
|
||||
&-block {
|
||||
float: left;
|
||||
}
|
||||
&-block:not(:last-child) {
|
||||
margin-bottom: 20px;
|
||||
margin-right: 20px;
|
||||
@include tablet-min {
|
||||
margin-bottom: 30px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
& .title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $c-green;
|
||||
color: $green;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
&-text {
|
||||
& .text {
|
||||
font-weight: 300;
|
||||
font-size: 14px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
&__admin {
|
||||
}
|
||||
&__admin {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
order: 2;
|
||||
@include tablet-min {
|
||||
order: 3;
|
||||
padding: 40px;
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
order: 2;
|
||||
@include tablet-min {
|
||||
order: 3;
|
||||
padding: 40px;
|
||||
padding-top: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
&-title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: $c-green;
|
||||
padding-bottom: 20px;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
padding-bottom: 20px;
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Movie from './Movie.vue';
|
||||
import Movie from './Movie';
|
||||
export default {
|
||||
components: { Movie }
|
||||
}
|
||||
|
||||
@@ -9,19 +9,36 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Movie from './Movie.vue';
|
||||
import Movie from './Movie';
|
||||
|
||||
export default {
|
||||
props: ['id', 'type'],
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: { Movie },
|
||||
created(){
|
||||
let that = this
|
||||
window.addEventListener('keyup', function(e){
|
||||
if (e.keyCode == 27) {
|
||||
that.$popup.close()
|
||||
methods: {
|
||||
checkEventForEscapeKey(event) {
|
||||
if (event.keyCode == 27) {
|
||||
this.$popup.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created(){
|
||||
window.addEventListener('keyup', this.checkEventForEscapeKey)
|
||||
document.getElementsByTagName("body")[0].classList += " no-scroll";
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keyup', this.checkEventForEscapeKey)
|
||||
document.getElementsByTagName("body")[0].classList.remove("no-scroll");
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,7 +53,7 @@ export default {
|
||||
z-index: 20;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: rgba($c-dark, 0.93);
|
||||
background: rgba($dark, 0.93);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow: auto;
|
||||
&__box{
|
||||
@@ -44,7 +61,7 @@ export default {
|
||||
max-width: 768px;
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
background: $c-dark;
|
||||
background: $background-color-secondary;
|
||||
padding-bottom: 50px;
|
||||
@include tablet-min{
|
||||
padding-bottom: 0;
|
||||
@@ -71,7 +88,7 @@ export default {
|
||||
left: 10px;
|
||||
width: 20px;
|
||||
height: 2px;
|
||||
background: $c-white;
|
||||
background: $white;
|
||||
}
|
||||
&:before{
|
||||
transform: rotate(45deg);
|
||||
@@ -80,7 +97,7 @@ export default {
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
&:hover{
|
||||
background: $c-green;
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,328 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class='movies-list' v-if="!error">
|
||||
<header class='list-header'>
|
||||
<h2 class='header__title'>{{ listTitle }}</h2>
|
||||
|
||||
<router-link class='header__view-more'
|
||||
:to="'/list/' + list.route"
|
||||
v-if='shortList'>
|
||||
View All</router-link>
|
||||
|
||||
<div v-else style="line-height: 0;">
|
||||
<span class='header__result-count' v-if="totalResults">{{ resultCount }} results</span>
|
||||
<loading-placeholder v-else :count="1" lineClass='short nomargin'></loading-placeholder>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- <ul class="filter">
|
||||
<li class="filter-item" v-for="(item, index) in results" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item.title }}</li>
|
||||
</ul> -->
|
||||
|
||||
<ul class='results'>
|
||||
<movies-list-item v-for='movie in results' :movie="movie" :shortList="shortList"></movies-list-item>
|
||||
</ul>
|
||||
|
||||
<loader v-if="loader" />
|
||||
|
||||
<div class='end-section' v-if="!shortList">
|
||||
<seasoned-button v-if="currentPage < totalPages" @click="loadMore">load more</seasoned-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else style="display: flex; height: 50vh; width: 100%; justify-content: center; align-items: center;">
|
||||
|
||||
<h1 v-if="error">{{ error }}</h1>
|
||||
<h1 v-else>Unable to load list: {{ listTitle }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import MoviesListItem from '@/components/MoviesListItem.vue'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import LoadingPlaceholder from '@/components/ui/LoadingPlaceholder.vue'
|
||||
import Loader from '@/components/ui/Loader.vue'
|
||||
import { searchTmdb, getTmdbListByPath } from '@/api.js'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
propList: Object
|
||||
},
|
||||
components: { MoviesListItem, SeasonedButton, LoadingPlaceholder, Loader },
|
||||
data() {
|
||||
return {
|
||||
listTitle: 'No listname found',
|
||||
results: [],
|
||||
currentPage: 1,
|
||||
totalResults: 0,
|
||||
totalPages: -1,
|
||||
fetchingResults: false,
|
||||
error: undefined,
|
||||
loader: false,
|
||||
|
||||
filters: {
|
||||
status: {
|
||||
elms: ['all', 'requested', 'downloading', 'downloaded'],
|
||||
selected: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultCount() {
|
||||
return this.totalResults.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.propList) {
|
||||
this.list = this.propList
|
||||
}
|
||||
|
||||
this.setPageFromUrlQuery()
|
||||
this.parseURI()
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
if (this.results.length === 0 && this.error === undefined) {
|
||||
this.loader = true
|
||||
}
|
||||
}, 200)
|
||||
},
|
||||
methods: {
|
||||
setPageFromUrlQuery() {
|
||||
if (this.$route.query.page)
|
||||
this.currentPage = this.$route.query.page
|
||||
console.log('url page param found', this.currentPage)
|
||||
},
|
||||
getListByName(name) {
|
||||
return storage.homepageLists.filter(list => list.route === name)[0]
|
||||
},
|
||||
parseURI() {
|
||||
const currentRouteName = this.$route.name
|
||||
|
||||
// route name is list - we are in a list view
|
||||
if (currentRouteName === 'list') {
|
||||
const nameParam = this.$route.params.name
|
||||
if (this.getListByName(nameParam)) {
|
||||
this.list = this.getListByName(nameParam)
|
||||
this.listTitle = this.list.title
|
||||
this.fetchListitems()
|
||||
} else {
|
||||
this.error = `Unable to load list: `
|
||||
}
|
||||
} // route name is search - we are searcing
|
||||
else if (currentRouteName === 'search') {
|
||||
if (this.$route.query.query) {
|
||||
this.query = decodeURIComponent(this.$route.query.query)
|
||||
this.listTitle = 'Search results: ' + this.query
|
||||
this.fetchSearchItems()
|
||||
} else {
|
||||
this.error = 'Search query is not defined, please try again'
|
||||
}
|
||||
|
||||
} // no matched route found - using prop to fetch list items
|
||||
else {
|
||||
this.listTitle = this.list.title
|
||||
this.fetchListitems()
|
||||
}
|
||||
|
||||
document.title = this.listTitle
|
||||
},
|
||||
// TODO these should receive a path not get it from list instance
|
||||
fetchListitems() {
|
||||
getTmdbListByPath(this.list.path, this.currentPage)
|
||||
.then(this.parseResponse)
|
||||
.catch(error => {
|
||||
console.error(error)
|
||||
this.error = 'Network error'
|
||||
})
|
||||
},
|
||||
fetchSearchItems() {
|
||||
searchTmdb(this.query, this.currentPage)
|
||||
.then(this.parseResponse)
|
||||
},
|
||||
|
||||
// TODO what parts are modular and what parts do we want the component to deal with
|
||||
// if we pass in some object and then as we initialize we set to local variables.
|
||||
// This way we call the http-api from outside and pass the response in to the component[0]
|
||||
// Could also parse the response we are requesting then return a clean object we can
|
||||
// pass down[1].
|
||||
|
||||
// [0] if this is done we should also take the page, total pages, total results and
|
||||
// the list of results. Maybe also the title of the list or use local title as fallback?
|
||||
// [1] an issue with this that duplicate code will be needed for doing the same with
|
||||
// url params and paths.
|
||||
// (What if we eliminated folder based routes and implemented the routes in hashes
|
||||
// with single page applications today the navigation is simple enought that it
|
||||
// would maybe not be needed to have a path-route but a hash-local.storage
|
||||
// implementation; would allow sharing and remembering paths is just silly for most
|
||||
// Single-Page-Applications that are tightly scoped applications)
|
||||
parseResponse(response) {
|
||||
const data = response.data
|
||||
if (data.page > data.total_pages) {
|
||||
console.error('You have reached the end')
|
||||
this.error = 'You have reached the end'
|
||||
return
|
||||
}
|
||||
|
||||
if (this.results.length) {
|
||||
this.results.push(...data.results)
|
||||
} else {
|
||||
this.results = this.shortList ? data.results.slice(0,12) : data.results
|
||||
}
|
||||
this.page = data.page
|
||||
this.totalPages = data.total_pages
|
||||
this.totalResults = data.total_results || data.results.length
|
||||
|
||||
this.loader = false
|
||||
|
||||
console.info(`Response from list: ${this.listTitle}`, { results: this.results, page: this.page, totalPages: this.totalPages, totalResults: this.totalResults })
|
||||
},
|
||||
loadMore(){
|
||||
this.currentPage++;
|
||||
|
||||
console.log('path and name:', this.$route.path, this.$route.name)
|
||||
let url = ''
|
||||
|
||||
if (this.$route.path.includes('list'))
|
||||
url = `/#${this.$route.path}?page=${this.currentPage}`
|
||||
else if (this.$route.path.includes('search'))
|
||||
url = `/#/search?query=${this.query}&page=${this.currentPage}`
|
||||
|
||||
console.log('new url', url)
|
||||
window.history.replaceState({}, 'foo', url)
|
||||
|
||||
this.parseURI()
|
||||
},
|
||||
// sort() {
|
||||
// console.log(this.showFilters)
|
||||
// },
|
||||
// toggleFilter(item, index){
|
||||
// this.showFilter = this.showFilter ? false : true;
|
||||
// // this.results = this.results.filter(result => result.status != 'downloaded')
|
||||
// },
|
||||
// applyFilter(item, index) {
|
||||
// this.filter = item;
|
||||
// this.filters.status.selected = index;
|
||||
// console.log('applied query filter: ', item, index)
|
||||
// this.fetchCategory()
|
||||
// }
|
||||
},
|
||||
watch: {
|
||||
$route: function () {
|
||||
console.log('updated route')
|
||||
this.results = false
|
||||
this.currentPage = 1
|
||||
this.setPageFromUrlQuery()
|
||||
this.parseURI()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/elements";
|
||||
|
||||
.movies-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 15px;
|
||||
|
||||
.results {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 10px;
|
||||
|
||||
@include tablet-min{
|
||||
padding: 23px 15px;
|
||||
}
|
||||
@include tablet-landscape-min{
|
||||
padding: 16px 25px;
|
||||
}
|
||||
@include desktop-min{
|
||||
padding: 8px 30px;
|
||||
}
|
||||
|
||||
.header__title {
|
||||
line-height: 18px;
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #081c24;
|
||||
font-weight: 300;
|
||||
// flex-basis: 50%;
|
||||
text-transform: capitalize;
|
||||
|
||||
@include tablet-min{
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.header__result-count {
|
||||
font-size: 12px;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
color: rgba(8,28,36,.5);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.header__view-more {
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
letter-spacing: .5px;
|
||||
color: rgba($c-dark, 0.5);
|
||||
text-decoration: none;
|
||||
transition: color .5s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:after{
|
||||
content: " →";
|
||||
}
|
||||
&:hover{
|
||||
color: $c-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.end-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@import "./src/scss/media-queries";
|
||||
.form__group-input {
|
||||
padding: 10px 5px 10px 15px;
|
||||
margin-left: 0;
|
||||
height: 38px;
|
||||
width: 150px;
|
||||
font-size: 15px;
|
||||
@include desktop-min {
|
||||
width: 200px;
|
||||
font-size: 17px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,136 +1,249 @@
|
||||
<template>
|
||||
<li class="movies-item" :class="{'shortList': shortList}">
|
||||
<a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)">
|
||||
<li class="movie-item" :class="{ shortList: shortList }">
|
||||
<figure class="movie-item__poster">
|
||||
<img
|
||||
class="movie-item__img"
|
||||
ref="poster-image"
|
||||
@click="openMoviePopup(movie.id, movie.type)"
|
||||
:alt="posterAltText"
|
||||
:data-src="poster"
|
||||
src="~assets/placeholder.png"
|
||||
/>
|
||||
|
||||
<figure class="movies-item__poster">
|
||||
<img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
|
||||
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
|
||||
</figure>
|
||||
<div class="movies-item__content">
|
||||
<p class="movies-item__title">{{ movie.title }}</p>
|
||||
<p class="movies-item__title">{{ movie.year }}</p>
|
||||
<div v-if="movie.download" class="progress">
|
||||
<progress :value="movie.download.progress" max="100"></progress>
|
||||
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||
</div>
|
||||
</a>
|
||||
</figure>
|
||||
|
||||
<div class="movie-item__info">
|
||||
<p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
|
||||
<p v-if="movie.year">{{ movie.year }}</p>
|
||||
<p v-if="movie.type == 'person'">
|
||||
Known for: {{ movie.known_for_department }}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import img from '../directives/v-image.js'
|
||||
import img from "../directives/v-image";
|
||||
|
||||
export default {
|
||||
props: ['movie', 'shortList'],
|
||||
props: {
|
||||
movie: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
directives: {
|
||||
img: img
|
||||
},
|
||||
data(){
|
||||
return{
|
||||
noImage: false
|
||||
data() {
|
||||
return {
|
||||
poster: undefined,
|
||||
observed: false,
|
||||
posterSizes: [
|
||||
{
|
||||
id: "w500",
|
||||
minWidth: 500
|
||||
},
|
||||
{
|
||||
id: "w342",
|
||||
minWidth: 342
|
||||
},
|
||||
{
|
||||
id: "w185",
|
||||
minWidth: 185
|
||||
},
|
||||
{
|
||||
id: "w154",
|
||||
minWidth: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
posterAltText: function () {
|
||||
const type = this.movie.type || "";
|
||||
const title = this.movie.title || this.movie.name;
|
||||
return this.movie.poster
|
||||
? `Poster for ${type} ${title}`
|
||||
: `Missing image for ${type} ${title}`;
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (this.movie.poster != null) {
|
||||
this.poster = "https://image.tmdb.org/t/p/w500" + this.movie.poster;
|
||||
} else {
|
||||
this.poster = "/no-image.png";
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
const poster = this.$refs["poster-image"];
|
||||
if (poster == null) return;
|
||||
|
||||
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting && this.observed == false) {
|
||||
const lazyImage = entry.target;
|
||||
lazyImage.src = lazyImage.dataset.src;
|
||||
lazyImage.className = lazyImage.className + " is-loaded";
|
||||
this.observed = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
imageObserver.observe(poster);
|
||||
},
|
||||
methods: {
|
||||
// TODO handle missing images better and load diff sizes based on screen size
|
||||
poster() {
|
||||
if (this.movie.poster) {
|
||||
return 'https://image.tmdb.org/t/p/w500' + this.movie.poster
|
||||
} else {
|
||||
this.noImage = true
|
||||
}
|
||||
},
|
||||
openMoviePopup(id, type){
|
||||
this.$popup.open(id, type)
|
||||
openMoviePopup(id, type) {
|
||||
this.$popup.open(id, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
.movies-item {
|
||||
@import "./src/scss/main";
|
||||
|
||||
.movie-item {
|
||||
padding: 10px;
|
||||
width: 50%;
|
||||
background-color: $background-color;
|
||||
transition: background-color 0.5s ease;
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
padding: 15px;
|
||||
width: 33%;
|
||||
}
|
||||
@include tablet-landscape-min{
|
||||
padding: 20px;
|
||||
@include tablet-landscape-min {
|
||||
padding: 15px;
|
||||
width: 25%;
|
||||
}
|
||||
@include desktop-min{
|
||||
padding: 30px;
|
||||
@include desktop-min {
|
||||
padding: 15px;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
@include desktop-lg-min{
|
||||
padding: 20px;
|
||||
width: 16.5%;
|
||||
@include desktop-lg-min {
|
||||
padding: 15px;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
&.shortList {
|
||||
display: none;
|
||||
|
||||
&:nth-child(-n+6) { // show first 6
|
||||
display: block;
|
||||
}
|
||||
|
||||
@include tablet-landscape-min{
|
||||
&:nth-child(-n+8) { // show first 8
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include desktop-min{
|
||||
&:nth-child(-n+10) { // show first 10
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@include desktop-lg-min{
|
||||
display: block; // show all
|
||||
}
|
||||
&:hover &__info > p {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
&__link{
|
||||
&__poster {
|
||||
text-decoration: none;
|
||||
color: rgba($c-dark, 0.5);
|
||||
color: $text-color-70;
|
||||
font-weight: 300;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 1s ease, transform 0.5s ease;
|
||||
&.is-loaded {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($dark, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&__content{
|
||||
|
||||
&__info {
|
||||
padding-top: 15px;
|
||||
}
|
||||
&__poster{
|
||||
transition: transform 0.5s ease, box-shadow 0.3s ease;
|
||||
transform: translateZ(0);
|
||||
background: $c-white;
|
||||
}
|
||||
&__img{
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transform: scale(0.97) translateZ(0);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
&.is-loaded{
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
font-weight: 300;
|
||||
|
||||
> p {
|
||||
color: $text-color-70;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
cursor: pointer;
|
||||
@include mobile-ls-min {
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&__link:not(.no-image):hover &__poster{
|
||||
transform: scale(1.03);
|
||||
box-shadow: 0 0 10px rgba($c-dark, 0.1);
|
||||
}
|
||||
|
||||
.no-image {
|
||||
background-color: var(--text-color);
|
||||
color: var(--background-color);
|
||||
width: 100%;
|
||||
height: 383px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
span {
|
||||
font-size: 1.5rem;
|
||||
width: 70%;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
&__title{
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.5px;
|
||||
transition: color 0.5s ease;
|
||||
@include mobile-ls-min{
|
||||
font-size: 12px;
|
||||
}
|
||||
@include tablet-min{
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
&__link:hover &__title{
|
||||
color: $c-dark;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-bottom: 0.8rem;
|
||||
|
||||
> progress {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
> span {
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4rem;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
progress {
|
||||
border-radius: 4px;
|
||||
height: 1.4rem;
|
||||
}
|
||||
progress::-webkit-progress-bar {
|
||||
background-color: rgba($black, 0.55);
|
||||
border-radius: 4px;
|
||||
}
|
||||
progress::-webkit-progress-value {
|
||||
background-color: $green-70;
|
||||
border-radius: 4px;
|
||||
}
|
||||
progress::-moz-progress-bar {
|
||||
/* style rules */
|
||||
background-color: green;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,151 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<nav class="nav">
|
||||
<router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App">
|
||||
<svg class="nav__logo-image">
|
||||
<use xlink:href="#svgLogo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
<div class="nav__hamburger" @click="toggleNav">
|
||||
<div class="bar"></div>
|
||||
<div class="bar"></div>
|
||||
<div class="bar"></div>
|
||||
</div>
|
||||
<ul class="nav__list">
|
||||
<li class="nav__item" v-for="item in listTypes">
|
||||
<router-link class="nav__link" :to="'/list/' + item.route">
|
||||
<div class="nav__link-wrap">
|
||||
<!-- <img :src="item.icon" class="nav__link-icon"> -->
|
||||
<svg class="nav__link-icon">
|
||||
<use :xlink:href="'#icon_' + item.route"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="nav__item nav__item--profile">
|
||||
<router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Sign in</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Profile</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="spacer"></div>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<router-link
|
||||
class="nav__logo"
|
||||
:to="{ name: 'home' }"
|
||||
exact
|
||||
title="Vue.js — TMDb App"
|
||||
>
|
||||
<svg class="nav__logo-image">
|
||||
<use xlink:href="#svgLogo"></use>
|
||||
</svg>
|
||||
</router-link>
|
||||
|
||||
<div class="nav__hamburger" @click="toggleNav">
|
||||
<div v-for="_ in 3" class="bar"></div>
|
||||
</div>
|
||||
|
||||
<ul class="nav__list">
|
||||
<li class="nav__item" v-for="item in listTypes">
|
||||
<router-link class="nav__link" :to="'/list/' + item.route">
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use :xlink:href="'#icon_' + item.route"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">{{ item.title }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
|
||||
<li class="nav__item mobile-only"></li>
|
||||
|
||||
<li class="nav__item nav__item--profile">
|
||||
<router-link
|
||||
class="nav__link nav__link--profile"
|
||||
:to="{ name: 'signin' }"
|
||||
v-if="!userLoggedIn"
|
||||
>
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Sign in</span>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<router-link
|
||||
class="nav__link nav__link--profile"
|
||||
:to="{ name: 'profile' }"
|
||||
v-if="userLoggedIn"
|
||||
>
|
||||
<div class="nav__link-wrap">
|
||||
<svg class="nav__link-icon">
|
||||
<use xlink:href="#iconLogin"></use>
|
||||
</svg>
|
||||
<span class="nav__link-title">Profile</span>
|
||||
</div>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import storage from "@/storage";
|
||||
|
||||
export default {
|
||||
data(){
|
||||
data() {
|
||||
return {
|
||||
listTypes: storage.homepageLists,
|
||||
userLoggedIn: localStorage.getItem('token') ? true : false
|
||||
}
|
||||
userLoggedIn: localStorage.getItem("token") ? true : false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
setUserStatus(){
|
||||
this.userLoggedIn = localStorage.getItem('token') ? true : false;
|
||||
setUserStatus() {
|
||||
this.userLoggedIn = localStorage.getItem("token") ? true : false;
|
||||
},
|
||||
toggleNav(){
|
||||
document.querySelector('.nav__hamburger').classList.toggle('nav__hamburger--active');
|
||||
document.querySelector('.nav__list').classList.toggle('nav__list--active');
|
||||
toggleNav() {
|
||||
document
|
||||
.querySelector(".nav__hamburger")
|
||||
.classList.toggle("nav__hamburger--active");
|
||||
document
|
||||
.querySelector(".nav__list")
|
||||
.classList.toggle("nav__list--active");
|
||||
}
|
||||
},
|
||||
created(){
|
||||
eventHub.$on('setUserStatus', this.setUserStatus);
|
||||
created() {
|
||||
// TODO move this to state manager
|
||||
eventHub.$on("setUserStatus", this.setUserStatus);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
.spacer {
|
||||
@include mobile-only {
|
||||
width: 100%;
|
||||
height: $header-size-mobile;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
transition: background 0.5s ease;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: $c-white;
|
||||
height: var(--header-size);
|
||||
z-index: 10;
|
||||
display: block;
|
||||
color: $text-color;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
top: 0;
|
||||
bottom: unset;
|
||||
width: 95px;
|
||||
height: 100vh;
|
||||
}
|
||||
&__logo{
|
||||
width: 55px;
|
||||
height: $header-size-mobile;
|
||||
&__logo {
|
||||
width: 95px;
|
||||
height: $header-size;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $c-dark;
|
||||
@include tablet-min{
|
||||
width: 95px;
|
||||
height: $header-size;
|
||||
background: $background-nav-logo;
|
||||
|
||||
@include mobile-only {
|
||||
align-items: flex-start;
|
||||
padding-top: 0.5rem;
|
||||
width: 55px;
|
||||
}
|
||||
&-image{
|
||||
|
||||
&-image {
|
||||
width: 35px;
|
||||
height: 31px;
|
||||
fill: $c-green;
|
||||
fill: $green;
|
||||
transition: transform 0.5s ease;
|
||||
@include tablet-min{
|
||||
|
||||
@include tablet-min {
|
||||
width: 45px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
&:hover &-image{
|
||||
&:hover &-image {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
}
|
||||
&__hamburger{
|
||||
&__hamburger {
|
||||
display: block;
|
||||
position: fixed;
|
||||
width: 55px;
|
||||
height: 50px;
|
||||
top: 0;
|
||||
bottom: 1.5rem;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
background: $c-white;
|
||||
z-index: 10;
|
||||
border-left: 1px solid $c-light;
|
||||
@include tablet-min{
|
||||
border-left: 1px solid $background-color;
|
||||
@include tablet-min {
|
||||
display: none;
|
||||
}
|
||||
.bar{
|
||||
.bar {
|
||||
position: absolute;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
background: rgba($c-dark, 0.5);
|
||||
background-color: $text-color-70;
|
||||
transition: all 300ms ease;
|
||||
&:nth-child(1){
|
||||
&:nth-child(1) {
|
||||
left: 16px;
|
||||
top: 17px;
|
||||
}
|
||||
&:nth-child(2){
|
||||
&:nth-child(2) {
|
||||
left: 16px;
|
||||
top: 25px;
|
||||
&:after {
|
||||
@@ -155,19 +178,18 @@ export default {
|
||||
top: 0px;
|
||||
width: 23px;
|
||||
height: 1px;
|
||||
background: transparent;
|
||||
transition: all 300ms ease;
|
||||
}
|
||||
}
|
||||
&:nth-child(3){
|
||||
&:nth-child(3) {
|
||||
right: 15px;
|
||||
top: 33px;
|
||||
}
|
||||
}
|
||||
&--active{
|
||||
.bar{
|
||||
&--active {
|
||||
.bar {
|
||||
&:nth-child(1),
|
||||
&:nth-child(3){
|
||||
&:nth-child(3) {
|
||||
width: 0;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
@@ -175,12 +197,13 @@ export default {
|
||||
}
|
||||
&:nth-child(2):after {
|
||||
transform: rotate(-90deg);
|
||||
background: rgba($c-dark, 0.5);
|
||||
// background: rgba($c-dark, 0.5);
|
||||
background-color: $text-color-70;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&__list{
|
||||
&__list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@@ -189,23 +212,28 @@ export default {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 50px;
|
||||
background: rgba($c-white, 0.98);
|
||||
border-top: 1px solid $c-light;
|
||||
@include mobile-only{
|
||||
border-top: 1px solid $background-color;
|
||||
|
||||
@include mobile-only {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: unset;
|
||||
bottom: var(--header-size);
|
||||
height: min-content;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: calc(100vh - 50px);
|
||||
transition: all 0.5s ease;
|
||||
background-color: $background-95;
|
||||
text-align: left;
|
||||
&--active{
|
||||
|
||||
&--active {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
display: flex;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -213,31 +241,45 @@ export default {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
&__item{
|
||||
@include mobile-only{
|
||||
display: inline-block;
|
||||
&__item {
|
||||
transition: background 0.5s ease, color 0.5s ease, border 0.5s ease;
|
||||
background-color: $background-color-secondary;
|
||||
color: $text-color-70;
|
||||
|
||||
@include mobile-only {
|
||||
flex: 0 0 33.3%;
|
||||
text-align: center;
|
||||
width: 50%;
|
||||
border-bottom: 1px solid $c-light;
|
||||
&:nth-child(odd){
|
||||
border-right: 1px solid $c-light;
|
||||
border-bottom: 1px solid $background-color;
|
||||
&:nth-child(odd) {
|
||||
border-right: 1px solid $background-color;
|
||||
|
||||
&:last-child {
|
||||
// flex: 0 0 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
width: 100%;
|
||||
border-bottom: 1px solid $c-light;
|
||||
&--profile{
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
&--profile {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: $header-size;
|
||||
height: $header-size;
|
||||
border-bottom: 0;
|
||||
border-left: 1px solid $c-light;
|
||||
border-left: 1px solid $background-color;
|
||||
}
|
||||
}
|
||||
&:hover,
|
||||
.is-active {
|
||||
color: $text-color;
|
||||
background-color: $background-color;
|
||||
}
|
||||
}
|
||||
&__link{
|
||||
&__link {
|
||||
background-color: inherit; // a elements have a transparent background
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -248,8 +290,6 @@ export default {
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: rgba($c-dark, 0.7);
|
||||
transition: color 0.5s ease, background 0.5s ease;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
&-wrap {
|
||||
@@ -258,49 +298,38 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include mobile-only{
|
||||
@include mobile-only {
|
||||
font-size: 10px;
|
||||
padding: 20px 0;
|
||||
}
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
width: 95px;
|
||||
height: 95px;
|
||||
font-size: 9px;
|
||||
&--profile{
|
||||
&--profile {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
background: $c-white;
|
||||
background-color: $background-color-secondary;
|
||||
}
|
||||
}
|
||||
&-icon{
|
||||
&-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: rgba($c-dark, 0.7);
|
||||
transition: fill 0.5s ease;
|
||||
@include tablet-min{
|
||||
fill: $text-color-70;
|
||||
@include tablet-min {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
&-title{
|
||||
&-title {
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
&:hover{
|
||||
color: $c-dark;
|
||||
}
|
||||
&:hover &-icon{
|
||||
fill: $c-dark;
|
||||
}
|
||||
&.is-active{
|
||||
color: $c-dark;
|
||||
background: $c-light;
|
||||
}
|
||||
&.is-active &-icon{
|
||||
fill: $c-dark;
|
||||
&:hover &-icon,
|
||||
&.is-active &-icon {
|
||||
fill: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
<section class="profile">
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<header class="profile__header">
|
||||
<h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
|
||||
|
||||
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
|
||||
|
||||
<div class="button--group">
|
||||
<seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
||||
<seasoned-button @click="toggleSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
||||
|
||||
<seasoned-button @click="logOut">Log out</seasoned-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<settings v-if="showSettings"></settings>
|
||||
|
||||
<movies-list :propList="user_requestsList"></movies-list>
|
||||
|
||||
<list-header title="User requests" :info="resultCount" />
|
||||
<results-list v-if="results" :results="results" />
|
||||
</div>
|
||||
|
||||
<section class="not-found" v-if="!userLoggedIn">
|
||||
@@ -28,62 +29,71 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import MoviesList from '@/components/MoviesList.vue'
|
||||
import Settings from '@/components/Settings.vue'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import storage from '@/storage'
|
||||
import store from '@/store'
|
||||
import ListHeader from '@/components/ListHeader'
|
||||
import ResultsList from '@/components/ResultsList'
|
||||
import Settings from '@/components/Settings'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
|
||||
import { getEmoji } from '@/api.js'
|
||||
// import CreatedLists from './CreatedLists.vue'
|
||||
import { getEmoji, getUserRequests } from '@/api'
|
||||
|
||||
export default {
|
||||
components: { MoviesList, Settings, SeasonedButton },
|
||||
components: { ListHeader, ResultsList, Settings, SeasonedButton },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
userName: '',
|
||||
emoji: '',
|
||||
showSettings: false,
|
||||
user_requestsList: storage.user_requestsList
|
||||
results: undefined,
|
||||
totalResults: undefined,
|
||||
showSettings: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
resultCount() {
|
||||
if (this.results === undefined)
|
||||
return
|
||||
|
||||
const loadedResults = this.results.length
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
|
||||
return `${loadedResults} of ${totalResults} results`
|
||||
},
|
||||
username: () => store.getters['userModule/username']
|
||||
},
|
||||
methods: {
|
||||
createSession(token){
|
||||
axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
|
||||
.then(function(resp){
|
||||
let data = resp.data;
|
||||
if(data.success){
|
||||
let id = data.session_id;
|
||||
localStorage.setItem('session_id', id);
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.userLoggedIn = true;
|
||||
this.getUserInfo();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
getUserInfo(){
|
||||
this.userName = localStorage.getItem('username');
|
||||
},
|
||||
toggleSettings() {
|
||||
this.showSettings = this.showSettings ? false : true;
|
||||
|
||||
if (this.showSettings) {
|
||||
this.$router.replace({ query: { settings: true} })
|
||||
} else {
|
||||
this.$router.replace({ name: 'profile' })
|
||||
}
|
||||
},
|
||||
logOut(){
|
||||
localStorage.clear();
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'home' });
|
||||
this.$router.push('logout')
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'Profile' + storage.pageTitlePostfix;
|
||||
storage.backTitle = document.title;
|
||||
if(!localStorage.getItem('token')){
|
||||
this.userLoggedIn = false;
|
||||
} else {
|
||||
this.userLoggedIn = true;
|
||||
this.getUserInfo();
|
||||
|
||||
this.showSettings = window.location.toString().includes('settings=true')
|
||||
|
||||
getUserRequests()
|
||||
.then(results => {
|
||||
this.results = results.results
|
||||
this.totalResults = results.total_results
|
||||
})
|
||||
|
||||
getEmoji()
|
||||
.then(resp => this.emoji = resp.data.emoji )
|
||||
.then(resp => {
|
||||
const { emoji } = resp
|
||||
this.emoji = emoji
|
||||
store.dispatch('documentTitle/updateEmoji', emoji)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,6 +103,10 @@ export default {
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.button--group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// DUPLICATE CODE
|
||||
.profile{
|
||||
&__header{
|
||||
@@ -100,7 +114,17 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba($c-dark, 0.05);
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
@include mobile-only {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.button--group {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min{
|
||||
padding: 29px 30px;
|
||||
}
|
||||
@@ -115,7 +139,7 @@ export default {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
@include tablet-min{
|
||||
font-size: 18px;
|
||||
|
||||
@@ -1,202 +1,110 @@
|
||||
<template>
|
||||
<section class="profile">
|
||||
<div class="profile__content">
|
||||
<h2 class='settings__header'>Register new user</h2>
|
||||
<section>
|
||||
<h1>Register new user</h1>
|
||||
|
||||
<form class="form">
|
||||
<seasoned-input text="username" icon="Email"
|
||||
@inputValue="setValue('username', $event)"></seasoned-input>
|
||||
<seasoned-input text="password" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('password', $event)"></seasoned-input>
|
||||
<seasoned-input text="repeat password" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('passwordRepeat', $event)"></seasoned-input>
|
||||
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" @enter="submit"/>
|
||||
|
||||
<transition name="message-fade">
|
||||
<div class="message" :class="messageClass" v-if="showMessage">
|
||||
<span class="message-text">{{ messageText }}</span>
|
||||
<span class="message-dismiss" v-on:click="dismissMessage">X</span>
|
||||
</div>
|
||||
</transition>
|
||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
|
||||
<seasoned-input placeholder="repeat password" icon="Keyhole" type="password" :value.sync="passwordRepeat" @enter="submit"/>
|
||||
|
||||
<div class="form__group">
|
||||
<seasoned-button @click="requestNewUser">Register</seasoned-button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="form__group">
|
||||
<router-link class="form__group-link" :to="{name: 'signin'}" exact title="Sign in here">
|
||||
<span class="form__group-signin">Sign in here</span>
|
||||
</router-link>
|
||||
</div>
|
||||
<seasoned-button @click="submit">Register</seasoned-button>
|
||||
<router-link class="link" to="/signin">Have a user? Sign in here</router-link>
|
||||
|
||||
</div>
|
||||
<section class="not-found" v-if="userLoggedIn === false">
|
||||
<div class="not-found__content">
|
||||
<h2 class="not-found__title">Authentication Request Failed</h2>
|
||||
<button class="not-found__button button">Log In</button>
|
||||
</div>
|
||||
</section>
|
||||
<seasoned-messages :messages.sync="messages"></seasoned-messages>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import storage from '@/storage.js'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput.vue'
|
||||
import { register } from '@/api'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
|
||||
export default {
|
||||
components: { SeasonedButton, SeasonedInput },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
passwordRepeat: undefined,
|
||||
showMessage: false,
|
||||
messageClass: 'message-success',
|
||||
messageText: 'hello world'
|
||||
components: { SeasonedButton, SeasonedInput, SeasonedMessages },
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
username: null,
|
||||
password: null,
|
||||
passwordRepeat: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
requestNewUser(){
|
||||
let username = this.username
|
||||
let password = this.password
|
||||
let password_re = this.passwordRepeat
|
||||
submit() {
|
||||
this.messages = [];
|
||||
let { username, password, passwordRepeat } = this;
|
||||
|
||||
let verifyCredentials = this.checkCredentials(username, password, password_re);
|
||||
|
||||
if (verifyCredentials.verified) {
|
||||
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
.then(function(resp) {
|
||||
let data = resp.data;
|
||||
if (username == null || username.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing username' })
|
||||
return
|
||||
} else if (password == null || password.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing password' })
|
||||
return
|
||||
} else if (passwordRepeat == null || passwordRepeat.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing repeat password' })
|
||||
return
|
||||
} else if (passwordRepeat != password) {
|
||||
this.messages.push({ type: 'error', title: 'Passwords do not match' })
|
||||
return
|
||||
}
|
||||
|
||||
this.registerUser(username, password)
|
||||
},
|
||||
registerUser(username, password) {
|
||||
register(username, password, true)
|
||||
.then(data => {
|
||||
if (data.success){
|
||||
this.msg(data.message, 'success');
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', username);
|
||||
localStorage.setItem('admin', data.admin)
|
||||
|
||||
const jwtData = parseJwt(data.token)
|
||||
localStorage.setItem('username', jwtData['username']);
|
||||
localStorage.setItem('admin', jwtData['admin'] || false);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function(error){
|
||||
this.msg(error.response.data.error, 'warning')
|
||||
}.bind(this));
|
||||
}
|
||||
else {
|
||||
this.msg(verifyCredentials.reason, 'warning');
|
||||
}
|
||||
},
|
||||
checkCredentials(username, password, password_re) {
|
||||
if (password !== password_re) {
|
||||
return {
|
||||
verified: false,
|
||||
reason: 'Passwords do not match'
|
||||
}
|
||||
}
|
||||
else if (username === undefined) {
|
||||
return {
|
||||
verified: false,
|
||||
reason: 'Please insert username'
|
||||
}
|
||||
}
|
||||
else {
|
||||
return {
|
||||
verified: true,
|
||||
reason: 'Verified credentials'
|
||||
}
|
||||
}
|
||||
},
|
||||
msg(text, status){
|
||||
if (status === 'warning')
|
||||
this.messageClass = 'message-warning';
|
||||
else if (status === 'success')
|
||||
this.messageClass = 'message-success';
|
||||
else
|
||||
this.messageClass = 'message-info';
|
||||
this.messageText = text;
|
||||
this.showMessage = true;
|
||||
// setTimeout(() => this.showMessage = false, 3500);
|
||||
},
|
||||
dismissMessage(){
|
||||
this.showMessage = false;
|
||||
},
|
||||
setValue(l, t) {
|
||||
this[l] = t
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
},
|
||||
logOut(){
|
||||
localStorage.clear();
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'Profile' + storage.pageTitlePostfix;
|
||||
storage.backTitle = document.title;
|
||||
},
|
||||
mounted(){
|
||||
// this.$refs.email.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/message";
|
||||
|
||||
// DUPLICATE CODE
|
||||
.settings {
|
||||
padding: 35px;
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
&__header {
|
||||
@include tablet-min {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.profile__content {
|
||||
padding: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
// TODO, fix this. if single child it adds weird margin
|
||||
> div:last-child {
|
||||
.link {
|
||||
display: block;
|
||||
width: max-content;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__group{
|
||||
justify-content: unset;
|
||||
&__input-icon {
|
||||
margin-top: 8px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
&-input {
|
||||
padding: 10px 5px 10px 45px;
|
||||
height: 40px;
|
||||
font-size: 17px;
|
||||
width: 75%;
|
||||
// @include desktop-min {
|
||||
// width: 400px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
68
src/components/ResultsList.vue
Normal file
68
src/components/ResultsList.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<ul class="results" :class="{'shortList': shortList}">
|
||||
<movies-list-item v-for='movie in results' :movie="movie" />
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import MoviesListItem from '@/components/MoviesListItem'
|
||||
|
||||
export default {
|
||||
components: { MoviesListItem },
|
||||
props: {
|
||||
results: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
shortList: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import './src/scss/media-queries';
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
&.shortList > li {
|
||||
display: none;
|
||||
|
||||
&:nth-child(-n+4) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
.results.shortList > li:nth-child(-n+6) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include tablet-landscape-min {
|
||||
.results.shortList > li:nth-child(-n+8) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include desktop-min {
|
||||
.results.shortList > li:nth-child(-n+10) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@include desktop-lg-min {
|
||||
.results.shortList > li:nth-child(-n+16) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
138
src/components/Search.vue
Normal file
138
src/components/Search.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<list-header :title="title" :info="resultCount" :sticky="true" />
|
||||
|
||||
<results-list :results="results" />
|
||||
|
||||
<div v-if="page < totalPages" class="fullwidth-button">
|
||||
<seasoned-button @click="loadMore">load more</seasoned-button>
|
||||
</div>
|
||||
|
||||
<div class="notFound" v-if="results.length == 0 && loading == false">
|
||||
<h1 class="notFound-title">
|
||||
No results for search: <b>{{ query }}</b>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<loader v-if="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notFound {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&-title {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { searchTmdb } from "@/api";
|
||||
import ListHeader from "@/components/ListHeader";
|
||||
import ResultsList from "@/components/ResultsList";
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import Loader from "@/components/ui/Loader";
|
||||
|
||||
export default {
|
||||
components: { ListHeader, ResultsList, SeasonedButton, Loader },
|
||||
props: {
|
||||
propQuery: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
propPage: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
query: String,
|
||||
title: String,
|
||||
page: Number,
|
||||
adult: undefined,
|
||||
mediaType: null,
|
||||
totalPages: 0,
|
||||
results: [],
|
||||
totalResults: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
resultCount() {
|
||||
const loadedResults = this.results.length;
|
||||
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞";
|
||||
return `${loadedResults} of ${totalResults} results`;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search(
|
||||
query = this.query,
|
||||
page = this.page,
|
||||
adult = this.adult,
|
||||
mediaType = this.mediaType
|
||||
) {
|
||||
searchTmdb(query, page, adult, mediaType).then(this.parseResponse);
|
||||
},
|
||||
parseResponse(data) {
|
||||
if (this.results.length > 0) {
|
||||
this.results.push(...data.results);
|
||||
} else {
|
||||
this.results = data.results;
|
||||
}
|
||||
|
||||
this.totalPages = data.total_pages;
|
||||
this.totalResults = data.total_results || data.results.length;
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
loadMore() {
|
||||
this.page++;
|
||||
|
||||
window.history.replaceState(
|
||||
{},
|
||||
"search",
|
||||
`/#/search?query=${this.query}&page=${this.page}`
|
||||
);
|
||||
this.search();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
const { query, page, adult, media_type } = this.$route.query;
|
||||
|
||||
if (!query) {
|
||||
// abort
|
||||
console.error("abort, no query");
|
||||
}
|
||||
this.query = decodeURIComponent(query);
|
||||
this.page = page || 1;
|
||||
this.adult = adult || this.adult;
|
||||
this.mediaType = media_type || this.mediaType;
|
||||
this.title = `Search results: ${this.query}`;
|
||||
|
||||
this.search();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
@include mobile-only {
|
||||
.page-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.fullwidth-button {
|
||||
width: 100%;
|
||||
margin: 1rem 0;
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,176 +1,290 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
<div class="search">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for a movie or show"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
v-model="query"
|
||||
@input="handleInput"
|
||||
@click="focus = true"
|
||||
@keydown.escape="handleEscape"
|
||||
@keyup.enter="handleSubmit"
|
||||
@keydown.up="navigateUp"
|
||||
@keydown.down="navigateDown" />
|
||||
|
||||
<svg class="search--icon"><use xlink:href="#iconSearch"></use></svg>
|
||||
</div>
|
||||
<!-- <div> -->
|
||||
<div class="search">
|
||||
<input
|
||||
ref="input"
|
||||
type="text"
|
||||
placeholder="Search for movie or show"
|
||||
aria-label="Search input for finding a movie or show"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
tabindex="1"
|
||||
v-model="query"
|
||||
@input="handleInput"
|
||||
@click="focus = true"
|
||||
@keydown.escape="handleEscape"
|
||||
@keyup.enter="handleSubmit"
|
||||
@keydown.up="navigateUp"
|
||||
@keydown.down="navigateDown"
|
||||
/>
|
||||
|
||||
<svg class="search-icon" fill="currentColor" @click="handleSubmit">
|
||||
<use xlink:href="#iconSearch"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<!--
|
||||
<transition name="fade">
|
||||
<div class="dropdown" v-if="!disabled && focus && query.length > 0">
|
||||
<div class="dropdown--results">
|
||||
<div class="filter">
|
||||
<h2>Filter your search:</h2>
|
||||
|
||||
<ul v-for="(item, index) in elasticSearchResults"
|
||||
@click="$popup.open(item.id, item.type)"
|
||||
:class="{ active: index + 1 === selectedResult}">
|
||||
|
||||
{{ item.name }}
|
||||
</ul>
|
||||
<div class="filter-items">
|
||||
<toggle-button
|
||||
:options="searchTypes"
|
||||
:selected.sync="selectedSearchType"
|
||||
/>
|
||||
|
||||
<label
|
||||
>Adult
|
||||
<input type="checkbox" value="adult" v-model="adult" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<seasoned-button class="end-section" fullWidth="true"
|
||||
@click="focus = false" :active="elasticSearchResults.length + 1 === selectedResult">
|
||||
<hr />
|
||||
|
||||
<div class="dropdown-results" v-if="elasticSearchResults.length">
|
||||
<ul
|
||||
v-for="(item, index) in elasticSearchResults"
|
||||
@click="openResult(item, index + 1)"
|
||||
:class="{ active: index + 1 === selectedResult }"
|
||||
>
|
||||
{{
|
||||
item.name
|
||||
}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-else class="dropdown">
|
||||
<div class="dropdown-results">
|
||||
<h2 class="not-found">
|
||||
No results for query: <b>{{ query }}</b>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<seasoned-button
|
||||
class="end-section"
|
||||
fullWidth="true"
|
||||
@click="focus = false"
|
||||
:active="elasticSearchResults.length + 1 === selectedResult"
|
||||
>
|
||||
close
|
||||
</seasoned-button>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import SeasonedButton from "@/components/ui/SeasonedButton";
|
||||
import ToggleButton from "@/components/ui/ToggleButton";
|
||||
|
||||
import { elasticSearchMoviesAndShows } from '@/api.js'
|
||||
import config from '@/config.json'
|
||||
import { elasticSearchMoviesAndShows } from "@/api";
|
||||
import config from "@/config.json";
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
name: "SearchInput",
|
||||
components: {
|
||||
SeasonedButton
|
||||
SeasonedButton,
|
||||
ToggleButton
|
||||
},
|
||||
props: ['value'],
|
||||
props: ["value"],
|
||||
data() {
|
||||
return {
|
||||
adult: true,
|
||||
searchTypes: ["all", "movie", "show", "person"],
|
||||
selectedSearchType: "all",
|
||||
|
||||
query: this.value,
|
||||
focus: false,
|
||||
disabled: false,
|
||||
scrollListener: undefined,
|
||||
scrollDistance: 0,
|
||||
elasticSearchResults: '',
|
||||
elasticSearchResults: [],
|
||||
selectedResult: 0
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
focus: function(val) {
|
||||
focus: function (val) {
|
||||
if (val === true) {
|
||||
window.addEventListener('scroll', this.disableFocus)
|
||||
window.addEventListener("scroll", this.disableFocus);
|
||||
} else {
|
||||
window.removeEventListener('scroll', this.disableFocus)
|
||||
this.scrollDistance = 0
|
||||
window.removeEventListener("scroll", this.disableFocus);
|
||||
this.scrollDistance = 0;
|
||||
}
|
||||
},
|
||||
adult: function (value) {
|
||||
this.handleInput();
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
const elasticUrl = config.ELASTIC_URL
|
||||
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === '') {
|
||||
this.disabled = true
|
||||
const elasticUrl = config.ELASTIC_URL;
|
||||
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") {
|
||||
this.disabled = true;
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
console.log('scroll eventlistener not removed, destroying!')
|
||||
window.removeEventListener('scroll', this.disableFocus)
|
||||
console.log("scroll eventlistener not removed, destroying!");
|
||||
window.removeEventListener("scroll", this.disableFocus);
|
||||
},
|
||||
methods: {
|
||||
navigateDown() {
|
||||
this.focus = true
|
||||
this.selectedResult++
|
||||
this.focus = true;
|
||||
this.selectedResult++;
|
||||
},
|
||||
navigateUp() {
|
||||
this.focus = true
|
||||
this.selectedResult--
|
||||
},
|
||||
handleInput(e){
|
||||
this.selectedResult = 0
|
||||
this.$emit('input', this.query);
|
||||
this.focus = true;
|
||||
this.selectedResult--;
|
||||
const input = this.$refs.input;
|
||||
const textLength = input.value.length;
|
||||
|
||||
if (! this.focus) {
|
||||
setTimeout(() => {
|
||||
input.focus();
|
||||
input.setSelectionRange(textLength, textLength + 1);
|
||||
}, 1);
|
||||
},
|
||||
openResult(item, index) {
|
||||
this.selectedResult = index;
|
||||
this.$popup.open(item.id, item.type);
|
||||
},
|
||||
handleInput(e) {
|
||||
this.selectedResult = 0;
|
||||
this.$emit("input", this.query);
|
||||
|
||||
if (!this.focus) {
|
||||
this.focus = true;
|
||||
}
|
||||
|
||||
elasticSearchMoviesAndShows(this.query)
|
||||
.then(resp => {
|
||||
const data = resp.data.hits.hits
|
||||
elasticSearchMoviesAndShows(this.query).then(resp => {
|
||||
const data = resp.hits.hits;
|
||||
|
||||
this.elasticSearchResults = data.map(item => {
|
||||
const index = item._index.slice(0, -1)
|
||||
if (index === 'movie') {
|
||||
let results = data.map(item => {
|
||||
const index = item._index.slice(0, -1);
|
||||
if (index === "movie" || item._source.original_title) {
|
||||
return {
|
||||
name: item._source.original_title,
|
||||
id: item._source.id,
|
||||
type: index
|
||||
}
|
||||
} else if (index === 'show') {
|
||||
adult: item._source.adult,
|
||||
type: "movie"
|
||||
};
|
||||
} else if (index === "show" || item._source.original_name) {
|
||||
return {
|
||||
name: item._source.original_name,
|
||||
id: item._source.id,
|
||||
type: index
|
||||
}
|
||||
adult: item._source.adult,
|
||||
type: "show"
|
||||
};
|
||||
}
|
||||
})
|
||||
console.log(this.elasticSearchResults)
|
||||
})
|
||||
});
|
||||
results = this.removeDuplicates(results);
|
||||
this.elasticSearchResults = results;
|
||||
});
|
||||
},
|
||||
removeDuplicates(searchResults) {
|
||||
let filteredResults = [];
|
||||
searchResults.map(result => {
|
||||
const numberOfDuplicates = filteredResults.filter(
|
||||
filterItem => filterItem.id == result.id
|
||||
);
|
||||
if (numberOfDuplicates.length >= 1) {
|
||||
return null;
|
||||
}
|
||||
filteredResults.push(result);
|
||||
});
|
||||
|
||||
if (this.adult == false) {
|
||||
filteredResults = filteredResults.filter(
|
||||
result => result.adult == false
|
||||
);
|
||||
}
|
||||
|
||||
return filteredResults;
|
||||
},
|
||||
handleSubmit() {
|
||||
let searchResults = this.elasticSearchResults
|
||||
let searchResults = this.elasticSearchResults;
|
||||
|
||||
if (this.selectedResult > searchResults.length) {
|
||||
this.focus = false
|
||||
this.selectedResult = 0
|
||||
this.focus = false;
|
||||
this.selectedResult = 0;
|
||||
} else if (this.selectedResult > 0) {
|
||||
const resultItem = searchResults[this.selectedResult - 1]
|
||||
this.$popup.open(resultItem.id, resultItem.type)
|
||||
const resultItem = searchResults[this.selectedResult - 1];
|
||||
this.$popup.open(resultItem.id, resultItem.type);
|
||||
} else {
|
||||
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'))
|
||||
this.$router.push({ name: 'search', query: { query: encodedQuery }});
|
||||
this.focus = false
|
||||
this.selectedResult = 0
|
||||
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'));
|
||||
const media_type =
|
||||
this.selectedSearchType !== "all" ? this.selectedSearchType : null;
|
||||
this.$router.push({
|
||||
name: "search",
|
||||
query: { query: encodedQuery, adult: this.adult, media_type }
|
||||
});
|
||||
this.focus = false;
|
||||
this.selectedResult = 0;
|
||||
}
|
||||
},
|
||||
handleEscape() {
|
||||
if (this.$popup.isOpen) {
|
||||
console.log('THIS WAS FUCKOING OPEN!')
|
||||
console.log("THIS WAS FUCKOING OPEN!");
|
||||
} else {
|
||||
this.focus = false
|
||||
this.focus = false;
|
||||
}
|
||||
},
|
||||
disableFocus(_) {
|
||||
this.focus = false
|
||||
this.focus = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
@import './src/scss/main';
|
||||
|
||||
@import "./src/scss/main";
|
||||
|
||||
.fade-enter-active {
|
||||
transition: opacity .2s;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity .2s;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.filter {
|
||||
// background-color: rgba(004, 122, 125, 0.2);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1rem 2rem;
|
||||
|
||||
h2 {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-items {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
> :not(:first-child) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -179,7 +293,7 @@ export default {
|
||||
z-index: 5;
|
||||
min-height: $header-size;
|
||||
right: 0px;
|
||||
background-color: white;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
@include mobile-only {
|
||||
position: fixed;
|
||||
@@ -188,7 +302,11 @@ export default {
|
||||
width: calc(100%);
|
||||
}
|
||||
|
||||
&--results {
|
||||
.not-found {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-results {
|
||||
padding-left: 60px;
|
||||
width: 100%;
|
||||
|
||||
@@ -203,75 +321,82 @@ export default {
|
||||
width: calc(100% - 25px);
|
||||
max-width: fit-content;
|
||||
|
||||
list-style: none;
|
||||
list-style: none;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $text-color-50;
|
||||
|
||||
&.active, &:hover, &:active {
|
||||
color: $c-dark;
|
||||
border-bottom: 2px solid black;
|
||||
&.active,
|
||||
&:hover,
|
||||
&:active {
|
||||
color: $text-color;
|
||||
border-bottom: 2px solid $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search {
|
||||
height: $header-size-mobile;
|
||||
height: $header-size;
|
||||
display: flex;
|
||||
position: fixed;
|
||||
flex-wrap: wrap;
|
||||
z-index: 5;
|
||||
z-index: 16;
|
||||
border: 0;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
// TODO check if this is for mobile
|
||||
width: calc(100% - 110px);
|
||||
// width: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 55px;
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
position: relative;
|
||||
height: $header-size;
|
||||
width: 100%;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
input {
|
||||
// height: 75px;
|
||||
display: block;
|
||||
height: calc($header-size - 1.5rem);
|
||||
width: 100%;
|
||||
padding: 13px 20px 13px 45px;
|
||||
padding: 13px 0 13px 45px;
|
||||
outline: none;
|
||||
margin: 0;
|
||||
margin-bottom: auto;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: $c-dark;
|
||||
background-color: $background-color-secondary;
|
||||
font-weight: 300;
|
||||
font-size: 19px;
|
||||
color: $text-color;
|
||||
transition: background-color 0.5s ease, color 0.5s ease;
|
||||
|
||||
@include tablet-min {
|
||||
height: calc($header-size);
|
||||
padding: 13px 30px 13px 60px;
|
||||
}
|
||||
}
|
||||
|
||||
&--icon{
|
||||
&-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: rgba($c-dark, 0.5);
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 15px;
|
||||
|
||||
@include tablet-min{
|
||||
@include tablet-min {
|
||||
top: 27px;
|
||||
left: 25px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -3,25 +3,33 @@
|
||||
<div class="profile__content" v-if="userLoggedIn">
|
||||
<section class='settings'>
|
||||
<h3 class='settings__header'>Plex account</h3>
|
||||
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
|
||||
|
||||
<form class="form">
|
||||
<seasoned-input text="plex username" icon="Email"
|
||||
@inputValue="setValue('plexUsername', $event)"/>
|
||||
<seasoned-input text="plex password" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('plexPassword', $event)"/>
|
||||
<div v-if="!hasPlexUser">
|
||||
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
|
||||
|
||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||
</form>
|
||||
<form class="form">
|
||||
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
||||
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
|
||||
:value.sync="plexPassword" @submit="authenticatePlex" />
|
||||
|
||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||
</form>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="settings__info">Awesome, your account is already authenticated with plex! Enjoy viewing your seasoned search history, plex watch history and real-time torrent download progress.</span>
|
||||
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button>
|
||||
</div>
|
||||
<seasoned-messages :messages.sync="messages" />
|
||||
|
||||
<hr class='setting__divider'>
|
||||
|
||||
<h3 class='settings__header'>Change password</h3>
|
||||
<form class="form">
|
||||
<seasoned-input text="new password" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('newPass', $event)"/>
|
||||
<seasoned-input text="repeat new password" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('newPassConfirm', $event)"/>
|
||||
<seasoned-input placeholder="new password" icon="Keyhole" type="password"
|
||||
:value.sync="newPassword" />
|
||||
|
||||
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password"
|
||||
:value.sync="newPasswordRepeat" />
|
||||
|
||||
<seasoned-button @click="changePassword">change password</seasoned-button>
|
||||
</form>
|
||||
@@ -42,49 +50,78 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput.vue'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import store from '@/store'
|
||||
import storage from '@/storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
|
||||
import { plexAuthenticate } from '@/api.js'
|
||||
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
|
||||
|
||||
export default {
|
||||
components: { SeasonedInput, SeasonedButton },
|
||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
plexUsername: undefined,
|
||||
plexPassword: undefined,
|
||||
newPass: undefined,
|
||||
newPassConfirm: undefined
|
||||
messages: [],
|
||||
plexUsername: null,
|
||||
plexPassword: null,
|
||||
newPassword: null,
|
||||
newPasswordRepeat: null,
|
||||
emoji: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasPlexUser: function() {
|
||||
return this.settings && this.settings['plex_userid']
|
||||
},
|
||||
settings: {
|
||||
get: () => {
|
||||
return store.getters['userModule/settings']
|
||||
},
|
||||
set: function(newSettings) {
|
||||
store.dispatch('userModule/setSettings', newSettings)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue(l, t) {
|
||||
console.log('l, t', l, t)
|
||||
this[l] = t
|
||||
},
|
||||
changePassword() {
|
||||
return
|
||||
},
|
||||
authenticatePlex() {
|
||||
async authenticatePlex() {
|
||||
let username = this.plexUsername
|
||||
let password = this.plexPassword
|
||||
|
||||
plexAuthenticate(username, password)
|
||||
.then((resp) => {
|
||||
let data = resp.data;
|
||||
console.log('response from plex:', data.user)
|
||||
const response = await linkPlexAccount(username, password)
|
||||
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log('error: ', error)
|
||||
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
},
|
||||
async unauthenticatePlex() {
|
||||
const response = await unlinkPlexAccount()
|
||||
|
||||
this.messages.push({
|
||||
type: response.success ? 'success' : 'error',
|
||||
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
|
||||
message: response.message
|
||||
})
|
||||
|
||||
if (response.success)
|
||||
getSettings().then(settings => this.settings = settings)
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'Settings' + storage.pageTitlePostfix;
|
||||
storage.backTitle = document.title;
|
||||
if (localStorage.getItem('token')){
|
||||
const token = localStorage.getItem('token') || false;
|
||||
if (token){
|
||||
this.userLoggedIn = true
|
||||
}
|
||||
}
|
||||
@@ -94,6 +131,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -123,12 +161,16 @@ a {
|
||||
}
|
||||
}
|
||||
.settings {
|
||||
padding: 35px;
|
||||
padding: 3rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
&__header {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
@@ -141,7 +183,7 @@ a {
|
||||
display: block;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(8, 28, 36, 0.05);
|
||||
border-bottom: 1px solid $text-color-50;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 70px;
|
||||
margin-left: 20px;
|
||||
|
||||
@@ -1,158 +1,114 @@
|
||||
<template>
|
||||
<section class="profile">
|
||||
<div class="profile__content">
|
||||
<h2 class='settings__header'>Sign in</h2>
|
||||
<section>
|
||||
<h1>Sign in</h1>
|
||||
|
||||
<form class="form">
|
||||
<div class="form__buffer"></div>
|
||||
<seasoned-input placeholder="username"
|
||||
icon="Email"
|
||||
type="email"
|
||||
@enter="submit"
|
||||
:value.sync="username" />
|
||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
|
||||
|
||||
<seasoned-input text="username" icon="Email" type="username"
|
||||
@inputValue="setValue('username', $event)" />
|
||||
<seasoned-input text="username" icon="Keyhole" type="password"
|
||||
@inputValue="setValue('password', $event)" />
|
||||
<seasoned-button @click="submit">sign in</seasoned-button>
|
||||
<router-link class="link" to="/register">Don't have a user? Register here</router-link>
|
||||
|
||||
<seasoned-button @click="signin">sign in</seasoned-button>
|
||||
|
||||
<transition name="message-fade">
|
||||
<div class="message" :class="messageClass" v-if="showMessage">
|
||||
<span class="message-text">{{ messageText }}</span>
|
||||
<span class="message-dismiss" @click="showMessage=false">X</span>
|
||||
</div>
|
||||
</transition>
|
||||
</form>
|
||||
|
||||
<div class="form__group">
|
||||
<router-link class="form__group-link" :to="{name: 'register'}" exact title="Sign in here">
|
||||
<span class="form__group-signin">Don't have a user? Register here</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<seasoned-messages :messages.sync="messages"></seasoned-messages>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import storage from '../storage.js'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput.vue'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton.vue'
|
||||
import { login } from '@/api'
|
||||
import storage from '../storage'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||
import { parseJwt } from '@/utils'
|
||||
|
||||
export default {
|
||||
components: { SeasonedInput, SeasonedButton },
|
||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||
data(){
|
||||
return{
|
||||
userLoggedIn: '',
|
||||
showMessage: false,
|
||||
messageClass: 'message-success',
|
||||
messageText: 'hello world',
|
||||
username: undefined,
|
||||
password: undefined
|
||||
messages: [],
|
||||
username: null,
|
||||
password: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setValue(l, t) {
|
||||
this[l] = t
|
||||
},
|
||||
signin(){
|
||||
submit() {
|
||||
this.messages = [];
|
||||
let username = this.username;
|
||||
let password = this.password;
|
||||
|
||||
axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
|
||||
username: username,
|
||||
password: password
|
||||
})
|
||||
.then(function (resp){
|
||||
let data = resp.data;
|
||||
if (data.success){
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', username);
|
||||
localStorage.setItem('admin', data.admin);
|
||||
this.userLoggedIn = true;
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
}.bind(this))
|
||||
.catch(function (error){
|
||||
if (error.message.endsWith('401'))
|
||||
this.msg('Incorrect username or password ', 'warning')
|
||||
else
|
||||
this.msg(error.message, 'warning')
|
||||
}.bind(this));
|
||||
},
|
||||
msg(text, status){
|
||||
if (status === 'warning')
|
||||
this.messageClass = 'message-warning';
|
||||
else if (status === 'success')
|
||||
this.messageClass = 'message-success';
|
||||
else
|
||||
this.messageClass = 'message-info';
|
||||
this.messageText = text;
|
||||
this.showMessage = true;
|
||||
// setTimeout(() => this.showMessage = false, 3500);
|
||||
},
|
||||
toggleView(){
|
||||
this.register = false;
|
||||
if (username == null || username.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing username' })
|
||||
return
|
||||
}
|
||||
|
||||
if (password == null || password.length == 0) {
|
||||
this.messages.push({ type: 'error', title: 'Missing password' })
|
||||
return
|
||||
}
|
||||
|
||||
this.signin(username, password)
|
||||
},
|
||||
signin(username, password) {
|
||||
login(username, password, true)
|
||||
.then(data => {
|
||||
if (data.success){
|
||||
const jwtData = parseJwt(data.token)
|
||||
localStorage.setItem('token', data.token);
|
||||
localStorage.setItem('username', jwtData['username']);
|
||||
localStorage.setItem('admin', jwtData['admin'] || false);
|
||||
|
||||
eventHub.$emit('setUserStatus');
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 401) {
|
||||
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
|
||||
}
|
||||
else {
|
||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
created(){
|
||||
document.title = 'Sign in' + storage.pageTitlePostfix;
|
||||
storage.backTitle = document.title;
|
||||
if (this.userLoggedIn == true) {
|
||||
this.$router.push({ name: 'profile' })
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
// this.$refs.email.focus();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/message";
|
||||
|
||||
// DUPLICATE CODE
|
||||
.settings {
|
||||
padding: 35px;
|
||||
section {
|
||||
padding: 1.3rem;
|
||||
|
||||
&__header {
|
||||
@include tablet-min {
|
||||
padding: 4rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
line-height: 16px;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.profile__content {
|
||||
padding: 35px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form {
|
||||
> div:last-child {
|
||||
.link {
|
||||
display: block;
|
||||
width: max-content;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
&__group{
|
||||
justify-content: unset;
|
||||
&__input-icon {
|
||||
margin-top: 8px;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
&-input {
|
||||
padding: 10px 5px 10px 45px;
|
||||
height: 40px;
|
||||
font-size: 17px;
|
||||
width: 75%;
|
||||
// @include desktop-min {
|
||||
// width: 400px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,53 +1,108 @@
|
||||
<template>
|
||||
<div v-if="show">
|
||||
<h2 class="title">torrents: {{ query }}</h2>
|
||||
<div v-if="show" class="container">
|
||||
<h2 class="torrentHeader-text">Searching for: {{ editedSearchQuery || query }}</h2>
|
||||
<!-- <div class="torrentHeader">
|
||||
<span class="torrentHeader-text">Searching for: </span>
|
||||
|
||||
|
||||
<span id="search" :contenteditable="editSearchQuery ? true : false" class="torrentHeader-text editable">{{ editedSearchQuery || query }}</span>
|
||||
|
||||
|
||||
<svg v-if="!editSearchQuery" class="torrentHeader-editIcon" @click="toggleEditSearchQuery">
|
||||
<use xlink:href="#icon_radar"></use>
|
||||
</svg>
|
||||
|
||||
<svg v-else class="torrentHeader-editIcon" @click="toggleEditSearchQuery">
|
||||
<use xlink:href="#icon_check"></use>
|
||||
</svg>
|
||||
|
||||
</div> -->
|
||||
|
||||
<div v-if="listLoaded">
|
||||
<ul class="filter">
|
||||
<li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li>
|
||||
</ul>
|
||||
<div v-if="torrents.length > 0">
|
||||
<!-- <ul class="filter">
|
||||
<li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li>
|
||||
</ul> -->
|
||||
|
||||
<toggle-button :options="release_types" :selected.sync="selectedRelaseType" class="toggle"></toggle-button>
|
||||
|
||||
|
||||
<table>
|
||||
<tr class="table__header noselect">
|
||||
<th @click="sortTable('name')">
|
||||
<span>Name</span>
|
||||
<span v-if="prevCol === 'name' && direction">↑</span>
|
||||
<span v-if="prevCol === 'name' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('seed')">
|
||||
<span>Seed</span>
|
||||
<span v-if="prevCol === 'seed' && direction">↑</span>
|
||||
<span v-if="prevCol === 'seed' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('size')">
|
||||
<span>Size</span>
|
||||
<span v-if="prevCol === 'size' && direction">↑</span>
|
||||
<span v-if="prevCol === 'size' && !direction">↓</span>
|
||||
<th>
|
||||
<span>Magnet</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="torrent in torrents" class="table__content">
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
||||
<td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download">
|
||||
<svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr class="table__header noselect">
|
||||
<th @click="sortTable('name')" :class="selectedSortableClass('name')">
|
||||
<span>Name</span>
|
||||
<span v-if="prevCol === 'name' && direction">↑</span>
|
||||
<span v-if="prevCol === 'name' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('seed')" :class="selectedSortableClass('seed')">
|
||||
<span>Seed</span>
|
||||
<span v-if="prevCol === 'seed' && direction">↑</span>
|
||||
<span v-if="prevCol === 'seed' && !direction">↓</span>
|
||||
</th>
|
||||
<th @click="sortTable('size')" :class="selectedSortableClass('size')">
|
||||
<span>Size</span>
|
||||
<span v-if="prevCol === 'size' && direction">↑</span>
|
||||
<span v-if="prevCol === 'size' && !direction">↓</span>
|
||||
<th>
|
||||
<span>Magnet</span>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-for="torrent in torrents" class="table__content">
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.name }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
|
||||
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
|
||||
<td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download">
|
||||
<svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
">
|
||||
<seasonedButton @click="resetTorrentsAndToggleEditSearchQuery">Edit search query</seasonedButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else style="display: flex;
|
||||
padding-bottom: 2rem;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;">
|
||||
<h2>No results found</h2>
|
||||
<br />
|
||||
|
||||
<div class="editQuery" v-if="editSearchQuery">
|
||||
|
||||
<seasonedInput placeholder="Torrent query" icon="_torrents" :value.sync="editedSearchQuery" @enter="fetchTorrents(editedSearchQuery)" />
|
||||
|
||||
<div style="height: 45px; width: 5px;"></div>
|
||||
|
||||
<seasonedButton @click="fetchTorrents(editedSearchQuery)">Search</seasonedButton>
|
||||
</div>
|
||||
|
||||
<seasonedButton @click="toggleEditSearchQuery" :active="editSearchQuery ? true : false">Edit search query</seasonedButton>
|
||||
</div>
|
||||
<i v-else class="torrentloader"></i>
|
||||
</div>
|
||||
<div v-else class="torrentloader"><i></i></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@/storage.js'
|
||||
import { sortableSize } from '@/utils.js'
|
||||
import { searchTorrents, addMagnet } from '@/api.js'
|
||||
import storage from '@/storage'
|
||||
import store from '@/store'
|
||||
import { sortableSize } from '@/utils'
|
||||
import { searchTorrents, addMagnet } from '@/api'
|
||||
|
||||
import SeasonedButton from '@/components/ui/SeasonedButton'
|
||||
import SeasonedInput from '@/components/ui/SeasonedInput'
|
||||
import ToggleButton from '@/components/ui/ToggleButton'
|
||||
|
||||
export default {
|
||||
components: { SeasonedButton, SeasonedInput, ToggleButton },
|
||||
props: {
|
||||
query: {
|
||||
type: String,
|
||||
@@ -58,51 +113,73 @@ export default {
|
||||
require: true
|
||||
},
|
||||
tmdb_type: String,
|
||||
admin: String,
|
||||
admin: Boolean,
|
||||
show: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listLoaded: false,
|
||||
torrents: undefined,
|
||||
torrents: [],
|
||||
torrentResponse: undefined,
|
||||
currentPage: 0,
|
||||
prevCol: '',
|
||||
direction: false,
|
||||
release_types: ['all'],
|
||||
selectedRelaseType: 'all'
|
||||
selectedRelaseType: 'all',
|
||||
editSearchQuery: false,
|
||||
editedSearchQuery: ''
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
if (localStorage.getItem('admin')) {
|
||||
this.fetchTorrents()
|
||||
}
|
||||
store.dispatch('torrentModule/reset')
|
||||
},
|
||||
watch: {
|
||||
selectedRelaseType: function(newValue) {
|
||||
this.applyFilter(newValue)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectedSortableClass(headerName) {
|
||||
return headerName === this.prevCol ? 'active' : ''
|
||||
},
|
||||
resetTorrentsAndToggleEditSearchQuery() {
|
||||
this.torrents = []
|
||||
this.toggleEditSearchQuery()
|
||||
},
|
||||
toggleEditSearchQuery() {
|
||||
this.editSearchQuery = !this.editSearchQuery;
|
||||
},
|
||||
expand(event, name) {
|
||||
const existingExpandedElement = document.getElementsByClassName('expanded')[0]
|
||||
|
||||
const clickedElement = event.target.parentNode;
|
||||
const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]
|
||||
|
||||
if (existingExpandedElement) {
|
||||
console.log('exists')
|
||||
const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded'
|
||||
|
||||
existingExpandedElement.remove()
|
||||
const table = document.getElementsByTagName('table')[0]
|
||||
table.style.display = 'block'
|
||||
|
||||
if (expandedSibling) {
|
||||
console.log('sibling is here')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log('expand event', event)
|
||||
const nameRow = document.createElement('tr')
|
||||
const nameCol = document.createElement('td')
|
||||
nameRow.className = 'expanded'
|
||||
nameRow.dataset[scopedStyleDataVariable] = "";
|
||||
nameCol.innerText = name
|
||||
nameCol.dataset[scopedStyleDataVariable] = "";
|
||||
|
||||
nameRow.appendChild(nameCol)
|
||||
|
||||
event.target.parentNode.insertAdjacentElement('afterend', nameRow)
|
||||
clickedElement.insertAdjacentElement('afterend', nameRow)
|
||||
},
|
||||
sendTorrent(magnet, name, event){
|
||||
this.$notifications.info({
|
||||
@@ -112,7 +189,6 @@ export default {
|
||||
})
|
||||
|
||||
event.target.parentNode.classList.add('active')
|
||||
|
||||
addMagnet(magnet, name, this.tmdb_id)
|
||||
.catch((resp) => { console.log('error:', resp.data) })
|
||||
.then((resp) => {
|
||||
@@ -128,7 +204,6 @@ export default {
|
||||
if (this.prevCol === col && sameDirection === false) {
|
||||
this.direction = !this.direction
|
||||
}
|
||||
console.log('col and more', col, sameDirection)
|
||||
|
||||
switch (col) {
|
||||
case 'name':
|
||||
@@ -184,38 +259,43 @@ export default {
|
||||
this.torrents = torrents.filter(torrent => torrent.release_type.includes(item))
|
||||
this.sortTable(this.prevCol, true)
|
||||
},
|
||||
fetchTorrents(){
|
||||
searchTorrents(this.query, 'all', this.currentPage, storage.token)
|
||||
.then(resp => {
|
||||
let data = resp.data;
|
||||
console.log('data results', data.results);
|
||||
this.torrentResponse = data.results;
|
||||
this.torrents = data.results;
|
||||
updateResultCountInStore() {
|
||||
store.dispatch('torrentModule/setResults', this.torrents)
|
||||
store.dispatch('torrentModule/setResultCount', this.torrentResponse.length)
|
||||
},
|
||||
fetchTorrents(query=undefined){
|
||||
this.listLoaded = false;
|
||||
this.editSearchQuery = false;
|
||||
|
||||
searchTorrents(query || this.query, 'all', this.currentPage, storage.token)
|
||||
.then(data => {
|
||||
this.torrentResponse = [...data.results];
|
||||
this.torrents = data.results;
|
||||
this.listLoaded = true;
|
||||
})
|
||||
.then(this.updateResultCountInStore)
|
||||
.then(this.findRelaseTypes)
|
||||
.catch(e => {
|
||||
const error = e.toString()
|
||||
this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found';
|
||||
this.listLoaded = true;
|
||||
})
|
||||
.then(this.findRelaseTypes)
|
||||
.catch(e => {
|
||||
const error = e.toString()
|
||||
this.errorMessage = error.indexOf('401') != -1 ? 'Permission denied' : 'Nothing found';
|
||||
this.listLoaded = true;
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
.expanded {
|
||||
display: flex;
|
||||
margin: 0 1rem;
|
||||
padding: 0.25rem 1rem;
|
||||
max-width: 100%;
|
||||
border-left: 1px solid rgba($c-dark, 0.5);
|
||||
border-right: 1px solid rgba($c-dark, 0.5);
|
||||
border-bottom: 1px solid rgba($c-dark, 0.5);
|
||||
border-left: 1px solid $text-color;
|
||||
border-right: 1px solid $text-color;
|
||||
border-bottom: 1px solid $text-color;
|
||||
|
||||
td {
|
||||
// border-left: 1px solid $c-dark;
|
||||
word-break: break-all;
|
||||
padding: 0.5rem 0.15rem;
|
||||
width: 100%;
|
||||
@@ -227,16 +307,50 @@ export default {
|
||||
@import "./src/scss/media-queries";
|
||||
@import "./src/scss/elements";
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: $c-green;
|
||||
.toggle {
|
||||
max-width: unset !important;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: $background-color;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.torrentHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 20px;
|
||||
@include tablet-min{
|
||||
font-size: 16px;
|
||||
|
||||
|
||||
&-text {
|
||||
font-weight: 400;
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: $green;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
&.editable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&-editIcon {
|
||||
margin-left: 10px;
|
||||
margin-top: -3px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
|
||||
&:hover {
|
||||
fill: $green;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,10 +363,9 @@ table {
|
||||
.table__content, .table__header {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0 1rem;
|
||||
border-left: 1px solid rgba($c-dark, 0.8);
|
||||
border-right: 1px solid rgba($c-dark, 0.8);
|
||||
border-bottom: 1px solid rgba($c-dark, 0.8);
|
||||
border-left: 1px solid $text-color;
|
||||
border-right: 1px solid $text-color;
|
||||
border-bottom: 1px solid $text-color;
|
||||
|
||||
th, td {
|
||||
display: flex;
|
||||
@@ -264,6 +377,7 @@ table {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
min-width: 75px;
|
||||
}
|
||||
|
||||
th:first-child, td:first-child {
|
||||
@@ -297,7 +411,7 @@ table {
|
||||
|
||||
.table__content {
|
||||
td:not(:last-child) {
|
||||
border-right: 1px solid rgba($c-dark, 0.8);
|
||||
border-right: 1px solid $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,12 +423,12 @@ table {
|
||||
}
|
||||
|
||||
.table__header {
|
||||
background-color: white;
|
||||
color: $c-dark;
|
||||
color: $text-color;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
background-color: $background-color-secondary;
|
||||
|
||||
border-top: 1px solid rgba($c-dark, 0.8);
|
||||
border-top: 1px solid $text-color;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
|
||||
@@ -340,47 +454,66 @@ table {
|
||||
}
|
||||
|
||||
th:not(:last-child) {
|
||||
border-right: 1px solid rgba($c-dark, 0.8);
|
||||
border-right: 1px solid $text-color;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.editQuery {
|
||||
display: flex;
|
||||
width: 70%;
|
||||
justify-content: center;
|
||||
|
||||
@include mobile-only {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.download {
|
||||
|
||||
&__icon {
|
||||
fill: rgba($c-dark, 0.6);
|
||||
fill: $text-color-70;
|
||||
height: 1.2rem;
|
||||
|
||||
&:hover {
|
||||
fill: $c-dark;
|
||||
fill: $text-color;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&.active &__icon {
|
||||
fill: $c-green;
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.torrentloader{
|
||||
animation: load 1s linear infinite;
|
||||
border: 2px solid $c-dark;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
margin: 2rem auto;
|
||||
width: 30px;
|
||||
&:after {
|
||||
border: 5px solid $c-green;
|
||||
.torrentloader {
|
||||
width: 100%;
|
||||
padding: 2rem 0;
|
||||
|
||||
i {
|
||||
animation: load 1s linear infinite;
|
||||
border: 2px solid $text-color;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
display: block;
|
||||
height: 30px;
|
||||
left: 50%;
|
||||
margin: 0 auto;
|
||||
width: 30px;
|
||||
|
||||
&:after {
|
||||
border: 5px solid $green;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
left: 10px;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
<template>
|
||||
<div class="action">
|
||||
<a class="action-link" :class="{'active': active}" @click="$emit('click')">
|
||||
<svg class="action-icon">
|
||||
<use v-if="active && iconRefActive" :xlink:href="iconRefActive"></use>
|
||||
<use v-else :xlink:href="iconRef"></use>
|
||||
</svg>
|
||||
<span class="action-text">{{ active && textActive ? textActive : text }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
iconRef: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
iconRefActive: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
textActive: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/loading-placeholder";
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.action {
|
||||
&-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: rgba($c-dark, 0.5);
|
||||
transition: color 0.5s ease;
|
||||
font-size: 11px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid rgba($c-dark, 0.05);
|
||||
&:hover {
|
||||
color: rgba($c-dark, 0.75);
|
||||
}
|
||||
&.active {
|
||||
color: $c-dark;
|
||||
}
|
||||
&.pending {
|
||||
color: #f8bd2d;
|
||||
}
|
||||
}
|
||||
&-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 0 10px 0 0;
|
||||
fill: rgba($c-dark, 0.5);
|
||||
transition: fill 0.5s ease, transform 0.5s ease;
|
||||
&.waiting {
|
||||
transform: scale(0.8, 0.8);
|
||||
}
|
||||
&.pending {
|
||||
fill: #f8bd2d;
|
||||
}
|
||||
}
|
||||
&-link:hover &-icon {
|
||||
fill: rgba($c-dark, 0.75);
|
||||
cursor: pointer;
|
||||
}
|
||||
&-link.active &-icon {
|
||||
fill: $c-green;
|
||||
}
|
||||
&-text {
|
||||
display: block;
|
||||
padding-top: 2px;
|
||||
cursor: pointer;
|
||||
margin:4.4px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -17,29 +17,26 @@
|
||||
align-items: center;
|
||||
|
||||
&--icon{
|
||||
border: 2px solid rgba($c-dark, 0.9);
|
||||
border: 2px solid $text-color-70;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
height: 40px;
|
||||
// left: 50%;
|
||||
// margin: -1.5em;
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
|
||||
&-spinner {
|
||||
// border: 2px solid transparent;
|
||||
display: block;
|
||||
display: block;
|
||||
animation: load 1s linear infinite;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
&:after {
|
||||
border: 7px solid rgba($c-green, 0.9);
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
left: 8px;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
}
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
&:after {
|
||||
border: 7px solid $green-90;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
left: 8px;
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes load {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="seasoned-button">
|
||||
<button type="button" class="button" @click="emit('click')" :class="{ active: active }"><slot></slot></button>
|
||||
</div>
|
||||
<button type="button" @click="emit('click')" :class="{ active: active }">
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -9,11 +9,14 @@
|
||||
export default {
|
||||
name: 'seasonedButton',
|
||||
props: {
|
||||
active: Boolean
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
emit() {
|
||||
console.log('emitted')
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
@@ -24,38 +27,39 @@ export default {
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.button{
|
||||
button {
|
||||
display: inline-block;
|
||||
border: 1px solid $c-dark;
|
||||
text-transform: uppercase;
|
||||
background: $c-dark;
|
||||
font-weight: 300;
|
||||
border: 1px solid $text-color;
|
||||
font-size: 11px;
|
||||
line-height: 2;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 5px 20px 4px 20px;
|
||||
text-transform: uppercase;
|
||||
min-height: 45px;
|
||||
padding: 5px 10px 4px 10px;
|
||||
margin: 0;
|
||||
margin-right: 0.3rem;
|
||||
color: $text-color;
|
||||
background: $background-color-secondary;
|
||||
cursor: pointer;
|
||||
color: $c-dark;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
transition: background 0.5s ease, color 0.5s ease;
|
||||
transition: background 0.5s ease, color 0.5s ease, border-color .5s ease;
|
||||
|
||||
@include tablet-min{
|
||||
font-size: 12px;
|
||||
@include desktop {
|
||||
font-size: 0.8rem;
|
||||
padding: 6px 20px 5px 20px;
|
||||
}
|
||||
&:active, &:hover{
|
||||
background: $c-dark;
|
||||
color: $c-white;
|
||||
|
||||
&:focus, &:active, &.active {
|
||||
background: $text-color;
|
||||
color: $background-color;
|
||||
}
|
||||
body:not(.touch) &:hover, &:focus{
|
||||
background: $c-dark;
|
||||
color: $c-white;
|
||||
}
|
||||
&.active {
|
||||
@extend .button;
|
||||
background: $c-dark;
|
||||
color: $c-white;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background: $text-color;
|
||||
color: $background-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
<template>
|
||||
<div class="group" :class="{ completed: value.length > 0 }">
|
||||
<div class="group" :class="{ completed: value }">
|
||||
<svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg>
|
||||
<input class="group__input" :type="tempType || type" ref="plex_username"
|
||||
v-model="value" :placeholder="text" @input="handleInput" />
|
||||
<input class="group__input" :type="tempType || type" @input="handleInput" v-model="inputValue"
|
||||
:placeholder="placeholder" @keyup.enter="submit" />
|
||||
|
||||
<i v-if="value.length > 0 && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
|
||||
<i v-if="value && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
text: { type: String },
|
||||
placeholder: { type: String },
|
||||
icon: { type: String },
|
||||
type: { type: String }
|
||||
type: { type: String, default: 'text' },
|
||||
value: { type: String, default: undefined }
|
||||
},
|
||||
data() {
|
||||
return { value: '', tempType: undefined }
|
||||
return {
|
||||
inputValue: this.value || undefined,
|
||||
tempType: undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInput(value) {
|
||||
console.log('this.value', this.value)
|
||||
this.$emit('inputValue', this.value)
|
||||
submit(event) {
|
||||
this.$emit('enter')
|
||||
},
|
||||
handleInput(event) {
|
||||
if (this.value !== undefined) {
|
||||
this.$emit('update:value', this.inputValue)
|
||||
} else {
|
||||
this.$emit('change', this.inputValue, event)
|
||||
}
|
||||
},
|
||||
toggleShowPassword() {
|
||||
if (this.tempType === 'text') {
|
||||
@@ -40,42 +50,45 @@ export default {
|
||||
.group{
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
width: 100%;
|
||||
|
||||
&:hover, &:focus {
|
||||
.group__input {
|
||||
border-color: $c-dark;
|
||||
border-color: $text-color;
|
||||
|
||||
&-icon {
|
||||
fill: $c-dark;
|
||||
fill: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.completed {
|
||||
.group__input {
|
||||
border-color: $c-dark;
|
||||
border-color: $text-color;
|
||||
|
||||
&-icon {
|
||||
fill: $c-dark;
|
||||
fill: $text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
width: 75%;
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
padding: 10px 10px 10px 45px;
|
||||
// padding: 15px 10px 15px 45px;
|
||||
outline: none;
|
||||
background-color: $c-white;
|
||||
color: $c-dark;
|
||||
background-color: $background-color-secondary;
|
||||
color: $text-color;
|
||||
font-weight: 100;
|
||||
font-size: 1.2rem;
|
||||
border: 1px solid rgba($c-dark, 0.5);
|
||||
margin-left: -2.2rem;
|
||||
// margin-bottom: 1rem;
|
||||
border: 1px solid $text-color-50;
|
||||
margin: 0;
|
||||
margin-left: -2.2rem !important;
|
||||
z-index: 3;
|
||||
transition: border-color .5s ease;
|
||||
transition: color .5s ease, background-color .5s ease, border .5s ease;
|
||||
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&-show {
|
||||
position: relative;
|
||||
@@ -85,14 +98,14 @@ export default {
|
||||
height: 100%;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
color: rgba($c-dark, 0.5);
|
||||
color: $text-color-50;
|
||||
}
|
||||
}
|
||||
|
||||
&__input-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
fill: rgba($c-dark, 0.5);
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease;
|
||||
pointer-events: none;
|
||||
margin-top: 10px;
|
||||
|
||||
161
src/components/ui/SeasonedMessages.vue
Normal file
161
src/components/ui/SeasonedMessages.vue
Normal file
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<transition-group name="fade">
|
||||
<div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index">
|
||||
<span class="pinstripe"></span>
|
||||
<div>
|
||||
<h2 class="title">{{ message.title || defaultTitles[message.type] }}</h2>
|
||||
<span v-if="message.message" class="message">{{ message.message }}</span>
|
||||
</div>
|
||||
|
||||
<button class="dismiss" @click="clicked(message)">X</button>
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: {
|
||||
messages: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
defaultTitles: {
|
||||
error: 'Unexpected error',
|
||||
warning: 'Something went wrong',
|
||||
undefined: 'Something went wrong'
|
||||
},
|
||||
localMessages: [...this.messages]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
reversedMessages() {
|
||||
return [...this.messages].reverse()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clicked(e) {
|
||||
const removedMessage = [...this.messages].filter(mes => mes !== e)
|
||||
this.$emit('update:messages', removedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.fade-enter-active {
|
||||
transition: opacity .4s;
|
||||
}
|
||||
.fade-leave-active {
|
||||
transition: opacity .1s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.message {
|
||||
width: 100%;
|
||||
max-width: 35rem;
|
||||
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: $text-color-70;
|
||||
|
||||
> div {
|
||||
margin: 10px 24px;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.25px;
|
||||
margin: 0;
|
||||
font-size: 1.3rem;
|
||||
color: $text-color;
|
||||
transition: color .5s ease;
|
||||
}
|
||||
.message {
|
||||
font-weight: 300;
|
||||
color: $text-color-70;
|
||||
transition: color .5s ease;
|
||||
margin: 0.2rem 0 0.5rem;
|
||||
}
|
||||
|
||||
@include mobile-only {
|
||||
> div {
|
||||
margin: 6px 6px;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
span {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.pinstripe {
|
||||
width: 0.5rem;
|
||||
background-color: $color-error-highlight;
|
||||
}
|
||||
|
||||
.dismiss {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: transparent;
|
||||
border: unset;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
|
||||
top: 0;
|
||||
float: right;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
padding: 0;
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
color: $text-color-70;
|
||||
transition: color .5s ease;
|
||||
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
background-color: $color-success;
|
||||
|
||||
.pinstripe {
|
||||
background-color: $color-success-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
background-color: $color-error;
|
||||
|
||||
.pinstripe {
|
||||
background-color: $color-error-highlight;
|
||||
}
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: $color-warning;
|
||||
|
||||
.pinstripe {
|
||||
background-color: $color-warning-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
100
src/components/ui/ToggleButton.vue
Normal file
100
src/components/ui/ToggleButton.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="toggle-container">
|
||||
<button v-for="option in options" class="toggle-button" @click="toggle(option)"
|
||||
:class="toggleValue === option ? 'selected' : null"
|
||||
>{{ option }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
options: {
|
||||
Array,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleValue: this.selected || this.options[0]
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
this.toggle(this.toggleValue)
|
||||
},
|
||||
methods: {
|
||||
toggle(toggleValue) {
|
||||
this.toggleValue = toggleValue;
|
||||
if (this.selected !== undefined) {
|
||||
this.$emit('update:selected', toggleValue)
|
||||
} else {
|
||||
this.$emit('change', toggleValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
|
||||
$background: $background-ui;
|
||||
$background-selected: $background-color-secondary;
|
||||
|
||||
.toggle-container {
|
||||
width: 100%;
|
||||
max-width: 15rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// padding: 0.2rem;
|
||||
background-color: $background;
|
||||
border: 2px solid $background;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid $background;
|
||||
border-right: 4px solid $background;
|
||||
|
||||
.toggle-button {
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
font-weight: normal;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0;
|
||||
border: 0;
|
||||
color: $text-color;
|
||||
// background-color: $text-color-5;
|
||||
background-color: $background;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.selected {
|
||||
color: $text-color;
|
||||
// background-color: $background-color-secondary;
|
||||
background-color: $background-selected;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
// &:first-of-type, &:last-of-type {
|
||||
// border-left: 4px solid $background;
|
||||
// border-right: 4px solid $background;
|
||||
// }
|
||||
|
||||
|
||||
// &:first-of-type {
|
||||
// border-top-left-radius: 4px;
|
||||
// border-bottom-left-radius: 4px;
|
||||
// }
|
||||
|
||||
// &:last-of-type {
|
||||
// border-top-right-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
57
src/components/ui/darkmodeToggle.vue
Normal file
57
src/components/ui/darkmodeToggle.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="darkToggle">
|
||||
<span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
darkmode: this.supported
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleDarkmode() {
|
||||
this.darkmode = !this.darkmode;
|
||||
document.body.className = this.darkmode ? "dark" : "light";
|
||||
},
|
||||
supported() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle["colorScheme"] != null)
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
return false;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
darkmodeToggleIcon() {
|
||||
return this.darkmode ? "🌝" : "🌚";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/media-queries";
|
||||
.darkToggle {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
cursor: pointer;
|
||||
// background-color: red;
|
||||
position: fixed;
|
||||
margin-bottom: 10px;
|
||||
margin-right: 2px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
@include mobile-only {
|
||||
margin-bottom: 5rem;
|
||||
}
|
||||
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
138
src/components/ui/sidebarListElem.vue
Normal file
138
src/components/ui/sidebarListElem.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div>
|
||||
<a @click="$emit('click')">
|
||||
<li>
|
||||
<figure v-if="iconRef" :class="activeClassIfActive">
|
||||
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
|
||||
</figure>
|
||||
|
||||
<span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
|
||||
|
||||
<span v-if="supplementaryText" class="supplementary-text">
|
||||
{{ supplementaryText }}
|
||||
</span>
|
||||
</li>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// TODO if a image is hovered and we can't set the hover color we want to
|
||||
// go into it and change the fill
|
||||
export default {
|
||||
props: {
|
||||
iconRef: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
iconRefActive: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
textActive: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
supplementaryText: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconRefNameIfActive() {
|
||||
const { iconRefActive, iconRef, active } = this
|
||||
|
||||
if ((iconRefActive && iconRef) && active) {
|
||||
return iconRefActive
|
||||
}
|
||||
return iconRef
|
||||
},
|
||||
contentTextToDisplay() {
|
||||
const { textActive, active, $slots } = this
|
||||
|
||||
if (textActive && active)
|
||||
return textActive
|
||||
|
||||
if ($slots.default && $slots.default.length > 0)
|
||||
return $slots.default[0].text
|
||||
|
||||
return ''
|
||||
},
|
||||
activeClassIfActive() {
|
||||
return this.active ? 'active' : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "./src/scss/variables";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
color: $text-color-50;
|
||||
transition: color 0.5s ease;
|
||||
font-size: 11px;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid $text-color-5;
|
||||
|
||||
&:hover {
|
||||
color: $text-color;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
fill: $text-color;
|
||||
cursor: pointer;
|
||||
transform: scale(1.1, 1.1);
|
||||
}
|
||||
}
|
||||
.active {
|
||||
color: $text-color;
|
||||
|
||||
.icon {
|
||||
fill: $green;
|
||||
}
|
||||
}
|
||||
.pending {
|
||||
color: #f8bd2d;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-left: 26px;
|
||||
}
|
||||
|
||||
.supplementary-text {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
figure {
|
||||
position: absolute;
|
||||
|
||||
> svg {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 7px 0 0;
|
||||
fill: $text-color-50;
|
||||
transition: fill 0.5s ease, transform 0.5s ease;
|
||||
|
||||
& .waiting {
|
||||
transform: scale(0.8, 0.8);
|
||||
}
|
||||
& .pending {
|
||||
fill: #f8bd2d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@ import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import router from './routes'
|
||||
import store from './store'
|
||||
|
||||
import Toast from './plugins/Toast'
|
||||
import DataTablee from 'vue-data-tablee'
|
||||
@@ -16,9 +17,12 @@ Vue.use(Toast)
|
||||
Vue.use(DataTablee)
|
||||
Vue.use(VModal, { dialog: true })
|
||||
|
||||
store.dispatch('darkmodeModule/findAndSetDarkmodeSupported')
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
router,
|
||||
store,
|
||||
components: { App },
|
||||
template: '<App />'
|
||||
})
|
||||
|
||||
23
src/modules/darkmodeModule.js
Normal file
23
src/modules/darkmodeModule.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
darkmodeSupported: undefined,
|
||||
userChoice: undefined
|
||||
},
|
||||
getters: {
|
||||
darkmodeSupported: (state) => {
|
||||
return state.darkmodeSupported
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
SET_DARKMODE_SUPPORT: (state, browserSupported) => {
|
||||
state.darkmodeSupported = browserSupported
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
findAndSetDarkmodeSupported({ commit }) {
|
||||
const browserSupported = window.matchMedia('(prefers-color-scheme)').media !== 'not all'
|
||||
commit('SET_DARKMODE_SUPPORT', browserSupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/modules/documentTitle.js
Normal file
41
src/modules/documentTitle.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const capitalize = (string) => {
|
||||
return string.includes(' ') ?
|
||||
string.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).replace('_', ' ')).join(' ')
|
||||
: string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
const setDocumentTitle = (state) => {
|
||||
document.title = `${state.emoji} ${state.titlePrefix} | ${capitalize(state.title)}`
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
emoji: '',
|
||||
titlePrefix: 'seasoned',
|
||||
title: undefined
|
||||
},
|
||||
getters: {
|
||||
title: (state) => {
|
||||
return state.title
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
SET_EMOJI: (state, emoji) => {
|
||||
state.emoji = emoji
|
||||
setDocumentTitle(state)
|
||||
},
|
||||
SET_TITLE: (state, title) => {
|
||||
state.title = title
|
||||
setDocumentTitle(state)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
updateEmoji({ commit }, emoji) {
|
||||
commit('SET_EMOJI', emoji)
|
||||
},
|
||||
updateTitle({ commit }, title) {
|
||||
commit('SET_TITLE', title)
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/modules/torrentModule.js
Normal file
40
src/modules/torrentModule.js
Normal file
@@ -0,0 +1,40 @@
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
results: [],
|
||||
resultCount: null
|
||||
},
|
||||
getters: {
|
||||
results: (state) => {
|
||||
return state.results
|
||||
},
|
||||
resultCount: (state) => {
|
||||
return state.resultCount
|
||||
}
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_RESULTS: (state, results) => {
|
||||
state.results = results;
|
||||
},
|
||||
SET_RESULT_COUNT: (state, count) => {
|
||||
state.resultCount = count;
|
||||
},
|
||||
RESET: (state) => {
|
||||
state.results = []
|
||||
state.resultCount = null
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setResults({ commit }, results) {
|
||||
commit('SET_RESULTS', results)
|
||||
},
|
||||
setResultCount({ commit }, count) {
|
||||
commit('SET_RESULT_COUNT', count)
|
||||
},
|
||||
reset({ commit }) {
|
||||
commit('RESET')
|
||||
}
|
||||
}
|
||||
}
|
||||
112
src/modules/userModule.js
Normal file
112
src/modules/userModule.js
Normal file
@@ -0,0 +1,112 @@
|
||||
import { getSettings } from '@/api'
|
||||
|
||||
function setLocalStorageByKey(key, value) {
|
||||
if (value instanceof Object || value instanceof Array) {
|
||||
value = JSON.stringify(value)
|
||||
}
|
||||
const buff = Buffer.from(value)
|
||||
const encodedValue = buff.toString('base64')
|
||||
localStorage.setItem(key, encodedValue)
|
||||
}
|
||||
|
||||
function getLocalStorageByKey(key) {
|
||||
const encodedValue = localStorage.getItem(key)
|
||||
if (encodedValue == null) {
|
||||
return undefined
|
||||
}
|
||||
const buff = new Buffer(encodedValue, 'base64')
|
||||
const value = buff.toString('utf-8')
|
||||
|
||||
try {
|
||||
return JSON.parse(value)
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const ifMissingSettingsAndTokenExistsFetchSettings =
|
||||
() => getLocalStorageByKey('token') ? getSettings() : null
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
admin: false,
|
||||
settings: undefined,
|
||||
username: undefined,
|
||||
plex_userid: undefined
|
||||
},
|
||||
getters: {
|
||||
admin: (state) => {
|
||||
return state.admin
|
||||
},
|
||||
settings: (state, foo, bar) => {
|
||||
console.log('is this called?')
|
||||
const settings = state.settings || getLocalStorageByKey('settings')
|
||||
if (settings instanceof Object) {
|
||||
return settings
|
||||
}
|
||||
|
||||
ifMissingSettingsAndTokenExistsFetchSettings()
|
||||
return undefined
|
||||
},
|
||||
username: (state) => {
|
||||
const settings = state.settings || getLocalStorageByKey('settings')
|
||||
|
||||
if (settings instanceof Object && settings.hasOwnProperty('user_name')) {
|
||||
return settings.user_name
|
||||
}
|
||||
|
||||
ifMissingSettingsAndTokenExistsFetchSettings()
|
||||
return undefined
|
||||
},
|
||||
plex_userid: (state) => {
|
||||
const settings = state.settings || getLocalStorageByKey('settings')
|
||||
console.log('plex_userid from store', settings)
|
||||
|
||||
if (settings instanceof Object && settings.hasOwnProperty('plex_userid')) {
|
||||
return settings.plex_userid
|
||||
}
|
||||
|
||||
ifMissingSettingsAndTokenExistsFetchSettings()
|
||||
return undefined
|
||||
},
|
||||
isPlexAuthenticated: (state) => {
|
||||
const settings = state.settings || getLocalStorageByKey('settings')
|
||||
if (settings == null)
|
||||
return false
|
||||
|
||||
const hasPlexId = settings['plex_userid']
|
||||
return hasPlexId != null ? true : false
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
SET_ADMIN: (state, isAdmin) => {
|
||||
state.admin = isAdmin
|
||||
},
|
||||
SET_USERNAME: (state, username) => {
|
||||
state.username = username
|
||||
console.log('username')
|
||||
setLocalStorageByKey('username', username)
|
||||
},
|
||||
SET_SETTINGS: (state, settings) => {
|
||||
state.settings = settings
|
||||
console.log('settings')
|
||||
setLocalStorageByKey('settings', settings)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setAdmin: ({commit}, isAdmin) => {
|
||||
if (!(isAdmin instanceof Object)) {
|
||||
throw "Parameter is not a boolean value."
|
||||
}
|
||||
commit('SET_ADMIN', isAdmin)
|
||||
},
|
||||
setSettings: ({commit}, settings) => {
|
||||
console.log('settings input', settings)
|
||||
if (!(settings instanceof Object)) {
|
||||
throw "Parameter is not a object."
|
||||
}
|
||||
commit('SET_SETTINGS', settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router';
|
||||
import store from '@/store'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
@@ -10,27 +11,34 @@ let routes = [
|
||||
path: '/',
|
||||
component: (resolve) => require(['./components/Home.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'activity',
|
||||
path: '/activity',
|
||||
meta: { requiresAuth: true },
|
||||
component: (resolve) => require(['./components/ActivityPage.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'profile',
|
||||
path: '/profile',
|
||||
meta: { requiresAuth: true },
|
||||
component: (resolve) => require(['./components/Profile.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
path: '/list/:name',
|
||||
component: (resolve) => require(['./components/MoviesList.vue'], resolve)
|
||||
component: (resolve) => require(['./components/ListPage.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'request',
|
||||
path: '/request/all',
|
||||
components: {
|
||||
'request-router-view': require('./components/MoviesList.vue')
|
||||
'request-router-view': require('./components/ListPage.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
path: '/search',
|
||||
component: (resolve) => require(['./components/MoviesList.vue'], resolve)
|
||||
component: (resolve) => require(['./components/Search.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
@@ -38,16 +46,16 @@ let routes = [
|
||||
component: (resolve) => require(['./components/Register.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'signin',
|
||||
path: '/signin',
|
||||
component: (resolve) => require(['./components/Signin.vue'], resolve)
|
||||
name: 'settings',
|
||||
path: '/settings',
|
||||
meta: { requiresAuth: true },
|
||||
component: (resolve) => require(['./components/Settings.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'settings',
|
||||
path: '/profile/settings',
|
||||
components: {
|
||||
'search-router-view': require('./components/Settings.vue')
|
||||
}
|
||||
name: 'signin',
|
||||
path: '/signin',
|
||||
alias: '/login',
|
||||
component: (resolve) => require(['./components/Signin.vue'], resolve)
|
||||
},
|
||||
// {
|
||||
// name: 'user-requests',
|
||||
@@ -61,6 +69,17 @@ let routes = [
|
||||
path: '/404',
|
||||
component: (resolve) => require(['./components/404.vue'], resolve)
|
||||
},
|
||||
{
|
||||
name: 'logout',
|
||||
path: '/logout',
|
||||
component: {
|
||||
template: '<div></div>',
|
||||
created() {
|
||||
localStorage.clear();
|
||||
this.$router.push({ name: 'home' });
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
redirect: '/'
|
||||
@@ -72,18 +91,27 @@ let routes = [
|
||||
];
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'hash',
|
||||
mode: 'history',
|
||||
base: '/',
|
||||
routes,
|
||||
linkActiveClass: 'is-active'
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
store.dispatch('documentTitle/updateTitle', to.name)
|
||||
|
||||
// Toggle mobile nav
|
||||
if(document.querySelector('.nav__hamburger--active')){
|
||||
document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active');
|
||||
document.querySelector('.nav__list').classList.remove('nav__list--active');
|
||||
}
|
||||
|
||||
if (to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (localStorage.getItem('token') == null) {
|
||||
next({ path: '/signin' });
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
|
||||
@@ -2,21 +2,18 @@
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
.filter {
|
||||
// margin: 10px 10px 20px;
|
||||
margin: 1rem;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
list-style: none;
|
||||
border: 1px solid;
|
||||
border-radius: 2px;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
transition: color .2s ease;
|
||||
// justify-content: space-evenly;
|
||||
|
||||
&-item {
|
||||
padding: 6px 15px;
|
||||
background-color: $c-white;
|
||||
background-color: $background-color-secondary;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 200;
|
||||
@@ -24,16 +21,17 @@
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
white-space:nowrap;
|
||||
// overflow: hidden;
|
||||
|
||||
&:nth-child(n+2) {
|
||||
border-left: solid 1px;
|
||||
}
|
||||
&.active, &:hover {
|
||||
border-color: transparent;
|
||||
background-color: #091c24;
|
||||
color: $c-green;
|
||||
background-color: $teal;
|
||||
color: $green;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@include tablet-min {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
|
||||
.noselect {
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently */
|
||||
}
|
||||
|
||||
.end-section {
|
||||
@@ -21,4 +20,30 @@
|
||||
> div:not(:first-child) {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
|
||||
&-direction-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-direction-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
&-align-items-center {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.position {
|
||||
&-relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-absolute {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,31 @@ $tablet-p-width: 768px;
|
||||
$tablet-l-width: 1024px;
|
||||
$desktop-width: 1200px;
|
||||
$desktop-l-width: 1600px;
|
||||
$mobile-width: 768px;
|
||||
|
||||
@mixin desktop {
|
||||
@media (min-width: #{$mobile-width + 1px}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: #{$mobile-width}) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-only {
|
||||
@include mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
@include desktop {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Media
|
||||
@mixin mobile-only{
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// TODO move all this to a plugin or something
|
||||
|
||||
.message-enter-active {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
.message-fade-leave-active {
|
||||
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.message-fade-enter, .message-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.message{
|
||||
width: 75%;
|
||||
max-width: 35rem;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 1rem;
|
||||
padding: 12px 15px 12px 15px;
|
||||
position: relative;
|
||||
&-text{
|
||||
font-weight: 300;
|
||||
}
|
||||
&-dismiss{
|
||||
position: absolute;
|
||||
font-size: 17px;
|
||||
font-weight: 100;
|
||||
top: 0;
|
||||
right: 0;
|
||||
margin-top: 2px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.message-warning{
|
||||
background-color: #f2dede;
|
||||
border: 1px solid #b75b91;
|
||||
color: #b75b91;
|
||||
}
|
||||
.message-success{
|
||||
background-color: #dff0d9;
|
||||
border: 1px solid #3e7549;
|
||||
color: #3e7549;
|
||||
}
|
||||
@@ -1,12 +1,127 @@
|
||||
// Colors
|
||||
$c-green: #01d277;
|
||||
$c-dark: #081c24;
|
||||
$c-white: #ffffff;
|
||||
$c-light: #f8f8f8;
|
||||
$c-green-light: #dff0d9;
|
||||
$c-green-dark: #3e7549;
|
||||
$c-red-light: #f2dede;
|
||||
$c-red-dark: #b75b91;
|
||||
// @import "./media-queries";
|
||||
@import "./src/scss/media-queries";
|
||||
|
||||
$header-size: 75px;
|
||||
$header-size-mobile: 50px;
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--text-color: #081c24;
|
||||
--text-color-70: rgba(8, 28, 36, 0.7);
|
||||
--text-color-50: rgba(8, 28, 36, 0.5);
|
||||
--text-color-5: rgba(8, 28, 36, 0.05);
|
||||
--text-color-secondary: orange;
|
||||
--background-color: #f8f8f8;
|
||||
--background-color-secondary: #ffffff;
|
||||
--background-ui: #edeef0;
|
||||
--background-95: rgba(255, 255, 255, 0.95);
|
||||
--background-70: rgba(255, 255, 255, 0.7);
|
||||
--background-40: rgba(255, 255, 255, 0.4);
|
||||
|
||||
--background-nav-logo: #081c24;
|
||||
--color-green: #01d277;
|
||||
--color-green-90: rgba(1, 210, 119, 0.9);
|
||||
--color-green-70: rgba(1, 210, 119, 0.73);
|
||||
--color-teal: #091c24;
|
||||
--color-black: #081c24;
|
||||
--white: #fff;
|
||||
--white-70: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--color-warning: rgba(241, 188, 53, 0.7);
|
||||
--color-warning-highlight: #f1bc35;
|
||||
--color-success: rgba(0, 100, 66, 0.8);
|
||||
--color-success-text: #fff;
|
||||
--color-success-highlight: rgb(0, 100, 66);
|
||||
--color-error: rgba(220, 48, 35, 0.8);
|
||||
--color-error-highlight: #dc3023;
|
||||
|
||||
--header-size: 75px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--text-color: #fff;
|
||||
--text-color-70: rgba(255, 255, 255, 0.7);
|
||||
--text-color-50: rgba(255, 255, 255, 0.5);
|
||||
--text-color-5: rgba(255, 255, 255, 0.05);
|
||||
--text-color-secondary: orange;
|
||||
--background-color: rgba(17, 17, 17, 1);
|
||||
--background-color-secondary: rgba(6, 7, 8, 1);
|
||||
--background-ui: #202125;
|
||||
--background-95: rgba(17, 17, 17, 0.95);
|
||||
--background-70: rgba(17, 17, 17, 0.8);
|
||||
--background-40: rgba(17, 17, 17, 0.4);
|
||||
}
|
||||
}
|
||||
|
||||
@include mobile-only {
|
||||
:root {
|
||||
--header-size: calc(50px + 1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
$header-size: var(--header-size);
|
||||
|
||||
$dark: rgb(30, 31, 34);
|
||||
$green: var(--color-green);
|
||||
$green-90: var(--color-green-90);
|
||||
$green-70: var(--color-green-70);
|
||||
$teal: #091c24;
|
||||
$black: #081c24;
|
||||
$black-80: rgba(0, 0, 0, 0.8);
|
||||
$white: #fff;
|
||||
$white-80: rgba(255, 255, 255, 0.8);
|
||||
|
||||
$text-color: var(--text-color) !default;
|
||||
$text-color-70: var(--text-color-70) !default;
|
||||
$text-color-50: var(--text-color-50) !default;
|
||||
$text-color-5: var(--text-color-5) !default;
|
||||
$text-color-secondary: var(--text-color-secondary) !default;
|
||||
$background-color: var(--background-color) !default;
|
||||
$background-color-secondary: var(--background-color-secondary) !default;
|
||||
$background-ui: var(--background-ui) !default;
|
||||
$background-95: var(--background-95) !default;
|
||||
$background-70: var(--background-70) !default;
|
||||
$background-40: var(--background-40) !default;
|
||||
$background-dark-85: rgba($dark, 0.85) !default;
|
||||
$background-nav-logo: var(--background-nav-logo) !default;
|
||||
$color-warning: var(--color-warning) !default;
|
||||
$color-warning-highlight: var(--color-warning-highlight) !default;
|
||||
$color-success: var(--color-success) !default;
|
||||
$color-success-highlight: var(--color-success-highlight) !default;
|
||||
$color-error: var(--color-error) !default;
|
||||
$color-error-highlight: var(--color-error-highlight) !default;
|
||||
|
||||
.halloween {
|
||||
--text-color: #6a318c;
|
||||
--text-color-secondary: #fb5a33;
|
||||
--background-color: #80c350;
|
||||
--background-color-secondary: #ff9234;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--text-color: #fff;
|
||||
--text-color-70: rgba(255, 255, 255, 0.7);
|
||||
--text-color-50: rgba(255, 255, 255, 0.5);
|
||||
--text-color-5: rgba(255, 255, 255, 0.05);
|
||||
--text-color-secondary: orange;
|
||||
--background-color: rgba(17, 17, 17, 1);
|
||||
--background-color-secondary: rgba(6, 7, 8, 1);
|
||||
--background-ui: #202125;
|
||||
--background-95: rgba(17, 17, 17, 0.95);
|
||||
--background-70: rgba(17, 17, 17, 0.8);
|
||||
--background-40: rgba(17, 17, 17, 0.4);
|
||||
}
|
||||
|
||||
.light {
|
||||
--text-color: #081c24;
|
||||
--text-color-70: rgba(8, 28, 36, 0.7);
|
||||
--text-color-50: rgba(8, 28, 36, 0.5);
|
||||
--text-color-5: rgba(8, 28, 36, 0.05);
|
||||
--text-color-secondary: orange;
|
||||
--background-color: #f8f8f8;
|
||||
--background-color-secondary: #ffffff;
|
||||
--background-ui: #edeef0;
|
||||
--background-95: rgba(255, 255, 255, 0.95);
|
||||
--background-70: rgba(255, 255, 255, 0.7);
|
||||
--background-40: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
20
src/store.js
Normal file
20
src/store.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import darkmodeModule from './modules/darkmodeModule'
|
||||
import documentTitle from './modules/documentTitle'
|
||||
import torrentModule from './modules/torrentModule'
|
||||
import userModule from './modules/userModule'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
darkmodeModule,
|
||||
documentTitle,
|
||||
torrentModule,
|
||||
userModule
|
||||
}
|
||||
})
|
||||
|
||||
export default store
|
||||
22
src/utils.js
22
src/utils.js
@@ -1,11 +1,23 @@
|
||||
function sortableSize(string) {
|
||||
const sortableSize = (string) => {
|
||||
const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
const [numStr, unit] = string.split(' ');
|
||||
if (UNITS.indexOf(unit) === -1)
|
||||
|
||||
if (UNITS.indexOf(unit) === -1)
|
||||
return string
|
||||
const exponent = UNITS.indexOf(unit) * 3
|
||||
|
||||
const exponent = UNITS.indexOf(unit) * 3
|
||||
return numStr * (Math.pow(10, exponent))
|
||||
}
|
||||
};
|
||||
|
||||
const parseJwt = (token) => {
|
||||
var base64Url = token.split('.')[1];
|
||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
|
||||
return JSON.parse(jsonPayload);
|
||||
};
|
||||
|
||||
|
||||
export { sortableSize }
|
||||
export { sortableSize, parseJwt }
|
||||
@@ -12,13 +12,20 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
'scss': 'vue-style-loader!css-loader!sass-loader',
|
||||
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
|
||||
use: [
|
||||
{
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
'scss': 'vue-style-loader!css-loader!sass-loader',
|
||||
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
loader: 'vue-svg-inline-loader'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
@@ -39,6 +46,7 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.js', '.vue', '.json', 'scss'],
|
||||
alias: {
|
||||
'vue$': 'vue/dist/vue.common.js',
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
|
||||
Reference in New Issue
Block a user