11 Commits

38 changed files with 1910 additions and 2527 deletions

View File

@@ -1,44 +0,0 @@
---
kind: pipeline
type: docker
name: seasoned build
platform:
os: linux
arch: amd64
steps:
- name: frontend_install
image: node:13.6.0
commands:
- node -v
- yarn --version
- name: deploy
image: appleboy/drone-ssh
pull: true
secrets:
- ssh_key
when:
event:
- push
branch:
- master
- drone-test
status: success
settings:
host: 10.0.0.114
username: root
key:
from_secret: ssh_key
command_timeout: 600s
script:
- /home/kevin/deploy/seasoned.sh
trigger:
branch:
- master
event:
include:
- pull_request
- push

View File

@@ -1,10 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"bracketSpacing": true,
"arrowParens": "avoid",
"vueIndentScriptAndStyle": false,
"trailingComma": "none"
}

View File

@@ -21,6 +21,10 @@
<symbol id="icon_now_playing" viewBox="0 0 30 30"> <symbol id="icon_now_playing" viewBox="0 0 30 30">
<title>Now Playing</title> <title>Now Playing</title>
<path d="M27.9847266,7.50322266 C25.9822852,4.03494141 22.749082,1.55390625 18.8806055,0.517382812 C15.0121875,-0.519257812 10.9716797,0.0127148437 7.50322266,2.01527344 C4.03482422,4.01777344 1.55390625,7.25097656 0.517382812,11.1194531 C-0.519140625,14.9878711 0.0128320312,19.0284961 2.01527344,22.4967773 C4.01765625,25.9650586 7.25097656,28.4460937 11.1193945,29.4826172 C12.4111523,29.8287891 13.7219531,30 15.0244336,30 C17.6224219,30 20.1866016,29.3186133 22.4968359,27.9847852 C25.9651172,25.9823437 28.4461523,22.7491406 29.4826758,18.8806641 C30.5192578,15.0121289 29.987168,10.9716211 27.9847266,7.50322266 Z M27.9743555,18.476543 C27.0457617,21.9421289 24.8231836,24.8387109 21.715957,26.6326172 C18.6088477,28.426582 14.989043,28.9030664 11.523457,27.9745898 C8.0578125,27.0459961 5.16128906,24.823418 3.36732422,21.7161914 C1.57341797,18.609082 1.096875,14.9892188 2.02552734,11.5235742 C2.95417969,8.05798828 5.17675781,5.16152344 8.28392578,3.3675 C10.35375,2.17248047 12.6505664,1.56210937 14.9782031,1.56210937 C16.1448047,1.56210937 17.3195508,1.71550781 18.4763672,2.02552734 C21.9419531,2.95412109 24.8385352,5.17669922 26.6324414,8.28392578 C28.4264063,11.3910937 28.9030078,15.0108984 27.9743555,18.476543 Z M22.1940234,13.5850781 L12.5538281,8.01925781 C12.0422461,7.72388672 11.4314648,7.72400391 10.9198828,8.01919922 C10.4083008,8.31451172 10.1028516,8.84355469 10.1028516,9.43423828 L10.1028516,20.5658789 C10.1028516,21.1565625 10.4082422,21.6855469 10.9198828,21.980918 C11.1756445,22.1286328 11.4561328,22.2024023 11.7367383,22.2024023 C12.0174023,22.2024023 12.2980078,22.128457 12.5537695,21.9808594 L22.194082,16.4150977 C22.7056055,16.119668 23.0109375,15.5906836 23.0109375,15 C23.0109375,14.409375 22.7055469,13.8803906 22.1940234,13.5850781 Z M21.4132031,15.0629297 L11.7729492,20.6286914 C11.7611719,20.6355469 11.7366211,20.649668 11.7005273,20.6286914 C11.6643164,20.6077734 11.6643164,20.5795312 11.6643164,20.5659375 L11.6643164,9.43429687 C11.6643164,9.42070312 11.6643164,9.39246094 11.7005273,9.37154297 C11.714707,9.36333984 11.7270703,9.36052734 11.7376172,9.36052734 C11.7540234,9.36052734 11.7658594,9.36738281 11.7730664,9.37154297 L21.4132617,14.9373633 C21.4250391,14.9441602 21.4494727,14.9582812 21.4494727,15.0001172 C21.4494727,15.0419531 21.4249219,15.0561328 21.4132031,15.0629297 Z M24.2169727,7.87734375 C22.3601953,5.47863281 19.5689648,3.86707031 16.5588867,3.45580078 C16.1321484,3.39738281 15.7380469,3.69638672 15.6796289,4.12371094 C15.6213281,4.55091797 15.920332,4.94455078 16.3475391,5.00296875 C18.9556641,5.35927734 21.3738867,6.75544922 22.9822266,8.83318359 C23.1360937,9.03193359 23.3668945,9.13599609 23.6001562,9.13599609 C23.7670898,9.13599609 23.9353125,9.08273437 24.0774609,8.97257813 C24.418418,8.70867187 24.4808789,8.21830078 24.2169727,7.87734375 Z" fill-rule="nonzero"></path> <path d="M27.9847266,7.50322266 C25.9822852,4.03494141 22.749082,1.55390625 18.8806055,0.517382812 C15.0121875,-0.519257812 10.9716797,0.0127148437 7.50322266,2.01527344 C4.03482422,4.01777344 1.55390625,7.25097656 0.517382812,11.1194531 C-0.519140625,14.9878711 0.0128320312,19.0284961 2.01527344,22.4967773 C4.01765625,25.9650586 7.25097656,28.4460937 11.1193945,29.4826172 C12.4111523,29.8287891 13.7219531,30 15.0244336,30 C17.6224219,30 20.1866016,29.3186133 22.4968359,27.9847852 C25.9651172,25.9823437 28.4461523,22.7491406 29.4826758,18.8806641 C30.5192578,15.0121289 29.987168,10.9716211 27.9847266,7.50322266 Z M27.9743555,18.476543 C27.0457617,21.9421289 24.8231836,24.8387109 21.715957,26.6326172 C18.6088477,28.426582 14.989043,28.9030664 11.523457,27.9745898 C8.0578125,27.0459961 5.16128906,24.823418 3.36732422,21.7161914 C1.57341797,18.609082 1.096875,14.9892188 2.02552734,11.5235742 C2.95417969,8.05798828 5.17675781,5.16152344 8.28392578,3.3675 C10.35375,2.17248047 12.6505664,1.56210937 14.9782031,1.56210937 C16.1448047,1.56210937 17.3195508,1.71550781 18.4763672,2.02552734 C21.9419531,2.95412109 24.8385352,5.17669922 26.6324414,8.28392578 C28.4264063,11.3910937 28.9030078,15.0108984 27.9743555,18.476543 Z M22.1940234,13.5850781 L12.5538281,8.01925781 C12.0422461,7.72388672 11.4314648,7.72400391 10.9198828,8.01919922 C10.4083008,8.31451172 10.1028516,8.84355469 10.1028516,9.43423828 L10.1028516,20.5658789 C10.1028516,21.1565625 10.4082422,21.6855469 10.9198828,21.980918 C11.1756445,22.1286328 11.4561328,22.2024023 11.7367383,22.2024023 C12.0174023,22.2024023 12.2980078,22.128457 12.5537695,21.9808594 L22.194082,16.4150977 C22.7056055,16.119668 23.0109375,15.5906836 23.0109375,15 C23.0109375,14.409375 22.7055469,13.8803906 22.1940234,13.5850781 Z M21.4132031,15.0629297 L11.7729492,20.6286914 C11.7611719,20.6355469 11.7366211,20.649668 11.7005273,20.6286914 C11.6643164,20.6077734 11.6643164,20.5795312 11.6643164,20.5659375 L11.6643164,9.43429687 C11.6643164,9.42070312 11.6643164,9.39246094 11.7005273,9.37154297 C11.714707,9.36333984 11.7270703,9.36052734 11.7376172,9.36052734 C11.7540234,9.36052734 11.7658594,9.36738281 11.7730664,9.37154297 L21.4132617,14.9373633 C21.4250391,14.9441602 21.4494727,14.9582812 21.4494727,15.0001172 C21.4494727,15.0419531 21.4249219,15.0561328 21.4132031,15.0629297 Z M24.2169727,7.87734375 C22.3601953,5.47863281 19.5689648,3.86707031 16.5588867,3.45580078 C16.1321484,3.39738281 15.7380469,3.69638672 15.6796289,4.12371094 C15.6213281,4.55091797 15.920332,4.94455078 16.3475391,5.00296875 C18.9556641,5.35927734 21.3738867,6.75544922 22.9822266,8.83318359 C23.1360937,9.03193359 23.3668945,9.13599609 23.6001562,9.13599609 C23.7670898,9.13599609 23.9353125,9.08273437 24.0774609,8.97257813 C24.418418,8.70867187 24.4808789,8.21830078 24.2169727,7.87734375 Z" fill-rule="nonzero"></path>
</symbol>
<symbol id="icon_top_rated" viewBox="0 0 30 30">
<title>Top Rated</title>
<path d="M24.7750847,5.22491532 C24.7021599,5.15199056 24.6169531,5.09595364 24.52407,5.05757218 C24.4304192,5.01919073 24.3313951,5 24.2323709,5 L8.84447835,5.00076763 C8.41997947,5.00076763 8.07684927,5.34466546 8.07684927,5.76839671 C8.07684927,6.19289559 8.4207471,6.53602579 8.84447835,6.53602579 L22.3785467,6.53525816 L5.22510723,23.6894653 C4.92496426,23.9896082 4.92496426,24.4747498 5.22510723,24.7748928 C5.3747949,24.9245804 5.57130794,24.9998081 5.76782099,24.9998081 C5.96433403,24.9998081 6.16084708,24.9245804 6.31053475,24.7748928 L23.4647418,7.62068568 L23.4647418,21.1539864 C23.4647418,21.5784853 23.807872,21.9216155 24.2323709,21.9216155 C24.6568698,21.9216155 25,21.5784853 25,21.1539864 L25,5.76762908 C25,5.66860493 24.9808093,5.56958078 24.9424278,5.47593003 C24.9040464,5.38304691 24.8480094,5.29784008 24.7750847,5.22491532 Z"></path>
</symbol> </symbol>
<symbol id="icon_popular" viewBox="0 0 30 30"> <symbol id="icon_popular" viewBox="0 0 30 30">
<title>Popular</title> <title>Popular</title>
@@ -121,8 +125,9 @@
l246.17-246.175C512.959,136.021,512.959,129.804,509.121,125.966z" fill-rule="nozero" transform="scale(1.4)"></path> l246.17-246.175C512.959,136.021,512.959,129.804,509.121,125.966z" fill-rule="nozero" transform="scale(1.4)"></path>
</symbol> </symbol>
<div id="app"></div> <div id="app"></div>
<script type="text/javascript" src="/build.js"></script> <script type="text/javascript" src="dist/build.js"></script>
</body> </body>
<script src="https://cdn.ravenjs.com/3.23.1/vue/raven.min.js" crossorigin="anonymous"></script> <script src="https://cdn.ravenjs.com/3.23.1/vue/raven.min.js" crossorigin="anonymous"></script>
<!-- <script>Raven.config('https://c1fa1a17de3d4b24abcd05161648fe4d@sentry.io/300063').install();</script> -->
</html> </html>

View File

