Merge branch 'feature/user-graphs' into refactor
This commit is contained in:
		| @@ -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", | ||||
|   | ||||
							
								
								
									
										178
									
								
								src/api.js
									
									
									
									
									
								
							
							
						
						
									
										178
									
								
								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 | ||||
| } | ||||
|   | ||||
							
								
								
									
										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 }"> | ||||
|     <h2>{{ title }}</h2> | ||||
|  | ||||
|     <span v-if="info" class="result-count">{{ info }}</span> | ||||
|     <router-link v-else-if="link" :to="link" class='view-more'> | ||||
|     <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}`"> | ||||
|       View All | ||||
|     </router-link> | ||||
|   </header>   | ||||
| @@ -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 { | ||||
| <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; | ||||
|   padding: 1.8rem 12px; | ||||
|   align-items: center; | ||||
|   padding-left: 0.75rem; | ||||
|   padding-right: 0.75rem; | ||||
|  | ||||
|   &.sticky { | ||||
|     background-color: $background-color; | ||||
| @@ -51,22 +58,19 @@ header { | ||||
|     position: -webkit-sticky; | ||||
|     top: $header-size; | ||||
|     z-index: 4; | ||||
|  | ||||
|     padding-bottom: 1rem; | ||||
|     margin-bottom: 1.5rem; | ||||
|   } | ||||
|  | ||||
|   h2 { | ||||
|     font-size: 18px; | ||||
|     font-size: 1.4rem; | ||||
|     font-weight: 300; | ||||
|     text-transform: capitalize; | ||||
|     line-height: 18px; | ||||
|     line-height: 1.4rem; | ||||
|     margin: 0; | ||||
|     color: $text-color; | ||||
|   } | ||||
|  | ||||
|   .view-more { | ||||
|     font-size: 13px; | ||||
|     font-size: 0.9rem; | ||||
|     font-weight: 300; | ||||
|     letter-spacing: .5px; | ||||
|     color: $text-color-70; | ||||
| @@ -82,12 +86,13 @@ header { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .result-count { | ||||
|   .info { | ||||
|     font-size: 13px; | ||||
|     font-weight: 300; | ||||
|     letter-spacing: .5px; | ||||
|     color: $text-color; | ||||
|     text-decoration: none; | ||||
|     text-align: right; | ||||
|   } | ||||
|  | ||||
|   @include tablet-min { | ||||
|   | ||||
| @@ -151,7 +151,7 @@ export default { | ||||
|       this.title = movie.title | ||||
|       this.poster = movie.poster | ||||
|       this.backdrop = movie.backdrop | ||||
|       this.matched = movie.existsInPlex | ||||
|       this.matched = movie.exists_in_plex || false | ||||
|       this.checkIfRequested(movie) | ||||
|         .then(status => this.requested = status) | ||||
|  | ||||
| @@ -161,9 +161,7 @@ export default { | ||||
|       return await getRequestStatus(movie.id, movie.type) | ||||
|     }, | ||||
|     nestedDataToString(data) { | ||||
|       let nestedArray = [] | ||||
|       data.forEach(item => nestedArray.push(item)); | ||||
|       return nestedArray.join(', '); | ||||
|       return data.join(', ') | ||||
|     }, | ||||
|     sendRequest(){ | ||||
|       request(this.id, this.type, storage.token) | ||||
| @@ -200,7 +198,7 @@ export default { | ||||
|     this.prevDocumentTitle = store.getters['documentTitle/title'] | ||||
|  | ||||
|     if (this.type === 'movie') { | ||||
|       getMovie(this.id) | ||||
|       getMovie(this.id, true) | ||||
|         .then(this.parseResponse) | ||||
|         .catch(error => { | ||||
|           this.$router.push({ name: '404' }); | ||||
|   | ||||
| @@ -6,6 +6,11 @@ | ||||
|       <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 }}</p> | ||||
| @@ -67,7 +72,7 @@ export default { | ||||
|   } | ||||
|  | ||||
|   @include desktop-lg-min{ | ||||
|     padding: 20px; | ||||
|     padding: 15px; | ||||
|     width: 12.5%; | ||||
|   } | ||||
|  | ||||
| @@ -115,3 +120,46 @@ 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> | ||||
| @@ -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,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 => { | ||||
|   | ||||
| @@ -18,7 +18,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import axios from 'axios' | ||||
| import { register } from '@/api' | ||||
| import SeasonedButton from '@/components/ui/SeasonedButton' | ||||
| import SeasonedInput from '@/components/ui/SeasonedInput' | ||||
| import SeasonedMessages from '@/components/ui/SeasonedMessages' | ||||
| @@ -40,12 +40,9 @@ 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; | ||||
|  | ||||
|         register(username, password) | ||||
|           .then(data => { | ||||
|             if (data.success){ | ||||
|               localStorage.setItem('token', data.token); | ||||
|               localStorage.setItem('username', username); | ||||
| @@ -56,7 +53,7 @@ export default { | ||||
|             } | ||||
|         }) | ||||
|         .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 { | ||||
|   | ||||
| @@ -3,6 +3,8 @@ | ||||
|     <div class="profile__content" v-if="userLoggedIn"> | ||||
|       <section class='settings'> | ||||
|         <h3 class='settings__header'>Plex account</h3> | ||||
|  | ||||
|         <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"> | ||||
| @@ -11,9 +13,13 @@ | ||||
|               :value.sync="plexPassword" @submit="authenticatePlex" /> | ||||
|  | ||||
|             <seasoned-button @click="authenticatePlex">link plex account</seasoned-button> | ||||
|  | ||||
|           <seasoned-messages :messages.sync="messages" /> | ||||
|           </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'> | ||||
|  | ||||
| @@ -44,12 +50,13 @@ | ||||
| </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 { plexAuthenticate } from '@/api' | ||||
| import { getSettings, updateSettings, linkPlexAccount, unlinkPlexAccount } from '@/api' | ||||
|  | ||||
| export default { | ||||
|   components: { SeasonedInput, SeasonedButton, SeasonedMessages }, | ||||
| @@ -60,7 +67,21 @@ export default { | ||||
|       plexUsername: null, | ||||
|       plexPassword: 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: { | ||||
| @@ -70,26 +91,37 @@ export default { | ||||
|     changePassword() { | ||||
|       return | ||||
|     }, | ||||
|     authenticatePlex() { | ||||
|     async authenticatePlex() { | ||||
|       let username = this.plexUsername | ||||
|       let password = this.plexPassword | ||||
|  | ||||
|       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' }) | ||||
|       const response = await linkPlexAccount(username, password) | ||||
|  | ||||
|         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(){ | ||||
|     if (localStorage.getItem('token')){ | ||||
|     const token = localStorage.getItem('token') || false; | ||||
|     if (token){ | ||||
|       this.userLoggedIn = true | ||||
|     } | ||||
|   } | ||||
| @@ -151,7 +183,7 @@ a { | ||||
|       display: block; | ||||
|       height: 1px; | ||||
|       border: 0; | ||||
|       border-bottom: 1px solid rgba(8, 28, 36, 0.05); | ||||
|       border-bottom: 1px solid $text-color-50; | ||||
|       margin-top: 30px; | ||||
|       margin-bottom: 70px; | ||||
|       margin-left: 20px; | ||||
|   | ||||
| @@ -2,7 +2,10 @@ | ||||
|   <section> | ||||
|     <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-button @click="signin">sign in</seasoned-button> | ||||
| @@ -16,7 +19,7 @@ | ||||
|  | ||||
|  | ||||
| <script> | ||||
| import axios from 'axios' | ||||
| import { login } from '@/api' | ||||
| import storage from '../storage' | ||||
| import SeasonedInput from '@/components/ui/SeasonedInput' | ||||
| import SeasonedButton from '@/components/ui/SeasonedButton' | ||||
| @@ -39,23 +42,19 @@ export default { | ||||
|       let username = this.username; | ||||
|       let password = this.password; | ||||
|  | ||||
|       axios.post(`https://api.kevinmidboe.com/api/v1/user/login`, { | ||||
|         username: username, | ||||
|         password: password | ||||
|       }) | ||||
|       .then(resp => { | ||||
|         let data = resp.data; | ||||
|       login(username, password) | ||||
|         .then(data => { | ||||
|           if (data.success){ | ||||
|             localStorage.setItem('token', data.token); | ||||
|             localStorage.setItem('username', username); | ||||
|           localStorage.setItem('admin', data.admin); | ||||
|             localStorage.setItem('admin', data.admin || false); | ||||
|  | ||||
|             eventHub.$emit('setUserStatus'); | ||||
|             this.$router.push({ name: 'profile' }) | ||||
|           } | ||||
|         }) | ||||
|         .catch(error => { | ||||
|         if (error.message.endsWith('401')) { | ||||
|           if (error.status === 401) { | ||||
|             this.messages.push({ type: 'warning', title: 'Access denied', message: 'Incorrect username or password' }) | ||||
|           } | ||||
|           else { | ||||
|   | ||||
							
								
								
									
										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> | ||||
							
								
								
									
										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: '/', | ||||
|     component: (resolve) => require(['./components/Home.vue'], resolve) | ||||
|   }, | ||||
|   { | ||||
|     name: 'activity', | ||||
|     path: '/activity', | ||||
|     component: (resolve) => require(['./components/ActivityPage.vue'], resolve) | ||||
|   }, | ||||
|   { | ||||
|     name: 'profile', | ||||
|     path: '/profile', | ||||
|   | ||||
| @@ -22,3 +22,29 @@ | ||||
|     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; | ||||
|   } | ||||
| } | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   } | ||||
| }) | ||||
|  | ||||
|   | ||||
							
								
								
									
										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" | ||||
|   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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user