Compare commits

..

11 Commits

29 changed files with 529 additions and 1177 deletions

View File

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

View File

@@ -13,7 +13,6 @@
"dependencies": {
"axios": "^0.18.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"chart.js": "^2.9.2",
"connect-history-api-fallback": "^1.3.0",
"express": "^4.16.1",
"vue": "^2.5.2",

View File

@@ -2,7 +2,6 @@ import axios from 'axios'
import storage from '@/storage'
import config from '@/config.json'
import path from 'path'
import store from '@/store'
const SEASONED_URL = config.SEASONED_URL
const ELASTIC_URL = config.ELASTIC_URL
@@ -11,13 +10,6 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX
// TODO
// - Move autorization token and errors here?
const checkStatusAndReturnJson = (response) => {
if (!response.ok) {
throw resp
}
return response.json()
}
// - - - TMDB - - -
/**
@@ -26,18 +18,12 @@ const checkStatusAndReturnJson = (response) => {
* @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response
*/
const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => {
const getMovie = (id, credits=false) => {
const url = new URL('v2/movie', SEASONED_URL)
url.pathname = path.join(url.pathname, id.toString())
if (checkExistance) {
url.searchParams.append('check_existance', true)
}
if (credits) {
url.searchParams.append('credits', true)
}
if(release_dates) {
url.searchParams.append('release_dates', true)
}
return fetch(url.href)
.then(resp => resp.json())
@@ -50,12 +36,9 @@ const getMovie = (id, checkExistance=false, credits=false, release_dates=false)
* @param {boolean} [credits=false] Include credits
* @returns {object} Tmdb response
*/
const getShow = (id, checkExistance=false, credits=false) => {
const getShow = (id, credits=false) => {
const url = new URL('v2/show', SEASONED_URL)
url.pathname = path.join(url.pathname, id.toString())
if (checkExistance) {
url.searchParams.append('check_existance', true)
}
if (credits) {
url.searchParams.append('credits', true)
}
@@ -65,24 +48,6 @@ const getShow = (id, checkExistance=false, credits=false) => {
.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.
* @param {string} name List the fetch
@@ -131,19 +96,12 @@ const getUserRequests = (page=1) => {
* @param {number} [page=1]
* @returns {object} Tmdb response
*/
const searchTmdb = (query, page=1, adult=false, mediaType=null) => {
const searchTmdb = (query, page=1) => {
const url = new URL('v2/search', SEASONED_URL)
if (mediaType != null && ['movie', 'show', 'person'].includes(mediaType)) {
url.pathname += `/${mediaType}`
}
url.searchParams.append('query', query)
url.searchParams.append('page', page)
url.searchParams.append('adult', adult)
const headers = { authorization: localStorage.getItem('token') }
return fetch(url.href, { headers })
return fetch(url.href)
.then(resp => resp.json())
.catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error })
}
@@ -252,138 +210,32 @@ const getRequestStatus = (id, type, authorization_token=undefined) => {
.catch(err => Promise.reject(err))
}
// - - - Seasoned user endpoints - - -
const register = (username, password) => {
const url = new URL('v1/user', SEASONED_URL)
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
}
return fetch(url.href, options)
.then(resp => resp.json())
.catch(error => {
console.error('Unexpected error occured before receiving response. Error:', error)
// TODO log to sentry the issue here
throw error
})
}
const login = (username, password) => {
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 plexAuthenticate = (username, password) => {
const url = new URL('https://plex.tv/api/v2/users/signin')
const headers = {
'Content-Type': 'application/json',
authorization: storage.token
'X-Plex-Platform': 'Linux',
'X-Plex-Version': 'v2.0.24',
'X-Plex-Platform-Version': '4.13.0-36-generic',
'X-Plex-Device-Name': 'Tautulli',
'X-Plex-Client-Identifier': '123'
}
return fetch(url.href, {
method: 'POST',
headers,
body: JSON.stringify(body)
})
.then(resp => resp.json())
.catch(error => { console.error(`api error linking plex account: ${username}`); throw error })
}
let formData = new FormData()
formData.set('login', username)
formData.set('password', password)
formData.set('rememberMe', false)
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 })
return axios({
method: 'POST',
url: url.href,
headers: headers,
data: formData
})
.catch(error => { console.error(`api error authentication plex: ${username}`); throw error })
}
@@ -450,7 +302,6 @@ const elasticSearchMoviesAndShows = (query) => {
export {
getMovie,
getShow,
getPerson,
getTmdbMovieListByName,
searchTmdb,
getUserRequests,
@@ -459,13 +310,7 @@ export {
addMagnet,
request,
getRequestStatus,
linkPlexAccount,
unlinkPlexAccount,
register,
login,
getSettings,
updateSettings,
fetchChart,
plexAuthenticate,
getEmoji,
elasticSearchMoviesAndShows
}

View File

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

View File

@@ -2,11 +2,8 @@
<header :class="{ 'sticky': sticky }">
<h2>{{ title }}</h2>
<div v-if="info instanceof Array" class="flex flex-direction-column">
<span v-for="item in info" class="info">{{ item }}</span>
</div>
<span v-else class="info">{{ info }}</span>
<router-link v-if="link" :to="link" class='view-more' :aria-label="`View all ${title}`">
<span v-if="info" class="result-count">{{ info }}</span>
<router-link v-else-if="link" :to="link" class='view-more'>
View All
</router-link>
</header>
@@ -22,10 +19,10 @@ export default {
sticky: {
type: Boolean,
required: false,
default: true
default: false
},
info: {
type: [String, Array],
type: String,
required: false
},
link: {
@@ -40,16 +37,12 @@ export default {
<style lang="scss" scoped>
@import './src/scss/variables';
@import './src/scss/media-queries';
@import './src/scss/main';
header {
width: 100%;
min-height: 80px;
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 0.75rem;
padding-right: 0.75rem;
padding: 1.8rem 12px;
&.sticky {
background-color: $background-color;
@@ -58,19 +51,22 @@ header {
position: -webkit-sticky;
top: $header-size;
z-index: 4;
padding-bottom: 1rem;
margin-bottom: 1.5rem;
}
h2 {
font-size: 1.4rem;
font-size: 18px;
font-weight: 300;
text-transform: capitalize;
line-height: 1.4rem;
line-height: 18px;
margin: 0;
color: $text-color;
}
.view-more {
font-size: 0.9rem;
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color-70;
@@ -86,13 +82,12 @@ header {
}
}
.info {
.result-count {
font-size: 13px;
font-weight: 300;
letter-spacing: .5px;
color: $text-color;
text-decoration: none;
text-align: right;
}
@include tablet-min {

View File

@@ -1,7 +1,7 @@
<template>
<div>
<list-header :title="listTitle" :info="info" :sticky="true" />
<list-header :title="listTitle" :info="resultCount" :sticky="true" />
<results-list :results="results" v-if="results" />
@@ -30,8 +30,7 @@ export default {
results: [],
page: 1,
totalPages: 0,
totalResults: 0,
loading: true
totalResults: 0
}
},
computed: {
@@ -43,24 +42,18 @@ export default {
console.log('routelistname', routeListName)
return routeListName.includes('_') ? routeListName.split('_').join(' ') : routeListName
},
info() {
if (this.results.length === 0)
return [null, null]
return [this.pageCount, this.resultCount]
},
resultCount() {
if (this.results.length === 0)
return ''
const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results`
},
pageCount() {
return `Page ${this.page} of ${this.totalPages}`
}
},
methods: {
loadMore() {
console.log(this.$route)
this.loading = true;
this.page++
window.history.replaceState({}, 'search', `/#/${this.$route.fullPath}?page=${this.page}`)
@@ -89,8 +82,6 @@ export default {
// TODO handle if list is not found
console.log('404 this is not a tmdb list')
}
this.loading = false
}
},
created() {

View File

@@ -32,23 +32,28 @@
<!-- SIDEBAR ACTIONS -->
<div class="movie__actions" v-if="movie">
<sidebar-list-element :iconRef="'#iconNot_exsits'" :active="matched"
:iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'">
<sidebar-list-element :iconRef="'#iconNot_exsits'" :active="requested"
:iconRefActive="'#iconExists'" :textActive="'Already in plex 🎉'" :class="requested ? 'rotate-180' : null">
Not yet in plex
</sidebar-list-element>
<sidebar-list-element @click="sendRequest" :iconRef="'#iconSent'"
:active="requested" :textActive="'Requested to be downloaded'">
Request to be downloaded?
</sidebar-list-element>
<sidebar-list-element v-if="admin" @click="showTorrents=!showTorrents"
:iconRef="'#icon_torrents'" :active="showTorrents"
:supplementaryText="numberOfTorrentResults">
Search for torrents
</sidebar-list-element>
<sidebar-list-element @click="showIssueForm = !showIssueForm"
:iconRef="null"
:active="showIssueForm">
&nbsp; &nbsp;Report an issue!
</sidebar-list-element>
<sidebar-list-element @click="openTmdb" :iconRef="'#icon_info'">
See more info
</sidebar-list-element>
@@ -64,14 +69,16 @@
<!-- MOVIE INFO -->
<div class="movie__info">
<div class="movie__description" v-if="movie"> {{ movie.overview }}</div>
<!-- Loading placeholder -->
<div v-else class="movie__description">
<div v-if="!movie" class="movie__description">
<loading-placeholder :count="12" />
</div>
<div class="movie__details" v-if="movie">
<div class="movie__details" v-if="movie && !showIssueForm">
<div class="movie__description">
{{ movie.overview }}
</div>
<div v-if="movie.year" class="movie__details-block">
<h2 class="movie__details-title">Release Date</h2>
<div class="movie__details-text">{{ movie.year }}</div>
@@ -93,6 +100,17 @@
</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>
<!-- TODO: change this classname, this is general -->
@@ -122,12 +140,23 @@ import Person from './Person'
import SidebarListElement from './ui/sidebarListElem'
import store from '@/store'
import LoadingPlaceholder from './ui/LoadingPlaceholder'
import RadioButtons from './ui/RadioButtons'
import TextArea from './ui/TextArea'
import SeasonedButton from './ui/SeasonedButton'
import { getMovie, getPerson, getShow, request, getRequestStatus } from '@/api'
import { getMovie, getShow, request, getRequestStatus } from '@/api'
export default {
props: ['id', 'type'],
components: { TorrentList, Person, LoadingPlaceholder, SidebarListElement },
components: {
TorrentList,
Person,
LoadingPlaceholder,
SidebarListElement,
RadioButtons,
TextArea,
SeasonedButton
},
directives: { img: img }, // TODO decide to remove or use
data(){
return{
@@ -140,9 +169,11 @@ export default {
matched: false,
userLoggedIn: storage.sessionId ? true : false,
requested: false,
admin: localStorage.getItem('admin') == "true" ? true : false,
admin: localStorage.getItem('admin'),
showTorrents: false,
compact: false
compact: false,
showIssueForm: false,
selectedIssue: null
}
},
methods: {
@@ -151,7 +182,7 @@ export default {
this.title = movie.title
this.poster = movie.poster
this.backdrop = movie.backdrop
this.matched = movie.exists_in_plex || false
this.matched = movie.existsInPlex
this.checkIfRequested(movie)
.then(status => this.requested = status)
@@ -161,7 +192,9 @@ export default {
return await getRequestStatus(movie.id, movie.type)
},
nestedDataToString(data) {
return data.join(', ')
let nestedArray = []
data.forEach(item => nestedArray.push(item));
return nestedArray.join(', ');
},
sendRequest(){
request(this.id, this.type, storage.token)
@@ -175,6 +208,15 @@ export default {
const tmdbType = this.type === 'show' ? 'tv' : this.type
window.location.href = 'https://www.themoviedb.org/' + tmdbType + '/' + this.id
},
reportIssue() {
if (this.showIssueForm) {
this.$notifications.success({
title: 'Issue successfully submitted',
description: 'Reported issue: Missing subtitles',
timeout: 300000
})
}
}
},
watch: {
id: function(val){
@@ -189,6 +231,35 @@ export default {
numberOfTorrentResults: () => {
let numTorrents = store.getters['torrentModule/resultCount']
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() {
@@ -198,19 +269,13 @@ export default {
this.prevDocumentTitle = store.getters['documentTitle/title']
if (this.type === 'movie') {
getMovie(this.id, true)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
} else if (this.type == 'person') {
getPerson(this.id, true)
getMovie(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
})
} else {
getShow(this.id, true)
getShow(this.id)
.then(this.parseResponse)
.catch(error => {
this.$router.push({ name: '404' });
@@ -228,6 +293,9 @@ export default {
@import "./src/scss/media-queries";
.movie {
background-color: $background-color;
color: $text-color;
&__wrap {
display: flex;
&--header {
@@ -241,9 +309,6 @@ export default {
@include tablet-min{
flex-direction: row;
}
background-color: $background-color;
color: $text-color;
}
}
&__header {
@@ -364,15 +429,19 @@ export default {
font-size: 13px;
line-height: 1.8;
margin-bottom: 20px;
flex: 0 0 100%;
@include tablet-min {
margin-bottom: 30px;
font-size: 14px;
}
}
&__details {
&-block {
float: left;
}
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
&-block:not(:last-child) {
margin-bottom: 20px;
margin-right: 20px;
@@ -421,4 +490,24 @@ export default {
}
}
}
.issueForm {
// padding: 40px;
.issueOptions {
margin-top: 1rem;
}
.seasonOptions {
margin-top: 2rem;
h2 {
margin-bottom: 1rem;
}
> :not(h2) {
margin-left: 1rem;
}
}
}
</style>

View File

@@ -6,14 +6,9 @@
<figure class="movies-item__poster">
<img v-if="!noImage" class="movies-item__img" src="~assets/placeholder.png" v-img="poster()" alt="">
<img v-if="noImage" class="movies-item__img is-loaded" src="~assets/no-image.png" alt="">
<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>
<div class="movies-item__content">
<p class="movies-item__title">{{ movie.title || movie.name }}</p>
<p class="movies-item__title">{{ movie.title }}</p>
<p class="movies-item__title">{{ movie.year }}</p>
</div>
</a>
@@ -72,7 +67,7 @@ export default {
}
@include desktop-lg-min{
padding: 15px;
padding: 20px;
width: 12.5%;
}
@@ -120,46 +115,3 @@ export default {
}
}
</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>

View File

@@ -2,7 +2,7 @@
<section class="profile">
<div class="profile__content" v-if="userLoggedIn">
<header class="profile__header">
<h2 class="profile__title">{{ emoji }} Welcome {{ username }}</h2>
<h2 class="profile__title">{{ emoji }} Welcome {{ userName }}</h2>
<div class="button--group">
<seasoned-button @click="showSettings = !showSettings">{{ showSettings ? 'hide settings' : 'show settings' }}</seasoned-button>
@@ -13,7 +13,7 @@
<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" />
</div>
@@ -43,6 +43,7 @@ export default {
data(){
return{
userLoggedIn: '',
userName: '',
emoji: '',
results: undefined,
totalResults: undefined,
@@ -57,10 +58,25 @@ export default {
const loadedResults = this.results.length
const totalResults = this.totalResults < 10000 ? this.totalResults : '∞'
return `${loadedResults} of ${totalResults} results`
},
username: () => store.getters['userModule/username']
}
},
methods: {
createSession(token){
axios.get(`https://api.themoviedb.org/3/authentication/session/new?api_key=${storage.apiKey}&request_token=${token}`)
.then(function(resp){
let data = resp.data;
if(data.success){
let id = data.session_id;
localStorage.setItem('session_id', id);
eventHub.$emit('setUserStatus');
this.userLoggedIn = true;
this.getUserInfo();
}
}.bind(this));
},
getUserInfo(){
this.userName = localStorage.getItem('username');
},
toggleSettings() {
this.showSettings = this.showSettings ? false : true;
},
@@ -75,6 +91,7 @@ export default {
this.userLoggedIn = false;
} else {
this.userLoggedIn = true;
this.getUserInfo();
getUserRequests()
.then(results => {

View File

@@ -18,7 +18,7 @@
</template>
<script>
import { register } from '@/api'
import axios from 'axios'
import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
@@ -40,20 +40,23 @@ export default {
let verifyCredentials = this.checkCredentials(username, password, passwordRepeat);
if (verifyCredentials.verified) {
axios.post(`https://api.kevinmidboe.com/api/v1/user`, {
username: username,
password: password
})
.then(resp => {
let data = resp.data;
if (data.success){
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin)
register(username, password)
.then(data => {
if (data.success){
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin)
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
})
.catch(error => {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.response.data.error })
});
}
else {

View File

@@ -8,26 +8,10 @@
<seasoned-button @click="loadMore">load more</seasoned-button>
</div>
<div class="notFound" v-if="results.length == 0 && loading == false">
<h1 class="notFound-title">No results for search: <b>{{ query }}</b></h1>
</div>
<loader v-if="loading" />
<loader v-if="!results.length" />
</div>
</template>
<style lang="scss" scoped>
.notFound {
display: flex;
justify-content: center;
align-items: center;
&-title {
font-weight: 400;
}
}
</style>
<script>
import { searchTmdb } from '@/api'
import ListHeader from '@/components/ListHeader'
@@ -53,8 +37,6 @@ export default {
query: String,
title: String,
page: Number,
adult: undefined,
mediaType: null,
totalPages: 0,
results: [],
totalResults: []
@@ -68,8 +50,8 @@ export default {
}
},
methods: {
search(query=this.query, page=this.page, adult=this.adult, mediaType=this.mediaType) {
searchTmdb(query, page, adult, mediaType)
search(query=this.query, page=this.page) {
searchTmdb(query, page)
.then(this.parseResponse)
},
parseResponse(data) {
@@ -92,16 +74,14 @@ export default {
}
},
created() {
const { query, page, adult, media_type } = this.$route.query
const { query, page } = this.$route.query
if (!query) {
// abort
console.error('abort, no query')
}
this.query = decodeURIComponent(query)
this.page = page || 1
this.adult = adult || this.adult
this.mediaType = media_type || this.mediaType
this.page = page ? page : 1
this.title = `Search results: ${this.query}`
this.search()

View File

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

View File

@@ -2,10 +2,7 @@
<section>
<h1>Sign in</h1>
<seasoned-input placeholder="username"
icon="Email"
type="email"
:value.sync="username" />
<seasoned-input placeholder="username" icon="Email" type="username" :value.sync="username" />
<seasoned-input placeholder="password" icon="Keyhole" type="password" :value.sync="password" @enter="signin"/>
<seasoned-button @click="signin">sign in</seasoned-button>
@@ -19,12 +16,11 @@
<script>
import { login } from '@/api'
import axios from 'axios'
import storage from '../storage'
import SeasonedInput from '@/components/ui/SeasonedInput'
import SeasonedButton from '@/components/ui/SeasonedButton'
import SeasonedMessages from '@/components/ui/SeasonedMessages'
import { parseJwt } from '@/utils'
export default {
components: { SeasonedInput, SeasonedButton, SeasonedMessages },
@@ -43,26 +39,29 @@ export default {
let username = this.username;
let password = this.password;
login(username, password)
.then(data => {
if (data.success){
const jwtData = parseJwt(data.token)
localStorage.setItem('token', data.token);
localStorage.setItem('username', jwtData['username']);
localStorage.setItem('admin', jwtData['admin'] || false);
axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, {
username: username,
password: password
})
.then(resp => {
let data = resp.data;
if (data.success){
localStorage.setItem('token', data.token);
localStorage.setItem('username', username);
localStorage.setItem('admin', data.admin);
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' })
}
else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
}
});
eventHub.$emit('setUserStatus');
this.$router.push({ name: 'profile' })
}
})
.catch(error => {
if (error.message.endsWith('401')) {
this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' })
}
else {
this.messages.push({ type: 'error', title: 'Unexpected error', message: error.message })
}
});
}
},
created(){

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ export default {
font-size: 11px;
line-height: 2;
height: 45px;
letter-spacing: 0.5px;
letter-spacing: 1.2px;
padding: 5px 20px 4px 20px;
margin: 0;
margin-right: 0.3rem;

View File

@@ -70,7 +70,7 @@ export default {
.message {
width: 100%;
max-width: 35rem;
min-height: 75px;
height: 75px;
display: flex;
margin-top: 1rem;

View File

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

View File

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

View File

@@ -11,19 +11,13 @@ export default {
data() {
return {
darkmode: this.supported
darkmode: window.getComputedStyle(document.body).colorScheme.includes('dark')
}
},
methods: {
toggleDarkmode() {
this.darkmode = !this.darkmode;
document.body.className = this.darkmode ? 'dark' : 'light'
},
supported() {
const computedStyle = window.getComputedStyle(document.body)
if (computedStyle['colorScheme'] != null)
return computedStyle.colorScheme.includes('dark')
return false
}
},
computed: {
@@ -47,7 +41,7 @@ export default {
margin-right: 2px;
bottom: 0;
right: 0;
z-index: 10;
z-index: 1;
-webkit-user-select: none;
-moz-user-select: none;

View File

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

View File

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

View File

@@ -11,11 +11,6 @@ let routes = [
path: '/',
component: (resolve) => require(['./components/Home.vue'], resolve)
},
{
name: 'activity',
path: '/activity',
component: (resolve) => require(['./components/ActivityPage.vue'], resolve)
},
{
name: 'profile',
path: '/profile',

View File

@@ -22,29 +22,3 @@
margin-left: 1rem;
}
}
.flex {
display: flex;
&-direction-column {
flex-direction: column;
}
&-direction-row {
flex-direction: row;
}
&-align-items-center {
align-items: center;
}
}
.position {
&-relative {
position: relative;
}
&-absolute {
position: absolute;
}
}

View File

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

View File

@@ -11,15 +11,13 @@
--text-color-secondary: orange;
--background-color: #f8f8f8;
--background-color-secondary: #ffffff;
--background-ui: #edeef0;
--background-95: rgba(255, 255, 255, 0.95);
--background-70: rgba(255, 255, 255, 0.7);
--background-40: rgba(255, 255, 255, 0.4);
--background-nav-logo: #081c24;
--color-green: #01d277;
--color-green-90: rgba(1, 210, 119, .9);
--color-green-70: rgba(1, 210, 119, .73);
--color-teal: #091c24;
--color-black: #081c24;
--white: #fff;
@@ -44,12 +42,11 @@
--text-color-50: rgba(255, 255, 255, 0.5);
--text-color-5: rgba(255, 255, 255, 0.05);
--text-color-secondary: orange;
--background-color: rgba(17, 17, 17, 1);
--background-color-secondary: rgba(6, 7, 8, 1);
--background-ui: #202125;
--background-95: rgba(17, 17, 17, 0.95);
--background-70: rgba(17, 17, 17, 0.8);
--background-40: rgba(17, 17, 17, 0.4);
--background-color: #1e1f22;
--background-color-secondary: #111111;
--background-95: rgba(30, 31, 34, 0.95);
--background-70: rgba(30, 31, 34, 0.8);
--background-40: rgba(30, 31, 34, 0.4);
}
}
@@ -64,7 +61,6 @@ $header-size: var(--header-size);
$dark: rgb(30, 31, 34);
$green: var(--color-green);
$green-90: var(--color-green-90);
$green-70: var(--color-green-70);
$teal: #091c24;
$black: #081c24;
$black-80: rgba(0,0,0,0.8);
@@ -78,7 +74,6 @@ $text-color-5: var(--text-color-5) !default;
$text-color-secondary: var(--text-color-secondary) !default;
$background-color: var(--background-color) !default;
$background-color-secondary: var(--background-color-secondary) !default;
$background-ui: var(--background-ui) !default;
$background-95: var(--background-95) !default;
$background-70: var(--background-70) !default;
$background-40: var(--background-40) !default;
@@ -104,12 +99,11 @@ $color-error-highlight: var(--color-error-highlight) !default;
--text-color-50: rgba(255, 255, 255, 0.5);
--text-color-5: rgba(255, 255, 255, 0.05);
--text-color-secondary: orange;
--background-color: rgba(17, 17, 17, 1);
--background-color-secondary: rgba(6, 7, 8, 1);
--background-ui: #202125;
--background-95: rgba(17, 17, 17, 0.95);
--background-70: rgba(17, 17, 17, 0.8);
--background-40: rgba(17, 17, 17, 0.4);
--background-color: #1e1f22;
--background-color-secondary: #111111;
--background-95: rgba(30, 31, 34, 0.95);
--background-70: rgba(30, 31, 34, 0.7);
--color-teal: #091c24;
}
.light {
@@ -117,11 +111,13 @@ $color-error-highlight: var(--color-error-highlight) !default;
--text-color-70: rgba(8, 28, 36, 0.7);
--text-color-50: rgba(8, 28, 36, 0.5);
--text-color-5: rgba(8, 28, 36, 0.05);
--text-color-inverted: #fff;
--text-color-secondary: orange;
--background-color: #f8f8f8;
--background-color-secondary: #ffffff;
--background-ui: #edeef0;
--background-95: rgba(255, 255, 255, 0.95);
--background-70: rgba(255, 255, 255, 0.7);
--background-40: rgba(255, 255, 255, 0.4);
--background-nav-logo: #081c24;
--color-green: #01d277;
--color-teal: #091c24;
}

View File

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

View File

@@ -7,17 +7,7 @@ const sortableSize = (string) => {
const exponent = UNITS.indexOf(unit) * 3
return numStr * (Math.pow(10, exponent))
};
const parseJwt = (token) => {
var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
};
}
export { sortableSize, parseJwt }
export { sortableSize }

View File

@@ -1729,29 +1729,6 @@ character-reference-invalid@^1.0.0:
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.3.tgz#1647f4f726638d3ea4a750cf5d1975c1c7919a85"
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:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@@ -1896,7 +1873,7 @@ collection-visit@^1.0.0:
map-visit "^1.0.0"
object-visit "^1.0.0"
color-convert@^1.3.0, color-convert@^1.9.0, color-convert@^1.9.3:
color-convert@^1.3.0, color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -4683,11 +4660,6 @@ module-deps-sortable@5.0.0:
through2 "^2.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:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"