diff --git a/package.json b/package.json index fde1d5a..27505fd 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "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", diff --git a/src/api.js b/src/api.js index a72d585..d5096b8 100644 --- a/src/api.js +++ b/src/api.js @@ -2,6 +2,7 @@ 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 @@ -10,6 +11,13 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX // TODO // - Move autorization token and errors here? +const checkStatusAndReturnJson = (response) => { + if (!response.ok) { + throw resp + } + return response.json() +} + // - - - TMDB - - - /** @@ -18,12 +26,18 @@ const ELASTIC_INDEX = config.ELASTIC_INDEX * @param {boolean} [credits=false] Include credits * @returns {object} Tmdb response */ -const getMovie = (id, credits=false) => { +const getMovie = (id, checkExistance=false, credits=false, release_dates=false) => { const url = new URL('v2/movie', SEASONED_URL) url.pathname = path.join(url.pathname, id.toString()) + if (checkExistance) { + url.searchParams.append('check_existance', true) + } if (credits) { url.searchParams.append('credits', true) } + if(release_dates) { + url.searchParams.append('release_dates', true) + } return fetch(url.href) .then(resp => resp.json()) @@ -119,7 +133,9 @@ const searchTmdb = (query, page=1) => { url.searchParams.append('query', query) url.searchParams.append('page', page) - return fetch(url.href) + const headers = { authorization: localStorage.getItem('token') } + + return fetch(url.href, { headers }) .then(resp => resp.json()) .catch(error => { console.error(`api error searching: ${query}, page: ${page}`); throw error }) } @@ -221,32 +237,138 @@ const getRequestStatus = (id, type, authorization_token=undefined) => { .catch(err => Promise.reject(err)) } -// - - - Authenticate with plex - - - +// - - - Seasoned user endpoints - - - -const plexAuthenticate = (username, password) => { - const url = new URL('https://plex.tv/api/v2/users/signin') - - const headers = { - 'Content-Type': 'application/json', - 'X-Plex-Platform': 'Linux', - 'X-Plex-Version': 'v2.0.24', - 'X-Plex-Platform-Version': '4.13.0-36-generic', - 'X-Plex-Device-Name': 'Tautulli', - 'X-Plex-Client-Identifier': '123' +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 }) } - let formData = new FormData() - formData.set('login', username) - formData.set('password', password) - formData.set('rememberMe', false) - - return axios({ - method: 'POST', - url: url.href, - headers: headers, - data: formData + 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 }) - .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 }) } @@ -322,7 +444,13 @@ export { addMagnet, request, getRequestStatus, - plexAuthenticate, + linkPlexAccount, + unlinkPlexAccount, + register, + login, + getSettings, + updateSettings, + fetchChart, getEmoji, elasticSearchMoviesAndShows } diff --git a/src/components/ActivityPage.vue b/src/components/ActivityPage.vue new file mode 100644 index 0000000..123a50d --- /dev/null +++ b/src/components/ActivityPage.vue @@ -0,0 +1,316 @@ + + + + + \ No newline at end of file diff --git a/src/components/ListHeader.vue b/src/components/ListHeader.vue index d36d163..1453ded 100644 --- a/src/components/ListHeader.vue +++ b/src/components/ListHeader.vue @@ -2,8 +2,11 @@

{{ title }}

- {{ info }} - +
+ {{ item }} +
+ {{ info }} + View All
@@ -19,10 +22,10 @@ export default { sticky: { type: Boolean, required: false, - default: false + default: true }, info: { - type: String, + type: [String, Array], required: false }, link: { @@ -37,12 +40,16 @@ export default { + + \ No newline at end of file diff --git a/src/components/Profile.vue b/src/components/Profile.vue index af8ea41..3504328 100644 --- a/src/components/Profile.vue +++ b/src/components/Profile.vue @@ -2,7 +2,7 @@
-

{{ emoji }} Welcome {{ userName }}

+

{{ emoji }} Welcome {{ username }}

{{ showSettings ? 'hide settings' : 'show settings' }} @@ -13,7 +13,7 @@ - +
@@ -43,7 +43,6 @@ export default { data(){ return{ userLoggedIn: '', - userName: '', emoji: '', results: undefined, totalResults: undefined, @@ -58,25 +57,10 @@ 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; }, @@ -91,7 +75,6 @@ export default { this.userLoggedIn = false; } else { this.userLoggedIn = true; - this.getUserInfo(); getUserRequests() .then(results => { diff --git a/src/components/Register.vue b/src/components/Register.vue index 5d94ff1..38d5c43 100644 --- a/src/components/Register.vue +++ b/src/components/Register.vue @@ -18,7 +18,7 @@ + + \ No newline at end of file diff --git a/src/modules/userModule.js b/src/modules/userModule.js new file mode 100644 index 0000000..71e8d7e --- /dev/null +++ b/src/modules/userModule.js @@ -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) + } + } +} \ No newline at end of file diff --git a/src/routes.js b/src/routes.js index eecb58c..4dbd90d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -11,6 +11,11 @@ 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', diff --git a/src/scss/main.scss b/src/scss/main.scss index 918d0c9..249019c 100644 --- a/src/scss/main.scss +++ b/src/scss/main.scss @@ -21,4 +21,30 @@ > div:not(:first-child) { margin-left: 1rem; } +} + +.flex { + display: flex; + + &-direction-column { + flex-direction: column; + } + + &-direction-row { + flex-direction: row; + } + + &-align-items-center { + align-items: center; + } +} + +.position { + &-relative { + position: relative; + } + + &-absolute { + position: absolute; + } } \ No newline at end of file diff --git a/src/scss/variables.scss b/src/scss/variables.scss index 6705122..b377ffd 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -11,6 +11,7 @@ --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); @@ -18,6 +19,7 @@ --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; @@ -47,6 +49,7 @@ --background-95: rgba(30, 31, 34, 0.95); --background-70: rgba(30, 31, 34, 0.8); --background-40: rgba(30, 31, 34, 0.4); + --background-ui: #202125; } } @@ -61,6 +64,7 @@ $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); @@ -74,6 +78,7 @@ $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; @@ -103,6 +108,7 @@ $color-error-highlight: var(--color-error-highlight) !default; --background-color-secondary: #111111; --background-95: rgba(30, 31, 34, 0.95); --background-70: rgba(30, 31, 34, 0.7); + --background-ui: #202125; --color-teal: #091c24; } @@ -117,6 +123,7 @@ $color-error-highlight: var(--color-error-highlight) !default; --background-color-secondary: #ffffff; --background-95: rgba(255, 255, 255, 0.95); --background-70: rgba(255, 255, 255, 0.7); + --background-ui: #edeef0; --background-nav-logo: #081c24; --color-green: #01d277; --color-teal: #091c24; diff --git a/src/store.js b/src/store.js index 32e0e3d..aaba307 100644 --- a/src/store.js +++ b/src/store.js @@ -1,17 +1,19 @@ 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: { - torrentModule, darkmodeModule, - documentTitle + documentTitle, + torrentModule, + userModule } }) diff --git a/yarn.lock b/yarn.lock index be7a22d..9a60d7d 100644 --- a/yarn.lock +++ b/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" 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" @@ -1873,7 +1896,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.3.0, color-convert@^1.9.0, color-convert@^1.9.3: 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== @@ -4660,6 +4683,11 @@ 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"