@@ -13,7 +13,6 @@
"dependencies": { "dependencies": {
"axios": "^0.18.1", "axios": "^0.18.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-plugin-transform-object-rest-spread": "^6.26.0",
"chart.js": "^2.9.2",
"connect-history-api-fallback": "^1.3.0", "connect-history-api-fallback": "^1.3.0",
"express": "^4.16.1", "express": "^4.16.1",
"vue": "^2.5.2", "vue": "^2.5.2",
@@ -30,7 +29,7 @@
"@babel/runtime": "^7.4.5", "@babel/runtime": "^7.4.5",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"cross-env": "^3.0.0", "cross-env": "^3.0.0",
"css-loader": "^3.4.2", "css-loader": "^0.25.0",
"documentation": "^11.0.0", "documentation": "^11.0.0",
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"node-sass": "^4.5.0", "node-sass": "^4.5.0",

View File

@@ -1,32 +1,37 @@
<template> <template>
<div id="app"> <div id="app">
<!-- Header and hamburger navigation --> <!-- Header and hamburger navigation -->
<navigation></navigation> <navigation></navigation>
<search-input v-model="query"></search-input>
<!-- Header with search field -->
<!-- TODO move this to the navigation component -->
<header class="header">
<search-input v-model="query"></search-input>
</header>
<!-- Movie popup that will show above existing rendered content --> <!-- Movie popup that will show above existing rendered content -->
<movie-popup <movie-popup v-if="moviePopupIsVisible" :id="popupID" :type="popupType"></movie-popup>
v-if="moviePopupIsVisible"
:id="popupID"
:type="popupType"
></movie-popup>
<darkmode-toggle /> <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" :key="$route.fullPath"></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'
import MoviePopup from "@/components/MoviePopup"; import MoviePopup from '@/components/MoviePopup'
import SearchInput from "@/components/SearchInput"; import SearchInput from '@/components/SearchInput'
import DarkmodeToggle from "@/components/ui/darkmodeToggle"; import DarkmodeToggle from '@/components/ui/darkmodeToggle'
export default { export default {
name: "app", name: 'app',
components: { components: {
Navigation, Navigation,
MoviePopup, MoviePopup,
@@ -35,42 +40,39 @@ export default {
}, },
data() { data() {
return { return {
query: "", query: '',
moviePopupIsVisible: false, moviePopupIsVisible: false,
popupID: 0, popupID: 0,
popupType: "movie" popupType: 'movie'
}; }
}, },
created() { created(){
let that = this; let that = this
Vue.prototype.$popup = { Vue.prototype.$popup = {
get isOpen() { get isOpen() {
return that.moviePopupIsVisible; return that.moviePopupIsVisible
}, },
open: (id, type) => { open: (id, type) => {
this.popupID = id || this.popupID; this.popupID = id || this.popupID
this.popupType = type || this.popupType; this.popupType = type || this.popupType
this.moviePopupIsVisible = true; this.moviePopupIsVisible = true
console.log("opened"); console.log('opened')
}, },
close: () => { close: () => {
this.moviePopupIsVisible = false; this.moviePopupIsVisible = false
console.log("closed"); console.log('closed')
} }
}; }
console.log( console.log('MoviePopup registered at this.$popup and has state: ', this.$popup.isOpen)
"MoviePopup registered at this.$popup and has state: ",
this.$popup.isOpen
);
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/variables"; @import "./src/scss/variables";
.content { .content {
@include tablet-min { @include tablet-min{
width: calc(100% - 95px); width: calc(100% - 95px);
margin-top: $header-size; margin-top: $header-size;
margin-left: 95px; margin-left: 95px;
@@ -84,42 +86,38 @@ export default {
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
* { *{
box-sizing: border-box; box-sizing: border-box;
} }
html { html {
height: 100%; height: 100%;
} }
body { body{
margin: 0; margin: 0;
padding: 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: $background-color;
color: $text-color; color: $text-color;
transition: background-color 0.5s ease, color 0.5s ease; transition: background-color .5s ease, color .5s ease;
&.hidden { &.hidden{
overflow: hidden; overflow: hidden;
} }
} }
h1, h1,h2,h3 {
h2, transition: color .5s ease;
h3 {
transition: color 0.5s ease;
} }
a:any-link { a:any-link {
color: inherit; color: inherit;
} }
input, input, textarea, button{
textarea, font-family: 'Roboto', sans-serif;
button {
font-family: "Roboto", sans-serif;
} }
figure { figure{
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
img { img{
display: block; display: block;
// max-width: 100%; // max-width: 100%;
height: auto; height: auto;
@@ -129,16 +127,16 @@ img {
overflow: hidden; overflow: hidden;
} }
.wrapper { .wrapper{
position: relative; position: relative;
} }
.header { .header{
position: fixed; position: fixed;
z-index: 15; z-index: 15;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@include tablet-min { @include tablet-min{
width: calc(100% - 170px); width: calc(100% - 170px);
margin-left: 95px; margin-left: 95px;
border-top: 0; border-top: 0;
@@ -148,16 +146,14 @@ img {
} }
// router view transition // router view transition
.fade-enter-active, .fade-enter-active, .fade-leave-active {
.fade-leave-active {
transition-property: opacity; transition-property: opacity;
transition-duration: 0.25s; transition-duration: 0.25s;
} }
.fade-enter-active { .fade-enter-active {
transition-delay: 0.25s; transition-delay: 0.25s;
} }
.fade-enter, .fade-enter, .fade-leave-active {
.fade-leave-active { opacity: 0
opacity: 0;
} }
</style> </style>

View File

@@ -2,7 +2,6 @@ import axios from 'axios'
import storage from '@/storage' import storage from '@/storage'
import config from '@/config.json' import config from '@/config.json'
import path from 'path' import path from 'path'
import store from '@/store'
const SEASONED_URL = config.SEASONED_URL const SEASONED_URL = config.SEASONED_URL
const ELASTIC_URL = config.ELASTIC_URL const ELASTIC_URL = config.ELASTIC_URL
@@ -11,13 +10,6 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
// TODO // TODO
// - Move autorization token and errors here? // - Move autorization token and errors here?
const checkStatusAndReturnJson = (response) => {
if (!response.ok) {
throw resp
}
return response.json()
}
// - - - TMDB - - - // - - - TMDB - - -
/** /**
@@ -26,18 +18,12 @@ const checkStatusAndReturnJson = (response) => {
* @param {boolean} [credits=false] Include credits * @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => { const getMovie = (id, credits=false) => {
const url = new URL('v2/movie', SEASONED_URL) const url = new URL('v2/movie', SEASONED_URL)
url.pathname = path.join(url.pathname, id.toString()) url.pathname = path.join(url.pathname, id.toString())
if (checkExistance) {
url.searchParams.append('check_existance', true)
}
if (credits) { if (credits) {
url.searchParams.append('credits', true) url.searchParams.append('credits', true)
} }
if(release_dates) {
url.searchParams.append('release_dates', true)
}
return fetch(url.href) return fetch(url.href)
.then(resp => resp.json()) .then(resp => resp.json())
@@ -50,12 +36,9 @@ const getMovie = (id, checkExistance=false, credits=false, release_dates=false)
* @param {boolean} [credits=false] Include credits * @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const getShow = (id, checkExistance=false, credits=false) => { const getShow = (id, credits=false) => {
const url = new URL('v2/show', SEASONED_URL) const url = new URL('v2/show', SEASONED_URL)
url.pathname = path.join(url.pathname, id.toString()) url.pathname = path.join(url.pathname, id.toString())
if (checkExistance) {
url.searchParams.append('check_existance', true)
}
if (credits) { if (credits) {
url.searchParams.append('credits', true) url.searchParams.append('credits', true)
} }
@@ -65,24 +48,6 @@ const getShow = (id, checkExistance=false, credits=false) => {
.catch(error => { console.error(`api error getting show: ${id}`); throw error }) .catch(error => { console.error(`api error getting show: ${id}`); throw error })
} }
/**
* Fetches tmdb person by id. Can optionally include cast credits in result object.
* @param {number} id
* @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response
*/
const getPerson = (id, credits=false) => {
const url = new URL('v2/person', SEASONED_URL)
url.pathname = path.join(url.pathname, id.toString())
if (credits) {
url.searchParams.append('credits', true)
}
return fetch(url.href)
.then(resp => resp.json())
.catch(error => { console.error(`api error getting person: ${id}`); throw error })
}
/** /**
* Fetches tmdb list by name. * Fetches tmdb list by name.
* @param {string} name List the fetch * @param {string} name List the fetch
@@ -131,19 +96,12 @@ const getUserRequests = (page=1) => {
* @param {number} [page=1] * @param {number} [page=1]
* @returns {object} Tmdb response * @returns {object} Tmdb response
*/ */
const searchTmdb = (query, page=1, adult=false, mediaType=null) => { const searchTmdb = (query, page=1) => {
const url = new URL('v2/search', SEASONED_URL) const url = new URL('v2/search', SEASONED_URL)
if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) {
url.pathname += `/${mediaType}`
}
url.searchParams.append('query', query) url.searchParams.append('query', query)
url.searchParams.append('page', page) url.searchParams.append('page', page)
url.searchParams.append('adult', adult)
const headers = { authorization: localStorage.getItem('token') } return fetch(url.href)
return fetch(url.href, { headers })
.then(resp => resp.json()) .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 })
} }
@@ -252,156 +210,32 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
.catch(err => Promise.reject(err)) .catch(err => Promise.reject(err))
} }
const watchLink = (title, year, authorization_token=undefined) => {
const url = new URL('v1/plex/watch-link', SEASONED_URL)
url.searchParams.append('title', title)
url.searchParams.append('year', year)
const headers = {
'Authorization': authorization_token,
'Content-Type': 'application/json'
}
return fetch(url.href, { headers })
.then(resp => resp.json())
.then(response => response.link)
}
// - - - Seasoned user endpoints - - -
const register = (username, password) => {
const url = new URL('v1/user', SEASONED_URL)
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
}
return fetch(url.href, options)
.then(resp => resp.json())
.catch(error => {
console.error('Unexpected error occured before receiving response. Error:', error)
// TODO log to sentry the issue here
throw error
})
}
const login = (username, password, throwError=false) => {
const url = new URL('v1/user/login', SEASONED_URL)
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
}
return fetch(url.href, options)
.then(resp => {
if (resp.status == 200)
return resp.json();
if (throwError)
throw resp;
else
console.error("Error occured when trying to sign in.\nError:", resp);
})
}
const getSettings = () => {
const settingsExists = (value) => {
if (value instanceof Object && value.hasOwnProperty('settings'))
return value;
throw "Settings does not exist in response object.";
}
const commitSettingsToStore = (response) => {
store.dispatch('userModule/setSettings', response.settings)
return response
}
const url = new URL('v1/user/settings', SEASONED_URL)
const authorization_token = localStorage.getItem('token')
const headers = authorization_token ? {
'Authorization': authorization_token,
'Content-Type': 'application/json'
} : {}
return fetch(url.href, { headers })
.then(resp => resp.json())
.then(settingsExists)
.then(commitSettingsToStore)
.then(response => response.settings)
.catch(error => { console.log('api error getting user settings'); throw error })
}
const updateSettings = (settings) => {
const url = new URL('v1/user/settings', SEASONED_URL)
const authorization_token = localStorage.getItem('token')
const headers = authorization_token ? {
'Authorization': authorization_token,
'Content-Type': 'application/json'
} : {}
return fetch(url.href, {
method: 'PUT',
headers,
body: JSON.stringify(settings)
})
.then(resp => resp.json())
.catch(error => { console.log('api error updating user settings'); throw error })
}
// - - - Authenticate with plex - - - // - - - Authenticate with plex - - -
const linkPlexAccount = (username, password) => { const plexAuthenticate = (username, password) => {
const url = new URL('v1/user/link_plex', SEASONED_URL) const url = new URL('https://plex.tv/api/v2/users/signin')
const body = { username, password }
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
authorization: storage.token 'X-Plex-Platform': 'Linux',
'X-Plex-Version': 'v2.0.24',
'X-Plex-Platform-Version': '4.13.0-36-generic',
'X-Plex-Device-Name': 'Tautulli',
'X-Plex-Client-Identifier': '123'
} }
return fetch(url.href, { let formData = new FormData()
method: 'POST', formData.set('login', username)
headers, formData.set('password', password)
body: JSON.stringify(body) formData.set('rememberMe', false)
})
.then(resp => resp.json())
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error })
}
const unlinkPlexAccount = (username, password) => { return axios({
const url = new URL('v1/user/unlink_plex', SEASONED_URL) method: 'POST',
const headers = { url: url.href,
'Content-Type': 'application/json', headers: headers,
authorization: storage.token data: formData
} })
.catch(error => { console.error(`api error authentication plex: ${username}`); throw error })
return fetch(url.href, {
method: 'POST',
headers
})
.then(resp => resp.json())
.catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error })
}
// - - - User graphs - - -
const fetchChart = (urlPath, days, chartType) => {
const url = new URL('v1/user' + urlPath, SEASONED_URL)
url.searchParams.append('days', days)
url.searchParams.append('y_axis', chartType)
const authorization_token = localStorage.getItem('token')
const headers = authorization_token ? {
'Authorization': authorization_token,
'Content-Type': 'application/json'
} : {}
return fetch(url.href, { headers })
.then(resp => resp.json())
.catch(error => { console.log('api error fetching chart'); throw error })
} }
@@ -468,7 +302,6 @@ const elasticSearchMoviesAndShows = (query) => {
export { export {
getMovie, getMovie,
getShow, getShow,
getPerson,
getTmdbMovieListByName, getTmdbMovieListByName,
searchTmdb, searchTmdb,
getUserRequests, getUserRequests,
@@ -476,15 +309,8 @@ export {
searchTorrents, searchTorrents,
addMagnet, addMagnet,
request, request,
watchLink,
getRequestStatus, getRequestStatus,
linkPlexAccount, plexAuthenticate,
unlinkPlexAccount,
register,
login,
getSettings,
updateSettings,
fetchChart,
getEmoji, getEmoji,
elasticSearchMoviesAndShows elasticSearchMoviesAndShows
} }

View File

@@ -1,74 +1,38 @@
<template> <template>
<div> <section class="not-found">
<section class="not-found"> <h1 class="not-found__title">Page Not Found</h1>
<h1 class="not-found__title">Page Not Found</h1> </section>
</section>
<seasoned-button class="button" @click="goBack">go back to previous page</seasoned-button>
</div>
</template> </template>
<script>
import store from '@/store'
import SeasonedButton from '@/components/ui/SeasonedButton'
export default {
components: { SeasonedButton },
methods: {
goBack() {
this.$router.go(-1)
}
},
created() {
if (this.$popup.isOpen == true)
this.$popup.close()
}
}
</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/media-queries";
.button {
font-size: 1.2rem;
position: fixed;
top: 50%;
left: calc(50% + 46px);
transform: translate(-50%, -50%);
@include mobile {
top: 60%;
left: 50%;
font-size: 1rem;
width: content;
}
}
.not-found { .not-found {
display: flex; display: flex;
height: calc(100vh - var(--header-size)); height: calc(100vh - var(--header-size));
width: 100%;
background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%; background: url('~assets/pulp-fiction.jpg') no-repeat 50% 50%;
background-size: cover; background-size: cover;
align-items: center; justify-content: center;
flex-direction: column;
&::before { &:before {
content: ""; content: "";
position: absolute; position: absolute;
height: calc(100vh - var(--header-size)); height: calc(100vh - var(--header-size));
width: 100%; width: 100%;
pointer-events: none;
background: $background-40; background: $background-40;
} }
&__title { &__title {
margin-top: 30vh; padding-top: 40vh;
font-size: 2.5rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
color: $text-color; color: $text-color;
position: relative; position: relative;
margin: 0;
@include tablet-min { @include tablet-min {
font-size: 3.5rem; font-size: 2.3rem;
} }
} }
} }

View File

@@ -1,316 +0,0 @@
<template>
<div class="wrapper" v-if="hasPlexUser">
<h1>Your watch activity</h1>
<div class="filter">
<h2>Filter</h2>
<div class="filter-item">
<label class="desktop-only">Days:</label>
<input class="dayinput"
v-model="days"
placeholder="number of days"
type="number"
pattern="[0-9]*"
:style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/>
<!-- <datalist id="days">
<option v-for="index in 1500" :value="index" :key="index"></option>
</datalist> -->
</div>
<toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" />
</div>
<div class="chart-section">
<h3 class="chart-header">Activity per day:</h3>
<div class="chart">
<canvas ref="activityCanvas"></canvas>
</div>
<h3 class="chart-header">Activity per day of week:</h3>
<div class="chart">
<canvas ref="playsByDayOfWeekCanvas"></canvas>
</div>
</div>
</div>
<div v-else>
<h1>Must be authenticated</h1>
</div>
</template>
<script>
import store from '@/store'
import ToggleButton from '@/components/ui/ToggleButton';
import { fetchChart } from '@/api'
var Chart = require('chart.js');
Chart.defaults.global.elements.point.radius = 0
Chart.defaults.global.elements.point.hitRadius = 10
Chart.defaults.global.elements.point.pointHoverRadius = 10
Chart.defaults.global.elements.point.hoverBorderWidth = 4
export default {
components: { ToggleButton },
data() {
return {
days: 30,
selectedChartDataType: 'plays',
charts: [{
name: 'Watch activity',
ref: 'activityCanvas',
data: null,
urlPath: '/plays_by_day',
graphType: 'line'
}, {
name: 'Plays by day of week',
ref: 'playsByDayOfWeekCanvas',
data: null,
urlPath: '/plays_by_dayofweek',
graphType: 'bar'
}],
chartData: [{
type: 'plays',
tooltipLabel: 'Play count',
},{
type: 'duration',
tooltipLabel: 'Watched duration',
valueConvertFunction: this.convertSecondsToHumanReadable
}],
gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5')
}
},
computed: {
hasPlexUser() {
return store.getters['userModule/plex_userid'] != null ? true : false
},
chartTypes() {
return this.chartData.map(chart => chart.type)
},
selectedChartType() {
return this.chartData.filter(data => data.type == this.selectedChartDataType)[0]
}
},
watch: {
hasPlexUser(newValue, oldValue) {
if (newValue != oldValue && newValue == true) {
this.fetchChartData(this.charts)
}
},
days(newValue) {
if (newValue !== '') {
this.fetchChartData(this.charts)
}
},
selectedChartDataType(selectedChartDataType) {
this.fetchChartData(this.charts)
}
},
beforeMount() {
if (typeof(this.days) == 'number') {
this.days = this.days.toString()
}
},
methods: {
fetchChartData(charts) {
if (this.hasPlexUser == false) {
return
}
for (let chart of charts) {
fetchChart(chart.urlPath, this.days, this.selectedChartType.type)
.then(data => {
this.series = data.data.series.filter(group => group.name === 'TV')[0].data; // plays pr date in groups (movie/tv/music)
this.categories = data.data.categories; // dates
const x_labels = data.data.categories.map(date => {
if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
const [year, month, day] = date.split('-')
return `${day}.${month}`
}
return date
})
let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data
let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data
const datasets = [{
label: `Movies watch last ${ this.days } days`,
data: y_activityMovies,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
},
{
label: `Shows watch last ${ this.days } days`,
data: y_activityTV,
backgroundColor: 'rgba(255, 159, 64, 0.2)',
borderColor: 'rgba(255, 159, 64, 1)',
borderWidth: 1
}
]
if (chart.data == null) {
this.generateChart(chart, x_labels, datasets)
} else {
chart.data.clear();
chart.data.data.labels = x_labels;
chart.data.data.datasets = datasets;
chart.data.update();
}
})
}
},
generateChart(chart, labels, datasets) {
const chartInstance = new Chart(this.$refs[chart.ref], {
type: chart.graphType,
data: {
labels: labels,
datasets: datasets
},
options: {
// hitRadius: 8,
maintainAspectRatio: false,
tooltips: {
callbacks: {
title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`,
label: (tooltipItem, data) => {
let label = data.datasets[tooltipItem.datasetIndex].label
let value = tooltipItem.value;
let text = 'Duration watched'
const context = label.split(' ')[0]
if (context) {
text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}`
}
if (this.selectedChartType.valueConvertFunction) {
value = this.selectedChartType.valueConvertFunction(tooltipItem.value)
}
return ` ${text}: ${value}`
}
}
},
scales: {
yAxes: [{
gridLines: {
color: this.gridColor
},
stacked: chart.graphType === 'bar',
ticks: {
// suggestedMax: 10000,
callback: (value, index, values) => {
if (this.selectedChartType.valueConvertFunction) {
return this.selectedChartType.valueConvertFunction(value, values)
}
return value
},
beginAtZero: true
}
}],
xAxes: [{
stacked: chart.graphType === 'bar',
gridLines: {
display: false,
}
}]
}
}
});
chart.data = chartInstance;
},
convertSecondsToHumanReadable(value, values=null) {
const highestValue = values ? values[0] : value;
// minutes
if (highestValue < 3600) {
const minutes = Math.floor(value / 60);
value = `${minutes} m`
}
// hours and minutes
else if (highestValue > 3600 && highestValue < 86400) {
const hours = Math.floor(value / 3600);
const minutes = Math.floor(value % 3600 / 60);
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`
}
// days and hours
else if (highestValue > 86400 && highestValue < 31557600) {
const days = Math.floor(value / 86400);
const hours = Math.floor(value % 86400 / 3600);
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`
}
// years and days
else if (highestValue > 31557600) {
const years = Math.floor(value / 31557600);
const days = Math.floor(value % 31557600 / 86400);
value = years != 0 ? `${years} y ${days} d` : `${days} d`
}
return value
}
}
}
</script>
<style lang="scss" scoped>
@import "./src/scss/variables";
.wrapper {
padding: 2rem;
@include mobile-only {
padding: 0 0.8rem;
}
}
.filter {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
margin-bottom: 2rem;
h2 {
margin-bottom: 0.5rem;
width: 100%;
font-weight: 400;
}
&-item:not(:first-of-type) {
margin-left: 1rem;
}
.dayinput {
font-size: 1.2rem;
max-width: 3rem;
background-color: $background-ui;
color: $text-color;
}
}
.chart-section {
display: flex;
flex-wrap: wrap;
.chart {
position: relative;
height: 35vh;
width: 90vw;
margin-bottom: 2rem;
}
.chart-header {
font-weight: 300;
}
}
</style>

View File

@@ -12,76 +12,74 @@
</template> </template>
<script> <script>
import LandingBanner from "@/components/LandingBanner"; import LandingBanner from '@/components/LandingBanner'
import ListHeader from "@/components/ListHeader"; import ListHeader from '@/components/ListHeader'
import ResultsList from "@/components/ResultsList"; import ResultsList from '@/components/ResultsList'
import Loader from "@/components/ui/Loader"; import Loader from '@/components/ui/Loader'
import { getTmdbMovieListByName, getRequests } from "@/api"; import { getTmdbMovieListByName, getRequests } from '@/api'
export default { export default {
name: "home", name: 'home',
components: { LandingBanner, ResultsList, ListHeader, Loader }, components: { LandingBanner, ResultsList, ListHeader, Loader },
data() { data(){
return { return {
imageFile: "/pulp-fiction.jpg", imageFile: 'dist/pulp-fiction.jpg',
requests: [], requests: [],
nowplaying: [], nowplaying: [],
upcoming: [], upcoming: [],
popular: [] popular: []
}; }
}, },
computed: { computed: {
lists() { lists() {
return [ return [
{ {
title: "Requests", title: 'Requests',
route: "request", route: 'request',
data: this.requests data: this.requests
}, },
{ {
title: "Now playing", title: 'Now playing',
route: "now_playing", route: 'now_playing',
data: this.nowplaying data: this.nowplaying
}, },
{ {
title: "Upcoming", title: 'Upcoming',
route: "upcoming", route: 'upcoming',
data: this.upcoming data: this.upcoming
}, },
{ {
title: "Popular", title: 'Popular',
route: "popular", route: 'popular',
data: this.popular data: this.popular
} }
]; ]
} }
}, },
methods: { methods: {
fetchRequests() { fetchRequests() {
getRequests().then(results => (this.requests = results.results)); getRequests()
.then(results => this.requests = results.results)
}, },
fetchNowPlaying() { fetchNowPlaying() {
getTmdbMovieListByName("now_playing").then( getTmdbMovieListByName('now_playing')
results => (this.nowplaying = results.results) .then(results => this.nowplaying = results.results)
);
}, },
fetchUpcoming() { fetchUpcoming() {
getTmdbMovieListByName("upcoming").then( getTmdbMovieListByName('upcoming')
results => (this.upcoming = results.results) .then(results => this.upcoming = results.results)
);
}, },
fetchPopular() { fetchPopular() {
getTmdbMovieListByName("popular").then( getTmdbMovieListByName('popular')
results => (this.popular = results.results) .then(results => this.popular = results.results)
);
} }
}, },
created() { created(){
this.fetchRequests(); this.fetchRequests()
this.fetchNowPlaying(); this.fetchNowPlaying()
this.fetchUpcoming(); this.fetchUpcoming()
this.fetchPopular(); this.fetchPopular()
} }
}; }
</script> </script>

View File

@@ -1,10 +1,8 @@
<template> <template>
<header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }"> <header v-bind:style="{ 'background-image': 'url(' + imageFile + ')' }">
<div class="container"> <div class="container">
<h1 class="title">Request movies or tv shows</h1> <h1 class="title">Request new movies or tv shows for plex</h1>
<strong class="subtitle" <strong class="subtitle">Made with Vue.js</strong>
>Create a profile to track and view requests</strong
>
</div> </div>
</header> </header>
</template> </template>
@@ -19,15 +17,15 @@ export default {
}, },
data() { data() {
return { return {
imageFile: "/pulp-fiction.jpg" imageFile: 'dist/pulp-fiction.jpg'
}; }
}, },
beforeMount() { beforeMount() {
if (this.image && this.image.length > 0) { if (this.image && this.image.length > 0) {
this.imageFile = this.image; this.imageFile = this.image
} }
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -57,15 +55,15 @@ header {
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: $background-70; background-color: $background-70;
transition: background-color 0.5s ease; transition: background-color .5s ease;
} }
.container { .container {
text-align: center; text-align: center;
position: relative; position: relative;
transition: color 0.5s ease; transition: color .5s ease;
} }
.title { .title {
font-weight: 500; font-weight: 500;
font-size: 22px; font-size: 22px;
@@ -74,8 +72,8 @@ header {
color: $text-color; color: $text-color;
margin: 0; margin: 0;
@include tablet-min { @include tablet-min{
font-size: 2.5rem; font-size: 28px;
} }
} }
@@ -86,9 +84,9 @@ header {
color: $text-color-70; color: $text-color-70;
margin: 5px 0; margin: 5px 0;
@include tablet-min { @include tablet-min{
font-size: 1.3rem; font-size: 16px;
} }
} }
} }
</style> </style>

View File

@@ -1,20 +1,12 @@
<template> <template>
<header :class="{ sticky: sticky }"> <header :class="{ 'sticky': sticky }">
<h2>{{ title }}</h2> <h2>{{ title }}</h2>
<div v-if="info instanceof Array" class="flex flex-direction-column"> <span v-if="info" class="result-count">{{ info }}</span>
<span v-for="item in info" class="info">{{ item }}</span> <router-link v-else-if="link" :to="link" class='view-more'>
</div>
<span v-else class="info">{{ info }}</span>
<router-link
v-if="link"
:to="link"
class="view-more"
:aria-label="`View all ${title}`"
>
View All View All
</router-link> </router-link>
</header> </header>
</template> </template>
<script> <script>
@@ -27,10 +19,10 @@ export default {
sticky: { sticky: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true default: false
}, },
info: { info: {
type: [String, Array], type: String,
required: false required: false
}, },
link: { link: {
@@ -38,80 +30,72 @@ export default {
required: false required: false
} }
} }
}; }
</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/media-queries';
@import "./src/scss/main";
header { header {
width: 100%; width: 100%;
min-height: 45px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; padding: 1.8rem 12px;
padding-left: 0.75rem;
padding-right: 0.75rem;
@include tablet-min {
min-height: 65px;
}
&.sticky { &.sticky {
background-color: $background-color; background-color: $background-color;
position: sticky; position: sticky;
position: -webkit-sticky; position: -webkit-sticky;
top: 0; top: $header-size;
z-index: 4; z-index: 4;
@include tablet-min { padding-bottom: 1rem;
top: $header-size; margin-bottom: 1.5rem;
}
} }
h2 { h2 {
font-size: 1.4rem; font-size: 18px;
font-weight: 300; font-weight: 300;
text-transform: capitalize; text-transform: capitalize;
line-height: 1.4rem; line-height: 18px;
margin: 0; margin: 0;
color: $text-color; color: $text-color;
} }
.view-more { .view-more {
font-size: 0.9rem; font-size: 13px;
font-weight: 300; font-weight: 300;
letter-spacing: 0.5px; letter-spacing: .5px;
color: $text-color-70; color: $text-color-70;
text-decoration: none; text-decoration: none;
transition: color 0.5s ease; transition: color .5s ease;
cursor: pointer; cursor: pointer;
&:after { &:after{
content: " →"; content: " →";
} }
&:hover { &:hover{
color: $text-color; color: $text-color;
} }
} }
.info { .result-count {
font-size: 13px; font-size: 13px;
font-weight: 300; font-weight: 300;
letter-spacing: 0.5px; letter-spacing: .5px;
color: $text-color; color: $text-color;
text-decoration: none; text-decoration: none;
text-align: right;
} }
@include tablet-min { @include tablet-min {
padding-left: 1.25rem; padding-left: 1.25rem;;
} }
@include desktop-lg-min { @include desktop-lg-min {
padding-left: 1.75rem; padding-left: 1.75rem;
} }
} }
</style>
</style>

