Compare commits
52 Commits
feat/submi
...
drone-test
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c8463f06c | |||
| a34313ea6f | |||
| 0ac90f1b17 | |||
| d0c32c267a | |||
| 9586d71ca0 | |||
| 64aa607924 | |||
| 32669e5bef | |||
| 6edad3991f | |||
| 50acf0bedc | |||
| d4369ec7a4 | |||
| c16543099e | |||
| f2a65d755c | |||
| 532993e9dd | |||
| d19d72ce0c | |||
| d1820a08cf | |||
| bc73665b12 | |||
| 9edb19569a | |||
| 7802a89d15 | |||
| 915260f41b | |||
| 0d57e9a03b | |||
| 582207d453 | |||
| b1b08bfa04 | |||
| 14e883672d | |||
| 7a405140db | |||
| 35497f5bd2 | |||
| 91b19785d6 | |||
| a301d21cc2 | |||
| a2a4b9a553 | |||
| 45f45559fd | |||
| 458256132a | |||
| 0f2c166e1c | |||
| 1c7a688cb8 | |||
| 6269f178e9 | |||
| 3e7527ee19 | |||
| 2236316863 | |||
| cc2fded193 | |||
| f32e0a8ab0 | |||
| ec6e6d2ba0 | |||
| ca85635b03 | |||
| 32257dc64e | |||
| 6bba319735 | |||
| dcce972fdc | |||
| 32e25fb983 | |||
| e7882869e6 | |||
| d0a251f69a | |||
| 9bc7f29162 | |||
| 3ff963f007 | |||
| bcfce66ec0 | |||
| 33e3ee3489 | |||
| e3502a7690 | |||
| 8d09ba4d07 | |||
| ba670d06aa |
44
.drone.yml
Normal file
44
.drone.yml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: default
|
||||||
|
|
||||||
|
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
|
||||||
|
- drone-test
|
||||||
|
event:
|
||||||
|
include:
|
||||||
|
- pull_request
|
||||||
|
- push
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
209
src/api.js
209
src/api.js
@@ -2,6 +2,7 @@ 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
|
||||||
@@ -10,6 +11,13 @@ 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 - - -
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,12 +26,18 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
|
|||||||
* @param {boolean} [credits=false] Include credits
|
* @param {boolean} [credits=false] Include credits
|
||||||
* @returns {object} Tmdb response
|
* @returns {object} Tmdb response
|
||||||
*/
|
*/
|
||||||
const getMovie = (id, credits=false) => {
|
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => {
|
||||||
const url = new URL('v2/movie', SEASONED_URL)
|
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())
|
||||||
@@ -36,9 +50,12 @@ const getMovie = (id, credits=false) => {
|
|||||||
* @param {boolean} [credits=false] Include credits
|
* @param {boolean} [credits=false] Include credits
|
||||||
* @returns {object} Tmdb response
|
* @returns {object} Tmdb response
|
||||||
*/
|
*/
|
||||||
const getShow = (id, credits=false) => {
|
const getShow = (id, checkExistance=false, 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)
|
||||||
}
|
}
|
||||||
@@ -48,6 +65,24 @@ const getShow = (id, 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
|
||||||
@@ -96,12 +131,19 @@ 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) => {
|
const searchTmdb = (query, page=1, adult=false, mediaType=null) => {
|
||||||
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)
|
||||||
|
|
||||||
return fetch(url.href)
|
const headers = { authorization: localStorage.getItem('token') }
|
||||||
|
|
||||||
|
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 })
|
||||||
}
|
}
|
||||||
@@ -210,32 +252,138 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
|
|||||||
.catch(err => Promise.reject(err))
|
.catch(err => Promise.reject(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// - - - Authenticate with plex - - -
|
// - - - Seasoned user endpoints - - -
|
||||||
|
|
||||||
const plexAuthenticate = (username, password) => {
|
const register = (username, password) => {
|
||||||
const url = new URL('https://plex.tv/api/v2/users/signin')
|
const url = new URL('v1/user', SEASONED_URL)
|
||||||
|
const options = {
|
||||||
const headers = {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json',
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'X-Plex-Platform': 'Linux',
|
body: JSON.stringify({ username, password })
|
||||||
'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'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let formData = new FormData()
|
return fetch(url.href, options)
|
||||||
formData.set('login', username)
|
.then(resp => resp.json())
|
||||||
formData.set('password', password)
|
.catch(error => {
|
||||||
formData.set('rememberMe', false)
|
console.error('Unexpected error occured before receiving response. Error:', error)
|
||||||
|
// TODO log to sentry the issue here
|
||||||
return axios({
|
throw error
|
||||||
method: 'POST',
|
|
||||||
url: url.href,
|
|
||||||
headers: headers,
|
|
||||||
data: formData
|
|
||||||
})
|
})
|
||||||
.catch(error => { console.error(`api error authentication plex: ${username}`); throw error })
|
}
|
||||||
|
|
||||||
|
const login = (username, password) => {
|
||||||
|
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 => resp.json())
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Unexpected error occured before receiving response. Error:', error)
|
||||||
|
// TODO log to sentry the issue here
|
||||||
|
throw error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSettings = () => {
|
||||||
|
const settingsExists = (value) => {
|
||||||
|
if (value instanceof Object && value.hasOwnProperty('settings'))
|
||||||
|
return value;
|
||||||
|
throw "Settings does not exist in response object.";
|
||||||
|
}
|
||||||
|
const commitSettingsToStore = (response) => {
|
||||||
|
store.dispatch('userModule/setSettings', response.settings)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||||
|
|
||||||
|
const authorization_token = localStorage.getItem('token')
|
||||||
|
const headers = authorization_token ? {
|
||||||
|
'Authorization': authorization_token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
return fetch(url.href, { headers })
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.then(settingsExists)
|
||||||
|
.then(commitSettingsToStore)
|
||||||
|
.then(response => response.settings)
|
||||||
|
.catch(error => { console.log('api error getting user settings'); throw error })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSettings = (settings) => {
|
||||||
|
const url = new URL('v1/user/settings', SEASONED_URL)
|
||||||
|
|
||||||
|
const authorization_token = localStorage.getItem('token')
|
||||||
|
const headers = authorization_token ? {
|
||||||
|
'Authorization': authorization_token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
return fetch(url.href, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(settings)
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(error => { console.log('api error updating user settings'); throw error })
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - Authenticate with plex - - -
|
||||||
|
|
||||||
|
const linkPlexAccount = (username, password) => {
|
||||||
|
const url = new URL('v1/user/link_plex', SEASONED_URL)
|
||||||
|
const body = { username, password }
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: storage.token
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url.href, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error })
|
||||||
|
}
|
||||||
|
|
||||||
|
const unlinkPlexAccount = (username, password) => {
|
||||||
|
const url = new URL('v1/user/unlink_plex', SEASONED_URL)
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
authorization: storage.token
|
||||||
|
}
|
||||||
|
|
||||||
|
return fetch(url.href, {
|
||||||
|
method: 'POST',
|
||||||
|
headers
|
||||||
|
})
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(error => { console.error(`api error unlinking plex account: ${username}`); throw error })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// - - - User graphs - - -
|
||||||
|
|
||||||
|
const fetchChart = (urlPath, days, chartType) => {
|
||||||
|
const url = new URL('v1/user' + urlPath, SEASONED_URL)
|
||||||
|
url.searchParams.append('days', days)
|
||||||
|
url.searchParams.append('y_axis', chartType)
|
||||||
|
|
||||||
|
const authorization_token = localStorage.getItem('token')
|
||||||
|
const headers = authorization_token ? {
|
||||||
|
'Authorization': authorization_token,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
} : {}
|
||||||
|
|
||||||
|
return fetch(url.href, { headers })
|
||||||
|
.then(resp => resp.json())
|
||||||
|
.catch(error => { console.log('api error fetching chart'); throw error })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -302,6 +450,7 @@ const elasticSearchMoviesAndShows = (query) => {
|
|||||||
export {
|
export {
|
||||||
getMovie,
|
getMovie,
|
||||||
getShow,
|
getShow,
|
||||||
|
getPerson,
|
||||||
getTmdbMovieListByName,
|
getTmdbMovieListByName,
|
||||||
searchTmdb,
|
searchTmdb,
|
||||||
getUserRequests,
|
getUserRequests,
|
||||||
@@ -310,7 +459,13 @@ export {
|
|||||||
addMagnet,
|
addMagnet,
|
||||||
request,
|
request,
|
||||||
getRequestStatus,
|
getRequestStatus,
|
||||||
plexAuthenticate,
|
linkPlexAccount,
|
||||||
|
unlinkPlexAccount,
|
||||||
|
register,
|
||||||
|
login,
|
||||||
|
getSettings,
|
||||||
|
updateSettings,
|
||||||
|
fetchChart,
|
||||||
getEmoji,
|
getEmoji,
|
||||||
elasticSearchMoviesAndShows
|
elasticSearchMoviesAndShows
|
||||||
}
|
}
|
||||||
|
|||||||
316
src/components/ActivityPage.vue
Normal file
316
src/components/ActivityPage.vue
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper" v-if="hasPlexUser">
|
||||||
|
<h1>Your watch activity</h1>
|
||||||
|
|
||||||
|
<div class="filter">
|
||||||
|
<h2>Filter</h2>
|
||||||
|
|
||||||
|
<div class="filter-item">
|
||||||
|
<label class="desktop-only">Days:</label>
|
||||||
|
<input class="dayinput"
|
||||||
|
v-model="days"
|
||||||
|
placeholder="number of days"
|
||||||
|
type="number"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
:style="{maxWidth: `${3 + (0.5 * days.length)}rem`}"/>
|
||||||
|
<!-- <datalist id="days">
|
||||||
|
<option v-for="index in 1500" :value="index" :key="index"></option>
|
||||||
|
</datalist> -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<toggle-button class="filter-item" :options="chartTypes" :selected.sync="selectedChartDataType" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chart-section">
|
||||||
|
<h3 class="chart-header">Activity per day:</h3>
|
||||||
|
<div class="chart">
|
||||||
|
<canvas ref="activityCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="chart-header">Activity per day of week:</h3>
|
||||||
|
<div class="chart">
|
||||||
|
<canvas ref="playsByDayOfWeekCanvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h1>Must be authenticated</h1>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from '@/store'
|
||||||
|
import ToggleButton from '@/components/ui/ToggleButton';
|
||||||
|
import { fetchChart } from '@/api'
|
||||||
|
|
||||||
|
var Chart = require('chart.js');
|
||||||
|
Chart.defaults.global.elements.point.radius = 0
|
||||||
|
Chart.defaults.global.elements.point.hitRadius = 10
|
||||||
|
Chart.defaults.global.elements.point.pointHoverRadius = 10
|
||||||
|
Chart.defaults.global.elements.point.hoverBorderWidth = 4
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { ToggleButton },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
days: 30,
|
||||||
|
selectedChartDataType: 'plays',
|
||||||
|
charts: [{
|
||||||
|
name: 'Watch activity',
|
||||||
|
ref: 'activityCanvas',
|
||||||
|
data: null,
|
||||||
|
urlPath: '/plays_by_day',
|
||||||
|
graphType: 'line'
|
||||||
|
}, {
|
||||||
|
name: 'Plays by day of week',
|
||||||
|
ref: 'playsByDayOfWeekCanvas',
|
||||||
|
data: null,
|
||||||
|
urlPath: '/plays_by_dayofweek',
|
||||||
|
graphType: 'bar'
|
||||||
|
}],
|
||||||
|
chartData: [{
|
||||||
|
type: 'plays',
|
||||||
|
tooltipLabel: 'Play count',
|
||||||
|
},{
|
||||||
|
type: 'duration',
|
||||||
|
tooltipLabel: 'Watched duration',
|
||||||
|
valueConvertFunction: this.convertSecondsToHumanReadable
|
||||||
|
}],
|
||||||
|
gridColor: getComputedStyle(document.documentElement).getPropertyValue('--text-color-5')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasPlexUser() {
|
||||||
|
return store.getters['userModule/plex_userid'] != null ? true : false
|
||||||
|
},
|
||||||
|
chartTypes() {
|
||||||
|
return this.chartData.map(chart => chart.type)
|
||||||
|
},
|
||||||
|
selectedChartType() {
|
||||||
|
return this.chartData.filter(data => data.type == this.selectedChartDataType)[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
hasPlexUser(newValue, oldValue) {
|
||||||
|
if (newValue != oldValue && newValue == true) {
|
||||||
|
this.fetchChartData(this.charts)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
days(newValue) {
|
||||||
|
if (newValue !== '') {
|
||||||
|
this.fetchChartData(this.charts)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectedChartDataType(selectedChartDataType) {
|
||||||
|
this.fetchChartData(this.charts)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
if (typeof(this.days) == 'number') {
|
||||||
|
this.days = this.days.toString()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchChartData(charts) {
|
||||||
|
if (this.hasPlexUser == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let chart of charts) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
fetchChart(chart.urlPath, this.days, this.selectedChartType.type)
|
||||||
|
.then(data => {
|
||||||
|
this.series = data.data.series.filter(group => group.name === 'TV')[0].data; // plays pr date in groups (movie/tv/music)
|
||||||
|
this.categories = data.data.categories; // dates
|
||||||
|
|
||||||
|
const x_labels = data.data.categories.map(date => {
|
||||||
|
if (date.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)) {
|
||||||
|
const [year, month, day] = date.split('-')
|
||||||
|
return `${day}.${month}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return date
|
||||||
|
})
|
||||||
|
let y_activityMovies = data.data.series.filter(group => group.name === 'Movies')[0].data
|
||||||
|
let y_activityTV = data.data.series.filter(group => group.name === 'TV')[0].data
|
||||||
|
|
||||||
|
const datasets = [{
|
||||||
|
label: `Movies watch last ${ this.days } days`,
|
||||||
|
data: y_activityMovies,
|
||||||
|
backgroundColor: 'rgba(54, 162, 235, 0.2)',
|
||||||
|
borderColor: 'rgba(54, 162, 235, 1)',
|
||||||
|
borderWidth: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `Shows watch last ${ this.days } days`,
|
||||||
|
data: y_activityTV,
|
||||||
|
backgroundColor: 'rgba(255, 159, 64, 0.2)',
|
||||||
|
borderColor: 'rgba(255, 159, 64, 1)',
|
||||||
|
borderWidth: 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (chart.data == null) {
|
||||||
|
this.generateChart(chart, x_labels, datasets)
|
||||||
|
} else {
|
||||||
|
chart.data.clear();
|
||||||
|
chart.data.data.labels = x_labels;
|
||||||
|
chart.data.data.datasets = datasets;
|
||||||
|
chart.data.update();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
generateChart(chart, labels, datasets) {
|
||||||
|
const chartInstance = new Chart(this.$refs[chart.ref], {
|
||||||
|
type: chart.graphType,
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
// hitRadius: 8,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
tooltips: {
|
||||||
|
callbacks: {
|
||||||
|
title: (tooltipItem, data) => `Watch date: ${tooltipItem[0].label}`,
|
||||||
|
label: (tooltipItem, data) => {
|
||||||
|
let label = data.datasets[tooltipItem.datasetIndex].label
|
||||||
|
let value = tooltipItem.value;
|
||||||
|
let text = 'Duration watched'
|
||||||
|
|
||||||
|
const context = label.split(' ')[0]
|
||||||
|
if (context) {
|
||||||
|
text = `${context} ${this.selectedChartType.tooltipLabel.toLowerCase()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedChartType.valueConvertFunction) {
|
||||||
|
value = this.selectedChartType.valueConvertFunction(tooltipItem.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ` ${text}: ${value}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
yAxes: [{
|
||||||
|
gridLines: {
|
||||||
|
color: this.gridColor
|
||||||
|
},
|
||||||
|
stacked: chart.graphType === 'bar',
|
||||||
|
ticks: {
|
||||||
|
// suggestedMax: 10000,
|
||||||
|
callback: (value, index, values) => {
|
||||||
|
if (this.selectedChartType.valueConvertFunction) {
|
||||||
|
return this.selectedChartType.valueConvertFunction(value, values)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
beginAtZero: true
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
xAxes: [{
|
||||||
|
stacked: chart.graphType === 'bar',
|
||||||
|
gridLines: {
|
||||||
|
display: false,
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chart.data = chartInstance;
|
||||||
|
},
|
||||||
|
convertSecondsToHumanReadable(value, values=null) {
|
||||||
|
const highestValue = values ? values[0] : value;
|
||||||
|
|
||||||
|
// minutes
|
||||||
|
if (highestValue < 3600) {
|
||||||
|
const minutes = Math.floor(value / 60);
|
||||||
|
|
||||||
|
value = `${minutes} m`
|
||||||
|
}
|
||||||
|
// hours and minutes
|
||||||
|
else if (highestValue > 3600 && highestValue < 86400) {
|
||||||
|
const hours = Math.floor(value / 3600);
|
||||||
|
const minutes = Math.floor(value % 3600 / 60);
|
||||||
|
|
||||||
|
value = hours != 0 ? `${hours} h ${minutes} m` : `${minutes} m`
|
||||||
|
}
|
||||||
|
// days and hours
|
||||||
|
else if (highestValue > 86400 && highestValue < 31557600) {
|
||||||
|
const days = Math.floor(value / 86400);
|
||||||
|
const hours = Math.floor(value % 86400 / 3600);
|
||||||
|
|
||||||
|
value = days != 0 ? `${days} d ${hours} h` : `${hours} h`
|
||||||
|
}
|
||||||
|
// years and days
|
||||||
|
else if (highestValue > 31557600) {
|
||||||
|
const years = Math.floor(value / 31557600);
|
||||||
|
const days = Math.floor(value % 31557600 / 86400);
|
||||||
|
|
||||||
|
value = years != 0 ? `${years} y ${days} d` : `${days} d`
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./src/scss/variables";
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
padding: 0 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&-item:not(:first-of-type) {
|
||||||
|
margin-left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dayinput {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
max-width: 3rem;
|
||||||
|
background-color: $background-ui;
|
||||||
|
color: $text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-section {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
position: relative;
|
||||||
|
height: 35vh;
|
||||||
|
width: 90vw;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-header {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -2,8 +2,11 @@
|
|||||||
<header :class="{ 'sticky': sticky }">
|
<header :class="{ 'sticky': sticky }">
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
<span v-if="info" class="result-count">{{ info }}</span>
|
<div v-if="info instanceof Array" class="flex flex-direction-column">
|
||||||
<router-link v-else-if="link" :to="link" class='view-more'>
|
<span v-for="item in info" class="info">{{ item }}</span>
|
||||||
|
</div>
|
||||||
|
<span v-else class="info">{{ info }}</span>
|
||||||
|
<router-link v-if="link" :to="link" class='view-more' :aria-label="`View all ${title}`">
|
||||||
View All
|
View All
|
||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
@@ -19,10 +22,10 @@ export default {
|
|||||||
sticky: {
|
sticky: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: true
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
type: String,
|
type: [String, Array],
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
@@ -37,12 +40,16 @@ export default {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import './src/scss/variables';
|
@import './src/scss/variables';
|
||||||
@import './src/scss/media-queries';
|
@import './src/scss/media-queries';
|
||||||
|
@import './src/scss/main';
|
||||||
|
|
||||||
header {
|
header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 80px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1.8rem 12px;
|
align-items: center;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
|
||||||
&.sticky {
|
&.sticky {
|
||||||
background-color: $background-color;
|
background-color: $background-color;
|
||||||
@@ -51,22 +58,19 @@ header {
|
|||||||
position: -webkit-sticky;
|
position: -webkit-sticky;
|
||||||
top: $header-size;
|
top: $header-size;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 18px;
|
font-size: 1.4rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
line-height: 18px;
|
line-height: 1.4rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-more {
|
.view-more {
|
||||||
font-size: 13px;
|
font-size: 0.9rem;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: .5px;
|
letter-spacing: .5px;
|
||||||
color: $text-color-70;
|
color: $text-color-70;
|
||||||
@@ -82,12 +86,13 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-count {
|
.info {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: .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 {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<list-header :title="listTitle" :info="resultCount" :sticky="true" />
|
<list-header :title="listTitle" :info="info" :sticky="true" />
|
||||||
|
|
||||||
<results-list :results="results" v-if="results" />
|
<results-list :results="results" v-if="results" />
|
||||||
|
|
||||||
@@ -30,7 +30,8 @@ export default {
|
|||||||
results: [],
|
results: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
totalPages: 0,
|
totalPages: 0,
|
||||||
totalResults: 0
|
totalResults: 0,
|
||||||
|
loading: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -42,18 +43,24 @@ export default {
|
|||||||
console.log('routelistname', routeListName)
|
console.log('routelistname', routeListName)
|
||||||
return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
|
return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
|
||||||
},
|
},
|
||||||
resultCount() {
|
info() {
|
||||||
if (this.results.length === 0)
|
if (this.results.length === 0)
|
||||||
return ''
|
return [null, null]
|
||||||
|
return [this.pageCount, this.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`
|
||||||
|
},
|
||||||
|
pageCount() {
|
||||||
|
return `Page ${this.page} of ${this.totalPages}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadMore() {
|
loadMore() {
|
||||||
console.log(this.$route)
|
console.log(this.$route)
|
||||||
|
this.loading = true;
|
||||||
this.page++
|
this.page++
|
||||||
|
|
||||||
window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
|
window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
|
||||||
@@ -82,6 +89,8 @@ export default {
|
|||||||
// 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() {
|
||||||
|
|||||||
@@ -32,28 +32,23 @@
|
|||||||
|
|
||||||
<!-- SIDEBAR ACTIONS -->
|
<!-- SIDEBAR ACTIONS -->
|
||||||
<div class="movie__actions" v-if="movie">
|
<div class="movie__actions" v-if="movie">
|
||||||
<sidebar-list-element :iconRef="'#iconNot_exsits'" :active="requested"
|
|
||||||
:iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'" :class="requested ? 'rotate-180' : null">
|
<sidebar-list-element :iconRef="'#iconNot_exsits'" :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" :iconRef="'#iconSent'"
|
<sidebar-list-element @click="sendRequest" :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 v-if="admin" @click="showTorrents=!showTorrents"
|
<sidebar-list-element v-if="admin" @click="showTorrents=!showTorrents"
|
||||||
:iconRef="'#icon_torrents'" :active="showTorrents"
|
:iconRef="'#icon_torrents'" :active="showTorrents"
|
||||||
:supplementaryText="numberOfTorrentResults">
|
:supplementaryText="numberOfTorrentResults">
|
||||||
|
|
||||||
Search for torrents
|
Search for torrents
|
||||||
</sidebar-list-element>
|
</sidebar-list-element>
|
||||||
|
|
||||||
<sidebar-list-element @click="showIssueForm = !showIssueForm"
|
|
||||||
:iconRef="null"
|
|
||||||
:active="showIssueForm">
|
|
||||||
⚠️ 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>
|
||||||
@@ -69,16 +64,14 @@
|
|||||||
|
|
||||||
<!-- MOVIE INFO -->
|
<!-- MOVIE INFO -->
|
||||||
<div class="movie__info">
|
<div class="movie__info">
|
||||||
|
<div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
|
||||||
|
|
||||||
<!-- Loading placeholder -->
|
<!-- Loading placeholder -->
|
||||||
<div v-if="!movie" class="movie__description">
|
<div v-else class="movie__description">
|
||||||
<loading-placeholder :count="12" />
|
<loading-placeholder :count="12" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="movie__details" v-if="movie && !showIssueForm">
|
<div class="movie__details" v-if="movie">
|
||||||
<div class="movie__description">
|
|
||||||
{{ movie.overview }}
|
|
||||||
</div>
|
|
||||||
<div v-if="movie.year" class="movie__details-block">
|
<div v-if="movie.year" class="movie__details-block">
|
||||||
<h2 class="movie__details-title">Release Date</h2>
|
<h2 class="movie__details-title">Release Date</h2>
|
||||||
<div class="movie__details-text">{{ movie.year }}</div>
|
<div class="movie__details-text">{{ movie.year }}</div>
|
||||||
@@ -100,17 +93,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div v-if="showIssueForm" class="issueForm">
|
|
||||||
<h2 class="movie__details-title">Report an issue</h2>
|
|
||||||
<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>
|
||||||
|
|
||||||
<!-- TODO: change this classname, this is general -->
|
<!-- TODO: change this classname, this is general -->
|
||||||
@@ -140,23 +122,12 @@ 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 { getMovie, getShow, request, getRequestStatus } from '@/api'
|
import { getMovie, getPerson, getShow, request, getRequestStatus } from '@/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['id', 'type'],
|
props: ['id', 'type'],
|
||||||
components: {
|
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
|
||||||
TorrentList,
|
|
||||||
Person,
|
|
||||||
LoadingPlaceholder,
|
|
||||||
SidebarListElement,
|
|
||||||
RadioButtons,
|
|
||||||
TextArea,
|
|
||||||
SeasonedButton
|
|
||||||
},
|
|
||||||
directives: { img: img }, // TODO decide to remove or use
|
directives: { img: img }, // TODO decide to remove or use
|
||||||
data(){
|
data(){
|
||||||
return{
|
return{
|
||||||
@@ -169,11 +140,9 @@ export default {
|
|||||||
matched: false,
|
matched: false,
|
||||||
userLoggedIn: storage.sessionId ? true : false,
|
userLoggedIn: storage.sessionId ? true : false,
|
||||||
requested: false,
|
requested: false,
|
||||||
admin: localStorage.getItem('admin'),
|
admin: localStorage.getItem('admin') == "true" ? true : false,
|
||||||
showTorrents: false,
|
showTorrents: false,
|
||||||
compact: false,
|
compact: false
|
||||||
showIssueForm: false,
|
|
||||||
selectedIssue: null
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -182,7 +151,7 @@ export default {
|
|||||||
this.title = movie.title
|
this.title = movie.title
|
||||||
this.poster = movie.poster
|
this.poster = movie.poster
|
||||||
this.backdrop = movie.backdrop
|
this.backdrop = movie.backdrop
|
||||||
this.matched = movie.existsInPlex
|
this.matched = movie.exists_in_plex || false
|
||||||
this.checkIfRequested(movie)
|
this.checkIfRequested(movie)
|
||||||
.then(status => this.requested = status)
|
.then(status => this.requested = status)
|
||||||
|
|
||||||
@@ -192,9 +161,7 @@ export default {
|
|||||||
return await getRequestStatus(movie.id, movie.type)
|
return await getRequestStatus(movie.id, movie.type)
|
||||||
},
|
},
|
||||||
nestedDataToString(data) {
|
nestedDataToString(data) {
|
||||||
let nestedArray = []
|
return data.join(', ')
|
||||||
data.forEach(item => nestedArray.push(item));
|
|
||||||
return nestedArray.join(', ');
|
|
||||||
},
|
},
|
||||||
sendRequest(){
|
sendRequest(){
|
||||||
request(this.id, this.type, storage.token)
|
request(this.id, this.type, storage.token)
|
||||||
@@ -208,15 +175,6 @@ export default {
|
|||||||
const tmdbType = this.type === 'show' ? 'tv' : this.type
|
const tmdbType = this.type === 'show' ? 'tv' : this.type
|
||||||
window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
|
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){
|
||||||
@@ -231,35 +189,6 @@ export default {
|
|||||||
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
|
||||||
},
|
|
||||||
issueOptions: function() {
|
|
||||||
return [{
|
|
||||||
value: 'playback',
|
|
||||||
text: 'Unable to play'
|
|
||||||
}, {
|
|
||||||
value: 'missing-episode',
|
|
||||||
text: 'Missing Episode',
|
|
||||||
subElements: this.seasonOptions
|
|
||||||
}, {
|
|
||||||
value: 'missing-subtitle',
|
|
||||||
text: 'Missing subtitles'
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
seasonOptions: function() {
|
|
||||||
if (this.movie.type !== 'show') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = []
|
|
||||||
const length = this.movie.seasons;
|
|
||||||
|
|
||||||
for (var i = 0; i < length; i++) {
|
|
||||||
options.push({
|
|
||||||
value: i+1,
|
|
||||||
text: `Season ${i+1}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@@ -269,13 +198,19 @@ export default {
|
|||||||
this.prevDocumentTitle = store.getters['documentTitle/title']
|
this.prevDocumentTitle = store.getters['documentTitle/title']
|
||||||
|
|
||||||
if (this.type === 'movie') {
|
if (this.type === 'movie') {
|
||||||
getMovie(this.id)
|
getMovie(this.id, true)
|
||||||
|
.then(this.parseResponse)
|
||||||
|
.catch(error => {
|
||||||
|
this.$router.push({ name: '404' });
|
||||||
|
})
|
||||||
|
} else if (this.type == 'person') {
|
||||||
|
getPerson(this.id, true)
|
||||||
.then(this.parseResponse)
|
.then(this.parseResponse)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$router.push({ name: '404' });
|
this.$router.push({ name: '404' });
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
getShow(this.id)
|
getShow(this.id, true)
|
||||||
.then(this.parseResponse)
|
.then(this.parseResponse)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.$router.push({ name: '404' });
|
this.$router.push({ name: '404' });
|
||||||
@@ -293,9 +228,6 @@ export default {
|
|||||||
@import "./src/scss/media-queries";
|
@import "./src/scss/media-queries";
|
||||||
|
|
||||||
.movie {
|
.movie {
|
||||||
background-color: $background-color;
|
|
||||||
color: $text-color;
|
|
||||||
|
|
||||||
&__wrap {
|
&__wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
&--header {
|
&--header {
|
||||||
@@ -309,6 +241,9 @@ export default {
|
|||||||
@include tablet-min{
|
@include tablet-min{
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
background-color: $background-color;
|
||||||
|
color: $text-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__header {
|
&__header {
|
||||||
@@ -429,19 +364,15 @@ export default {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
flex: 0 0 100%;
|
|
||||||
|
|
||||||
@include tablet-min {
|
@include tablet-min {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&__details {
|
&__details {
|
||||||
display: flex;
|
&-block {
|
||||||
width: 100%;
|
float: left;
|
||||||
flex-direction: row;
|
}
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
&-block:not(:last-child) {
|
&-block:not(:last-child) {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
@@ -490,24 +421,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.issueForm {
|
|
||||||
// padding: 40px;
|
|
||||||
|
|
||||||
.issueOptions {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.seasonOptions {
|
|
||||||
margin-top: 2rem;
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
> :not(h2) {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,9 +6,14 @@
|
|||||||
<figure class="movies-item__poster">
|
<figure class="movies-item__poster">
|
||||||
<img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
|
<img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
|
||||||
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
|
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
|
||||||
|
|
||||||
|
<div v-if="movie.download" class="progress">
|
||||||
|
<progress :value="movie.download.progress" max="100"></progress>
|
||||||
|
<span>{{ movie.download.state }}: {{ movie.download.progress }}%</span>
|
||||||
|
</div>
|
||||||
</figure>
|
</figure>
|
||||||
<div class="movies-item__content">
|
<div class="movies-item__content">
|
||||||
<p class="movies-item__title">{{ movie.title }}</p>
|
<p class="movies-item__title">{{ movie.title || movie.name }}</p>
|
||||||
<p class="movies-item__title">{{ movie.year }}</p>
|
<p class="movies-item__title">{{ movie.year }}</p>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -67,7 +72,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@include desktop-lg-min{
|
@include desktop-lg-min{
|
||||||
padding: 20px;
|
padding: 15px;
|
||||||
width: 12.5%;
|
width: 12.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,3 +120,46 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./src/scss/variables";
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
|
||||||
|
> progress {
|
||||||
|
width: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
> span {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
progress {
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 1.4rem;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-bar {
|
||||||
|
background-color: rgba($black, 0.55);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
progress::-webkit-progress-value {
|
||||||
|
background-color: $green-70;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
progress::-moz-progress-bar {
|
||||||
|
/* style rules */
|
||||||
|
background-color: green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<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="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
<seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
|
||||||
@@ -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,7 +43,6 @@ export default {
|
|||||||
data(){
|
data(){
|
||||||
return{
|
return{
|
||||||
userLoggedIn: '',
|
userLoggedIn: '',
|
||||||
userName: '',
|
|
||||||
emoji: '',
|
emoji: '',
|
||||||
results: undefined,
|
results: undefined,
|
||||||
totalResults: undefined,
|
totalResults: undefined,
|
||||||
@@ -58,25 +57,10 @@ 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;
|
||||||
},
|
},
|
||||||
@@ -91,7 +75,6 @@ export default {
|
|||||||
this.userLoggedIn = false;
|
this.userLoggedIn = false;
|
||||||
} else {
|
} else {
|
||||||
this.userLoggedIn = true;
|
this.userLoggedIn = true;
|
||||||
this.getUserInfo();
|
|
||||||
|
|
||||||
getUserRequests()
|
getUserRequests()
|
||||||
.then(results => {
|
.then(results => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import { register } 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 SeasonedMessages from '@/components/ui/SeasonedMessages'
|
import SeasonedMessages from '@/components/ui/SeasonedMessages'
|
||||||
@@ -40,23 +40,20 @@ export default {
|
|||||||
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
|
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
|
||||||
|
|
||||||
if (verifyCredentials.verified) {
|
if (verifyCredentials.verified) {
|
||||||
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
|
|
||||||
username: username,
|
register(username, password)
|
||||||
password: password
|
.then(data => {
|
||||||
})
|
if (data.success){
|
||||||
.then(resp => {
|
localStorage.setItem('token', data.token);
|
||||||
let data = resp.data;
|
localStorage.setItem('username', username);
|
||||||
if (data.success){
|
localStorage.setItem('admin', data.admin)
|
||||||
localStorage.setItem('token', data.token);
|
|
||||||
localStorage.setItem('username', username);
|
eventHub.$emit('setUserStatus');
|
||||||
localStorage.setItem('admin', data.admin)
|
this.$router.push({ name: 'profile' })
|
||||||
|
}
|
||||||
eventHub.$emit('setUserStatus');
|
|
||||||
this.$router.push({ name: 'profile' })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.error })
|
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -8,10 +8,26 @@
|
|||||||
<seasoned-button @click="loadMore">load more</seasoned-button>
|
<seasoned-button @click="loadMore">load more</seasoned-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<loader v-if="!results.length" />
|
<div class="notFound" v-if="results.length == 0 && loading == false">
|
||||||
|
<h1 class="notFound-title">No results for search: <b>{{ query }}</b></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<loader v-if="loading" />
|
||||||
</div>
|
</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'
|
||||||
@@ -37,6 +53,8 @@ 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: []
|
||||||
@@ -50,8 +68,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
search(query=this.query, page=this.page) {
|
search(query=this.query, page=this.page, adult=this.adult, mediaType=this.mediaType) {
|
||||||
searchTmdb(query, page)
|
searchTmdb(query, page, adult, mediaType)
|
||||||
.then(this.parseResponse)
|
.then(this.parseResponse)
|
||||||
},
|
},
|
||||||
parseResponse(data) {
|
parseResponse(data) {
|
||||||
@@ -74,14 +92,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
const { query, page } = this.$route.query
|
const { query, page, adult, media_type } = 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 ? page : 1
|
this.page = page || 1
|
||||||
|
this.adult = adult || this.adult
|
||||||
|
this.mediaType = media_type || this.mediaType
|
||||||
this.title = `Search results: ${this.query}`
|
this.title = `Search results: ${this.query}`
|
||||||
|
|
||||||
this.search()
|
this.search()
|
||||||
|
|||||||
@@ -3,17 +3,23 @@
|
|||||||
<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>
|
|
||||||
|
|
||||||
<form class="form">
|
<div v-if="!hasPlexUser">
|
||||||
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
<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 password" icon="Keyhole" type="password"
|
|
||||||
:value.sync="plexPassword" @submit="authenticatePlex" />
|
|
||||||
|
|
||||||
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
<form class="form">
|
||||||
|
<seasoned-input placeholder="plex username" icon="Email" :value.sync="plexUsername"/>
|
||||||
|
<seasoned-input placeholder="plex password" icon="Keyhole" type="password"
|
||||||
|
:value.sync="plexPassword" @submit="authenticatePlex" />
|
||||||
|
|
||||||
<seasoned-messages :messages.sync="messages" />
|
<seasoned-button @click="authenticatePlex">link plex account</seasoned-button>
|
||||||
</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'>
|
||||||
|
|
||||||
@@ -44,12 +50,13 @@
|
|||||||
</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 { plexAuthenticate } from '@/api'
|
import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
|
||||||
@@ -60,7 +67,21 @@ 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: {
|
||||||
@@ -70,26 +91,37 @@ export default {
|
|||||||
changePassword() {
|
changePassword() {
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
authenticatePlex() {
|
async authenticatePlex() {
|
||||||
let username = this.plexUsername
|
let username = this.plexUsername
|
||||||
let password = this.plexPassword
|
let password = this.plexPassword
|
||||||
|
|
||||||
plexAuthenticate(username, password)
|
const response = await linkPlexAccount(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' })
|
|
||||||
|
|
||||||
console.log('response from plex:', data.username)
|
this.messages.push({
|
||||||
|
type: response.success ? 'success' : 'error',
|
||||||
|
title: response.success ? 'Authenticated with plex' : 'Something went wrong',
|
||||||
|
message: response.message
|
||||||
})
|
})
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
this.messages.push({ type: 'error', title: 'Something went wrong', message: error.message })
|
if (response.success)
|
||||||
|
getSettings().then(settings => this.settings = settings)
|
||||||
|
},
|
||||||
|
async unauthenticatePlex() {
|
||||||
|
const response = await unlinkPlexAccount()
|
||||||
|
|
||||||
|
this.messages.push({
|
||||||
|
type: response.success ? 'success' : 'error',
|
||||||
|
title: response.success ? 'Unlinked plex account ' : 'Something went wrong',
|
||||||
|
message: response.message
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.success)
|
||||||
|
getSettings().then(settings => this.settings = settings)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created(){
|
created(){
|
||||||
if (localStorage.getItem('token')){
|
const token = localStorage.getItem('token') || false;
|
||||||
|
if (token){
|
||||||
this.userLoggedIn = true
|
this.userLoggedIn = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +183,7 @@ a {
|
|||||||
display: block;
|
display: block;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid rgba(8, 28, 36, 0.05);
|
border-bottom: 1px solid $text-color-50;
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
margin-bottom: 70px;
|
margin-bottom: 70px;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
<section>
|
<section>
|
||||||
<h1>Sign in</h1>
|
<h1>Sign in</h1>
|
||||||
|
|
||||||
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
|
<seasoned-input placeholder="username"
|
||||||
|
icon="Email"
|
||||||
|
type="email"
|
||||||
|
:value.sync="username" />
|
||||||
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
|
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
|
||||||
|
|
||||||
<seasoned-button @click="signin">sign in</seasoned-button>
|
<seasoned-button @click="signin">sign in</seasoned-button>
|
||||||
@@ -16,11 +19,12 @@
|
|||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from 'axios'
|
import { login } from '@/api'
|
||||||
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,29 +43,26 @@ export default {
|
|||||||
let username = this.username;
|
let username = this.username;
|
||||||
let password = this.password;
|
let password = this.password;
|
||||||
|
|
||||||
axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
|
login(username, password)
|
||||||
username: username,
|
.then(data => {
|
||||||
password: password
|
if (data.success){
|
||||||
})
|
const jwtData = parseJwt(data.token)
|
||||||
.then(resp => {
|
localStorage.setItem('token', data.token);
|
||||||
let data = resp.data;
|
localStorage.setItem('username', jwtData['username']);
|
||||||
if (data.success){
|
localStorage.setItem('admin', jwtData['admin'] || false);
|
||||||
localStorage.setItem('token', data.token);
|
|
||||||
localStorage.setItem('username', username);
|
eventHub.$emit('setUserStatus');
|
||||||
localStorage.setItem('admin', data.admin);
|
this.$router.push({ name: 'profile' })
|
||||||
|
}
|
||||||
eventHub.$emit('setUserStatus');
|
})
|
||||||
this.$router.push({ name: 'profile' })
|
.catch(error => {
|
||||||
}
|
if (error.status === 401) {
|
||||||
})
|
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
|
||||||
.catch(error => {
|
}
|
||||||
if (error.message.endsWith('401')) {
|
else {
|
||||||
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
|
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
||||||
}
|
}
|
||||||
else {
|
});
|
||||||
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created(){
|
created(){
|
||||||
|
|||||||
@@ -20,9 +20,11 @@
|
|||||||
|
|
||||||
<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>
|
||||||
@@ -97,9 +99,10 @@ 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 },
|
components: { SeasonedButton, SeasonedInput, ToggleButton },
|
||||||
props: {
|
props: {
|
||||||
query: {
|
query: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -110,7 +113,7 @@ export default {
|
|||||||
require: true
|
require: true
|
||||||
},
|
},
|
||||||
tmdb_type: String,
|
tmdb_type: String,
|
||||||
admin: String,
|
admin: Boolean,
|
||||||
show: Boolean
|
show: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -133,6 +136,11 @@ 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' : ''
|
||||||
@@ -147,27 +155,31 @@ 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)
|
||||||
|
|
||||||
event.target.parentNode.insertAdjacentElement('afterend', nameRow)
|
clickedElement.insertAdjacentElement('afterend', nameRow)
|
||||||
},
|
},
|
||||||
sendTorrent(magnet, name, event){
|
sendTorrent(magnet, name, event){
|
||||||
this.$notifications.info({
|
this.$notifications.info({
|
||||||
@@ -177,7 +189,6 @@ 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) => {
|
||||||
@@ -193,7 +204,6 @@ 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':
|
||||||
@@ -279,14 +289,13 @@ export default {
|
|||||||
@import "./src/scss/variables";
|
@import "./src/scss/variables";
|
||||||
.expanded {
|
.expanded {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 1rem;
|
padding: 0.25rem 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%;
|
||||||
@@ -298,8 +307,14 @@ 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 {
|
||||||
@@ -348,7 +363,6 @@ 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;
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -31,7 +31,7 @@ export default {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
height: 45px;
|
height: 45px;
|
||||||
letter-spacing: 1.2px;
|
letter-spacing: 0.5px;
|
||||||
padding: 5px 20px 4px 20px;
|
padding: 5px 20px 4px 20px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
margin-right: 0.3rem;
|
margin-right: 0.3rem;
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export default {
|
|||||||
.message {
|
.message {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 35rem;
|
max-width: 35rem;
|
||||||
height: 75px;
|
min-height: 75px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
<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>
|
|
||||||
100
src/components/ui/ToggleButton.vue
Normal file
100
src/components/ui/ToggleButton.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="toggle-container">
|
||||||
|
<button v-for="option in options" class="toggle-button" @click="toggle(option)"
|
||||||
|
:class="toggleValue === option ? 'selected' : null"
|
||||||
|
>{{ option }}</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
options: {
|
||||||
|
Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
toggleValue: this.selected || this.options[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.toggle(this.toggleValue)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggle(toggleValue) {
|
||||||
|
this.toggleValue = toggleValue;
|
||||||
|
if (this.selected !== undefined) {
|
||||||
|
this.$emit('update:selected', toggleValue)
|
||||||
|
} else {
|
||||||
|
this.$emit('change', toggleValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "./src/scss/variables";
|
||||||
|
|
||||||
|
$background: $background-ui;
|
||||||
|
$background-selected: $background-color-secondary;
|
||||||
|
|
||||||
|
.toggle-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 15rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
// padding: 0.2rem;
|
||||||
|
background-color: $background;
|
||||||
|
border: 2px solid $background;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid $background;
|
||||||
|
border-right: 4px solid $background;
|
||||||
|
|
||||||
|
.toggle-button {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
font-weight: normal;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border: 0;
|
||||||
|
color: $text-color;
|
||||||
|
// background-color: $text-color-5;
|
||||||
|
background-color: $background;
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
color: $text-color;
|
||||||
|
// background-color: $background-color-secondary;
|
||||||
|
background-color: $background-selected;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &:first-of-type, &:last-of-type {
|
||||||
|
// border-left: 4px solid $background;
|
||||||
|
// border-right: 4px solid $background;
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// &:first-of-type {
|
||||||
|
// border-top-left-radius: 4px;
|
||||||
|
// border-bottom-left-radius: 4px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// &:last-of-type {
|
||||||
|
// border-top-right-radius: 4px;
|
||||||
|
// border-bottom-right-radius: 4px;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,13 +11,19 @@ export default {
|
|||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
darkmode: window.getComputedStyle(document.body).colorScheme.includes('dark')
|
darkmode: this.supported
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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: {
|
||||||
@@ -41,7 +47,7 @@ export default {
|
|||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 1;
|
z-index: 10;
|
||||||
|
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a @click="$emit('click')"><li>
|
<a @click="$emit('click')">
|
||||||
<figure :class="activeClassIfActive" v-if="iconRefNameIfActive">
|
<li>
|
||||||
<svg class="icon">
|
<figure :class="activeClassIfActive">
|
||||||
<use :xlink:href="iconRefNameIfActive"/>
|
<svg class="icon"><use :xlink:href="iconRefNameIfActive"/></svg>
|
||||||
</svg>
|
</figure>
|
||||||
</figure>
|
|
||||||
|
|
||||||
<span :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
|
<span class="text" :class="activeClassIfActive">{{ contentTextToDisplay }}</span>
|
||||||
|
|
||||||
<span v-if="supplementaryText" class="supplementary-text">
|
<span v-if="supplementaryText" class="supplementary-text">
|
||||||
{{ supplementaryText }}
|
{{ supplementaryText }}
|
||||||
</span>
|
</span>
|
||||||
</li></a>
|
</li>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
iconRef: {
|
iconRef: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: true
|
||||||
},
|
},
|
||||||
iconRefActive: {
|
iconRefActive: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -87,37 +87,51 @@ li {
|
|||||||
&:hover {
|
&:hover {
|
||||||
color: $text-color-70;
|
color: $text-color-70;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: $text-color-70;
|
||||||
|
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 > .icon {
|
figure {
|
||||||
width: 18px;
|
position: absolute;
|
||||||
height: 18px;
|
|
||||||
margin: 0 7px 0 0;
|
> svg {
|
||||||
fill: $text-color-50;
|
position: relative;
|
||||||
transition: fill 0.5s ease, transform 0.5s ease;
|
top: 50%;
|
||||||
&.waiting {
|
width: 16px;
|
||||||
transform: scale(0.8, 0.8);
|
height: 16px;
|
||||||
}
|
margin: 0 7px 0 0;
|
||||||
&.pending {
|
fill: $text-color-50;
|
||||||
fill: #f8bd2d;
|
transition: fill 0.5s ease, transform 0.5s ease;
|
||||||
}
|
|
||||||
&:hover &-icon {
|
& .waiting {
|
||||||
fill: $text-color-70;
|
transform: scale(0.8, 0.8);
|
||||||
cursor: pointer;
|
}
|
||||||
}
|
& .pending {
|
||||||
&.active > svg {
|
fill: #f8bd2d;
|
||||||
fill: $green;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/modules/userModule.js
Normal file
104
src/modules/userModule.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@ let routes = [
|
|||||||
path: '/',
|
path: '/',
|
||||||
component: (resolve) => require(['./components/Home.vue'], resolve)
|
component: (resolve) => require(['./components/Home.vue'], resolve)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'activity',
|
||||||
|
path: '/activity',
|
||||||
|
component: (resolve) => require(['./components/ActivityPage.vue'], resolve)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'profile',
|
name: 'profile',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
|
|||||||
@@ -21,4 +21,30 @@
|
|||||||
> 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,31 @@ $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{
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
--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, .9);
|
--color-green-90: rgba(1, 210, 119, .9);
|
||||||
|
--color-green-70: rgba(1, 210, 119, .73);
|
||||||
--color-teal: #091c24;
|
--color-teal: #091c24;
|
||||||
--color-black: #081c24;
|
--color-black: #081c24;
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
@@ -42,11 +44,12 @@
|
|||||||
--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: #1e1f22;
|
--background-color: rgba(17, 17, 17, 1);
|
||||||
--background-color-secondary: #111111;
|
--background-color-secondary: rgba(6, 7, 8, 1);
|
||||||
--background-95: rgba(30, 31, 34, 0.95);
|
--background-ui: #202125;
|
||||||
--background-70: rgba(30, 31, 34, 0.8);
|
--background-95: rgba(17, 17, 17, 0.95);
|
||||||
--background-40: rgba(30, 31, 34, 0.4);
|
--background-70: rgba(17, 17, 17, 0.8);
|
||||||
|
--background-40: rgba(17, 17, 17, 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +64,7 @@ $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);
|
||||||
@@ -74,6 +78,7 @@ $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;
|
||||||
@@ -99,11 +104,12 @@ $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: #1e1f22;
|
--background-color: rgba(17, 17, 17, 1);
|
||||||
--background-color-secondary: #111111;
|
--background-color-secondary: rgba(6, 7, 8, 1);
|
||||||
--background-95: rgba(30, 31, 34, 0.95);
|
--background-ui: #202125;
|
||||||
--background-70: rgba(30, 31, 34, 0.7);
|
--background-95: rgba(17, 17, 17, 0.95);
|
||||||
--color-teal: #091c24;
|
--background-70: rgba(17, 17, 17, 0.8);
|
||||||
|
--background-40: rgba(17, 17, 17, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.light {
|
.light {
|
||||||
@@ -111,13 +117,11 @@ $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-nav-logo: #081c24;
|
--background-40: rgba(255, 255, 255, 0.4);
|
||||||
--color-green: #01d277;
|
|
||||||
--color-teal: #091c24;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
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: {
|
||||||
torrentModule,
|
|
||||||
darkmodeModule,
|
darkmodeModule,
|
||||||
documentTitle
|
documentTitle,
|
||||||
|
torrentModule,
|
||||||
|
userModule
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
14
src/utils.js
14
src/utils.js
@@ -7,7 +7,17 @@ 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 }
|
export { sortableSize, parseJwt }
|
||||||
30
yarn.lock
30
yarn.lock
@@ -1729,6 +1729,29 @@ character-reference-invalid@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz#1647f4f726638d3ea4a750cf5d1975c1c7919a85"
|
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz#1647f4f726638d3ea4a750cf5d1975c1c7919a85"
|
||||||
integrity sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==
|
integrity sha512-VOq6PRzQBam/8Jm6XBGk2fNEnHXAdGd6go0rtd4weAGECBamHDwwCQSOT12TACIYUZegUXnV6xBXqUssijtxIg==
|
||||||
|
|
||||||
|
chart.js@^2.9.2:
|
||||||
|
version "2.9.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.2.tgz#5f7397f2fc33ca406836dbaed3cc39943bbb9f80"
|
||||||
|
integrity sha512-AagP9h27gU7hhx8F64BOFpNZGV0R1Pz1nhsi0M1+KLhtniX6ElqLl0z0obKSiuGMl9tcRe6ZhruCGCJWmH6snQ==
|
||||||
|
dependencies:
|
||||||
|
chartjs-color "^2.1.0"
|
||||||
|
moment "^2.10.2"
|
||||||
|
|
||||||
|
chartjs-color-string@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
|
||||||
|
integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
|
||||||
|
dependencies:
|
||||||
|
color-name "^1.0.0"
|
||||||
|
|
||||||
|
chartjs-color@^2.1.0:
|
||||||
|
version "2.4.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
|
||||||
|
integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
|
||||||
|
dependencies:
|
||||||
|
chartjs-color-string "^0.6.0"
|
||||||
|
color-convert "^1.9.3"
|
||||||
|
|
||||||
chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.2:
|
chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.2:
|
||||||
version "2.1.8"
|
version "2.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
|
||||||
@@ -1873,7 +1896,7 @@ collection-visit@^1.0.0:
|
|||||||
map-visit "^1.0.0"
|
map-visit "^1.0.0"
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
color-convert@^1.3.0, color-convert@^1.9.0:
|
color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.3:
|
||||||
version "1.9.3"
|
version "1.9.3"
|
||||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||||
@@ -4660,6 +4683,11 @@ module-deps-sortable@5.0.0:
|
|||||||
through2 "^2.0.0"
|
through2 "^2.0.0"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
|
moment@^2.10.2:
|
||||||
|
version "2.24.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
|
||||||
|
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
|
||||||
|
|
||||||
ms@2.0.0:
|
ms@2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||||
|
|||||||
Reference in New Issue
Block a user