Compare commits

..

1 Commits

68 changed files with 5988 additions and 3716 deletions

350
docs/api/assets/anchor.js Normal file
View File

@@ -0,0 +1,350 @@
/*!
* 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;
});

View File

@@ -0,0 +1,12 @@
.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;
}

544
docs/api/assets/bass.css Normal file
View File

@@ -0,0 +1,544 @@
/*! 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.

View File

@@ -0,0 +1,93 @@
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.

View File

@@ -0,0 +1,23 @@
@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');
}

123
docs/api/assets/github.css Normal file
View File

@@ -0,0 +1,123 @@
/*
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;
}

168
docs/api/assets/site.js Normal file
View File

@@ -0,0 +1,168 @@
/* 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);

15
docs/api/assets/split.css Normal file
View File

@@ -0,0 +1,15 @@
.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;
}

586
docs/api/assets/split.js Normal file
View File

@@ -0,0 +1,586 @@
/*! 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;
});

140
docs/api/assets/style.css Normal file
View File

@@ -0,0 +1,140 @@
.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;
}

770
docs/api/index.html Normal file
View File

@@ -0,0 +1,770 @@
<!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>

File diff suppressed because one or more lines are too long

View File

@@ -11,7 +11,7 @@
"docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md" "docs": "documentation build src/api.js -f html -o docs/api && documentation build src/api.js -f md -o docs/api.md"
}, },
"dependencies": { "dependencies": {
"axios": "^0.18.1", "axios": "^0.15.3",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
"connect-history-api-fallback": "^1.3.0", "connect-history-api-fallback": "^1.3.0",
"express": "^4.16.1", "express": "^4.16.1",
@@ -34,9 +34,7 @@
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"node-sass": "^4.5.0", "node-sass": "^4.5.0",
"sass-loader": "^5.0.1", "sass-loader": "^5.0.1",
"schema-utils": "^2.4.1",
"vue-loader": "^10.0.0", "vue-loader": "^10.0.0",
"vue-svg-inline-loader": "^1.3.1",
"vue-template-compiler": "2.6.10", "vue-template-compiler": "2.6.10",
"webpack": "^2.2.0", "webpack": "^2.2.0",
"webpack-dev-server": "^2.2.0" "webpack-dev-server": "^2.2.0"

View File

@@ -5,8 +5,6 @@
<navigation></navigation> <navigation></navigation>
<!-- Header with search field --> <!-- Header with search field -->
<!-- TODO move this to the navigation component -->
<header class="header"> <header class="header">
<search-input v-model="query"></search-input> <search-input v-model="query"></search-input>
</header> </header>
@@ -14,29 +12,24 @@
<!-- Movie popup that will show above existing rendered content --> <!-- 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) --> <!-- Display the component assigned to the given route (default: home) -->
<router-view class="content" :key="$route.fullPath"></router-view> <router-view class="content"></router-view>
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from 'vue'
import Navigation from '@/components/Navigation' import Navigation from '@/components/Navigation.vue'
import MoviePopup from '@/components/MoviePopup' import MoviePopup from '@/components/MoviePopup.vue'
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput.vue'
import DarkmodeToggle from '@/components/ui/darkmodeToggle'
export default { export default {
name: 'app', name: 'app',
components: { components: {
Navigation, Navigation,
MoviePopup, MoviePopup,
SearchInput, SearchInput
DarkmodeToggle
}, },
data() { data() {
return { return {
@@ -74,7 +67,7 @@ export default {
.content { .content {
@include tablet-min{ @include tablet-min{
width: calc(100% - 95px); width: calc(100% - 95px);
margin-top: $header-size; padding-top: $header-size;
margin-left: 95px; margin-left: 95px;
position: relative; position: relative;
} }
@@ -82,34 +75,29 @@ export default {
</style> </style>
<style lang="scss"> <style lang="scss">
// @import "./src/scss/main"; @import "./src/scss/main";
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@font-face {
font-family: 'Roboto';
}
*{ *{
box-sizing: border-box; box-sizing: border-box;
} }
html { html, body{
height: 100%; height: 100%;
} }
body{ body{
margin: 0;
padding: 0;
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
line-height: 1.6; line-height: 1.6;
background: $background-color; background: $c-light;
color: $text-color; color: $c-dark;
transition: background-color .5s ease, color .5s ease;
&.hidden{ &.hidden{
overflow: hidden; overflow: hidden;
} }
} }
h1,h2,h3 {
transition: color .5s ease;
}
a:any-link {
color: inherit;
}
input, textarea, button{ input, textarea, button{
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
} }
@@ -128,6 +116,7 @@ img{
} }
.header{ .header{
position: fixed; position: fixed;
background: $c-white;
z-index: 15; z-index: 15;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -139,6 +128,61 @@ img{
border-bottom: 0; border-bottom: 0;
top: 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 // router view transition

View File

@@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import storage from '@/storage' import storage from '@/storage.js'
import config from '@/config.json' import config from '@/config.json'
import path from 'path' import path from 'path'
@@ -25,8 +25,7 @@ const getMovie = (id, credits=false) => {
url.searchParams.append('credits', true) url.searchParams.append('credits', true)
} }
return fetch(url.href) return axios.get(url.href)
.then(resp => resp.json())
.catch(error => { console.error(`api error getting movie: ${id}`); throw error }) .catch(error => { console.error(`api error getting movie: ${id}`); throw error })
} }
@@ -43,51 +42,24 @@ const getShow = (id, credits=false) => {
url.searchParams.append('credits', true) url.searchParams.append('credits', true)
} }
return fetch(url.href) return axios.get(url.href)
.then(resp => resp.json())
.catch(error => { console.error(`api error getting show: ${id}`); throw error }) .catch(error => { console.error(`api error getting show: ${id}`); throw error })
} }
/** /**
* Fetches tmdb list by name. * Fetches tmdb list by path.
* @param {string} name List the fetch * @param {string} listPath Path of list
* @param {number} [page=1] * @param {number} [page=1]
* @returns {object} Tmdb list response * @returns {object} Tmdb list response
*/ */
const getTmdbMovieListByName = (name, page=1) => { const getTmdbListByPath = (listPath, page=1) => {
const url = new URL('v2/movie/' + name, SEASONED_URL) const url = new URL(listPath, SEASONED_URL)
url.searchParams.append('page', page) url.searchParams.append('page', page)
// TODO - remove. this is temporary fix for user-requests endpoint (also import)
const headers = { authorization: storage.token } const headers = { authorization: storage.token }
return fetch(url.href, { headers: headers }) return axios.get(url.href, { headers: headers })
.then(resp => resp.json()) .catch(error => { console.error(`api error getting list: ${listPath}, page: ${page}`); throw error })
// .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())
} }
/** /**
@@ -101,8 +73,7 @@ const searchTmdb = (query, page=1) => {
url.searchParams.append('query', query) url.searchParams.append('query', query)
url.searchParams.append('page', page) url.searchParams.append('page', page)
return fetch(url.href) return axios.get(url.href)
.then(resp => resp.json())
.catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error }) .catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error })
} }
@@ -115,13 +86,12 @@ const searchTmdb = (query, page=1) => {
* @returns {object} Torrent response * @returns {object} Torrent response
*/ */
const searchTorrents = (query, authorization_token) => { const searchTorrents = (query, authorization_token) => {
const url = new URL('/api/v1/pirate/search', SEASONED_URL) const url = new URL('v1/pirate/search', SEASONED_URL)
url.searchParams.append('query', query) url.searchParams.append('query', query)
const headers = { authorization: storage.token } const headers = { authorization: storage.token }
return fetch(url.href, { headers: headers }) return axios.get(url.href, { headers: headers })
.then(resp => resp.json())
.catch(error => { console.error(`api error searching torrents: ${query}`); throw error }) .catch(error => { console.error(`api error searching torrents: ${query}`); throw error })
} }
@@ -142,9 +112,8 @@ const addMagnet = (magnet, name, tmdb_id) => {
} }
const headers = { authorization: storage.token } const headers = { authorization: storage.token }
return fetch(url.href, { method: 'POST', headers, body }) return axios.post(url.href, body, { headers: headers })
.then(resp => resp.json()) .catch(error => { console.error(`api error adding magnet: ${name}`); throw error })
.catch(error => { console.error(`api error adding magnet: ${name} ${error}`); throw error })
} }
// - - - Plex/Request - - - // - - - Plex/Request - - -
@@ -206,7 +175,9 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
// - - - Authenticate with plex - - - // - - - Authenticate with plex - - -
const plexAuthenticate = (username, password) => { const plexAuthenticate = (username, password) => {
const url = new URL('https://plex.tv/api/v2/users/signin') const url = new URL('https://plex.tv/users/sign_in.json')
url.searchParams.append('user[login]', username)
url.searchParams.append('user[password]', password)
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -217,17 +188,7 @@ const plexAuthenticate = (username, password) => {
'X-Plex-Client-Identifier': '123' 'X-Plex-Client-Identifier': '123'
} }
let formData = new FormData() return axios.post(url.href, { headers: headers })
formData.set('login', username)
formData.set('password', password)
formData.set('rememberMe', false)
return axios({
method: 'POST',
url: url.href,
headers: headers,
data: formData
})
.catch(error => { console.error(`api error authentication plex: ${username}`); throw error }) .catch(error => { console.error(`api error authentication plex: ${username}`); throw error })
} }
@@ -235,10 +196,9 @@ const plexAuthenticate = (username, password) => {
// - - - Random emoji - - - // - - - Random emoji - - -
const getEmoji = () => { const getEmoji = () => {
const url = new URL('v1/emoji', SEASONED_URL) const url = path.join(SEASONED_URL, 'v1/emoji')
return fetch(url.href) return axios.get(url)
.then(resp => resp.json())
.catch(error => { console.log('api error getting emoji'); throw error }) .catch(error => { console.log('api error getting emoji'); throw error })
} }
@@ -292,18 +252,4 @@ const elasticSearchMoviesAndShows = (query) => {
export { export { getMovie, getShow, getTmdbListByPath, searchTmdb, searchTorrents, addMagnet, request, getRequestStatus, plexAuthenticate, getEmoji, elasticSearchMoviesAndShows }
getMovie,
getShow,
getTmdbMovieListByName,
searchTmdb,
getUserRequests,
getRequests,
searchTorrents,
addMagnet,
request,
getRequestStatus,
plexAuthenticate,
getEmoji,
elasticSearchMoviesAndShows
}

View File

@@ -1,39 +1,72 @@
<template> <template>
<section class="not-found"> <section class="not-found">
<h1 class="not-found__title">Page Not Found</h1> <div class="not-found__content">
<h2 class="not-found__title">Page Not Found</h2>
</div>
</section> </section>
</template> </template>
<style lang="scss" scoped> <script>
@import "./src/scss/variables"; import storage from '../storage.js'
@import "./src/scss/media-queries"; export default {
created(){
.not-found { document.title = 'Page Not Found' + storage.pageTitlePostfix;
display: flex;
height: calc(100vh - var(--header-size));
width: 100%;
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
background-size: cover;
justify-content: center;
&:before {
content: "";
position: absolute;
height: calc(100vh - var(--header-size));
width: 100%;
background: $background-40;
}
&__title {
padding-top: 40vh;
font-size: 2rem;
font-weight: 500;
color: $text-color;
position: relative;
margin: 0;
@include tablet-min {
font-size: 2.3rem;
}
} }
} }
</script>
<style lang="scss">
@import "./src/scss/variables";
@import "./src/scss/media-queries";
.not-found{
width: 100%;
height: calc(100vh - 100px);
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%;
}
}
&__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> </style>

View File

@@ -2,84 +2,27 @@
<section> <section>
<LandingBanner /> <LandingBanner />
<div v-for="list in lists"> <movies-list v-for="item in homepageLists" :propList="item" :shortList="true"></movies-list>
<list-header :title="list.title" :link="'/list/' + list.route" />
<results-list :results="list.data" :shortList="true" />
<loader v-if="!list.data.length" />
</div>
</section> </section>
</template> </template>
<script> <script>
import LandingBanner from '@/components/LandingBanner' import storage from '../storage.js'
import ListHeader from '@/components/ListHeader' import LandingBanner from '@/components/LandingBanner.vue'
import ResultsList from '@/components/ResultsList' import MoviesList from './MoviesList.vue'
import Loader from '@/components/ui/Loader'
import { getTmdbMovieListByName, getRequests } from '@/api'
export default { export default {
name: 'home', name: 'home',
components: { LandingBanner, ResultsList, ListHeader, Loader }, components: { LandingBanner, MoviesList },
data(){ data(){
return { return {
imageFile: 'dist/pulp-fiction.jpg', homepageLists: storage.homepageLists,
requests: [], imageFile: 'dist/pulp-fiction.jpg'
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
}
]
}
},
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(){ created(){
this.fetchRequests() document.title = 'TMDb';
this.fetchNowPlaying() storage.backTitle = document.title;
this.fetchUpcoming()
this.fetchPopular()
} }
} }
</script> </script>

View File

@@ -26,6 +26,7 @@ export default {
} }
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -42,11 +43,10 @@ header {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
position: relative; position: relative;
background-color: $c-dark;
@include tablet-min { @include tablet-min {
height: 284px; height: 284px;
} }
&:before { &:before {
content: ""; content: "";
position: absolute; position: absolute;
@@ -54,14 +54,12 @@ header {
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: $background-70; background: rgba($c-light, 0.7);
transition: background-color .5s ease;
} }
.container { .container {
text-align: center; text-align: center;
position: relative; position: relative;
transition: color .5s ease;
} }
.title { .title {
@@ -69,9 +67,8 @@ header {
font-size: 22px; font-size: 22px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: $text-color; color: $c-dark;
margin: 0; margin: 0;
@include tablet-min{ @include tablet-min{
font-size: 28px; font-size: 28px;
} }
@@ -81,12 +78,35 @@ header {
display: block; display: block;
font-size: 14px; font-size: 14px;
font-weight: 300; font-weight: 300;
color: $text-color-70; color: $c-dark;
margin: 5px 0; margin: 5px 0;
@include tablet-min{ @include tablet-min{
font-size: 16px; 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;
}
}
} }
</style> </style>

View File

@@ -0,0 +1,530 @@
<template>
<div class="wrapper">
<div class="backdrop" :style="backdropStyle"></div>
<div class="title-container">
<h1> {{ title }} </h1>
<h2> ({{ movie.year }}) </h2>
</div>
<div class="opacity-overlay-bottom"></div>
<div class="info-container">
<div class="button">
<svg class="action-icon">
<use :xlink:href="'#iconSent'"></use>
</svg>
<span>Request movie</span>
</div>
<div class="button--grey">
<svg class="action-icon">
<use :xlink:href="'#icon_info'"></use>
</svg>
<span>See more info</span>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
@import "./src/scss/variables";
$blue: rgb(0,122,255);
$blue-hover: rgb(10,132,255);
$grey: rgb(44,44,46);
$grey-hover: rgb(58,58,60);
.action-icon {
transform: translateY(-0.2px);
width: 19px;
height: 19px;
margin-right: 8px;
// fill: rgba($c-dark, 0.5);
fill: white;
transition: fill 0.5s ease, transform 0.5s ease;
&.waiting {
transform: scale(0.8, 0.8);
}
&.pending {
fill: #f8bd2d;
}
}
@mixin button {
width: 300px;
min-height: 50px;
border-radius: 5px;
background-color: $blue;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
letter-spacing: 0.5px;
transition: transform 0.5s ease, box-shadow 0.3s ease;
&:hover {
background-color: $blue-hover;
transform: scale(1.03);
}
&:not(:last-of-type) {
margin-bottom: 0.7rem;
}
}
.title-container {
width: 100%;
position: absolute;
top: 0;
padding-left: 3rem;
h1 {
font-size: 5rem;
display: inline;
color: $c-green;
-webkit-text-stroke: 1.1px $c-green-dark;
// border: 1px solid $c-green;
// text-shadow: -1px 0 $c-green-dark, 0 1px $c-green-dark, 1px 0 $c-green-dark, 0 -1px $c-green-dark;
}
h2 {
font-size: 2.7rem;
display: inline;
color: $c-green;
-webkit-text-stroke: 1.1px $c-green-dark;
// border: 1px solid $c-green;
// text-shadow: -1px 0 $c-green-dark, 0 1px $c-green-dark, 1px 0 $c-green-dark, 0 -1px $c-green-dark;
}
}
.info-container {
width: calc(100% - 80px);
position: absolute;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
// background-color: red;
margin: 40px;
.button {
@include button;
&--grey {
@include button;
background-color: $grey;
&:hover{
background-color: $grey-hover;
}
span { color: white; }
}
span { color: white; }
}
}
.wrapper {
$width: 88vw;
position: absolute;
left: calc(50% - #{$width} / 2);
display: flex;
flex-direction: column;
width: $width;
height: 82vh;
// -webkit-mask-size: auto 50%;
}
.backdrop {
width: 100%;
height: 100%;
// -webkit-mask-image: linear-gradient(to top, transparent 5%, black);
border-radius: 0 0 8px 8px;
}
.opacity-overlay-bottom {
// background-color: #0c0a09;
position: absolute;
bottom: 0;
height: 55%;
width: 100%;
align-self: flex-end;
-webkit-mask: linear-gradient(to bottom, transparent 0%, black 100%);
mask: linear-gradient(to top, transparent 25%, black 75%);
-webkit-mask: linear-gradient(transparent, black);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-radius: 0 0 8px 8px;
// background: linear-gradient(180deg, rgba(0,0,0,0.29315476190476186) 0%, rgba(0,0,0,1) 100%, rgba(9,9,121,1) 100%, rgba(0,0,0,1) 100%);
}
</style>
<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 LoadingPlaceholder from './ui/LoadingPlaceholder.vue'
import { getMovie, getShow, request, getRequestStatus } from '@/api.js'
export default {
props: ['id', 'type'],
components: { TorrentList, Person, LoadingPlaceholder, SidebarAction },
directives: { img: img }, // TODO decide to remove or use
computed: {
backdropStyle() {
let value = ''
if (this.movie && this.backdrop) {
value = 'url(' + this.ASSET_URL + this.ASSET_SIZES[2] + this.backdrop + ')'
// value = 'url(https://kevinmidboe.com/assets/arrival.jpg)'
console.log('value', value)
}
console.log('bool', this.backdrop)
return { 'background-image': value, 'background-size': 'cover', 'background-repeat': 'no-repeat'}
return { 'background-image': movie && backdrop !== null ? 'url(' + this.ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }
}
},
data(){
return{
ASSET_URL: 'https://image.tmdb.org/t/p/',
ASSET_SIZES: ['w500', 'w780', 'original'],
movie: undefined,
title: undefined,
poster: undefined,
backdrop: undefined,
matched: false,
userLoggedIn: storage.sessionId ? true : false,
requested: false,
admin: localStorage.getItem('admin'),
showTorrents: false
}
},
methods: {
parseResponse(resp) {
const l = {fe: 0}
console.log('resp', 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
},
},
watch: {
id: function(val){
if (this.type === 'movie') {
this.fetchMovie(val);
} else {
this.fetchShow(val)
}
}
},
beforeDestroy() {
document.title = this.prevDocumentTitle
},
created(){
console.log('👋')
this.prevDocumentTitle = document.title
if (this.type === 'movie') {
getMovie(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
} else if (this.type === 'show') {
getShow(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
} else {
console.error('No type found, unable to fetch item')
}
console.log('admin: ', this.admin)
}
}
</script>
<style lang="scss" scoped>
@import "./src/scss/loading-placeholder";
@import "./src/scss/variables";
@import "./src/scss/media-queries";
.movie {
&__wrap {
display: flex;
&--header {
align-items: center;
height: 100%;
}
&--main {
display: flex;
flex-wrap: wrap;
flex-direction: column;
@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;
}
}
&__img {
display: block;
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);
}
}
&__title {
position: relative;
padding: 20px;
color: $c-green;
text-align: center;
width: 100%;
@include tablet-min {
width: 55%;
text-align: left;
margin-left: 45%;
padding: 30px 30px 30px 40px;
}
h1 {
font-weight: 500;
line-height: 1.4;
font-size: 24px;
@include tablet-min {
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;
}
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;
order: 1;
@include tablet-min {
order: 2;
padding: 40px;
width: 55%;
margin-left: 45%;
}
}
&__actions + &__info {
margin-left: 0;
}
&__description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
@include tablet-min {
margin-bottom: 30px;
font-size: 14px;
}
}
&__details {
&-block {
float: left;
}
&-block:not(:last-child) {
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min {
margin-bottom: 30px;
margin-right: 30px;
}
}
&-title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
font-size: 14px;
color: $c-green;
@include tablet-min {
font-size: 16px;
}
}
&-text {
font-weight: 300;
font-size: 14px;
margin-top: 5px;
}
}
&__admin {
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;
}
}
}
}
</style>

View File

@@ -1,101 +0,0 @@
<template>
<header :class="{ 'sticky': sticky }">
<h2>{{ title }}</h2>
<span v-if="info" class="result-count">{{ info }}</span>
<router-link v-else-if="link" :to="link" class='view-more'>
View All
</router-link>
</header>
</template>
<script>
export default {
props: {
title: {
type: String,
required: true
},
sticky: {
type: Boolean,
required: false,
default: false
},
info: {
type: String,
required: false
},
link: {
type: String,
required: false
}
}
}
</script>
<style lang="scss" scoped>
@import './src/scss/variables';
@import './src/scss/media-queries';
header {
width: 100%;
display: flex;
justify-content: space-between;
padding: 1.8rem 12px;
&.sticky {
background-color: $background-color;
position: sticky;
position: -webkit-sticky;
top: $header-size;
z-index: 4;
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
h2 {
font-size: 18px;
font-weight: 300;
text-transform: capitalize;
line-height: 18px;
margin: 0;
color: $text-color;
}
.view-more {
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color-70;
text-decoration: none;
transition: color .5s ease;
cursor: pointer;
&:after{
content: " →";
}
&:hover{
color: $text-color;
}
}
.result-count {
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color;
text-decoration: none;
}
@include tablet-min {
padding-left: 1.25rem;;
}
@include desktop-lg-min {
padding-left: 1.75rem;
}
}
</style>

View File

@@ -1,104 +0,0 @@
<template>
<div>
<list-header :title="listTitle" :info="resultCount" :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
}
},
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
},
resultCount() {
if (this.results.length === 0)
return ''
const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results`
}
},
methods: {
loadMore() {
console.log(this.$route)
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')
}
}
},
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>
.fullwidth-button {
width: 100%;
margin: 1rem 0;
padding-bottom: 2rem;
display: flex;
justify-content: center;
}
</style>

View File

@@ -2,7 +2,7 @@
<section class="movie"> <section class="movie">
<!-- HEADER w/ POSTER --> <!-- HEADER w/ POSTER -->
<header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }" :class="compact ? 'compact' : ''" @click="compact=!compact"> <header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }">
<div class="movie__wrap movie__wrap--header"> <div class="movie__wrap movie__wrap--header">
<figure class="movie__poster"> <figure class="movie__poster">
<img v-if="movie && poster === null" <img v-if="movie && poster === null"
@@ -20,8 +20,7 @@
</figure> </figure>
<div class="movie__title"> <div class="movie__title">
<h1 v-if="movie">{{ movie.title }}</h1> <h1>{{ title }}</h1>
<loading-placeholder v-else :count="1" />
</div> </div>
</div> </div>
</header> </header>
@@ -33,25 +32,22 @@
<!-- SIDEBAR ACTIONS --> <!-- SIDEBAR ACTIONS -->
<div class="movie__actions" v-if="movie"> <div class="movie__actions" v-if="movie">
<sidebar-list-element :iconRef="'#iconNot_exsits'" :active="matched" <sidebar-action
:iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'"> :text="'Not yet in plex'" :iconRef="'#iconNot_exsits'"
:textActive="'Already in plex 🎉'" :iconRefActive="'#iconExists'"
Not yet in plex :active="matched"></sidebar-action>
</sidebar-list-element> <sidebar-action
<sidebar-list-element @click="sendRequest" :iconRef="'#iconSent'" @click="sendRequest"
:active="requested" :textActive="'Requested to be downloaded'"> :text="'Request to be downloaded?'" :iconRef="'#iconSent'"
:textActive="'Requested to be downloaded'"
Request to be downloaded? :active="requested"></sidebar-action>
</sidebar-list-element> <sidebar-action
<sidebar-list-element v-if="admin" @click="showTorrents=!showTorrents" v-if="admin" @click="showTorrents=!showTorrents"
:iconRef="'#icon_torrents'" :active="showTorrents" :text="'Search for torrents'" :iconRef="'#icon_torrents'"
:supplementaryText="numberOfTorrentResults"> :active="showTorrents"></sidebar-action>
<sidebar-action
Search for torrents @click="openTmdb()"
</sidebar-list-element> :iconRef="'#icon_info'" :text="'See more info'"></sidebar-action>
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
See more info
</sidebar-list-element>
</div> </div>
<!-- Loading placeholder --> <!-- Loading placeholder -->
@@ -115,19 +111,19 @@
</template> </template>
<script> <script>
import storage from '@/storage' import storage from '@/storage.js'
import img from '@/directives/v-image' import img from '@/directives/v-image.js'
import TorrentList from './TorrentList' import TorrentList from './TorrentList.vue'
import Person from './Person' import Person from './Person.vue'
import SidebarListElement from './ui/sidebarListElem' import SidebarAction from './movie/SidebarAction.vue'
import store from '@/store'
import LoadingPlaceholder from './ui/LoadingPlaceholder'
import { getMovie, getShow, request, getRequestStatus } from '@/api' import LoadingPlaceholder from './ui/LoadingPlaceholder.vue'
import { getMovie, getShow, request, getRequestStatus } from '@/api.js'
export default { export default {
props: ['id', 'type'], props: ['id', 'type'],
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement }, components: { TorrentList, Person, LoadingPlaceholder, SidebarAction },
directives: { img: img }, // TODO decide to remove or use directives: { img: img }, // TODO decide to remove or use
data(){ data(){
return{ return{
@@ -141,12 +137,12 @@ export default {
userLoggedIn: storage.sessionId ? true : false, userLoggedIn: storage.sessionId ? true : false,
requested: false, requested: false,
admin: localStorage.getItem('admin'), admin: localStorage.getItem('admin'),
showTorrents: false, showTorrents: false
compact: false
} }
}, },
methods: { methods: {
parseResponse(movie) { parseResponse(resp) {
let movie = resp.data;
this.movie = { ...movie } this.movie = { ...movie }
this.title = movie.title this.title = movie.title
this.poster = movie.poster this.poster = movie.poster
@@ -155,7 +151,7 @@ export default {
this.checkIfRequested(movie) this.checkIfRequested(movie)
.then(status => this.requested = status) .then(status => this.requested = status)
store.dispatch('documentTitle/updateTitle', movie.title) document.title = movie.title + storage.pageTitlePostfix
}, },
async checkIfRequested(movie) { async checkIfRequested(movie) {
return await getRequestStatus(movie.id, movie.type) return await getRequestStatus(movie.id, movie.type)
@@ -187,17 +183,11 @@ export default {
} }
} }
}, },
computed: {
numberOfTorrentResults: () => {
let numTorrents = store.getters['torrentModule/resultCount']
return numTorrents !== null ? numTorrents + ' results' : null
}
},
beforeDestroy() { beforeDestroy() {
store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle) document.title = this.prevDocumentTitle
}, },
created(){ created(){
this.prevDocumentTitle = store.getters['documentTitle/title'] this.prevDocumentTitle = document.title
if (this.type === 'movie') { if (this.type === 'movie') {
getMovie(this.id) getMovie(this.id)
@@ -237,22 +227,15 @@ export default {
@include tablet-min{ @include tablet-min{
flex-direction: row; flex-direction: row;
} }
background-color: $background-color;
color: $text-color;
} }
} }
&__header { &__header {
$duration: 0.2s;
height: 250px; height: 250px;
transform: scaleY(1);
transition: height $duration ease;
transform-origin: top;
position: relative; position: relative;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50% 50%; background-position: 50% 50%;
background-color: $background-color; background-color: $c-dark;
@include tablet-min { @include tablet-min {
height: 350px; height: 350px;
} }
@@ -265,17 +248,14 @@ export default {
z-index: 0; z-index: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: $background-dark-85; background: rgba($c-dark, 0.85);
}
&.compact {
height: 100px;
} }
} }
&__poster { &__poster {
display: none; display: none;
@include tablet-min { @include tablet-min {
background: $background-color; background: $c-white;
height: 0; height: 0;
display: block; display: block;
position: absolute; position: absolute;
@@ -302,7 +282,7 @@ export default {
&__title { &__title {
position: relative; position: relative;
padding: 20px; padding: 20px;
color: $green; color: $c-green;
text-align: center; text-align: center;
width: 100%; width: 100%;
@include tablet-min { @include tablet-min {
@@ -319,8 +299,16 @@ export default {
font-size: 30px; font-size: 30px;
} }
} }
span {
display: block;
font-size: 14px;
font-weight: 300;
color: rgba($c-white, 0.7);
margin-top: 10px;
}
} }
&__main { &__main {
background: $c-light;
min-height: calc(100vh - 250px); min-height: calc(100vh - 250px);
@include tablet-min { @include tablet-min {
min-height: 0; min-height: 0;
@@ -330,16 +318,64 @@ export default {
} }
&__actions { &__actions {
text-align: center; text-align: center;
// min-height: 394px;
width: 100%; width: 100%;
order: 2; order: 2;
padding: 20px; padding: 20px;
border-top: 1px solid $text-color-5; border-top: 1px solid rgba($c-dark, 0.05);
@include tablet-min { @include tablet-min {
order: 1; order: 1;
width: 45%; width: 45%;
padding: 185px 0 40px 40px; padding: 185px 0 40px 40px;
border-top: 0; 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 { &__info {
width: 100%; width: 100%;
@@ -352,7 +388,7 @@ export default {
margin-left: 45%; margin-left: 45%;
} }
} }
&__info { &__actions + &__info {
margin-left: 0; margin-left: 0;
} }
&__description { &__description {
@@ -382,7 +418,7 @@ export default {
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
font-size: 14px; font-size: 14px;
color: $green; color: $c-green;
@include tablet-min { @include tablet-min {
font-size: 16px; font-size: 16px;
} }
@@ -409,7 +445,7 @@ export default {
text-transform: uppercase; text-transform: uppercase;
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
color: $green; color: $c-green;
padding-bottom: 20px; padding-bottom: 20px;
@include tablet-min { @include tablet-min {
font-size: 16px; font-size: 16px;

View File

@@ -1,12 +1,14 @@
<template> <template>
<div class="container info"> <div class="container info">
<movie :id="$route.params.id" :type="'page'"></movie> <!-- <movie :id="$route.params.id" :type="'page'"></movie> -->
<LargeMovie :id"$route.params.id"></LargeMovie>
</div> </div>
</template> </template>
<script> <script>
import Movie from './Movie'; import Movie from './Movie.vue';
import LargeMovie from './LargeMovie.vue';
export default { export default {
components: { Movie } components: { Movie, LargeMovie }
} }
</script> </script>

View File

@@ -1,7 +1,9 @@
<template> <template>
<div class="movie-popup" @click="$popup.close()"> <div class="movie-popup" @click="$popup.close()">
<div class="movie-popup__box" @click.stop> <div class="movie-popup__box" @click.stop>
<movie :id="id" :type="type"></movie> <!-- <movie :id="id" :type="type"></movie> -->
<large-movie :id="id" :type="type"></large-movie>
<button class="movie-popup__close" @click="$popup.close()"></button> <button class="movie-popup__close" @click="$popup.close()"></button>
</div> </div>
<i class="loader"></i> <i class="loader"></i>
@@ -9,34 +11,20 @@
</template> </template>
<script> <script>
import Movie from './Movie'; import Movie from './Movie.vue';
import LargeMovie from './LargeMovie.vue';
export default { export default {
props: { props: ['id', 'type'],
id: { components: { Movie, LargeMovie },
type: Number,
required: true
},
type: {
type: String,
required: true
}
},
components: { Movie },
methods: {
checkEventForEscapeKey(event) {
if (event.keyCode == 27) {
this.$popup.close()
}
}
},
created(){ created(){
window.addEventListener('keyup', this.checkEventForEscapeKey) let that = this
}, window.addEventListener('keyup', function(e){
beforeDestroy() { if (e.keyCode == 27) {
window.removeEventListener('keyup', this.checkEventForEscapeKey) that.$popup.close()
}
})
} }
} }
</script> </script>
@@ -51,21 +39,9 @@ export default {
z-index: 20; z-index: 20;
width: 100%; width: 100%;
height: 100vh; height: 100vh;
background: rgba($dark, 0.93); background: rgba($c-dark, 0.93);
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overflow: auto; overflow: auto;
&__box{
width: 100%;
max-width: 768px;
position: relative;
z-index: 5;
background: $background-color-secondary;
padding-bottom: 50px;
@include tablet-min{
padding-bottom: 0;
margin: 40px auto;
}
}
&__close{ &__close{
display: block; display: block;
position: absolute; position: absolute;
@@ -86,7 +62,7 @@ export default {
left: 10px; left: 10px;
width: 20px; width: 20px;
height: 2px; height: 2px;
background: $white; background: $c-white;
} }
&:before{ &:before{
transform: rotate(45deg); transform: rotate(45deg);
@@ -95,7 +71,7 @@ export default {
transform: rotate(-45deg); transform: rotate(-45deg);
} }
&:hover{ &:hover{
background: $green; background: $c-green;
} }
} }
} }

View File

@@ -0,0 +1,328 @@
<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>

View File

@@ -2,7 +2,6 @@
<li class="movies-item" :class="{'shortList': shortList}"> <li class="movies-item" :class="{'shortList': shortList}">
<a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)"> <a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)">
<!-- TODO change to picture element -->
<figure class="movies-item__poster"> <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" src="~assets/placeholder.png" v-img="poster()" alt="">
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt=""> <img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
@@ -16,7 +15,7 @@
</template> </template>
<script> <script>
import img from '../directives/v-image' import img from '../directives/v-image.js'
export default { export default {
props: ['movie', 'shortList'], props: ['movie', 'shortList'],
@@ -24,7 +23,7 @@ export default {
img: img img: img
}, },
data(){ data(){
return { return{
noImage: false noImage: false
} }
}, },
@@ -37,7 +36,7 @@ export default {
this.noImage = true this.noImage = true
} }
}, },
openMoviePopup(id, type) { openMoviePopup(id, type){
this.$popup.open(id, type) this.$popup.open(id, type)
} }
} }
@@ -47,33 +46,53 @@ export default {
<style lang="scss"> <style lang="scss">
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
.movies-item { .movies-item {
padding: 10px; padding: 10px;
width: 50%; width: 50%;
background-color: $background-color;
transition: background-color 0.5s ease;
@include tablet-min{ @include tablet-min{
padding: 15px; padding: 15px;
} }
@include tablet-landscape-min{ @include tablet-landscape-min{
padding: 15px; padding: 20px;
width: 25%; width: 25%;
} }
@include desktop-min{ @include desktop-min{
padding: 15px; padding: 30px;
width: 20%; width: 20%;
} }
@include desktop-lg-min{ @include desktop-lg-min{
padding: 20px; padding: 20px;
width: 12.5%; width: 16.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
}
} }
&__link{ &__link{
text-decoration: none; text-decoration: none;
color: $text-color-70; color: rgba($c-dark, 0.5);
font-weight: 300; font-weight: 300;
} }
&__content{ &__content{
@@ -82,6 +101,7 @@ export default {
&__poster{ &__poster{
transition: transform 0.5s ease, box-shadow 0.3s ease; transition: transform 0.5s ease, box-shadow 0.3s ease;
transform: translateZ(0); transform: translateZ(0);
background: $c-white;
} }
&__img{ &__img{
width: 100%; width: 100%;
@@ -95,14 +115,13 @@ export default {
} }
&__link:not(.no-image):hover &__poster{ &__link:not(.no-image):hover &__poster{
transform: scale(1.03); transform: scale(1.03);
box-shadow: 0 0 10px rgba($dark, 0.1); box-shadow: 0 0 10px rgba($c-dark, 0.1);
} }
&__title{ &__title{
margin: 0; margin: 0;
font-size: 11px; font-size: 11px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
transition: color 0.5s ease; transition: color 0.5s ease;
cursor: pointer;
@include mobile-ls-min{ @include mobile-ls-min{
font-size: 12px; font-size: 12px;
} }
@@ -111,7 +130,7 @@ export default {
} }
} }
&__link:hover &__title{ &__link:hover &__title{
color: $text-color; color: $c-dark;
} }
} }
</style> </style>

View File

@@ -6,15 +6,16 @@
<use xlink:href="#svgLogo"></use> <use xlink:href="#svgLogo"></use>
</svg> </svg>
</router-link> </router-link>
<div class="nav__hamburger" @click="toggleNav"> <div class="nav__hamburger" @click="toggleNav">
<div v-for="_ in 3" class="bar"></div> <div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div> </div>
<ul class="nav__list"> <ul class="nav__list">
<li class="nav__item" v-for="item in listTypes"> <li class="nav__item" v-for="item in listTypes">
<router-link class="nav__link" :to="'/list/' + item.route"> <router-link class="nav__link" :to="'/list/' + item.route">
<div class="nav__link-wrap"> <div class="nav__link-wrap">
<!-- <img :src="item.icon" class="nav__link-icon"> -->
<svg class="nav__link-icon"> <svg class="nav__link-icon">
<use :xlink:href="'#icon_' + item.route"></use> <use :xlink:href="'#icon_' + item.route"></use>
</svg> </svg>
@@ -22,9 +23,8 @@
</div> </div>
</router-link> </router-link>
</li> </li>
<li class="nav__item nav__item--profile"> <li class="nav__item nav__item--profile">
<router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn"> <router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
<div class="nav__link-wrap"> <div class="nav__link-wrap">
<svg class="nav__link-icon"> <svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use> <use xlink:href="#iconLogin"></use>
@@ -32,8 +32,7 @@
<span class="nav__link-title">Sign in</span> <span class="nav__link-title">Sign in</span>
</div> </div>
</router-link> </router-link>
<router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
<router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
<div class="nav__link-wrap"> <div class="nav__link-wrap">
<svg class="nav__link-icon"> <svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use> <use xlink:href="#iconLogin"></use>
@@ -44,13 +43,12 @@
</li> </li>
</ul> </ul>
</nav> </nav>
<div class="spacer"></div> <div class="spacer"></div>
</div> </div>
</template> </template>
<script> <script>
import storage from '@/storage' import storage from '@/storage.js'
export default { export default {
data(){ data(){
@@ -69,7 +67,6 @@ export default {
} }
}, },
created(){ created(){
// TODO move this to state manager
eventHub.$on('setUserStatus', this.setUserStatus); eventHub.$on('setUserStatus', this.setUserStatus);
} }
} }
@@ -78,59 +75,53 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
.icon {
width: 30px;
}
.spacer { .spacer {
@include mobile-only { @include mobile-only {
width: 100%; width: 100%;
height: $header-size; height: $header-size-mobile;
} }
} }
.nav { .nav {
transition: background .5s ease;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: 50px; height: 50px;
background: $c-white;
z-index: 10; z-index: 10;
display: block; display: block;
color: $text-color;
background-color: $background-color-secondary;
@include tablet-min{ @include tablet-min{
width: 95px; width: 95px;
height: 100vh; height: 100vh;
} }
&__logo { &__logo{
width: 55px; width: 55px;
height: $header-size; height: $header-size-mobile;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: $background-nav-logo; background: $c-dark;
@include tablet-min{ @include tablet-min{
width: 95px; width: 95px;
height: $header-size;
} }
&-image{ &-image{
width: 35px; width: 35px;
height: 31px; height: 31px;
fill: $green; fill: $c-green;
transition: transform 0.5s ease; transition: transform 0.5s ease;
@include tablet-min{ @include tablet-min{
width: 45px; width: 45px;
height: 40px; height: 40px;
} }
} }
&:hover &-image { &:hover &-image{
transform: scale(1.04); transform: scale(1.04);
} }
} }
&__hamburger { &__hamburger{
display: block; display: block;
position: fixed; position: fixed;
width: 55px; width: 55px;
@@ -138,22 +129,23 @@ export default {
top: 0; top: 0;
right: 0; right: 0;
cursor: pointer; cursor: pointer;
background: $c-white;
z-index: 10; z-index: 10;
border-left: 1px solid $background-color; border-left: 1px solid $c-light;
@include tablet-min{ @include tablet-min{
display: none; display: none;
} }
.bar { .bar{
position: absolute; position: absolute;
width: 23px; width: 23px;
height: 1px; height: 1px;
background-color: $text-color-70; background: rgba($c-dark, 0.5);
transition: all 300ms ease; transition: all 300ms ease;
&:nth-child(1) { &:nth-child(1){
left: 16px; left: 16px;
top: 17px; top: 17px;
} }
&:nth-child(2) { &:nth-child(2){
left: 16px; left: 16px;
top: 25px; top: 25px;
&:after { &:after {
@@ -163,15 +155,16 @@ export default {
top: 0px; top: 0px;
width: 23px; width: 23px;
height: 1px; height: 1px;
background: transparent;
transition: all 300ms ease; transition: all 300ms ease;
} }
} }
&:nth-child(3) { &:nth-child(3){
right: 15px; right: 15px;
top: 33px; top: 33px;
} }
} }
&--active { &--active{
.bar{ .bar{
&:nth-child(1), &:nth-child(1),
&:nth-child(3){ &:nth-child(3){
@@ -182,13 +175,12 @@ export default {
} }
&:nth-child(2):after { &:nth-child(2):after {
transform: rotate(-90deg); 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; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
@@ -197,22 +189,23 @@ export default {
position: fixed; position: fixed;
left: 0; left: 0;
top: 50px; top: 50px;
border-top: 1px solid $background-color; background: rgba($c-white, 0.98);
@include mobile-only { border-top: 1px solid $c-light;
display: flex; @include mobile-only{
flex-wrap: wrap;
font-size: 0; font-size: 0;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
background-color: $background-95; height: calc(100vh - 50px);
transition: all 0.5s ease;
text-align: left; text-align: left;
&--active{ &--active{
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
} }
@include tablet-min { @include tablet-min{
display: flex; display: flex;
background: transparent;
position: relative; position: relative;
display: block; display: block;
width: 100%; width: 100%;
@@ -220,44 +213,31 @@ export default {
top: 0; top: 0;
} }
} }
&__item { &__item{
transition: background .5s ease, color .5s ease, border .5s ease; @include mobile-only{
background-color: $background-color-secondary; display: inline-block;
color: $text-color-70;
@include mobile-only {
flex: 0 0 50%;
text-align: center; text-align: center;
border-bottom: 1px solid $background-color; width: 50%;
border-bottom: 1px solid $c-light;
&:nth-child(odd){ &:nth-child(odd){
border-right: 1px solid $background-color; border-right: 1px solid $c-light;
&:last-child {
// flex: 0 0 100%;
}
} }
} }
@include tablet-min { @include tablet-min{
width: 100%; width: 100%;
border-bottom: 1px solid $text-color-5; border-bottom: 1px solid $c-light;
&--profile{
&--profile {
position: fixed; position: fixed;
right: 0; right: 0;
top: 0; top: 0;
width: $header-size; width: $header-size;
height: $header-size; height: $header-size;
border-bottom: 0; border-bottom: 0;
border-left: 1px solid $background-color; border-left: 1px solid $c-light;
} }
} }
&:hover, .is-active {
color: $text-color;
background-color: $background-color;
}
} }
&__link { &__link{
background-color: inherit; // a elements have a transparent background
width: 100%; width: 100%;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -268,6 +248,8 @@ export default {
text-decoration: none; text-decoration: none;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
color: rgba($c-dark, 0.7);
transition: color 0.5s ease, background 0.5s ease;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
&-wrap { &-wrap {
@@ -276,38 +258,49 @@ export default {
align-items: center; align-items: center;
} }
@include mobile-only { @include mobile-only{
font-size: 10px; font-size: 10px;
padding: 20px 0; padding: 20px 0;
} }
@include tablet-min { @include tablet-min{
width: 95px; width: 95px;
height: 95px; height: 95px;
font-size: 9px; font-size: 9px;
&--profile { &--profile{
width: 75px; width: 75px;
height: 75px; height: 75px;
background-color: $background-color-secondary; background: $c-white;
} }
} }
&-icon { &-icon{
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $text-color-70; fill: rgba($c-dark, 0.7);
@include tablet-min { transition: fill 0.5s ease;
@include tablet-min{
width: 20px; width: 20px;
height: 20px; height: 20px;
margin-bottom: 5px; margin-bottom: 5px;
} }
} }
&-title { &-title{
margin-top: 5px; margin-top: 5px;
display: block; display: block;
width: 100%; width: 100%;
} }
&:hover &-icon, &.is-active &-icon { &:hover{
fill: $text-color; color: $c-dark;
}
&:hover &-icon{
fill: $c-dark;
}
&.is-active{
color: $c-dark;
background: $c-light;
}
&.is-active &-icon{
fill: $c-dark;
} }
} }
} }

View File

@@ -10,11 +10,10 @@
<seasoned-button @click="logOut">Log out</seasoned-button> <seasoned-button @click="logOut">Log out</seasoned-button>
</div> </div>
</header> </header>
<settings v-if="showSettings"></settings> <settings v-if="showSettings"></settings>
<list-header title="User requests" :info="resultCount"/> <movies-list :propList="user_requestsList"></movies-list>
<results-list v-if="results" :results="results" />
</div> </div>
<section class="not-found" v-if="!userLoggedIn"> <section class="not-found" v-if="!userLoggedIn">
@@ -29,35 +28,23 @@
</template> </template>
<script> <script>
import storage from '@/storage' import storage from '@/storage.js'
import store from '@/store' import MoviesList from '@/components/MoviesList.vue'
import ListHeader from '@/components/ListHeader' import Settings from '@/components/Settings.vue'
import ResultsList from '@/components/ResultsList' import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import Settings from '@/components/Settings'
import SeasonedButton from '@/components/ui/SeasonedButton'
import { getEmoji, getUserRequests } from '@/api' import { getEmoji } from '@/api.js'
// import CreatedLists from './CreatedLists.vue'
export default { export default {
components: { ListHeader, ResultsList, Settings, SeasonedButton }, components: { MoviesList, Settings, SeasonedButton },
data(){ data(){
return{ return{
userLoggedIn: '', userLoggedIn: '',
userName: '', userName: '',
emoji: '', emoji: '',
results: undefined, showSettings: false,
totalResults: undefined, user_requestsList: storage.user_requestsList
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`
} }
}, },
methods: { methods: {
@@ -87,24 +74,16 @@ export default {
} }
}, },
created(){ created(){
document.title = 'Profile' + storage.pageTitlePostfix;
storage.backTitle = document.title;
if(!localStorage.getItem('token')){ if(!localStorage.getItem('token')){
this.userLoggedIn = false; this.userLoggedIn = false;
} else { } else {
this.userLoggedIn = true; this.userLoggedIn = true;
this.getUserInfo(); this.getUserInfo();
getUserRequests()
.then(results => {
this.results = results.results
this.totalResults = results.total_results
})
getEmoji() getEmoji()
.then(resp => { .then(resp => this.emoji = resp.data.emoji )
const { emoji } = resp
this.emoji = emoji
store.dispatch('documentTitle/updateEmoji', emoji)
})
} }
} }
} }
@@ -114,10 +93,6 @@ export default {
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
.button--group {
display: flex;
}
// DUPLICATE CODE // DUPLICATE CODE
.profile{ .profile{
&__header{ &__header{
@@ -125,17 +100,7 @@ export default {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20px; padding: 20px;
border-bottom: 1px solid $text-color-5; border-bottom: 1px solid rgba($c-dark, 0.05);
@include mobile-only {
flex-direction: column;
align-items: flex-start;
.button--group {
padding-top: 2rem;
}
}
@include tablet-min{ @include tablet-min{
padding: 29px 30px; padding: 29px 30px;
} }
@@ -150,7 +115,7 @@ export default {
margin: 0; margin: 0;
font-size: 16px; font-size: 16px;
line-height: 16px; line-height: 16px;
color: $text-color; color: $c-dark;
font-weight: 300; font-weight: 300;
@include tablet-min{ @include tablet-min{
font-size: 18px; font-size: 18px;

View File

@@ -1,52 +1,80 @@
<template> <template>
<section> <section class="profile">
<h1>Register new user</h1> <div class="profile__content">
<h2 class='settings__header'>Register new user</h2>
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" /> <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="password" icon="Keyhole" type="password" <transition name="message-fade">
:value.sync="password" @enter="requestNewUser"/> <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="repeat password" icon="Keyhole" type="password" <div class="form__group">
:value.sync="passwordRepeat" @enter="requestNewUser"/> <seasoned-button @click="requestNewUser">Register</seasoned-button>
</div>
</form>
<seasoned-button @click="requestNewUser">Register</seasoned-button> <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>
<router-link class="link" to="/signin">Have a user? Sign in here</router-link> </div>
<seasoned-messages :messages.sync="messages"></seasoned-messages> <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>
</section> </section>
</template> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
import SeasonedButton from '@/components/ui/SeasonedButton' import storage from '@/storage.js'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedInput from '@/components/ui/SeasonedInput.vue'
export default { export default {
components: { SeasonedButton, SeasonedInput, SeasonedMessages }, components: { SeasonedButton, SeasonedInput },
data() { data(){
return { return{
messages: [], userLoggedIn: '',
username: null, username: undefined,
password: null, password: undefined,
passwordRepeat: null passwordRepeat: undefined,
showMessage: false,
messageClass: 'message-success',
messageText: 'hello world'
} }
}, },
methods: { methods: {
requestNewUser(){ requestNewUser(){
let { username, password, passwordRepeat } = this let username = this.username
let password = this.password
let password_re = this.passwordRepeat
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat); let verifyCredentials = this.checkCredentials(username, password, password_re);
if (verifyCredentials.verified) { if (verifyCredentials.verified) {
axios.post(`https://api.kevinmidboe.com/api/v1/user`, { axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
username: username, username: username,
password: password password: password
}) })
.then(resp => { .then(function(resp) {
let data = resp.data; let data = resp.data;
if (data.success){ if (data.success){
this.msg(data.message, 'success');
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('username', username); localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin) localStorage.setItem('admin', data.admin)
@@ -54,34 +82,28 @@ export default {
eventHub.$emit('setUserStatus'); eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' }) this.$router.push({ name: 'profile' })
} }
}) }.bind(this))
.catch(error => { .catch(function(error){
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.error }) this.msg(error.response.data.error, 'warning')
}); }.bind(this));
} }
else { else {
this.messages.push({ type: 'warning', title: 'Parse error', message: verifyCredentials.reason }) this.msg(verifyCredentials.reason, 'warning');
} }
}, },
checkCredentials(username, password, passwordRepeat) { checkCredentials(username, password, password_re) {
if (!username || username.length === 0) { if (password !== password_re) {
return {
verified: false,
reason: 'Fill inn username'
}
}
else if (!password || !passwordRepeat) {
return {
verified: false,
reason: "Fill inn both password fields"
}
}
else if (password !== passwordRepeat) {
return { return {
verified: false, verified: false,
reason: 'Passwords do not match' reason: 'Passwords do not match'
} }
} }
else if (username === undefined) {
return {
verified: false,
reason: 'Please insert username'
}
}
else { else {
return { return {
verified: true, verified: true,
@@ -89,38 +111,92 @@ export default {
} }
} }
}, },
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
},
logOut(){ logOut(){
localStorage.clear(); localStorage.clear();
eventHub.$emit('setUserStatus'); eventHub.$emit('setUserStatus');
this.$router.push({ name: 'home' }); this.$router.push({ name: 'home' });
} }
},
created(){
document.title = 'Profile' + storage.pageTitlePostfix;
storage.backTitle = document.title;
},
mounted(){
// this.$refs.email.focus();
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries";
@import "./src/scss/message";
section { // DUPLICATE CODE
padding: 1.3rem; .settings {
padding: 35px;
@include tablet-min { &__header {
padding: 4rem;
}
h1 {
margin: 0; margin: 0;
line-height: 16px; line-height: 16px;
color: $text-color; color: $c-dark;
font-weight: 300; font-weight: 300;
margin-bottom: 20px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;
} }
}
.profile__content {
padding: 35px;
display: flex;
justify-content: center;
flex-direction: column;
}
.link {
display: block; .center {
width: max-content; justify-content: center;
}
.form {
// TODO, fix this. if single child it adds weird margin
> div:last-child {
margin-top: 1rem; 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> </style>

View File

@@ -1,68 +0,0 @@
<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>

View File

@@ -1,102 +0,0 @@
<template>
<div>
<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>
<loader v-if="!results.length" />
</div>
</template>
<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,
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) {
searchTmdb(query, page)
.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 } = this.$route.query
if (!query) {
// abort
console.error('abort, no query')
}
this.query = decodeURIComponent(query)
this.page = page ? page : 1
this.title = `Search results: ${this.query}`
this.search()
}
}
</script>
<style lang="scss" scoped>
.fullwidth-button {
width: 100%;
margin: 1rem 0;
padding-bottom: 2rem;
display: flex;
justify-content: center;
}
</style>

View File

@@ -15,7 +15,7 @@
@keydown.up="navigateUp" @keydown.up="navigateUp"
@keydown.down="navigateDown" /> @keydown.down="navigateDown" />
<svg class="search--icon" fill="currentColor"><use xlink:href="#iconSearch"></use></svg> <svg class="search--icon"><use xlink:href="#iconSearch"></use></svg>
</div> </div>
<transition name="fade"> <transition name="fade">
@@ -43,9 +43,9 @@
</template> </template>
<script> <script>
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import { elasticSearchMoviesAndShows } from '@/api' import { elasticSearchMoviesAndShows } from '@/api.js'
import config from '@/config.json' import config from '@/config.json'
export default { export default {
@@ -104,21 +104,21 @@ export default {
elasticSearchMoviesAndShows(this.query) elasticSearchMoviesAndShows(this.query)
.then(resp => { .then(resp => {
const data = resp.hits.hits const data = resp.data.hits.hits
this.elasticSearchResults = data.map(item => { this.elasticSearchResults = data.map(item => {
const index = item._index.slice(0, -1) const index = item._index.slice(0, -1)
if (index === 'movie' || item._source.original_title) { if (index === 'movie') {
return { return {
name: item._source.original_title, name: item._source.original_title,
id: item._source.id, id: item._source.id,
type: 'movie' type: index
} }
} else if (index === 'show' || item._source.original_name) { } else if (index === 'show') {
return { return {
name: item._source.original_name, name: item._source.original_name,
id: item._source.id, id: item._source.id,
type: 'show' type: index
} }
} }
}) })
@@ -179,7 +179,7 @@ export default {
z-index: 5; z-index: 5;
min-height: $header-size; min-height: $header-size;
right: 0px; right: 0px;
background-color: $background-color-secondary; background-color: white;
@include mobile-only { @include mobile-only {
position: fixed; position: fixed;
@@ -209,51 +209,49 @@ export default {
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
color: $text-color-50;
&.active, &:hover, &:active { &.active, &:hover, &:active {
color: $text-color; color: $c-dark;
border-bottom: 2px solid $text-color; border-bottom: 2px solid black;
} }
} }
} }
} }
.search { .search {
height: $header-size; height: $header-size-mobile;
display: flex; display: flex;
position: fixed; position: fixed;
flex-wrap: wrap; flex-wrap: wrap;
z-index: 5; z-index: 5;
border: 0;
background-color: $background-color-secondary;
// TODO check if this is for mobile // TODO check if this is for mobile
width: calc(100% - 110px); width: calc(100% - 110px);
// width: 100%;
top: 0; top: 0;
right: 55px; right: 55px;
@include tablet-min{ @include tablet-min{
position: relative; position: relative;
height: $header-size;
width: 100%; width: 100%;
right: 0px; right: 0px;
} }
input { input {
// height: 75px;
display: block; display: block;
width: 100%; width: 100%;
padding: 13px 20px 13px 45px; padding: 13px 20px 13px 45px;
outline: none; outline: none;
margin: 0;
border: 0; border: 0;
background-color: $background-color-secondary; background-color: transparent;
color: $c-dark;
font-weight: 300; font-weight: 300;
font-size: 19px; font-size: 19px;
color: $text-color;
transition: background-color .5s ease, color .5s ease;
@include tablet-min { @include tablet-min {
padding: 13px 30px 13px 60px; padding: 13px 30px 13px 60px;
@@ -263,7 +261,7 @@ export default {
&--icon{ &--icon{
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $text-color-50; fill: rgba($c-dark, 0.5);
transition: fill 0.5s ease; transition: fill 0.5s ease;
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;

View File

@@ -6,24 +6,22 @@
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span> <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"> <form class="form">
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/> <seasoned-input text="plex username" icon="Email"
<seasoned-input placeholder="plex password" icon="Keyhole" type="password" @inputValue="setValue('plexUsername', $event)"/>
:value.sync="plexPassword" @submit="authenticatePlex" /> <seasoned-input text="plex password" icon="Keyhole" type="password"
@inputValue="setValue('plexPassword', $event)"/>
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button> <seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
<seasoned-messages :messages.sync="messages" />
</form> </form>
<hr class='setting__divider'> <hr class='setting__divider'>
<h3 class='settings__header'>Change password</h3> <h3 class='settings__header'>Change password</h3>
<form class="form"> <form class="form">
<seasoned-input placeholder="new password" icon="Keyhole" type="password" <seasoned-input text="new password" icon="Keyhole" type="password"
:value.sync="newPassword" /> @inputValue="setValue('newPass', $event)"/>
<seasoned-input text="repeat new password" icon="Keyhole" type="password"
<seasoned-input placeholder="repeat new password" icon="Keyhole" type="password" @inputValue="setValue('newPassConfirm', $event)"/>
:value.sync="newPasswordRepeat" />
<seasoned-button @click="changePassword">change password</seasoned-button> <seasoned-button @click="changePassword">change password</seasoned-button>
</form> </form>
@@ -44,27 +42,26 @@
</template> </template>
<script> <script>
import storage from '@/storage' import storage from '@/storage.js'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput.vue'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { plexAuthenticate } from '@/api' import { plexAuthenticate } from '@/api.js'
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton },
data(){ data(){
return{ return{
userLoggedIn: '', userLoggedIn: '',
messages: [], plexUsername: undefined,
plexUsername: null, plexPassword: undefined,
plexPassword: null, newPass: undefined,
newPassword: null, newPassConfirm: undefined
newPasswordRepeat: null
} }
}, },
methods: { methods: {
setValue(l, t) { setValue(l, t) {
console.log('l, t', l, t)
this[l] = t this[l] = t
}, },
changePassword() { changePassword() {
@@ -75,20 +72,18 @@ export default {
let password = this.plexPassword let password = this.plexPassword
plexAuthenticate(username, password) plexAuthenticate(username, password)
.then(resp => { .then((resp) => {
const data = resp.data let data = resp.data;
this.messages.push({ type: 'success', title: 'Authenticated with plex', message: 'Successfully linked plex account with seasoned request' }) console.log('response from plex:', data.user)
console.log('response from plex:', data.username)
}) })
.catch(error => { .catch((error) => {
console.error(error); console.log('error: ', error)
this.messages.push({ type: 'error', title: 'Something went wrong', message: error.message })
}) })
} }
}, },
created(){ created(){
document.title = 'Settings' + storage.pageTitlePostfix;
storage.backTitle = document.title;
if (localStorage.getItem('token')){ if (localStorage.getItem('token')){
this.userLoggedIn = true this.userLoggedIn = true
} }
@@ -99,7 +94,6 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
a { a {
text-decoration: none; text-decoration: none;
} }
@@ -134,7 +128,7 @@ a {
&__header { &__header {
margin: 0; margin: 0;
line-height: 16px; line-height: 16px;
color: $text-color; color: $c-dark;
font-weight: 300; font-weight: 300;
margin-bottom: 20px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;

View File

@@ -1,34 +1,52 @@
<template> <template>
<section> <section class="profile">
<h1>Sign in</h1> <div class="profile__content">
<h2 class='settings__header'>Sign in</h2>
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" /> <form class="form">
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/> <div class="form__buffer"></div>
<seasoned-button @click="signin">sign in</seasoned-button> <seasoned-input text="username" icon="Email" type="username"
@inputValue="setValue('username', $event)" />
<seasoned-input text="username" icon="Keyhole" type="password"
@inputValue="setValue('password', $event)" />
<router-link class="link" to="/register">Don't have a user? Register here</router-link> <seasoned-button @click="signin">sign in</seasoned-button>
<seasoned-messages :messages.sync="messages"></seasoned-messages>
<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>
</section> </section>
</template> </template>
<script> <script>
import axios from 'axios' import axios from 'axios'
import storage from '../storage' import storage from '../storage.js'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput.vue'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton.vue'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton },
data(){ data(){
return{ return{
messages: [], userLoggedIn: '',
username: null, showMessage: false,
password: null messageClass: 'message-success',
messageText: 'hello world',
username: undefined,
password: undefined
} }
}, },
methods: { methods: {
@@ -43,57 +61,98 @@ export default {
username: username, username: username,
password: password password: password
}) })
.then(resp => { .then(function (resp){
let data = resp.data; let data = resp.data;
if (data.success){ if (data.success){
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('username', username); localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin); localStorage.setItem('admin', data.admin);
this.userLoggedIn = true;
eventHub.$emit('setUserStatus'); eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' }) this.$router.push({ name: 'profile' })
} }
}) }.bind(this))
.catch(error => { .catch(function (error){
if (error.message.endsWith('401')) { if (error.message.endsWith('401'))
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' }) this.msg('Incorrect username or password ', 'warning')
} else
else { this.msg(error.message, 'warning')
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message }) }.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;
},
}, },
created(){ created(){
document.title = 'Sign in' + storage.pageTitlePostfix; document.title = 'Sign in' + storage.pageTitlePostfix;
storage.backTitle = document.title; storage.backTitle = document.title;
if (this.userLoggedIn == true) {
this.$router.push({ name: 'profile' })
}
},
mounted(){
// this.$refs.email.focus();
} }
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/message";
section { // DUPLICATE CODE
padding: 1.3rem; .settings {
padding: 35px;
@include tablet-min { &__header {
padding: 4rem;
}
h1 {
margin: 0; margin: 0;
line-height: 16px; line-height: 16px;
color: $text-color; color: $c-dark;
font-weight: 300; font-weight: 300;
margin-bottom: 20px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;
} }
}
.profile__content {
padding: 35px;
display: flex;
justify-content: center;
flex-direction: column;
}
.link { .form {
display: block; > div:last-child {
width: max-content;
margin-top: 1rem; 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> </style>

View File

@@ -1,105 +1,53 @@
<template> <template>
<div v-if="show" class="container"> <div v-if="show">
<h2 class="torrentHeader-text">Searching for: {{ editedSearchQuery || query }}</h2> <h2 class="title">torrents: {{ query }}</h2>
<!-- <div class="torrentHeader">
<span class="torrentHeader-text">Searching for:&nbsp;</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"> <div v-if="listLoaded">
<div v-if="torrents.length > 0"> <ul class="filter">
<ul class="filter"> <li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li>
<li class="filter-item" v-for="(item, index) in release_types" @click="applyFilter(item, index)" :class="{'active': item === selectedRelaseType}">{{ item }}</li> </ul>
</ul>
<table> <table>
<tr class="table__header noselect"> <tr class="table__header noselect">
<th @click="sortTable('name')" :class="selectedSortableClass('name')"> <th @click="sortTable('name')">
<span>Name</span> <span>Name</span>
<span v-if="prevCol === 'name' && direction"></span> <span v-if="prevCol === 'name' && direction"></span>
<span v-if="prevCol === 'name' && !direction"></span> <span v-if="prevCol === 'name' && !direction"></span>
</th> </th>
<th @click="sortTable('seed')" :class="selectedSortableClass('seed')"> <th @click="sortTable('seed')">
<span>Seed</span> <span>Seed</span>
<span v-if="prevCol === 'seed' && direction"></span> <span v-if="prevCol === 'seed' && direction"></span>
<span v-if="prevCol === 'seed' && !direction"></span> <span v-if="prevCol === 'seed' && !direction"></span>
</th> </th>
<th @click="sortTable('size')" :class="selectedSortableClass('size')"> <th @click="sortTable('size')">
<span>Size</span> <span>Size</span>
<span v-if="prevCol === 'size' && direction"></span> <span v-if="prevCol === 'size' && direction"></span>
<span v-if="prevCol === 'size' && !direction"></span> <span v-if="prevCol === 'size' && !direction"></span>
<th> <th>
<span>Magnet</span> <span>Magnet</span>
</th> </th>
</tr> </tr>
<tr v-for="torrent in torrents" class="table__content"> <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.name }}</td>
<td @click="expand($event, torrent.name)">{{ torrent.seed }}</td> <td @click="expand($event, torrent.name)">{{ torrent.seed }}</td>
<td @click="expand($event, torrent.name)">{{ torrent.size }}</td> <td @click="expand($event, torrent.name)">{{ torrent.size }}</td>
<td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download"> <td @click="sendTorrent(torrent.magnet, torrent.name, $event)" class="download">
<svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg> <svg class="download__icon"><use xlink:href="#iconUnmatched"></use></svg>
</td> </td>
</tr> </tr>
</table> </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> </div>
</div> <i v-else class="torrentloader"></i>
<div v-else class="torrentloader"><i></i></div>
</div> </div>
</template> </template>
<script> <script>
import storage from '@/storage' import storage from '@/storage.js'
import store from '@/store' import { sortableSize } from '@/utils.js'
import { sortableSize } from '@/utils' import { searchTorrents, addMagnet } from '@/api.js'
import { searchTorrents, addMagnet } from '@/api'
import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedInput from '@/components/ui/SeasonedInput'
export default { export default {
components: { SeasonedButton, SeasonedInput },
props: { props: {
query: { query: {
type: String, type: String,
@@ -116,34 +64,21 @@ export default {
data() { data() {
return { return {
listLoaded: false, listLoaded: false,
torrents: [], torrents: undefined,
torrentResponse: undefined, torrentResponse: undefined,
currentPage: 0, currentPage: 0,
prevCol: '', prevCol: '',
direction: false, direction: false,
release_types: ['all'], release_types: ['all'],
selectedRelaseType: 'all', selectedRelaseType: 'all'
editSearchQuery: false,
editedSearchQuery: ''
} }
}, },
beforeMount() { beforeMount() {
if (localStorage.getItem('admin')) { if (localStorage.getItem('admin')) {
this.fetchTorrents() this.fetchTorrents()
} }
store.dispatch('torrentModule/reset')
}, },
methods: { methods: {
selectedSortableClass(headerName) {
return headerName === this.prevCol ? 'active' : ''
},
resetTorrentsAndToggleEditSearchQuery() {
this.torrents = []
this.toggleEditSearchQuery()
},
toggleEditSearchQuery() {
this.editSearchQuery = !this.editSearchQuery;
},
expand(event, name) { expand(event, name) {
const existingExpandedElement = document.getElementsByClassName('expanded')[0] const existingExpandedElement = document.getElementsByClassName('expanded')[0]
@@ -249,41 +184,35 @@ export default {
this.torrents = torrents.filter(torrent => torrent.release_type.includes(item)) this.torrents = torrents.filter(torrent => torrent.release_type.includes(item))
this.sortTable(this.prevCol, true) this.sortTable(this.prevCol, true)
}, },
updateResultCountInStore() { fetchTorrents(){
store.dispatch('torrentModule/setResults', this.torrents) searchTorrents(this.query, 'all', this.currentPage, storage.token)
store.dispatch('torrentModule/setResultCount', this.torrentResponse.length) .then(resp => {
}, let data = resp.data;
fetchTorrents(query=undefined){ console.log('data results', data.results);
this.listLoaded = false; this.torrentResponse = data.results;
this.editSearchQuery = false; this.torrents = data.results;
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; 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> </script>
<style lang="scss" scoped> <style lang="scss">
@import "./src/scss/variables"; @import "./src/scss/variables";
.expanded { .expanded {
display: flex; display: flex;
margin: 0 1rem; margin: 0 1rem;
max-width: 100%; max-width: 100%;
border-left: 1px solid $text-color; border-left: 1px solid rgba($c-dark, 0.5);
border-right: 1px solid $text-color; border-right: 1px solid rgba($c-dark, 0.5);
border-bottom: 1px solid $text-color; border-bottom: 1px solid rgba($c-dark, 0.5);
td { td {
// border-left: 1px solid $c-dark; // border-left: 1px solid $c-dark;
@@ -298,44 +227,16 @@ export default {
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/elements"; @import "./src/scss/elements";
.container { .title {
background-color: $background-color; margin: 0;
} font-weight: 400;
text-transform: uppercase;
.torrentHeader { text-align: center;
display: flex; font-size: 14px;
align-items: center; color: $c-green;
justify-content: center;
padding-bottom: 20px; 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;
}
} }
} }
@@ -349,9 +250,9 @@ table {
display: flex; display: flex;
padding: 0; padding: 0;
margin: 0 1rem; margin: 0 1rem;
border-left: 1px solid $text-color; border-left: 1px solid rgba($c-dark, 0.8);
border-right: 1px solid $text-color; border-right: 1px solid rgba($c-dark, 0.8);
border-bottom: 1px solid $text-color; border-bottom: 1px solid rgba($c-dark, 0.8);
th, td { th, td {
display: flex; display: flex;
@@ -363,7 +264,6 @@ table {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
min-width: 75px;
} }
th:first-child, td:first-child { th:first-child, td:first-child {
@@ -397,7 +297,7 @@ table {
.table__content { .table__content {
td:not(:last-child) { td:not(:last-child) {
border-right: 1px solid $text-color; border-right: 1px solid rgba($c-dark, 0.8);
} }
} }
@@ -409,12 +309,12 @@ table {
} }
.table__header { .table__header {
color: $text-color; background-color: white;
color: $c-dark;
text-transform: uppercase; text-transform: uppercase;
cursor: pointer; cursor: pointer;
background-color: $background-color-secondary;
border-top: 1px solid $text-color; border-top: 1px solid rgba($c-dark, 0.8);
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-top-right-radius: 3px; border-top-right-radius: 3px;
@@ -440,63 +340,44 @@ table {
} }
th:not(:last-child) { th:not(:last-child) {
border-right: 1px solid $text-color; border-right: 1px solid rgba($c-dark, 0.8);
} }
} }
.editQuery {
display: flex;
width: 70%;
justify-content: center;
@include mobile-only {
width: 90%;
}
}
.download { .download {
&__icon { &__icon {
fill: $text-color-70; fill: rgba($c-dark, 0.6);
height: 1.2rem; height: 1.2rem;
&:hover { &:hover {
fill: $text-color; fill: $c-dark;
cursor: pointer; cursor: pointer;
} }
} }
&.active &__icon { &.active &__icon {
fill: $green; fill: $c-green;
} }
} }
.torrentloader { .torrentloader{
width: 100%; animation: load 1s linear infinite;
padding: 2rem 0; border: 2px solid $c-dark;
border-radius: 50%;
i { display: block;
animation: load 1s linear infinite; height: 30px;
border: 2px solid $text-color; left: 50%;
margin: 2rem auto;
width: 30px;
&:after {
border: 5px solid $c-green;
border-radius: 50%; border-radius: 50%;
display: block; content: '';
height: 30px; left: 10px;
left: 50%; position: absolute;
margin: 0 auto; top: 16px;
width: 30px;
&:after {
border: 5px solid $green;
border-radius: 50%;
content: '';
left: 10px;
position: absolute;
top: 16px;
}
} }
} }
@keyframes load { @keyframes load {

View File

@@ -0,0 +1,95 @@
<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>

View File

@@ -17,26 +17,29 @@
align-items: center; align-items: center;
&--icon{ &--icon{
border: 2px solid $text-color-70; border: 2px solid rgba($c-dark, 0.9);
border-radius: 50%; border-radius: 50%;
display: block; display: block;
height: 40px; height: 40px;
// left: 50%;
// margin: -1.5em;
position: absolute; position: absolute;
width: 40px; width: 40px;
&-spinner { &-spinner {
display: block; // border: 2px solid transparent;
display: block;
animation: load 1s linear infinite; animation: load 1s linear infinite;
height: 35px; height: 35px;
width: 35px; width: 35px;
&:after { &:after {
border: 7px solid $green-90; border: 7px solid rgba($c-green, 0.9);
border-radius: 50%; border-radius: 50%;
content: ''; content: '';
left: 8px; left: 8px;
position: absolute; position: absolute;
top: 22px; top: 22px;
} }
} }
} }
@keyframes load { @keyframes load {

View File

@@ -13,6 +13,7 @@ export default {
}, },
methods: { methods: {
emit() { emit() {
console.log('emitted')
this.$emit('click') this.$emit('click')
} }
} }
@@ -25,30 +26,36 @@ export default {
.button{ .button{
display: inline-block; display: inline-block;
border: 1px solid $text-color; border: 1px solid $c-dark;
text-transform: uppercase; text-transform: uppercase;
background: $c-dark;
font-weight: 300; font-weight: 300;
font-size: 11px; font-size: 11px;
line-height: 2; line-height: 2;
height: 45px;
letter-spacing: 0.5px; letter-spacing: 0.5px;
padding: 5px 20px 4px 20px; padding: 5px 20px 4px 20px;
margin: 0;
margin-right: 0.3rem;
cursor: pointer; cursor: pointer;
color: $text-color; color: $c-dark;
background: $background-color-secondary; background: transparent;
outline: none; outline: none;
transition: background 0.5s ease, color 0.5s ease, border-color .5s ease; transition: background 0.5s ease, color 0.5s ease;
@include tablet-min{ @include tablet-min{
font-size: 12px; font-size: 12px;
padding: 6px 20px 5px 20px; padding: 6px 20px 5px 20px;
} }
&:active, &:hover{
body:not(.touch) &:hover, &:focus, &:active, &.active { background: $c-dark;
background: $text-color; color: $c-white;
color: $background-color; }
body:not(.touch) &:hover, &:focus{
background: $c-dark;
color: $c-white;
}
&.active {
@extend .button;
background: $c-dark;
color: $c-white;
} }
} }
</style> </style>

View File

@@ -1,37 +1,27 @@
<template> <template>
<div class="group" :class="{ completed: value }"> <div class="group" :class="{ completed: value.length > 0 }">
<svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg> <svg class="group__input-icon"><use v-bind="{'xlink:href':'#icon' + icon}"></use></svg>
<input class="group__input" :type="tempType || type" @input="handleInput" v-model="inputValue" <input class="group__input" :type="tempType || type" ref="plex_username"
:placeholder="placeholder" @keyup.enter="submit" /> v-model="value" :placeholder="text" @input="handleInput" />
<i v-if="value && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i> <i v-if="value.length > 0 && type === 'password'" @click="toggleShowPassword" class="group__input-show noselect">show</i>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
placeholder: { type: String }, text: { type: String },
icon: { type: String }, icon: { type: String },
type: { type: String, default: 'text' }, type: { type: String }
value: { type: String, default: undefined }
}, },
data() { data() {
return { return { value: '', tempType: undefined }
inputValue: this.value || undefined,
tempType: undefined
}
}, },
methods: { methods: {
submit(event) { handleInput(value) {
this.$emit('enter') console.log('this.value', this.value)
}, this.$emit('inputValue', this.value)
handleInput(event) {
if (this.value !== undefined) {
this.$emit('update:value', this.inputValue)
} else {
this.$emit('change', this.inputValue, event)
}
}, },
toggleShowPassword() { toggleShowPassword() {
if (this.tempType === 'text') { if (this.tempType === 'text') {
@@ -50,45 +40,42 @@ export default {
.group{ .group{
display: flex; display: flex;
margin-bottom: 1rem; margin-bottom: 1rem;
width: 100%;
&:hover, &:focus { &:hover, &:focus {
.group__input { .group__input {
border-color: $text-color; border-color: $c-dark;
&-icon { &-icon {
fill: $text-color; fill: $c-dark;
} }
} }
} }
&.completed { &.completed {
.group__input { .group__input {
border-color: $text-color; border-color: $c-dark;
&-icon { &-icon {
fill: $text-color; fill: $c-dark;
} }
} }
} }
&__input { &__input {
width: 100%; width: 75%;
max-width: 35rem; max-width: 35rem;
padding: 10px 10px 10px 45px; padding: 10px 10px 10px 45px;
// padding: 15px 10px 15px 45px;
outline: none; outline: none;
background-color: $background-color-secondary; background-color: $c-white;
color: $text-color; color: $c-dark;
font-weight: 100; font-weight: 100;
font-size: 1.2rem; font-size: 1.2rem;
border: 1px solid $text-color-50; border: 1px solid rgba($c-dark, 0.5);
margin: 0; margin-left: -2.2rem;
margin-left: -2.2rem !important; // margin-bottom: 1rem;
z-index: 3; z-index: 3;
transition: color .5s ease, background-color .5s ease, border .5s ease; transition: border-color .5s ease;
border-radius: 0;
-webkit-appearance: none;
&-show { &-show {
position: relative; position: relative;
@@ -98,14 +85,14 @@ export default {
height: 100%; height: 100%;
font-size: 0.9rem; font-size: 0.9rem;
cursor: pointer; cursor: pointer;
color: $text-color-50; color: rgba($c-dark, 0.5);
} }
} }
&__input-icon { &__input-icon {
width: 24px; width: 24px;
height: 24px; height: 24px;
fill: $text-color-50; fill: rgba($c-dark, 0.5);
transition: fill 0.5s ease; transition: fill 0.5s ease;
pointer-events: none; pointer-events: none;
margin-top: 10px; margin-top: 10px;

View File

@@ -1,153 +0,0 @@
<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>{{ message.title || defaultTitles[message.type] }}</h2>
<span>{{ 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)
}
},
// watch: {
// messages(propState, oldState) {
// const newMessage = propState.filter(msg => !this.localMessages.includes(msg))
// console.log('newMessage', newMessage)
// this.localMessages = this.localMessages.concat(newMessage)
// }
// }
}
</script>
<style lang="scss" scoped>
@import "./src/scss/variables";
.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;
height: 75px;
display: flex;
margin-top: 1rem;
margin-bottom: 1rem;
color: $text-color-70;
> div {
margin: 6px 24px;
width: 100%;
}
h2 {
font-weight: 300;
letter-spacing: 0.25px;
margin: 0;
font-size: 1.3rem;
color: $text-color;
transition: color .5s ease;
}
span {
font-weight: 300;
color: $text-color-70;
transition: color .5s ease;
}
.pinstripe {
height: 100%;
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>

View File

@@ -1,51 +0,0 @@
<template>
<div class="darkToggle">
<span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
</div>
</template>
<script>
export default {
data() {
return {
darkmode: window.getComputedStyle(document.body).colorScheme.includes('dark')
}
},
methods: {
toggleDarkmode() {
this.darkmode = !this.darkmode;
document.body.className = this.darkmode ? 'dark' : 'light'
}
},
computed: {
darkmodeToggleIcon() {
return this.darkmode ? '🌝' : '🌚'
}
}
}
</script>
<style lang="scss" scoped>
.darkToggle {
height: 25px;
width: 25px;
cursor: pointer;
// background-color: red;
position: fixed;
margin-bottom: 10px;
margin-right: 2px;
bottom: 0;
right: 0;
z-index: 1;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

View File

@@ -1,122 +0,0 @@
<template>
<div>
<a @click="$emit('click')"><li>
<figure :class="activeClassIfActive">
<svg><use :xlink:href="iconRefNameIfActive"/></svg>
</figure>
<span :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: true
},
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-70;
cursor: pointer;
}
.active {
color: $text-color;
}
.pending {
color: #f8bd2d;
}
.supplementary-text {
flex-grow: 1;
text-align: right;
}
figure, figure > svg {
width: 18px;
height: 18px;
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;
}
&:hover &-icon {
fill: $text-color-70;
cursor: pointer;
}
&.active > svg {
fill: $green;
}
}
}
</style>

View File

@@ -2,7 +2,6 @@ import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import axios from 'axios' import axios from 'axios'
import router from './routes' import router from './routes'
import store from './store'
import Toast from './plugins/Toast' import Toast from './plugins/Toast'
import DataTablee from 'vue-data-tablee' import DataTablee from 'vue-data-tablee'
@@ -17,12 +16,9 @@ Vue.use(Toast)
Vue.use(DataTablee) Vue.use(DataTablee)
Vue.use(VModal, { dialog: true }) Vue.use(VModal, { dialog: true })
store.dispatch('darkmodeModule/findAndSetDarkmodeSupported')
new Vue({ new Vue({
el: '#app', el: '#app',
router, router,
store,
components: { App }, components: { App },
template: '<App />' template: '<App />'
}) })

View File

@@ -1,23 +0,0 @@
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)
}
}
}

View File

@@ -1,41 +0,0 @@
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: 'request',
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)
}
}
}

View File

@@ -1,40 +0,0 @@
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')
}
}
}

View File

@@ -1,6 +1,5 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import store from '@/store'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -19,35 +18,37 @@ let routes = [
{ {
name: 'list', name: 'list',
path: '/list/:name', path: '/list/:name',
component: (resolve) => require(['./components/ListPage.vue'], resolve) component: (resolve) => require(['./components/MoviesList.vue'], resolve)
}, },
{ {
name: 'request', name: 'request',
path: '/request/all', path: '/request/all',
components: { components: {
'request-router-view': require('./components/ListPage.vue') 'request-router-view': require('./components/MoviesList.vue')
} }
}, },
{ {
name: 'search', name: 'search',
path: '/search', path: '/search',
component: (resolve) => require(['./components/Search.vue'], resolve) component: (resolve) => require(['./components/MoviesList.vue'], resolve)
}, },
{ {
name: 'register', name: 'register',
path: '/register', path: '/register',
component: (resolve) => require(['./components/Register.vue'], resolve) component: (resolve) => require(['./components/Register.vue'], resolve)
}, },
{
name: 'settings',
path: '/settings',
component: (resolve) => require(['./components/Settings.vue'], resolve)
},
{ {
name: 'signin', name: 'signin',
path: '/signin', path: '/signin',
component: (resolve) => require(['./components/Signin.vue'], resolve) component: (resolve) => require(['./components/Signin.vue'], resolve)
}, },
{
name: 'settings',
path: '/profile/settings',
components: {
'search-router-view': require('./components/Settings.vue')
}
},
// { // {
// name: 'user-requests', // name: 'user-requests',
// path: '/profile/requests', // path: '/profile/requests',
@@ -78,8 +79,6 @@ const router = new VueRouter({
}); });
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
store.dispatch('documentTitle/updateTitle', to.name)
// Toggle mobile nav // Toggle mobile nav
if(document.querySelector('.nav__hamburger--active')){ if(document.querySelector('.nav__hamburger--active')){
document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active'); document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active');

View File

@@ -2,18 +2,21 @@
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
.filter { .filter {
// margin: 10px 10px 20px;
margin: 1rem; margin: 1rem;
padding: 0; padding: 0;
overflow: auto; overflow: auto;
list-style: none; list-style: none;
border: 1px solid; border: 1px solid;
border-radius: 2px; border-radius: 2px;
// overflow: hidden;
display: flex; display: flex;
transition: color .2s ease; transition: color .2s ease;
// justify-content: space-evenly;
&-item { &-item {
padding: 6px 15px; padding: 6px 15px;
background-color: $background-color-secondary; background-color: $c-white;
transition: color 0.2s ease; transition: color 0.2s ease;
font-size: 13px; font-size: 13px;
font-weight: 200; font-weight: 200;
@@ -21,17 +24,16 @@
text-align: center; text-align: center;
width: 100%; width: 100%;
white-space:nowrap; white-space:nowrap;
// overflow: hidden;
&:nth-child(n+2) { &:nth-child(n+2) {
border-left: solid 1px; border-left: solid 1px;
} }
&.active, &:hover { &.active, &:hover {
border-color: transparent; border-color: transparent;
background-color: $teal; background-color: #091c24;
color: $green; color: $c-green;
cursor: pointer; cursor: pointer;
} }
@include tablet-min { @include tablet-min {
font-size: 16px; font-size: 16px;
} }

43
src/scss/message.scss Normal file
View File

@@ -0,0 +1,43 @@
// 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;
}

View File

@@ -1,123 +1,12 @@
// Colors // Colors
// @import "./media-queries"; $c-green: #01d277;
@import "./src/scss/media-queries"; $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;
:root { $header-size: 75px;
color-scheme: light; $header-size-mobile: 50px;
--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-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, .9);
--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: #1e1f22;
--background-color-secondary: #111111;
--background-95: rgba(30, 31, 34, 0.95);
--background-70: rgba(30, 31, 34, 0.8);
--background-40: rgba(30, 31, 34, 0.4);
}
}
@include mobile-only {
:root {
--header-size: 50px;
}
}
$header-size: var(--header-size);
$dark: rgb(30, 31, 34);
$green: var(--color-green);
$green-90: var(--color-green-90);
$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-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: #1e1f22;
--background-color-secondary: #111111;
--background-95: rgba(30, 31, 34, 0.95);
--background-70: rgba(30, 31, 34, 0.7);
--color-teal: #091c24;
}
.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-inverted: #fff;
--text-color-secondary: orange;
--background-color: #f8f8f8;
--background-color-secondary: #ffffff;
--background-95: rgba(255, 255, 255, 0.95);
--background-70: rgba(255, 255, 255, 0.7);
--background-nav-logo: #081c24;
--color-green: #01d277;
--color-teal: #091c24;
}

View File

@@ -1,18 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import torrentModule from './modules/torrentModule'
import darkmodeModule from './modules/darkmodeModule'
import documentTitle from './modules/documentTitle'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
torrentModule,
darkmodeModule,
documentTitle
}
})
export default store

View File

@@ -1,11 +1,9 @@
const sortableSize = (string) => { function sortableSize(string) {
const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const UNITS = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const [numStr, unit] = string.split(' '); const [numStr, unit] = string.split(' ');
if (UNITS.indexOf(unit) === -1)
if (UNITS.indexOf(unit) === -1)
return string return string
const exponent = UNITS.indexOf(unit) * 3
const exponent = UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent)) return numStr * (Math.pow(10, exponent))
} }

View File

@@ -12,20 +12,13 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.vue$/, test: /\.vue$/,
use: [ loader: 'vue-loader',
{ options: {
loader: 'vue-loader', loaders: {
options: { 'scss': 'vue-style-loader!css-loader!sass-loader',
loaders: { 'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
'scss': 'vue-style-loader!css-loader!sass-loader',
'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax'
}
}
},
{
loader: 'vue-svg-inline-loader'
} }
] }
}, },
{ {
test: /\.js$/, test: /\.js$/,
@@ -46,7 +39,6 @@ module.exports = {
] ]
}, },
resolve: { resolve: {
extensions: ['.js', '.vue', '.json', 'scss'],
alias: { alias: {
'vue$': 'vue/dist/vue.common.js', 'vue$': 'vue/dist/vue.common.js',
'@': path.resolve(__dirname, './src'), '@': path.resolve(__dirname, './src'),

3113
yarn.lock

File diff suppressed because it is too large Load Diff