View File

@@ -1,6 +1,7 @@
<template> <template>
<div class="page-container"> <div>
<list-header :title="listTitle" :info="info" :sticky="true" /> <list-header :title="listTitle" :info="resultCount" :sticky="true" />
<results-list :results="results" v-if="results" /> <results-list :results="results" v-if="results" />
@@ -12,107 +13,87 @@
</div> </div>
</template> </template>
<script> <script>
import ListHeader from "@/components/ListHeader"; import ListHeader from '@/components/ListHeader'
import ResultsList from "@/components/ResultsList"; import ResultsList from '@/components/ResultsList'
import SeasonedButton from "@/components/ui/SeasonedButton"; import SeasonedButton from '@/components/ui/SeasonedButton'
import Loader from "@/components/ui/Loader"; import Loader from '@/components/ui/Loader'
import { getTmdbMovieListByName, getRequests } from "@/api"; import { getTmdbMovieListByName, getRequests } from '@/api'
import store from "@/store"; import store from '@/store'
export default { export default {
components: { ListHeader, ResultsList, SeasonedButton, Loader }, components: { ListHeader, ResultsList, SeasonedButton, Loader },
data() { data() {
return { return {
legalTmdbLists: ["now_playing", "upcoming", "popular"], legalTmdbLists: [ 'now_playing', 'upcoming', 'popular' ],
results: [], results: [],
page: 1, page: 1,
totalPages: 0, totalPages: 0,
totalResults: 0, totalResults: 0
loading: true }
};
}, },
computed: { computed: {
listTitle() { listTitle() {
if (this.results.length === 0) return ""; if (this.results.length === 0)
return ''
const routeListName = this.$route.params.name; const routeListName = this.$route.params.name
console.log("routelistname", routeListName); console.log('routelistname', routeListName)
return routeListName.includes("_") return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
? routeListName.split("_").join(" ")
: routeListName;
},
info() {
if (this.results.length === 0) return [null, null];
return [this.pageCount, this.resultCount];
}, },
resultCount() { resultCount() {
const loadedResults = this.results.length; if (this.results.length === 0)
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞"; return ''
return `${loadedResults} of ${totalResults} results`;
}, const loadedResults = this.results.length
pageCount() { const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `Page ${this.page} of ${this.totalPages}`; return `${loadedResults} of ${totalResults} results`
} }
}, },
methods: { methods: {
loadMore() { loadMore() {
console.log(this.$route); console.log(this.$route)
this.loading = true; this.page++
this.page++;
window.history.replaceState( window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
{}, this.init()
"search",
`/#/${this.$route.fullPath}?page=${this.page}`
);
this.init();
}, },
init() { init() {
const routeListName = this.$route.params.name; const routeListName = this.$route.params.name
if (routeListName === "request") { if (routeListName === 'request') {
getRequests(this.page).then(results => { getRequests(this.page)
this.results = this.results.concat(...results.results); .then(results => {
this.page = results.page; this.results = this.results.concat(...results.results)
this.totalPages = results.total_pages; this.page = results.page
this.totalResults = results.total_results; this.totalPages = results.total_pages
}); this.totalResults = results.total_results
})
} else if (this.legalTmdbLists.includes(routeListName)) { } else if (this.legalTmdbLists.includes(routeListName)) {
getTmdbMovieListByName(routeListName, this.page).then(results => { getTmdbMovieListByName(routeListName, this.page)
this.results = this.results.concat(...results.results); .then(results => {
this.page = results.page; this.results = this.results.concat(...results.results)
this.totalPages = results.total_pages; this.page = results.page
this.totalResults = results.total_results; this.totalPages = results.total_pages
}); this.totalResults = results.total_results
})
} else { } else {
// TODO handle if list is not found // TODO handle if list is not found
console.log("404 this is not a tmdb list"); console.log('404 this is not a tmdb list')
} }
this.loading = false;
} }
}, },
created() { created() {
if (this.results.length === 0) this.init(); if (this.results.length === 0)
this.init()
store.dispatch( store.dispatch('documentTitle/updateTitle', `${this.$router.history.current.name} ${this.$route.params.name}`)
"documentTitle/updateTitle",
`${this.$router.history.current.name} ${this.$route.params.name}`
);
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/media-queries";
@include mobile-only {
.page-container {
margin-top: 1rem;
}
}
.fullwidth-button { .fullwidth-button {
width: 100%; width: 100%;
margin: 1rem 0; margin: 1rem 0;

View File

@@ -1,62 +1,59 @@
<template> <template>
<section class="movie"> <section class="movie">
<!-- HEADER w/ POSTER -->
<header
ref="header"
:class="compact ? 'compact' : ''"
@click="compact = !compact"
>
<figure class="movie__poster">
<img
class="movie-item__img is-loaded"
ref="poster-image"
src="~assets/placeholder.png"
/>
</figure>
<h1 class="movie__title" v-if="movie">{{ movie.title }}</h1> <!-- HEADER w/ POSTER -->
<loading-placeholder v-else :count="1" /> <header class="movie__header" :style="{ 'background-image': movie && backdrop !== null ? 'url(' + ASSET_URL + ASSET_SIZES[1] + backdrop + ')' : '' }" :class="compact ? 'compact' : ''" @click="compact=!compact">
<div class="movie__wrap movie__wrap--header">
<figure class="movie__poster">
<img v-if="movie && poster === null"
class="movies-item__img is-loaded"
alt="movie poster image"
src="~assets/no-image.png">
<img v-else-if="poster === undefined"
class="movies-item__img grey"
alt="movie poster image">
<!-- src="~assets/placeholder.png"> -->
<img v-else
class="movies-item__img is-loaded"
alt="movie poster image"
:src="ASSET_URL + ASSET_SIZES[0] + poster">
</figure>
<div class="movie__title">
<h1 v-if="movie">{{ movie.title }}</h1>
<loading-placeholder v-else :count="1" />
</div>
</div>
</header> </header>
<!-- Siderbar and movie info --> <!-- Siderbar and movie info -->
<div class="movie__main"> <div class="movie__main">
<div class="movie__wrap movie__wrap--main"> <div class="movie__wrap movie__wrap--main">
<!-- SIDEBAR ACTIONS --> <!-- SIDEBAR ACTIONS -->
<div class="movie__actions" v-if="movie"> <div class="movie__actions" v-if="movie">
<sidebar-list-element <sidebar-list-element :iconRef="'#iconNot_exsits'" :active="requested"
:iconRef="'#iconNot_exsits'" :iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'" :class="requested ? 'rotate-180' : null">
:active="matched"
:iconRefActive="'#iconExists'"
:textActive="'Already in plex 🎉'"
>
Not yet in plex Not yet in plex
</sidebar-list-element> </sidebar-list-element>
<sidebar-list-element
@click="sendRequest" <sidebar-list-element @click="sendRequest" :iconRef="'#iconSent'"
:iconRef="'#iconSent'" :active="requested" :textActive="'Requested to be downloaded'">
:active="requested"
:textActive="'Requested to be downloaded'"
>
Request to be downloaded? Request to be downloaded?
</sidebar-list-element> </sidebar-list-element>
<sidebar-list-element <sidebar-list-element v-if="admin" @click="showTorrents=!showTorrents"
v-if="isPlexAuthenticated && matched" :iconRef="'#icon_torrents'" :active="showTorrents"
@click="openInPlex" :supplementaryText="numberOfTorrentResults">
:iconString="'⏯ '"
>
Watch in plex now!
</sidebar-list-element>
<sidebar-list-element
v-if="admin"
@click="showTorrents = !showTorrents"
:iconRef="'#icon_torrents'"
:active="showTorrents"
:supplementaryText="numberOfTorrentResults"
>
Search for torrents Search for torrents
</sidebar-list-element> </sidebar-list-element>
<sidebar-list-element @click="showIssueForm = !showIssueForm"
:iconRef="null"
:active="showIssueForm">
&nbsp; &nbsp;Report an issue!
</sidebar-list-element>
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'"> <sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
See more info See more info
</sidebar-list-element> </sidebar-list-element>
@@ -64,64 +61,55 @@
<!-- Loading placeholder --> <!-- Loading placeholder -->
<div class="movie__actions text-input__loading" v-else> <div class="movie__actions text-input__loading" v-else>
<div <div class="movie__actions-link" v-for="_ in admin ? Array(4) : Array(3)">
class="movie__actions-link" <div class="movie__actions-text text-input__loading--line" style="margin:9px; margin-left: -3px;"></div>
v-for="_ in admin ? Array(4) : Array(3)"
>
<div
class="movie__actions-text text-input__loading--line"
style="margin: 9px; margin-left: -3px"
></div>
</div> </div>
</div> </div>
<!-- MOVIE INFO --> <!-- MOVIE INFO -->
<div class="movie__info"> <div class="movie__info">
<!-- Loading placeholder --> <!-- Loading placeholder -->
<div <div v-if="!movie" class="movie__description">
class="movie__description noselect" <loading-placeholder :count="12" />
@click="truncatedDescription = !truncatedDescription"
v-if="!loading"
>
<span :class="truncatedDescription ? 'truncated' : null">{{
movie.overview
}}</span>
<button class="truncate-toggle"><i></i></button>
</div>
<div v-else class="movie__description">
<loading-placeholder :count="5" />
</div> </div>
<div class="movie__details" v-if="movie"> <div class="movie__details" v-if="movie && !showIssueForm">
<div v-if="movie.year"> <div class="movie__description">
<h2 class="title">Release Date</h2> {{ movie.overview }}
<div class="text">{{ movie.year }}</div> </div>
<div v-if="movie.year" class="movie__details-block">
<h2 class="movie__details-title">Release Date</h2>
<div class="movie__details-text">{{ movie.year }}</div>
</div> </div>
<div v-if="movie.rating"> <div v-if="movie.rank" class="movie__details-block">
<h2 class="title">Rating</h2> <h2 class="movie__details-title">Rating</h2>
<div class="text">{{ movie.rating }}</div> <div class="movie__details-text">{{ movie.rank }}</div>
</div> </div>
<div v-if="movie.type == 'show'"> <div v-if="movie.type == 'show'" class="movie__details-block">
<h2 class="title">Seasons</h2> <h2 class="movie__details-title">Seasons</h2>
<div class="text">{{ movie.seasons }}</div> <div class="movie__details-text">{{ movie.seasons }}</div>
</div> </div>
<div v-if="movie.genres"> <div v-if="movie.genres" class="movie__details-block">
<h2 class="title">Genres</h2> <h2 class="movie__details-title">Genres</h2>
<div class="text">{{ movie.genres.join(", ") }}</div> <div class="movie__details-text">{{ nestedDataToString(movie.genres) }}</div>
</div> </div>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Production status</h2>
<div class="text">{{ movie.production_status }}</div>
</div>
<div v-if="movie.type == 'show'">
<h2 class="title">Runtime</h2> <div v-if="showIssueForm" class="issueForm">
<div class="text">{{ movie.runtime[0] }} minutes</div> <h2 class="movie__details-title">Report an issue</h2>
</div> <RadioButtons class="issueOptions"
:options="issueOptions"
:value.sync="selectedIssue" />
<TextArea title="Additional information" :rows="3"
placeholder="Placeholder text" />
<SeasonedButton @click="reportIssue">Report issue</SeasonedButton>
</div> </div>
</div> </div>
@@ -129,64 +117,51 @@
<div class="movie__admin" v-if="movie && movie.credits"> <div class="movie__admin" v-if="movie && movie.credits">
<h2 class="movie__details-title">Cast</h2> <h2 class="movie__details-title">Cast</h2>
<div style="display: flex; flex-wrap: wrap"> <div style="display: flex; flex-wrap: wrap;">
<person <person v-for="cast in movie.credits.cast" :info="cast"
v-for="cast in movie.credits.cast" style="flex-basis: 0;"></person>
:info="cast" </div>
style="flex-basis: 0"
></person>
</div>
</div> </div>
</div> </div>
<!-- TORRENT LIST --> <!-- TORRENT LIST -->
<TorrentList <TorrentList v-if="movie" :show="showTorrents" :query="title" :tmdb_id="id"
v-if="movie" :admin="admin"></TorrentList>
:show="showTorrents"
:query="title"
:tmdb_id="id"
:admin="admin"
></TorrentList>
</div> </div>
</section> </section>
</template> </template>
<script> <script>
import storage from "@/storage"; import storage from '@/storage'
import img from "@/directives/v-image"; import img from '@/directives/v-image'
import TorrentList from "./TorrentList"; import TorrentList from './TorrentList'
import Person from "./Person"; import Person from './Person'
import SidebarListElement from "./ui/sidebarListElem"; import SidebarListElement from './ui/sidebarListElem'
import store from "@/store"; import store from '@/store'
import LoadingPlaceholder from "./ui/LoadingPlaceholder"; import LoadingPlaceholder from './ui/LoadingPlaceholder'
import RadioButtons from './ui/RadioButtons'
import TextArea from './ui/TextArea'
import SeasonedButton from './ui/SeasonedButton'
import { import { getMovie, getShow, request, getRequestStatus } from '@/api'
getMovie,
getPerson,
getShow,
request,
getRequestStatus,
watchLink
} from "@/api";
export default { export default {
// props: ['id', 'type'], props: ['id', 'type'],
props: { components: {
id: { TorrentList,
required: true, Person,
type: Number LoadingPlaceholder,
}, SidebarListElement,
type: { RadioButtons,
required: false, TextArea,
type: String SeasonedButton
}
}, },
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
directives: { img: img }, // TODO decide to remove or use directives: { img: img }, // TODO decide to remove or use
data() { data(){
return { return{
ASSET_URL: "https://image.tmdb.org/t/p/", ASSET_URL: 'https://image.tmdb.org/t/p/',
ASSET_SIZES: ["w500", "w780", "original"], ASSET_SIZES: ['w500', 'w780', 'original'],
movie: undefined, movie: undefined,
title: undefined, title: undefined,
poster: undefined, poster: undefined,
@@ -194,203 +169,133 @@ export default {
matched: false, matched: false,
userLoggedIn: storage.sessionId ? true : false, userLoggedIn: storage.sessionId ? true : false,
requested: false, requested: false,
admin: localStorage.getItem("admin") == "true" ? true : false, admin: localStorage.getItem('admin'),
showTorrents: false, showTorrents: false,
compact: false, compact: false,
loading: true, showIssueForm: false,
truncatedDescription: true selectedIssue: null
}; }
},
methods: {
parseResponse(movie) {
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)
store.dispatch('documentTitle/updateTitle', movie.title)
},
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
},
reportIssue() {
if (this.showIssueForm) {
this.$notifications.success({
title: 'Issue successfully submitted',
description: 'Reported issue: Missing subtitles',
timeout: 300000
})
}
}
}, },
watch: { watch: {
id: function (val) { id: function(val){
if (this.type === "movie") { if (this.type === 'movie') {
this.fetchMovie(val); this.fetchMovie(val);
} else { } else {
this.fetchShow(val); this.fetchShow(val)
}
},
backdrop: function (backdrop) {
if (backdrop != null) {
const style = {
backgroundImage:
"url(" + this.ASSET_URL + this.ASSET_SIZES[1] + backdrop + ")"
};
Object.assign(this.$refs.header.style, style);
} }
} }
}, },
computed: { computed: {
numberOfTorrentResults: () => { numberOfTorrentResults: () => {
let numTorrents = store.getters["torrentModule/resultCount"]; let numTorrents = store.getters['torrentModule/resultCount']
return numTorrents !== null ? numTorrents + " results" : null; return numTorrents !== null ? numTorrents + ' results' : null
}, },
isPlexAuthenticated: () => { issueOptions: function() {
return store.getters["userModule/isPlexAuthenticated"]; return [{
} value: 'playback',
}, text: 'Unable to play'
methods: { }, {
parseResponse(movie) { value: 'missing-episode',
this.loading = false; text: 'Missing Episode',
this.movie = { ...movie }; subElements: this.seasonOptions
this.title = movie.title; }, {
this.poster = movie.poster; value: 'missing-subtitle',
this.backdrop = movie.backdrop; text: 'Missing subtitles'
this.matched = movie.exists_in_plex || false; }]
this.checkIfRequested(movie).then(status => (this.requested = status));
store.dispatch("documentTitle/updateTitle", movie.title);
this.setPosterSrc();
}, },
async checkIfRequested(movie) { seasonOptions: function() {
return await getRequestStatus(movie.id, movie.type); if (this.movie.type !== 'show') {
}, return []
setPosterSrc() {
const poster = this.$refs["poster-image"];
if (this.poster == null) {
poster.src = "/no-image.png";
return;
} }
poster.src = `${this.ASSET_URL}${this.ASSET_SIZES[0]}${this.poster}`; const options = []
}, const length = this.movie.seasons;
sendRequest() {
request(this.id, this.type, storage.token).then(resp => {
if (resp.success) {
this.requested = true;
}
});
},
openInPlex() {
watchLink(this.title, this.movie.year, storage.token).then(
watchLink => (window.location = watchLink)
);
},
openTmdb() {
const tmdbType = this.type === "show" ? "tv" : this.type;
window.location.href =
"https://www.themoviedb.org/" + tmdbType + "/" + this.id;
}
},
created() {
this.prevDocumentTitle = store.getters["documentTitle/title"];
if (this.type === "movie") { for (var i = 0; i < length; i++) {
getMovie(this.id, true) options.push({
.then(this.parseResponse) value: i+1,
.catch(error => { text: `Season ${i+1}`
this.$router.push({ name: "404" }); })
}); }
} else if (this.type == "person") { return options;
getPerson(this.id, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: "404" });
});
} else {
getShow(this.id, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: "404" });
});
} }
}, },
beforeDestroy() { beforeDestroy() {
store.dispatch("documentTitle/updateTitle", this.prevDocumentTitle); store.dispatch('documentTitle/updateTitle', this.prevDocumentTitle)
},
created(){
this.prevDocumentTitle = store.getters['documentTitle/title']
if (this.type === 'movie') {
getMovie(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
} else {
getShow(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
}
console.log('admin: ', this.admin)
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/loading-placeholder"; @import "./src/scss/loading-placeholder";
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/main";
header {
$duration: 0.2s;
height: 250px;
transform: scaleY(1);
transition: height $duration ease;
transform-origin: top;
position: relative;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: $background-color;
display: flex;
align-items: center;
@include tablet-min {
height: 350px;
}
&:before {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
background: $background-dark-85;
}
@include mobile {
&.compact {
height: 100px;
}
}
}
.movie__poster {
display: none;
@include desktop {
background: $background-color;
height: 0;
display: block;
position: absolute;
width: calc(45% - 40px);
top: 40px;
left: 40px;
> img {
width: 100%;
}
}
}
.truncate-toggle {
border: none;
background: none;
width: 100%;
display: flex;
align-items: center;
text-align: center;
color: $text-color;
> i {
font-style: unset;
font-size: 0.7rem;
transition: 0.3s ease all;
transform: rotateY(180deg);
}
&::before,
&::after {
content: "";
flex: 1;
border-bottom: 1px solid $text-color-50;
}
&::before {
margin-right: 1rem;
}
&::after {
margin-left: 1rem;
}
}
.movie { .movie {
background-color: $background-color;
color: $text-color;
&__wrap { &__wrap {
display: flex; display: flex;
&--header { &--header {
@@ -401,12 +306,52 @@ header {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
flex-direction: column; flex-direction: column;
@include tablet-min { @include tablet-min{
flex-direction: row; flex-direction: row;
} }
}
}
&__header {
$duration: 0.2s;
height: 250px;
transform: scaleY(1);
transition: height $duration ease;
transform-origin: top;
position: relative;
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 50%;
background-color: $background-color;
@include tablet-min {
height: 350px;
}
&:before {
content: "";
display: block;
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
background: $background-dark-85;
}
&.compact {
height: 100px;
}
}
&__poster {
display: none;
@include tablet-min {
background: $background-color;
height: 0;
display: block;
position: absolute;
width: calc(45% - 40px);
top: 40px;
left: 40px;
background-color: $background-color;
color: $text-color;
} }
} }
@@ -452,67 +397,60 @@ header {
height: 100%; height: 100%;
} }
&__actions { &__actions {
text-align: center; text-align: center;
width: 100%; width: 100%;
order: 2;
padding: 20px;
border-top: 1px solid $text-color-5;
@include tablet-min {
order: 1;
width: 45%;
padding: 185px 0 40px 40px;
border-top: 0;
}
}
&__info {
width: 100%;
padding: 20px;
order: 1;
@include tablet-min {
order: 2; order: 2;
padding: 40px; padding: 20px;
width: 55%; border-top: 1px solid $text-color-5;
margin-left: 45%; @include tablet-min {
} order: 1;
} width: 45%;
&__info { padding: 185px 0 40px 40px;
margin-left: 0; border-top: 0;
}
&__description {
font-weight: 300;
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
& .truncated {
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
& + .truncate-toggle > i {
transform: rotateY(0deg) rotateZ(180deg);
} }
} }
&__info {
@include tablet-min { width: 100%;
margin-bottom: 30px; padding: 20px;
font-size: 14px; order: 1;
@include tablet-min {
order: 2;
padding: 40px;
width: 55%;
margin-left: 45%;
}
} }
} &__info {
&__details { margin-left: 0;
display: flex; }
flex-wrap: wrap; &__description {
font-weight: 300;
> div { font-size: 13px;
line-height: 1.8;
margin-bottom: 20px; margin-bottom: 20px;
margin-right: 20px; flex: 0 0 100%;
@include tablet-min { @include tablet-min {
margin-bottom: 30px; margin-bottom: 30px;
margin-right: 30px; font-size: 14px;
} }
& .title { }
&__details {
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
&-block:not(:last-child) {
margin-bottom: 20px;
margin-right: 20px;
@include tablet-min {
margin-bottom: 30px;
margin-right: 30px;
}
}
&-title {
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
text-transform: uppercase; text-transform: uppercase;
@@ -522,34 +460,53 @@ header {
font-size: 16px; font-size: 16px;
} }
} }
& .text { &-text {
font-weight: 300; font-weight: 300;
font-size: 14px; font-size: 14px;
margin-top: 5px; margin-top: 5px;
} }
} }
} &__admin {
&__admin {
width: 100%;
padding: 20px;
order: 2;
@include tablet-min {
order: 3;
padding: 40px;
padding-top: 0px;
width: 100%; width: 100%;
} padding: 20px;
&-title { order: 2;
margin: 0;
font-weight: 400;
text-transform: uppercase;
text-align: center;
font-size: 14px;
color: $green;
padding-bottom: 20px;
@include tablet-min { @include tablet-min {
font-size: 16px; 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: $green;
padding-bottom: 20px;
@include tablet-min {
font-size: 16px;
}
}
}
}
.issueForm {
// padding: 40px;
.issueOptions {
margin-top: 1rem;
}
.seasonOptions {
margin-top: 2rem;
h2 {
margin-bottom: 1rem;
}
> :not(h2) {
margin-left: 1rem;
} }
} }
} }

View File

@@ -1,249 +1,117 @@
<template> <template>
<li class="movie-item" :class="{ shortList: shortList }"> <li class="movies-item" :class="{'shortList': shortList}">
<figure class="movie-item__poster"> <a class="movies-item__link" :class="{'no-image': noImage}" @click.prevent="openMoviePopup(movie.id, movie.type)">
<img
class="movie-item__img"
ref="poster-image"
@click="openMoviePopup(movie.id, movie.type)"
:alt="posterAltText"
:data-src="poster"
src="~assets/placeholder.png"
/>
<div v-if="movie.download" class="progress"> <!-- TODO change to picture element -->
<progress :value="movie.download.progress" max="100"></progress> <figure class="movies-item__poster">
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span> <img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
</figure>
<div class="movies-item__content">
<p class="movies-item__title">{{ movie.title }}</p>
<p class="movies-item__title">{{ movie.year }}</p>
</div> </div>
</figure> </a>
<div class="movie-item__info">
<p v-if="movie.title || movie.name">{{ movie.title || movie.name }}</p>
<p v-if="movie.year">{{ movie.year }}</p>
<p v-if="movie.type == 'person'">
Known for: {{ movie.known_for_department }}
</p>
</div>
</li> </li>
</template> </template>
<script> <script>
import img from "../directives/v-image"; import img from '../directives/v-image'
export default { export default {
props: { props: ['movie', 'shortList'],
movie: {
type: Object,
required: true
},
shortList: {
type: Boolean,
required: false
}
},
directives: { directives: {
img: img img: img
}, },
data() { data(){
return { return {
poster: undefined, noImage: false
observed: false,
posterSizes: [
{
id: "w500",
minWidth: 500
},
{
id: "w342",
minWidth: 342
},
{
id: "w185",
minWidth: 185
},
{
id: "w154",
minWidth: 0
}
]
};
},
computed: {
posterAltText: function () {
const type = this.movie.type || "";
const title = this.movie.title || this.movie.name;
return this.movie.poster
? `Poster for ${type} ${title}`
: `Missing image for ${type} ${title}`;
} }
}, },
beforeMount() {
if (this.movie.poster != null) {
this.poster = "https://image.tmdb.org/t/p/w500" + this.movie.poster;
} else {
this.poster = "/no-image.png";
}
},
mounted() {
const poster = this.$refs["poster-image"];
if (poster == null) return;
const imageObserver = new IntersectionObserver((entries, imgObserver) => {
entries.forEach(entry => {
if (entry.isIntersecting && this.observed == false) {
const lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.className = lazyImage.className + " is-loaded";
this.observed = true;
}
});
});
imageObserver.observe(poster);
},
methods: { methods: {
// TODO handle missing images better and load diff sizes based on screen size
poster() {
if (this.movie.poster) {
return 'https://image.tmdb.org/t/p/w500' + this.movie.poster
} else {
this.noImage = true
}
},
openMoviePopup(id, type) { openMoviePopup(id, type) {
this.$popup.open(id, type); this.$popup.open(id, type)
} }
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss">
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/main";
.movie-item { .movies-item {
padding: 10px; padding: 10px;
width: 50%; width: 50%;
background-color: $background-color; background-color: $background-color;
transition: background-color 0.5s ease; transition: background-color 0.5s ease;
@include tablet-min { @include tablet-min{
padding: 15px; padding: 15px;
width: 33%;
} }
@include tablet-landscape-min { @include tablet-landscape-min{
padding: 15px; padding: 15px;
width: 25%; width: 25%;
} }
@include desktop-min { @include desktop-min{
padding: 15px; padding: 15px;
width: 20%; width: 20%;
} }
@include desktop-lg-min { @include desktop-lg-min{
padding: 15px; padding: 20px;
width: 12.5%; width: 12.5%;
} }
&:hover &__info > p { &__link{
color: $text-color;
}
&__poster {
text-decoration: none; text-decoration: none;
color: $text-color-70; color: $text-color-70;
font-weight: 300; font-weight: 300;
> img {
width: 100%;
opacity: 0;
transform: scale(0.97) translateZ(0);
transition: opacity 1s ease, transform 0.5s ease;
&.is-loaded {
opacity: 1;
transform: scale(1);
}
&:hover {
transform: scale(1.03);
box-shadow: 0 0 10px rgba($dark, 0.1);
}
}
} }
&__content{
&__info {
padding-top: 15px; padding-top: 15px;
font-weight: 300; }
&__poster{
> p { transition: transform 0.5s ease, box-shadow 0.3s ease;
color: $text-color-70; transform: translateZ(0);
margin: 0; }
font-size: 11px; &__img{
letter-spacing: 0.5px; width: 100%;
transition: color 0.5s ease; opacity: 0;
cursor: pointer; transform: scale(0.97) translateZ(0);
@include mobile-ls-min { transition: opacity 0.5s ease, transform 0.5s ease;
font-size: 12px; &.is-loaded{
} opacity: 1;
@include tablet-min { transform: scale(1);
font-size: 14px;
}
} }
} }
} &__link:not(.no-image):hover &__poster{
transform: scale(1.03);
.no-image { box-shadow: 0 0 10px rgba($dark, 0.1);
background-color: var(--text-color);
color: var(--background-color);
width: 100%;
height: 383px;
display: flex;
align-items: center;
justify-content: center;
span {
font-size: 1.5rem;
width: 70%;
text-align: center;
text-transform: uppercase;
} }
&__title{
&:hover { margin: 0;
transform: scale(1); font-size: 11px;
} letter-spacing: 0.5px;
} transition: color 0.5s ease;
</style> cursor: pointer;
@include mobile-ls-min{
<style lang="scss" scoped> font-size: 12px;
@import "./src/scss/variables"; }
@include tablet-min{
.progress { font-size: 14px;
position: absolute; }
bottom: 0; }
display: flex; &__link:hover &__title{
flex-direction: column; color: $text-color;
align-items: center;
width: 100%;
margin-bottom: 0.8rem;
> progress {
width: 95%;
}
> span {
position: absolute;
font-size: 1rem;
line-height: 1.4rem;
color: $white;
}
progress {
border-radius: 4px;
height: 1.4rem;
}
progress::-webkit-progress-bar {
background-color: rgba($black, 0.55);
border-radius: 4px;
}
progress::-webkit-progress-value {
background-color: $green-70;
border-radius: 4px;
}
progress::-moz-progress-bar {
/* style rules */
background-color: green;
} }
} }
</style> </style>

View File

@@ -1,93 +1,78 @@
<template> <template>
<nav class="nav"> <div>
<router-link <nav class="nav">
class="nav__logo" <router-link class="nav__logo" :to="{name: 'home'}" exact title="Vue.js — TMDb App">
:to="{ name: 'home' }" <svg class="nav__logo-image">
exact <use xlink:href="#svgLogo"></use>
title="Vue.js — TMDb App" </svg>
> </router-link>
<svg class="nav__logo-image">
<use xlink:href="#svgLogo"></use>
</svg>
</router-link>
<div class="nav__hamburger" @click="toggleNav"> <div class="nav__hamburger" @click="toggleNav">
<div v-for="_ in 3" class="bar"></div> <div v-for="_ in 3" 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">
<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>
<span class="nav__link-title">{{ item.title }}</span> <span class="nav__link-title">{{ item.title }}</span>
</div> </div>
</router-link> </router-link>
</li> </li>
<li class="nav__item mobile-only"></li> <li class="nav__item nav__item--profile">
<router-link class="nav__link nav__link--profile" :to="{name: 'signin'}" v-if="!userLoggedIn">
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Sign in</span>
</div>
</router-link>
<li class="nav__item nav__item--profile"> <router-link class="nav__link nav__link--profile" :to="{name: 'profile'}" v-if="userLoggedIn">
<router-link <div class="nav__link-wrap">
class="nav__link nav__link--profile" <svg class="nav__link-icon">
:to="{ name: 'signin' }" <use xlink:href="#iconLogin"></use>
v-if="!userLoggedIn" </svg>
> <span class="nav__link-title">Profile</span>
<div class="nav__link-wrap"> </div>
<svg class="nav__link-icon"> </router-link>
<use xlink:href="#iconLogin"></use> </li>
</svg> </ul>
<span class="nav__link-title">Sign in</span> </nav>
</div>
</router-link>
<router-link <div class="spacer"></div>
class="nav__link nav__link--profile" </div>
:to="{ name: 'profile' }"
v-if="userLoggedIn"
>
<div class="nav__link-wrap">
<svg class="nav__link-icon">
<use xlink:href="#iconLogin"></use>
</svg>
<span class="nav__link-title">Profile</span>
</div>
</router-link>
</li>
</ul>
</nav>
</template> </template>
<script> <script>
import storage from "@/storage"; import storage from '@/storage'
export default { export default {
data() { data(){
return { return {
listTypes: storage.homepageLists, listTypes: storage.homepageLists,
userLoggedIn: localStorage.getItem("token") ? true : false userLoggedIn: localStorage.getItem('token') ? true : false
};
},
methods: {
setUserStatus() {
this.userLoggedIn = localStorage.getItem("token") ? true : false;
},
toggleNav() {
document
.querySelector(".nav__hamburger")
.classList.toggle("nav__hamburger--active");
document
.querySelector(".nav__list")
.classList.toggle("nav__list--active");
} }
}, },
created() { methods: {
setUserStatus(){
this.userLoggedIn = localStorage.getItem('token') ? true : false;
},
toggleNav(){
document.querySelector('.nav__hamburger').classList.toggle('nav__hamburger--active');
document.querySelector('.nav__list').classList.toggle('nav__list--active');
}
},
created(){
// TODO move this to state manager // TODO move this to state manager
eventHub.$on("setUserStatus", this.setUserStatus); eventHub.$on('setUserStatus', this.setUserStatus);
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -98,45 +83,45 @@ export default {
width: 30px; width: 30px;
} }
.spacer {
@include mobile-only {
width: 100%;
height: $header-size;
}
}
.nav { .nav {
transition: background 0.5s ease; transition: background .5s ease;
position: fixed; position: fixed;
bottom: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
height: var(--header-size); height: 50px;
z-index: 10; z-index: 10;
display: block; display: block;
color: $text-color; color: $text-color;
background-color: $background-color-secondary; background-color: $background-color-secondary;
@include tablet-min { @include tablet-min{
top: 0;
bottom: unset;
width: 95px; width: 95px;
height: 100vh; height: 100vh;
} }
&__logo { &__logo {
width: 95px; width: 55px;
height: $header-size; height: $header-size;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: $background-nav-logo; background: $background-nav-logo;
@include tablet-min{
@include mobile-only { width: 95px;
align-items: flex-start;
padding-top: 0.5rem;
width: 55px;
} }
&-image{
&-image {
width: 35px; width: 35px;
height: 31px; height: 31px;
fill: $green; fill: $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;
} }
@@ -150,12 +135,12 @@ export default {
position: fixed; position: fixed;
width: 55px; width: 55px;
height: 50px; height: 50px;
bottom: 1.5rem; top: 0;
right: 0; right: 0;
cursor: pointer; cursor: pointer;
z-index: 10; z-index: 10;
border-left: 1px solid $background-color; border-left: 1px solid $background-color;
@include tablet-min { @include tablet-min{
display: none; display: none;
} }
.bar { .bar {
@@ -187,9 +172,9 @@ export default {
} }
} }
&--active { &--active {
.bar { .bar{
&:nth-child(1), &:nth-child(1),
&:nth-child(3) { &:nth-child(3){
width: 0; width: 0;
} }
&:nth-child(2) { &:nth-child(2) {
@@ -213,21 +198,15 @@ export default {
left: 0; left: 0;
top: 50px; top: 50px;
border-top: 1px solid $background-color; border-top: 1px solid $background-color;
@include mobile-only { @include mobile-only {
display: flex; display: flex;
position: absolute;
top: unset;
bottom: var(--header-size);
height: min-content;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 0; font-size: 0;
opacity: 0; opacity: 0;
visibility: hidden; visibility: hidden;
background-color: $background-95; background-color: $background-95;
text-align: left; text-align: left;
&--active{
&--active {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
} }
@@ -242,15 +221,15 @@ export default {
} }
} }
&__item { &__item {
transition: background 0.5s ease, color 0.5s ease, border 0.5s ease; transition: background .5s ease, color .5s ease, border .5s ease;
background-color: $background-color-secondary; background-color: $background-color-secondary;
color: $text-color-70; color: $text-color-70;
@include mobile-only { @include mobile-only {
flex: 0 0 33.3%; flex: 0 0 50%;
text-align: center; text-align: center;
border-bottom: 1px solid $background-color; border-bottom: 1px solid $background-color;
&:nth-child(odd) { &:nth-child(odd){
border-right: 1px solid $background-color; border-right: 1px solid $background-color;
&:last-child { &:last-child {
@@ -272,8 +251,7 @@ export default {
border-left: 1px solid $background-color; border-left: 1px solid $background-color;
} }
} }
&:hover, &:hover, .is-active {
.is-active {
color: $text-color; color: $text-color;
background-color: $background-color; background-color: $background-color;
} }
@@ -321,14 +299,14 @@ export default {
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, &:hover &-icon, &.is-active &-icon {
&.is-active &-icon {
fill: $text-color; fill: $text-color;
} }
} }

View File

@@ -2,10 +2,10 @@
<section class="profile"> <section class="profile">
<div class="profile__content" v-if="userLoggedIn"> <div class="profile__content" v-if="userLoggedIn">
<header class="profile__header"> <header class="profile__header">
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2> <h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
<div class="button--group"> <div class="button--group">
<seasoned-button @click="toggleSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button> <seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
<seasoned-button @click="logOut">Log out</seasoned-button> <seasoned-button @click="logOut">Log out</seasoned-button>
</div> </div>
@@ -13,7 +13,7 @@
<settings v-if="showSettings"></settings> <settings v-if="showSettings"></settings>
<list-header title="User requests" :info="resultCount" /> <list-header title="User requests" :info="resultCount"/>
<results-list v-if="results" :results="results" /> <results-list v-if="results" :results="results" />
</div> </div>
@@ -43,6 +43,7 @@ export default {
data(){ data(){
return{ return{
userLoggedIn: '', userLoggedIn: '',
userName: '',
emoji: '', emoji: '',
results: undefined, results: undefined,
totalResults: undefined, totalResults: undefined,
@@ -57,21 +58,32 @@ export default {
const loadedResults = this.results.length const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞' const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results` return `${loadedResults} of ${totalResults} results`
}, }
username: () => store.getters['userModule/username']
}, },
methods: { methods: {
createSession(token){
axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
.then(function(resp){
let data = resp.data;
if(data.success){
let id = data.session_id;
localStorage.setItem('session_id', id);
eventHub.$emit('setUserStatus');
this.userLoggedIn = true;
this.getUserInfo();
}
}.bind(this));
},
getUserInfo(){
this.userName = localStorage.getItem('username');
},
toggleSettings() { toggleSettings() {
this.showSettings = this.showSettings ? false : true; this.showSettings = this.showSettings ? false : true;
if (this.showSettings) {
this.$router.replace({ query: { settings: true} })
} else {
this.$router.replace({ name: 'profile' })
}
}, },
logOut(){ logOut(){
this.$router.push('logout') localStorage.clear();
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'home' });
} }
}, },
created(){ created(){
@@ -79,8 +91,7 @@ export default {
this.userLoggedIn = false; this.userLoggedIn = false;
} else { } else {
this.userLoggedIn = true; this.userLoggedIn = true;
this.getUserInfo();
this.showSettings = window.location.toString().includes('settings=true')
getUserRequests() getUserRequests()
.then(results => { .then(results => {

View File

@@ -2,20 +2,23 @@
<section> <section>
<h1>Register new user</h1> <h1>Register new user</h1>
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" @enter="submit"/> <seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/> <seasoned-input placeholder="password" icon="Keyhole" type="password"
<seasoned-input placeholder="repeat password" icon="Keyhole" type="password" :value.sync="passwordRepeat" @enter="submit"/> :value.sync="password" @enter="requestNewUser"/>
<seasoned-input placeholder="repeat password" icon="Keyhole" type="password"
:value.sync="passwordRepeat" @enter="requestNewUser"/>
<seasoned-button @click="requestNewUser">Register</seasoned-button>
<seasoned-button @click="submit">Register</seasoned-button>
<router-link class="link" to="/signin">Have a user? Sign in here</router-link> <router-link class="link" to="/signin">Have a user? Sign in here</router-link>
<seasoned-messages :messages.sync="messages"></seasoned-messages> <seasoned-messages :messages.sync="messages"></seasoned-messages>
</section> </section>
</template> </template>
<script> <script>
import { register } from '@/api' import axios from 'axios'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedMessages from '@/components/ui/SeasonedMessages'
@@ -31,47 +34,60 @@ export default {
} }
}, },
methods: { methods: {
submit() { requestNewUser(){
this.messages = []; let { username, password, passwordRepeat } = this
let { username, password, passwordRepeat } = this;
if (username == null || username.length == 0) { let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
this.messages.push({ type: 'error', title: 'Missing username' })
return
} else if (password == null || password.length == 0) {
this.messages.push({ type: 'error', title: 'Missing password' })
return
} else if (passwordRepeat == null || passwordRepeat.length == 0) {
this.messages.push({ type: 'error', title: 'Missing repeat password' })
return
} else if (passwordRepeat != password) {
this.messages.push({ type: 'error', title: 'Passwords do not match' })
return
}
this.registerUser(username, password) if (verifyCredentials.verified) {
}, axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
registerUser(username, password) { username: username,
register(username, password, true) password: password
.then(data => { })
.then(resp => {
let data = resp.data;
if (data.success){ if (data.success){
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
const jwtData = parseJwt(data.token) localStorage.setItem('username', username);
localStorage.setItem('username', jwtData['username']); localStorage.setItem('admin', data.admin)
localStorage.setItem('admin', jwtData['admin'] || false);
eventHub.$emit('setUserStatus'); eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' }) this.$router.push({ name: 'profile' })
} }
}) })
.catch(error => { .catch(error => {
if (error.status === 401) { this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.error })
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
}
else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
}
}); });
}
else {
this.messages.push({ type: 'warning', title: 'Parse error', message: verifyCredentials.reason })
}
},
checkCredentials(username, password, passwordRepeat) {
if (!username || username.length === 0) {
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 {
verified: false,
reason: 'Passwords do not match'
}
}
else {
return {
verified: true,
reason: 'Verified credentials'
}
}
}, },
logOut(){ logOut(){
localStorage.clear(); localStorage.clear();

View File

@@ -1,41 +1,23 @@
<template> <template>
<div class="page-container"> <div>
<list-header :title="title" :info="resultCount" :sticky="true" /> <list-header :title="title" :info="resultCount" :sticky="true" />
<results-list :results="results" /> <results-list :results="results" />
<div v-if="page < totalPages" class="fullwidth-button"> <div v-if="page < totalPages" class="fullwidth-button">
<seasoned-button @click="loadMore">load more</seasoned-button> <seasoned-button @click="loadMore">load more</seasoned-button>
</div> </div>
<div class="notFound" v-if="results.length == 0 && loading == false"> <loader v-if="!results.length" />
<h1 class="notFound-title">
No results for search: <b>{{ query }}</b>
</h1>
</div>
<loader v-if="loading" />
</div> </div>
</template> </template>
<style lang="scss" scoped>
.notFound {
display: flex;
justify-content: center;
align-items: center;
&-title {
font-weight: 400;
}
}
</style>
<script> <script>
import { searchTmdb } from "@/api"; import { searchTmdb } from '@/api'
import ListHeader from "@/components/ListHeader"; import ListHeader from '@/components/ListHeader'
import ResultsList from "@/components/ResultsList"; import ResultsList from '@/components/ResultsList'
import SeasonedButton from "@/components/ui/SeasonedButton"; import SeasonedButton from '@/components/ui/SeasonedButton'
import Loader from "@/components/ui/Loader"; import Loader from '@/components/ui/Loader'
export default { export default {
components: { ListHeader, ResultsList, SeasonedButton, Loader }, components: { ListHeader, ResultsList, SeasonedButton, Loader },
@@ -55,79 +37,60 @@ export default {
query: String, query: String,
title: String, title: String,
page: Number, page: Number,
adult: undefined,
mediaType: null,
totalPages: 0, totalPages: 0,
results: [], results: [],
totalResults: [] totalResults: []
}; }
}, },
computed: { computed: {
resultCount() { resultCount() {
const loadedResults = this.results.length; const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : "∞"; const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results`; return `${loadedResults} of ${totalResults} results`
} }
}, },
methods: { methods: {
search( search(query=this.query, page=this.page) {
query = this.query, searchTmdb(query, page)
page = this.page, .then(this.parseResponse)
adult = this.adult,
mediaType = this.mediaType
) {
searchTmdb(query, page, adult, mediaType).then(this.parseResponse);
}, },
parseResponse(data) { parseResponse(data) {
if (this.results.length > 0) { if (this.results.length > 0) {
this.results.push(...data.results); this.results.push(...data.results)
} else { } else {
this.results = data.results; this.results = data.results
} }
this.totalPages = data.total_pages; this.totalPages = data.total_pages
this.totalResults = data.total_results || data.results.length; this.totalResults = data.total_results || data.results.length
this.loading = false; this.loading = false
}, },
loadMore() { loadMore() {
this.page++; this.page++
window.history.replaceState( window.history.replaceState({}, 'search', `/#/search?query=${this.query}&page=${this.page}`)
{}, this.search()
"search",
`/#/search?query=${this.query}&page=${this.page}`
);
this.search();
} }
}, },
created() { created() {
const { query, page, adult, media_type } = this.$route.query; const { query, page } = this.$route.query
if (!query) { if (!query) {
// abort // abort
console.error("abort, no query"); console.error('abort, no query')
} }
this.query = decodeURIComponent(query); this.query = decodeURIComponent(query)
this.page = page || 1; this.page = page ? page : 1
this.adult = adult || this.adult; this.title = `Search results: ${this.query}`
this.mediaType = media_type || this.mediaType;
this.title = `Search results: ${this.query}`;
this.search(); this.search()
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/media-queries";
@include mobile-only {
.page-container {
margin-top: 1rem;
} }
} }
</script>
<style lang="scss" scoped>
.fullwidth-button { .fullwidth-button {
width: 100%; width: 100%;
margin: 1rem 0; margin: 1rem 0;
@@ -135,4 +98,5 @@ export default {
display: flex; display: flex;
justify-content: center; justify-content: center;
} }
</style>
</style>

View File

@@ -1,290 +1,184 @@
<template> <template>
<!-- <div> --> <div>
<div class="search">
<input <div class="search">
ref="input" <input
type="text" ref="input"
placeholder="Search for movie or show" type="text"
aria-label="Search input for finding a movie or show" placeholder="Search for a movie or show"
autocorrect="off" autocorrect="off"
autocapitalize="off" autocapitalize="off"
tabindex="1" v-model="query"
v-model="query" @input="handleInput"
@input="handleInput" @click="focus = true"
@click="focus = true" @keydown.escape="handleEscape"
@keydown.escape="handleEscape" @keyup.enter="handleSubmit"
@keyup.enter="handleSubmit" @keydown.up="navigateUp"
@keydown.up="navigateUp" @keydown.down="navigateDown" />
@keydown.down="navigateDown"
/> <svg class="search--icon" fill="currentColor"><use xlink:href="#iconSearch"></use></svg>
</div>
<svg class="search-icon" fill="currentColor" @click="handleSubmit">
<use xlink:href="#iconSearch"></use>
</svg>
</div>
<!--
<transition name="fade"> <transition name="fade">
<div class="dropdown" v-if="!disabled && focus && query.length > 0"> <div class="dropdown" v-if="!disabled && focus && query.length > 0">
<div class="filter"> <div class="dropdown--results">
<h2>Filter your search:</h2>
<div class="filter-items"> <ul v-for="(item, index) in elasticSearchResults"
<toggle-button @click="$popup.open(item.id, item.type)"
:options="searchTypes" :class="{ active: index + 1 === selectedResult}">
:selected.sync="selectedSearchType"
/> {{ item.name }}
<label
>Adult
<input type="checkbox" value="adult" v-model="adult" />
</label>
</div>
</div>
<hr />
<div class="dropdown-results" v-if="elasticSearchResults.length">
<ul
v-for="(item, index) in elasticSearchResults"
@click="openResult(item, index + 1)"
:class="{ active: index + 1 === selectedResult }"
>
{{
item.name
}}
</ul> </ul>
</div> </div>
<div v-else class="dropdown"> <seasoned-button class="end-section" fullWidth="true"
<div class="dropdown-results"> @click="focus = false" :active="elasticSearchResults.length + 1 === selectedResult">
<h2 class="not-found">
No results for query: <b>{{ query }}</b>
</h2>
</div>
</div>
<seasoned-button
class="end-section"
fullWidth="true"
@click="focus = false"
:active="elasticSearchResults.length + 1 === selectedResult"
>
close close
</seasoned-button> </seasoned-button>
</div> </div>
</transition> </transition>
</div> --> </div>
</template> </template>
<script> <script>
import SeasonedButton from "@/components/ui/SeasonedButton"; import SeasonedButton from '@/components/ui/SeasonedButton'
import ToggleButton from "@/components/ui/ToggleButton";
import { elasticSearchMoviesAndShows } from "@/api"; import { elasticSearchMoviesAndShows } from '@/api'
import config from "@/config.json"; import config from '@/config.json'
export default { export default {
name: "SearchInput", name: 'SearchInput',
components: { components: {
SeasonedButton, SeasonedButton
ToggleButton
}, },
props: ["value"], props: ['value'],
data() { data() {
return { return {
adult: true,
searchTypes: ["all", "movie", "show", "person"],
selectedSearchType: "all",
query: this.value, query: this.value,
focus: false, focus: false,
disabled: false, disabled: false,
scrollListener: undefined, scrollListener: undefined,
scrollDistance: 0, scrollDistance: 0,
elasticSearchResults: [], elasticSearchResults: '',
selectedResult: 0 selectedResult: 0
}; }
}, },
watch: { watch: {
focus: function (val) { focus: function(val) {
if (val === true) { if (val === true) {
window.addEventListener("scroll", this.disableFocus); window.addEventListener('scroll', this.disableFocus)
} else { } else {
window.removeEventListener("scroll", this.disableFocus); window.removeEventListener('scroll', this.disableFocus)
this.scrollDistance = 0; this.scrollDistance = 0
} }
},
adult: function (value) {
this.handleInput();
} }
}, },
beforeMount() { beforeMount() {
const elasticUrl = config.ELASTIC_URL; const elasticUrl = config.ELASTIC_URL
if (elasticUrl === undefined || elasticUrl === false || elasticUrl === "") { if (elasticUrl === undefined || elasticUrl === false || elasticUrl === '') {
this.disabled = true; this.disabled = true
} }
}, },
beforeDestroy() { beforeDestroy() {
console.log("scroll eventlistener not removed, destroying!"); console.log('scroll eventlistener not removed, destroying!')
window.removeEventListener("scroll", this.disableFocus); window.removeEventListener('scroll', this.disableFocus)
}, },
methods: { methods: {
navigateDown() { navigateDown() {
this.focus = true; this.focus = true
this.selectedResult++; this.selectedResult++
}, },
navigateUp() { navigateUp() {
this.focus = true; this.focus = true
this.selectedResult--; this.selectedResult--
const input = this.$refs.input; const input = this.$refs.input;
const textLength = input.value.length; const textLength = input.value.length
setTimeout(() => { setTimeout(() => {
input.focus(); input.focus()
input.setSelectionRange(textLength, textLength + 1); input.setSelectionRange(textLength, textLength + 1)
}, 1); }, 1)
}, },
openResult(item, index) { handleInput(e){
this.selectedResult = index; this.selectedResult = 0
this.$popup.open(item.id, item.type); this.$emit('input', this.query);
},
handleInput(e) {
this.selectedResult = 0;
this.$emit("input", this.query);
if (!this.focus) { if (! this.focus) {
this.focus = true; this.focus = true;
} }
elasticSearchMoviesAndShows(this.query).then(resp => { elasticSearchMoviesAndShows(this.query)
const data = resp.hits.hits; .then(resp => {
const data = resp.hits.hits
let results = 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' || item._source.original_title) {
return { return {
name: item._source.original_title, name: item._source.original_title,
id: item._source.id, id: item._source.id,
adult: item._source.adult, type: 'movie'
type: "movie" }
}; } else if (index === 'show' || item._source.original_name) {
} else if (index === "show" || item._source.original_name) {
return { return {
name: item._source.original_name, name: item._source.original_name,
id: item._source.id, id: item._source.id,
adult: item._source.adult, type: 'show'
type: "show" }
};
} }
}); })
results = this.removeDuplicates(results); console.log(this.elasticSearchResults)
this.elasticSearchResults = results; })
});
},
removeDuplicates(searchResults) {
let filteredResults = [];
searchResults.map(result => {
const numberOfDuplicates = filteredResults.filter(
filterItem => filterItem.id == result.id
);
if (numberOfDuplicates.length >= 1) {
return null;
}
filteredResults.push(result);
});
if (this.adult == false) {
filteredResults = filteredResults.filter(
result => result.adult == false
);
}
return filteredResults;
}, },
handleSubmit() { handleSubmit() {
let searchResults = this.elasticSearchResults; let searchResults = this.elasticSearchResults
if (this.selectedResult > searchResults.length) { if (this.selectedResult > searchResults.length) {
this.focus = false; this.focus = false
this.selectedResult = 0; this.selectedResult = 0
} else if (this.selectedResult > 0) { } else if (this.selectedResult > 0) {
const resultItem = searchResults[this.selectedResult - 1]; const resultItem = searchResults[this.selectedResult - 1]
this.$popup.open(resultItem.id, resultItem.type); this.$popup.open(resultItem.id, resultItem.type)
} else { } else {
const encodedQuery = encodeURI(this.query.replace('/ /g, "+"')); const encodedQuery = encodeURI(this.query.replace('/ /g, "+"'))
const media_type = this.$router.push({ name: 'search', query: { query: encodedQuery }});
this.selectedSearchType !== "all" ? this.selectedSearchType : null; this.focus = false
this.$router.push({ this.selectedResult = 0
name: "search",
query: { query: encodedQuery, adult: this.adult, media_type }
});
this.focus = false;
this.selectedResult = 0;
} }
}, },
handleEscape() { handleEscape() {
if (this.$popup.isOpen) { if (this.$popup.isOpen) {
console.log("THIS WAS FUCKOING OPEN!"); console.log('THIS WAS FUCKOING OPEN!')
} else { } else {
this.focus = false; this.focus = false
} }
}, },
disableFocus(_) { disableFocus(_) {
this.focus = false; this.focus = false
} }
} }
}; }
</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/media-queries";
@import "./src/scss/main"; @import './src/scss/main';
.fade-enter-active { .fade-enter-active {
transition: opacity 0.2s; transition: opacity .2s;
} }
.fade-leave-active { .fade-leave-active {
transition: opacity 0.2s; transition: opacity .2s;
} }
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
opacity: 0; opacity: 0;
} }
.filter {
// background-color: rgba(004, 122, 125, 0.2);
width: 100%;
display: flex;
flex-direction: column;
margin: 1rem 2rem;
h2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 400;
}
&-items {
display: flex;
flex-direction: row;
align-items: center;
> :not(:first-child) {
margin-left: 1rem;
}
}
}
hr {
display: block;
height: 1px;
border: 0;
border-bottom: 1px solid $text-color-50;
margin-top: 10px;
margin-bottom: 10px;
width: 90%;
}
.dropdown { .dropdown {
width: 100%; width: 100%;
position: relative; position: relative;
@@ -302,11 +196,7 @@ hr {
width: calc(100%); width: calc(100%);
} }
.not-found { &--results {
font-weight: 400;
}
&-results {
padding-left: 60px; padding-left: 60px;
width: 100%; width: 100%;
@@ -321,7 +211,7 @@ hr {
width: calc(100% - 25px); width: calc(100% - 25px);
max-width: fit-content; max-width: fit-content;
list-style: none; list-style: none;
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);
text-transform: capitalize; text-transform: capitalize;
cursor: pointer; cursor: pointer;
@@ -332,9 +222,7 @@ hr {
overflow: hidden; overflow: hidden;
color: $text-color-50; color: $text-color-50;
&.active, &.active, &:hover, &:active {
&:hover,
&:active {
color: $text-color; color: $text-color;
border-bottom: 2px solid $text-color; border-bottom: 2px solid $text-color;
} }
@@ -347,16 +235,16 @@ hr {
display: flex; display: flex;
position: fixed; position: fixed;
flex-wrap: wrap; flex-wrap: wrap;
z-index: 16; z-index: 5;
border: 0; border: 0;
background-color: $background-color-secondary; 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);
bottom: 0; top: 0;
right: 55px; right: 55px;
@include tablet-min { @include tablet-min{
position: relative; position: relative;
width: 100%; width: 100%;
right: 0px; right: 0px;
@@ -364,26 +252,23 @@ hr {
input { input {
display: block; display: block;
height: calc($header-size - 1.5rem);
width: 100%; width: 100%;
padding: 13px 0 13px 45px; padding: 13px 20px 13px 45px;
outline: none; outline: none;
margin: 0; margin: 0;
margin-bottom: auto;
border: 0; border: 0;
background-color: $background-color-secondary; background-color: $background-color-secondary;
font-weight: 300; font-weight: 300;
font-size: 19px; font-size: 19px;
color: $text-color; color: $text-color;
transition: background-color 0.5s ease, color 0.5s ease; transition: background-color .5s ease, color .5s ease;
@include tablet-min { @include tablet-min {
height: calc($header-size);
padding: 13px 30px 13px 60px; padding: 13px 30px 13px 60px;
} }
} }
&-icon { &--icon{
width: 20px; width: 20px;
height: 20px; height: 20px;
fill: $text-color-50; fill: $text-color-50;
@@ -393,10 +278,10 @@ hr {
left: 15px; left: 15px;
top: 15px; top: 15px;
@include tablet-min { @include tablet-min{
top: 27px; top: 27px;
left: 25px; left: 25px;
} }
} }
} }
</style> </style>

View File

@@ -3,23 +3,17 @@
<div class="profile__content" v-if="userLoggedIn"> <div class="profile__content" v-if="userLoggedIn">
<section class='settings'> <section class='settings'>
<h3 class='settings__header'>Plex account</h3> <h3 class='settings__header'>Plex account</h3>
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span>
<div v-if="!hasPlexUser"> <form class="form">
<span class="settings__info">Sign in to your plex account to get information about recently added movies and to see your watch history</span> <seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
:value.sync="plexPassword" @submit="authenticatePlex" />
<form class="form"> <seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
:value.sync="plexPassword" @submit="authenticatePlex" />
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button> <seasoned-messages :messages.sync="messages" />
</form> </form>
</div>
<div v-else>
<span class="settings__info">Awesome, your account is already authenticated with plex! Enjoy viewing your seasoned search history, plex watch history and real-time torrent download progress.</span>
<seasoned-button @click="unauthenticatePlex">un-link plex account</seasoned-button>
</div>
<seasoned-messages :messages.sync="messages" />
<hr class='setting__divider'> <hr class='setting__divider'>
@@ -50,13 +44,12 @@
</template> </template>
<script> <script>
import store from '@/store'
import storage from '@/storage' import storage from '@/storage'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api' import { plexAuthenticate } from '@/api'
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton, SeasonedMessages },
@@ -67,21 +60,7 @@ export default {
plexUsername: null, plexUsername: null,
plexPassword: null, plexPassword: null,
newPassword: null, newPassword: null,
newPasswordRepeat: null, newPasswordRepeat: null
emoji: null
}
},
computed: {
hasPlexUser: function() {
return this.settings && this.settings['plex_userid']
},
settings: {
get: () => {
return store.getters['userModule/settings']
},
set: function(newSettings) {
store.dispatch('userModule/setSettings', newSettings)
}
} }
}, },
methods: { methods: {
@@ -91,37 +70,26 @@ export default {
changePassword() { changePassword() {
return return
}, },
async authenticatePlex() { authenticatePlex() {
let username = this.plexUsername let username = this.plexUsername
let password = this.plexPassword let password = this.plexPassword
const response = await linkPlexAccount(username, password) plexAuthenticate(username, password)
.then(resp => {
const data = resp.data
this.messages.push({ type: 'success', title: 'Authenticated with plex', message: 'Successfully linked plex account with seasoned request' })
this.messages.push({ console.log('response from plex:', data.username)
type: response.success ? 'success' : 'error',
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
message: response.message
}) })
.catch(error => {
console.error(error);
if (response.success) this.messages.push({ type: 'error', title: 'Something went wrong', message: error.message })
getSettings().then(settings => this.settings = settings)
},
async unauthenticatePlex() {
const response = await unlinkPlexAccount()
this.messages.push({
type: response.success ? 'success' : 'error',
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
message: response.message
}) })
if (response.success)
getSettings().then(settings => this.settings = settings)
} }
}, },
created(){ created(){
const token = localStorage.getItem('token') || false; if (localStorage.getItem('token')){
if (token){
this.userLoggedIn = true this.userLoggedIn = true
} }
} }
@@ -183,7 +151,7 @@ a {
display: block; display: block;
height: 1px; height: 1px;
border: 0; border: 0;
border-bottom: 1px solid $text-color-50; border-bottom: 1px solid rgba(8, 28, 36, 0.05);
margin-top: 30px; margin-top: 30px;
margin-bottom: 70px; margin-bottom: 70px;
margin-left: 20px; margin-left: 20px;

View File

@@ -2,29 +2,25 @@
<section> <section>
<h1>Sign in</h1> <h1>Sign in</h1>
<seasoned-input placeholder="username" <seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
icon="Email" <seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
type="email"
@enter="submit" <seasoned-button @click="signin">sign in</seasoned-button>
:value.sync="username" />
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="submit"/>
<seasoned-button @click="submit">sign in</seasoned-button>
<router-link class="link" to="/register">Don't have a user? Register here</router-link> <router-link class="link" to="/register">Don't have a user? Register here</router-link>
<seasoned-messages :messages.sync="messages"></seasoned-messages> <seasoned-messages :messages.sync="messages"></seasoned-messages>
</section> </section>
</template> </template>
<script> <script>
import { login } from '@/api' import axios from 'axios'
import storage from '../storage' import storage from '../storage'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedMessages from '@/components/ui/SeasonedMessages' import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { parseJwt } from '@/utils'
export default { export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages }, components: { SeasonedInput, SeasonedButton, SeasonedMessages },
@@ -39,44 +35,33 @@ export default {
setValue(l, t) { setValue(l, t) {
this[l] = t this[l] = t
}, },
submit() { signin(){
this.messages = [];
let username = this.username; let username = this.username;
let password = this.password; let password = this.password;
if (username == null || username.length == 0) { axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
this.messages.push({ type: 'error', title: 'Missing username' }) username: username,
return password: password
} })
.then(resp => {
if (password == null || password.length == 0) { let data = resp.data;
this.messages.push({ type: 'error', title: 'Missing password' }) if (data.success){
return localStorage.setItem('token', data.token);
} localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin);
this.signin(username, password)
}, eventHub.$emit('setUserStatus');
signin(username, password) { this.$router.push({ name: 'profile' })
login(username, password, true) }
.then(data => { })
if (data.success){ .catch(error => {
const jwtData = parseJwt(data.token) if (error.message.endsWith('401')) {
localStorage.setItem('token', data.token); this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
localStorage.setItem('username', jwtData['username']); }
localStorage.setItem('admin', jwtData['admin'] || false); else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
eventHub.$emit('setUserStatus'); }
this.$router.push({ name: 'profile' }) });
}
})
.catch(error => {
if (error.status === 401) {
this.messages.push({ type: 'error', title: 'Access denied', message: 'Incorrect username or password' })
}
else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
}
});
} }
}, },
created(){ created(){

View File

@@ -20,11 +20,9 @@
<div v-if="listLoaded"> <div v-if="listLoaded">
<div v-if="torrents.length > 0"> <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>
<toggle-button :options="release_types" :selected.sync="selectedRelaseType" class="toggle"></toggle-button>
<table> <table>
@@ -99,10 +97,9 @@ import { searchTorrents, addMagnet } from '@/api'
import SeasonedButton from '@/components/ui/SeasonedButton' import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedInput from '@/components/ui/SeasonedInput' import SeasonedInput from '@/components/ui/SeasonedInput'
import ToggleButton from '@/components/ui/ToggleButton'
export default { export default {
components: { SeasonedButton, SeasonedInput, ToggleButton }, components: { SeasonedButton, SeasonedInput },
props: { props: {
query: { query: {
type: String, type: String,
@@ -113,7 +110,7 @@ export default {
require: true require: true
}, },
tmdb_type: String, tmdb_type: String,
admin: Boolean, admin: String,
show: Boolean show: Boolean
}, },
data() { data() {
@@ -136,11 +133,6 @@ export default {
} }
store.dispatch('torrentModule/reset') store.dispatch('torrentModule/reset')
}, },
watch: {
selectedRelaseType: function(newValue) {
this.applyFilter(newValue)
}
},
methods: { methods: {
selectedSortableClass(headerName) { selectedSortableClass(headerName) {
return headerName === this.prevCol ? 'active' : '' return headerName === this.prevCol ? 'active' : ''
@@ -155,31 +147,27 @@ export default {
expand(event, name) { expand(event, name) {
const existingExpandedElement = document.getElementsByClassName('expanded')[0] const existingExpandedElement = document.getElementsByClassName('expanded')[0]
const clickedElement = event.target.parentNode;
const scopedStyleDataVariable = Object.keys(clickedElement.dataset)[0]
if (existingExpandedElement) { if (existingExpandedElement) {
console.log('exists')
const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded' const expandedSibling = event.target.parentNode.nextSibling.className === 'expanded'
existingExpandedElement.remove() existingExpandedElement.remove()
const table = document.getElementsByTagName('table')[0]
table.style.display = 'block'
if (expandedSibling) { if (expandedSibling) {
console.log('sibling is here')
return return
} }
} }
console.log('expand event', event)
const nameRow = document.createElement('tr') const nameRow = document.createElement('tr')
const nameCol = document.createElement('td') const nameCol = document.createElement('td')
nameRow.className = 'expanded' nameRow.className = 'expanded'
nameRow.dataset[scopedStyleDataVariable] = "";
nameCol.innerText = name nameCol.innerText = name
nameCol.dataset[scopedStyleDataVariable] = "";
nameRow.appendChild(nameCol) nameRow.appendChild(nameCol)
clickedElement.insertAdjacentElement('afterend', nameRow) event.target.parentNode.insertAdjacentElement('afterend', nameRow)
}, },
sendTorrent(magnet, name, event){ sendTorrent(magnet, name, event){
this.$notifications.info({ this.$notifications.info({
@@ -189,6 +177,7 @@ export default {
}) })
event.target.parentNode.classList.add('active') event.target.parentNode.classList.add('active')
addMagnet(magnet, name, this.tmdb_id) addMagnet(magnet, name, this.tmdb_id)
.catch((resp) => { console.log('error:', resp.data) }) .catch((resp) => { console.log('error:', resp.data) })
.then((resp) => { .then((resp) => {
@@ -204,6 +193,7 @@ export default {
if (this.prevCol === col && sameDirection === false) { if (this.prevCol === col && sameDirection === false) {
this.direction = !this.direction this.direction = !this.direction
} }
console.log('col and more', col, sameDirection)
switch (col) { switch (col) {
case 'name': case 'name':
@@ -289,13 +279,14 @@ export default {
@import "./src/scss/variables"; @import "./src/scss/variables";
.expanded { .expanded {
display: flex; display: flex;
padding: 0.25rem 1rem; margin: 0 1rem;
max-width: 100%; max-width: 100%;
border-left: 1px solid $text-color; border-left: 1px solid $text-color;
border-right: 1px solid $text-color; border-right: 1px solid $text-color;
border-bottom: 1px solid $text-color; border-bottom: 1px solid $text-color;
td { td {
// border-left: 1px solid $c-dark;
word-break: break-all; word-break: break-all;
padding: 0.5rem 0.15rem; padding: 0.5rem 0.15rem;
width: 100%; width: 100%;
@@ -307,14 +298,8 @@ export default {
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
@import "./src/scss/elements"; @import "./src/scss/elements";
.toggle {
max-width: unset !important;
margin: 1rem 0;
}
.container { .container {
background-color: $background-color; background-color: $background-color;
padding: 0 1rem;
} }
.torrentHeader { .torrentHeader {
@@ -363,6 +348,7 @@ table {
.table__content, .table__header { .table__content, .table__header {
display: flex; display: flex;
padding: 0; padding: 0;
margin: 0 1rem;
border-left: 1px solid $text-color; border-left: 1px solid $text-color;
border-right: 1px solid $text-color; border-right: 1px solid $text-color;
border-bottom: 1px solid $text-color; border-bottom: 1px solid $text-color;

View File

@@ -0,0 +1,133 @@
<template>
<div>
<label v-for="option in options" class="radio" @click="selected = option.value">
<input type="radio" v-model="selected" :value="option.value" />
<label>{{ option.text }}</label>
<div class="sub-radios" v-if="option.subElements && selected === option.value">
<label class="radio" v-for="elem in option.subElements">
<input type="radio" v-model="selectedSubItem" :value="option.value + '-' + elem.value" />
<label>{{ elem.text }}</label>
</label>
</div>
</label>
</div>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true
},
value: {
required: false,
default: undefined
}
},
data() {
return {
selected: this.value || this.options[0].value,
selectedSubItem: null
};
},
beforeMount() {
this.handleChange()
},
watch: {
selected() {
this.handleChange();
}
},
methods: {
handleChange() {
if (this.value !== undefined) {
this.$emit("update:value", this.selected);
} else {
this.$emit("changed", this.selected);
}
}
}
};
</script>
<style lang="scss" scoped>
@import "./src/scss/variables.scss";
$radioSize: 16px;
$ui-border-width: 2px;
.sub-radios {
display: flex;
flex-direction: column;
flex: 0 0 100%;
margin-left: 1rem;
&:first-of-type {
margin-top: 1rem;
}
}
.radio {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 14px;
width: max-content;
input[type="radio"] {
display: block;
opacity: 0;
+ label {
position: relative;
display: inline-block;
cursor: pointer;
padding-left: 1.25rem;
font-weight: 300;
&::before {
content: "";
display: inline-block;
position: absolute;
left: -($radioSize / 4) * 4;
border-radius: 50%;
border: $ui-border-width solid $text-color-70;
width: $radioSize;
height: $radioSize;
}
&::after {
content: "";
position: absolute;
display: inline-block;
left: -($radioSize / 4) * 3;
top: $radioSize / 4;
border-radius: 50%;
width: ($radioSize / 4) * 3;
height: ($radioSize / 4) * 3;
}
}
&:checked,
&:hover {
+ label::after {
background-color: $green;
}
+ label::before {
border-color: $text-color;
}
}
&:focus {
+ label::before {
outline: $ui-border-width solid Highlight;
outline-style: auto;
outline-color: -webkit-focus-ring-color;
}
}
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template> <template>
<button type="button" @click="emit('click')" :class="{ active: active }"> <div class="seasoned-button">
<slot></slot> <button type="button" class="button" @click="emit('click')" :class="{ active: active }"><slot></slot></button>
</button> </div>
</template> </template>
<script> <script>
@@ -9,11 +9,7 @@
export default { export default {
name: 'seasonedButton', name: 'seasonedButton',
props: { props: {
active: { active: Boolean
type: Boolean,
default: false,
required: false
}
}, },
methods: { methods: {
emit() { emit() {
@@ -27,39 +23,32 @@ export default {
@import "./src/scss/variables"; @import "./src/scss/variables";
@import "./src/scss/media-queries"; @import "./src/scss/media-queries";
button { .button{
display: inline-block; display: inline-block;
border: 1px solid $text-color; border: 1px solid $text-color;
font-size: 11px;
font-weight: 300;
line-height: 1.5;
letter-spacing: 0.5px;
text-transform: uppercase; text-transform: uppercase;
min-height: 45px; font-weight: 300;
padding: 5px 10px 4px 10px; font-size: 11px;
line-height: 2;
height: 45px;
letter-spacing: 1.2px;
padding: 5px 20px 4px 20px;
margin: 0; margin: 0;
margin-right: 0.3rem; margin-right: 0.3rem;
cursor: pointer;
color: $text-color; color: $text-color;
background: $background-color-secondary; background: $background-color-secondary;
cursor: pointer;
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, border-color .5s ease;
@include desktop { @include tablet-min{
font-size: 0.8rem; font-size: 12px;
padding: 6px 20px 5px 20px; padding: 6px 20px 5px 20px;
} }
&:focus, &:active, &.active { body:not(.touch) &:hover, &:focus, &:active, &.active {
background: $text-color; background: $text-color;
color: $background-color; color: $background-color;
} }
@media (hover: hover) {
&:hover {
background: $text-color;
color: $background-color;
}
}
} }
</style> </style>

View File

@@ -3,8 +3,8 @@
<div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index"> <div class="message" v-for="(message, index) in reversedMessages" :class="message.type || 'warning'" :key="index">
<span class="pinstripe"></span> <span class="pinstripe"></span>
<div> <div>
<h2 class="title">{{ message.title || defaultTitles[message.type] }}</h2> <h2>{{ message.title || defaultTitles[message.type] }}</h2>
<span v-if="message.message" class="message">{{ message.message }}</span> <span>{{ message.message }}</span>
</div> </div>
<button class="dismiss" @click="clicked(message)">X</button> <button class="dismiss" @click="clicked(message)">X</button>
@@ -41,7 +41,14 @@ export default {
const removedMessage = [...this.messages].filter(mes => mes !== e) const removedMessage = [...this.messages].filter(mes => mes !== e)
this.$emit('update:messages', removedMessage) 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> </script>
@@ -63,6 +70,7 @@ export default {
.message { .message {
width: 100%; width: 100%;
max-width: 35rem; max-width: 35rem;
height: 75px;
display: flex; display: flex;
margin-top: 1rem; margin-top: 1rem;
@@ -70,12 +78,12 @@ export default {
color: $text-color-70; color: $text-color-70;
> div { > div {
margin: 10px 24px; margin: 6px 24px;
width: 100%; width: 100%;
} }
.title { h2 {
font-weight: 300; font-weight: 300;
letter-spacing: 0.25px; letter-spacing: 0.25px;
margin: 0; margin: 0;
@@ -83,11 +91,10 @@ export default {
color: $text-color; color: $text-color;
transition: color .5s ease; transition: color .5s ease;
} }
.message { span {
font-weight: 300; font-weight: 300;
color: $text-color-70; color: $text-color-70;
transition: color .5s ease; transition: color .5s ease;
margin: 0.2rem 0 0.5rem;
} }
@include mobile-only { @include mobile-only {
@@ -105,8 +112,9 @@ export default {
} }
.pinstripe { .pinstripe {
height: 100%;
width: 0.5rem; width: 0.5rem;
background-color: $color-error-highlight; // background-color: $color-error-highlight;
} }
.dismiss { .dismiss {

View File

@@ -0,0 +1,79 @@
<template>
<div class="wrapper">
<h3 v-if="title" class="title">{{ title }}</h3>
<textarea :placeholder="placeholder" @input="handleInput" v-model="value" :rows="rows" />
</div>
</template>
<script>
export default {
props: {
placeholder: {
type: String,
required: false
},
title: {
type: String,
required: false
},
rows: {
type: Number,
required: false,
default: 10
},
value: {
type: String,
required: false,
default: undefined
}
},
methods: {
handleInput(event) {
if (this.value !== undefined) {
this.$emit('update:value', this.value)
} else {
this.$emit('input', this.value, event)
}
}
}
}
</script>
<style lang="scss" scoped>
@import "./src/scss/variables.scss";
.wrapper {
width: 100%;
}
.title {
margin: 0;
font-weight: 400;
text-transform: uppercase;
font-size: 14px;
color: $green;
margin-bottom: 0.5rem;
@include tablet-min {
font-size: 16px;
}
}
textarea {
width: 100%;
font-size: 14px;
padding: 0.5rem;
border: 2px solid $text-color-50;
&:focus {
border-color: $text-color;
outline: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
}
</style>

View File

@@ -1,100 +0,0 @@
<template>
<div class="toggle-container">
<button v-for="option in options" class="toggle-button" @click="toggle(option)"
:class="toggleValue === option ? 'selected' : null"
>{{ option }}</button>
</div>
</template>
<script>
export default {
props: {
options: {
Array,
required: true
},
selected: {
type: String,
required: false,
default: undefined
}
},
data() {
return {
toggleValue: this.selected || this.options[0]
}
},
beforeMount() {
this.toggle(this.toggleValue)
},
methods: {
toggle(toggleValue) {
this.toggleValue = toggleValue;
if (this.selected !== undefined) {
this.$emit('update:selected', toggleValue)
} else {
this.$emit('change', toggleValue)
}
}
},
}
</script>
<style lang="scss" scoped>
@import "./src/scss/variables";
$background: $background-ui;
$background-selected: $background-color-secondary;
.toggle-container {
width: 100%;
max-width: 15rem;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
// padding: 0.2rem;
background-color: $background;
border: 2px solid $background;
border-radius: 8px;
border-left: 4px solid $background;
border-right: 4px solid $background;
.toggle-button {
font-size: 1rem;
line-height: 1rem;
font-weight: normal;
width: 100%;
padding: 0.5rem 0;
border: 0;
color: $text-color;
// background-color: $text-color-5;
background-color: $background;
text-transform: capitalize;
&.selected {
color: $text-color;
// background-color: $background-color-secondary;
background-color: $background-selected;
border-radius: 8px;
}
// &:first-of-type, &:last-of-type {
// border-left: 4px solid $background;
// border-right: 4px solid $background;
// }
// &:first-of-type {
// border-top-left-radius: 4px;
// border-bottom-left-radius: 4px;
// }
// &:last-of-type {
// border-top-right-radius: 4px;
// border-bottom-right-radius: 4px;
// }
}
}
</style>

View File

@@ -1,38 +1,36 @@
<template> <template>
<div class="darkToggle"> <div class="darkToggle">
<span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span> <span @click="toggleDarkmode()">{{ darkmodeToggleIcon }}</span>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
darkmode: this.supported darkmode: window.getComputedStyle(document.body).colorScheme.includes('dark')
}; }
}, },
methods: { methods: {
toggleDarkmode() { toggleDarkmode() {
this.darkmode = !this.darkmode; this.darkmode = !this.darkmode;
document.body.className = this.darkmode ? "dark" : "light"; document.body.className = this.darkmode ? 'dark' : 'light'
},
supported() {
const computedStyle = window.getComputedStyle(document.body);
if (computedStyle["colorScheme"] != null)
return computedStyle.colorScheme.includes("dark");
return false;
} }
}, },
computed: { computed: {
darkmodeToggleIcon() { darkmodeToggleIcon() {
return this.darkmode ? "🌝" : "🌚"; return this.darkmode ? '🌝' : '🌚'
} }
} }
}; }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "./src/scss/media-queries";
.darkToggle { .darkToggle {
height: 25px; height: 25px;
width: 25px; width: 25px;
@@ -43,15 +41,11 @@ export default {
margin-right: 2px; margin-right: 2px;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 10; z-index: 1;
@include mobile-only {
margin-bottom: 5rem;
}
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
user-select: none; user-select: none;
} }
</style> </style>

View File

@@ -1,18 +1,18 @@
<template> <template>
<div> <div>
<a @click="$emit('click')"> <a @click="$emit('click')"><li>
<li> <figure :class="activeClassIfActive" v-if="iconRefNameIfActive">
<figure v-if="iconRef" :class="activeClassIfActive"> <svg class="icon">
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg> <use :xlink:href="iconRefNameIfActive"/>
</figure> </svg>
</figure>
<span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span> <span :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
<span v-if="supplementaryText" class="supplementary-text"> <span v-if="supplementaryText" class="supplementary-text">
{{ supplementaryText }} {{ supplementaryText }}
</span> </span>
</li> </li></a>
</a>
</div> </div>
</template> </template>
@@ -85,53 +85,39 @@ li {
border-bottom: 1px solid $text-color-5; border-bottom: 1px solid $text-color-5;
&:hover { &:hover {
color: $text-color; color: $text-color-70;
cursor: pointer; cursor: pointer;
.icon {
fill: $text-color;
cursor: pointer;
transform: scale(1.1, 1.1);
}
} }
.active { .active {
color: $text-color; color: $text-color;
.icon {
fill: $green;
}
} }
.pending { .pending {
color: #f8bd2d; color: #f8bd2d;
} }
.text {
margin-left: 26px;
}
.supplementary-text { .supplementary-text {
flex-grow: 1; flex-grow: 1;
text-align: right; text-align: right;
} }
figure { figure, figure > .icon {
position: absolute; width: 18px;
height: 18px;
> svg { margin: 0 7px 0 0;
position: relative; fill: $text-color-50;
top: 50%; transition: fill 0.5s ease, transform 0.5s ease;
width: 16px; &.waiting {
height: 16px; transform: scale(0.8, 0.8);
margin: 0 7px 0 0; }
fill: $text-color-50; &.pending {
transition: fill 0.5s ease, transform 0.5s ease; fill: #f8bd2d;
}
& .waiting { &:hover &-icon {
transform: scale(0.8, 0.8); fill: $text-color-70;
} cursor: pointer;
& .pending { }
fill: #f8bd2d; &.active > svg {
} fill: $green;
} }
} }
} }

View File

@@ -11,8 +11,8 @@ const setDocumentTitle = (state) => {
export default { export default {
namespaced: true, namespaced: true,
state: { state: {
emoji: '', emoji: '🍕',
titlePrefix: 'seasoned', titlePrefix: 'request',
title: undefined title: undefined
}, },
getters: { getters: {

View File

@@ -1,112 +0,0 @@
import { getSettings } from '@/api'
function setLocalStorageByKey(key, value) {
if (value instanceof Object || value instanceof Array) {
value = JSON.stringify(value)
}
const buff = Buffer.from(value)
const encodedValue = buff.toString('base64')
localStorage.setItem(key, encodedValue)
}
function getLocalStorageByKey(key) {
const encodedValue = localStorage.getItem(key)
if (encodedValue == null) {
return undefined
}
const buff = new Buffer(encodedValue, 'base64')
const value = buff.toString('utf-8')
try {
return JSON.parse(value)
} catch {
return value
}
}
const ifMissingSettingsAndTokenExistsFetchSettings =
() => getLocalStorageByKey('token') ? getSettings() : null
export default {
namespaced: true,
state: {
admin: false,
settings: undefined,
username: undefined,
plex_userid: undefined
},
getters: {
admin: (state) => {
return state.admin
},
settings: (state, foo, bar) => {
console.log('is this called?')
const settings = state.settings || getLocalStorageByKey('settings')
if (settings instanceof Object) {
return settings
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
username: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
if (settings instanceof Object && settings.hasOwnProperty('user_name')) {
return settings.user_name
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
plex_userid: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
console.log('plex_userid from store', settings)
if (settings instanceof Object && settings.hasOwnProperty('plex_userid')) {
return settings.plex_userid
}
ifMissingSettingsAndTokenExistsFetchSettings()
return undefined
},
isPlexAuthenticated: (state) => {
const settings = state.settings || getLocalStorageByKey('settings')
if (settings == null)
return false
const hasPlexId = settings['plex_userid']
return hasPlexId != null ? true : false
}
},
mutations: {
SET_ADMIN: (state, isAdmin) => {
state.admin = isAdmin
},
SET_USERNAME: (state, username) => {
state.username = username
console.log('username')
setLocalStorageByKey('username', username)
},
SET_SETTINGS: (state, settings) => {
state.settings = settings
console.log('settings')
setLocalStorageByKey('settings', settings)
}
},
actions: {
setAdmin: ({commit}, isAdmin) => {
if (!(isAdmin instanceof Object)) {
throw "Parameter is not a boolean value."
}
commit('SET_ADMIN', isAdmin)
},
setSettings: ({commit}, settings) => {
console.log('settings input', settings)
if (!(settings instanceof Object)) {
throw "Parameter is not a object."
}
commit('SET_SETTINGS', settings)
}
}
}

View File

@@ -11,16 +11,9 @@ let routes = [
path: '/', path: '/',
component: (resolve) => require(['./components/Home.vue'], resolve) component: (resolve) => require(['./components/Home.vue'], resolve)
}, },
{
name: 'activity',
path: '/activity',
meta: { requiresAuth: true },
component: (resolve) => require(['./components/ActivityPage.vue'], resolve)
},
{ {
name: 'profile', name: 'profile',
path: '/profile', path: '/profile',
meta: { requiresAuth: true },
component: (resolve) => require(['./components/Profile.vue'], resolve) component: (resolve) => require(['./components/Profile.vue'], resolve)
}, },
{ {
@@ -48,13 +41,11 @@ let routes = [
{ {
name: 'settings', name: 'settings',
path: '/settings', path: '/settings',
meta: { requiresAuth: true },
component: (resolve) => require(['./components/Settings.vue'], resolve) component: (resolve) => require(['./components/Settings.vue'], resolve)
}, },
{ {
name: 'signin', name: 'signin',
path: '/signin', path: '/signin',
alias: '/login',
component: (resolve) => require(['./components/Signin.vue'], resolve) component: (resolve) => require(['./components/Signin.vue'], resolve)
}, },
// { // {
@@ -69,17 +60,6 @@ let routes = [
path: '/404', path: '/404',
component: (resolve) => require(['./components/404.vue'], resolve) component: (resolve) => require(['./components/404.vue'], resolve)
}, },
{
name: 'logout',
path: '/logout',
component: {
template: '<div></div>',
created() {
localStorage.clear();
this.$router.push({ name: 'home' });
}
}
},
{ {
path: '*', path: '*',
redirect: '/' redirect: '/'
@@ -91,7 +71,7 @@ let routes = [
]; ];
const router = new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'hash',
base: '/', base: '/',
routes, routes,
linkActiveClass: 'is-active' linkActiveClass: 'is-active'
@@ -105,13 +85,6 @@ router.beforeEach((to, from, next) => {
document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active'); document.querySelector('.nav__hamburger').classList.remove('nav__hamburger--active');
document.querySelector('.nav__list').classList.remove('nav__list--active'); document.querySelector('.nav__list').classList.remove('nav__list--active');
} }
if (to.matched.some(record => record.meta.requiresAuth)) {
if (localStorage.getItem('token') == null) {
next({ path: '/signin' });
}
}
next(); next();
}); });

View File

@@ -1,10 +1,11 @@
.noselect { .noselect {
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ -webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */ -khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */ -moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently */ user-select: none; /* Non-prefixed version, currently */
} }
.end-section { .end-section {
@@ -20,30 +21,4 @@
> div:not(:first-child) { > div:not(:first-child) {
margin-left: 1rem; margin-left: 1rem;
} }
} }
.flex {
display: flex;
&-direction-column {
flex-direction: column;
}
&-direction-row {
flex-direction: row;
}
&-align-items-center {
align-items: center;
}
}
.position {
&-relative {
position: relative;
}
&-absolute {
position: absolute;
}
}

View File

@@ -5,31 +5,6 @@ $tablet-p-width: 768px;
$tablet-l-width: 1024px; $tablet-l-width: 1024px;
$desktop-width: 1200px; $desktop-width: 1200px;
$desktop-l-width: 1600px; $desktop-l-width: 1600px;
$mobile-width: 768px;
@mixin desktop {
@media (min-width: #{$mobile-width + 1px}) {
@content;
}
}
@mixin mobile {
@media (max-width: #{$mobile-width}) {
@content;
}
}
.desktop-only {
@include mobile {
display: none;
}
}
.mobile-only {
@include desktop {
display: none;
}
}
// Media // Media
@mixin mobile-only{ @mixin mobile-only{

View File

@@ -11,19 +11,17 @@
--text-color-secondary: orange; --text-color-secondary: orange;
--background-color: #f8f8f8; --background-color: #f8f8f8;
--background-color-secondary: #ffffff; --background-color-secondary: #ffffff;
--background-ui: #edeef0;
--background-95: rgba(255, 255, 255, 0.95); --background-95: rgba(255, 255, 255, 0.95);
--background-70: rgba(255, 255, 255, 0.7); --background-70: rgba(255, 255, 255, 0.7);
--background-40: rgba(255, 255, 255, 0.4); --background-40: rgba(255, 255, 255, 0.4);
--background-nav-logo: #081c24; --background-nav-logo: #081c24;
--color-green: #01d277; --color-green: #01d277;
--color-green-90: rgba(1, 210, 119, 0.9); --color-green-90: rgba(1, 210, 119, .9);
--color-green-70: rgba(1, 210, 119, 0.73);
--color-teal: #091c24; --color-teal: #091c24;
--color-black: #081c24; --color-black: #081c24;
--white: #fff; --white: #fff;
--white-70: rgba(255, 255, 255, 0.7); --white-70: rgba(255,255,255,0.7);
--color-warning: rgba(241, 188, 53, 0.7); --color-warning: rgba(241, 188, 53, 0.7);
--color-warning-highlight: #f1bc35; --color-warning-highlight: #f1bc35;
@@ -31,7 +29,7 @@
--color-success-text: #fff; --color-success-text: #fff;
--color-success-highlight: rgb(0, 100, 66); --color-success-highlight: rgb(0, 100, 66);
--color-error: rgba(220, 48, 35, 0.8); --color-error: rgba(220, 48, 35, 0.8);
--color-error-highlight: #dc3023; --color-error-highlight: #DC3023;
--header-size: 75px; --header-size: 75px;
} }
@@ -44,18 +42,17 @@
--text-color-50: rgba(255, 255, 255, 0.5); --text-color-50: rgba(255, 255, 255, 0.5);
--text-color-5: rgba(255, 255, 255, 0.05); --text-color-5: rgba(255, 255, 255, 0.05);
--text-color-secondary: orange; --text-color-secondary: orange;
--background-color: rgba(17, 17, 17, 1); --background-color: #1e1f22;
--background-color-secondary: rgba(6, 7, 8, 1); --background-color-secondary: #111111;
--background-ui: #202125; --background-95: rgba(30, 31, 34, 0.95);
--background-95: rgba(17, 17, 17, 0.95); --background-70: rgba(30, 31, 34, 0.8);
--background-70: rgba(17, 17, 17, 0.8); --background-40: rgba(30, 31, 34, 0.4);
--background-40: rgba(17, 17, 17, 0.4);
} }
} }
@include mobile-only { @include mobile-only {
:root { :root {
--header-size: calc(50px + 1.5rem); --header-size: 50px;
} }
} }
@@ -64,12 +61,11 @@ $header-size: var(--header-size);
$dark: rgb(30, 31, 34); $dark: rgb(30, 31, 34);
$green: var(--color-green); $green: var(--color-green);
$green-90: var(--color-green-90); $green-90: var(--color-green-90);
$green-70: var(--color-green-70);
$teal: #091c24; $teal: #091c24;
$black: #081c24; $black: #081c24;
$black-80: rgba(0, 0, 0, 0.8); $black-80: rgba(0,0,0,0.8);
$white: #fff; $white: #fff;
$white-80: rgba(255, 255, 255, 0.8); $white-80: rgba(255,255,255,0.8);
$text-color: var(--text-color) !default; $text-color: var(--text-color) !default;
$text-color-70: var(--text-color-70) !default; $text-color-70: var(--text-color-70) !default;
@@ -78,7 +74,6 @@ $text-color-5: var(--text-color-5) !default;
$text-color-secondary: var(--text-color-secondary) !default; $text-color-secondary: var(--text-color-secondary) !default;
$background-color: var(--background-color) !default; $background-color: var(--background-color) !default;
$background-color-secondary: var(--background-color-secondary) !default; $background-color-secondary: var(--background-color-secondary) !default;
$background-ui: var(--background-ui) !default;
$background-95: var(--background-95) !default; $background-95: var(--background-95) !default;
$background-70: var(--background-70) !default; $background-70: var(--background-70) !default;
$background-40: var(--background-40) !default; $background-40: var(--background-40) !default;
@@ -104,12 +99,11 @@ $color-error-highlight: var(--color-error-highlight) !default;
--text-color-50: rgba(255, 255, 255, 0.5); --text-color-50: rgba(255, 255, 255, 0.5);
--text-color-5: rgba(255, 255, 255, 0.05); --text-color-5: rgba(255, 255, 255, 0.05);
--text-color-secondary: orange; --text-color-secondary: orange;
--background-color: rgba(17, 17, 17, 1); --background-color: #1e1f22;
--background-color-secondary: rgba(6, 7, 8, 1); --background-color-secondary: #111111;
--background-ui: #202125; --background-95: rgba(30, 31, 34, 0.95);
--background-95: rgba(17, 17, 17, 0.95); --background-70: rgba(30, 31, 34, 0.7);
--background-70: rgba(17, 17, 17, 0.8); --color-teal: #091c24;
--background-40: rgba(17, 17, 17, 0.4);
} }
.light { .light {
@@ -117,11 +111,13 @@ $color-error-highlight: var(--color-error-highlight) !default;
--text-color-70: rgba(8, 28, 36, 0.7); --text-color-70: rgba(8, 28, 36, 0.7);
--text-color-50: rgba(8, 28, 36, 0.5); --text-color-50: rgba(8, 28, 36, 0.5);
--text-color-5: rgba(8, 28, 36, 0.05); --text-color-5: rgba(8, 28, 36, 0.05);
--text-color-inverted: #fff;
--text-color-secondary: orange; --text-color-secondary: orange;
--background-color: #f8f8f8; --background-color: #f8f8f8;
--background-color-secondary: #ffffff; --background-color-secondary: #ffffff;
--background-ui: #edeef0;
--background-95: rgba(255, 255, 255, 0.95); --background-95: rgba(255, 255, 255, 0.95);
--background-70: rgba(255, 255, 255, 0.7); --background-70: rgba(255, 255, 255, 0.7);
--background-40: rgba(255, 255, 255, 0.4); --background-nav-logo: #081c24;
--color-green: #01d277;
--color-teal: #091c24;
} }

View File

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

View File

@@ -7,17 +7,7 @@ const sortableSize = (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))
}; }
const parseJwt = (token) => {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
};
export { sortableSize, parseJwt } export { sortableSize }

738
yarn.lock

File diff suppressed because it is too large Load Diff