diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index 7053c49..0000000
--- a/.editorconfig
+++ /dev/null
@@ -1,5 +0,0 @@
-[*.{js,jsx,ts,tsx,vue}]
-indent_style = space
-indent_size = 2
-trim_trailing_whitespace = true
-insert_final_newline = true
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7c550ac..bb1482f 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -35,6 +35,7 @@
+
@@ -45,8 +46,7 @@
-
-
+
diff --git a/package.json b/package.json
index 9c8d18f..6fd7f36 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,55 @@
"parserOptions": {
"parser": "@babel/eslint-parser"
},
- "rules": {}
+ "rules": {
+ "no-tabs": "off",
+ "indent": [
+ "error",
+ "tab",
+ {
+ "SwitchCase": 1,
+ "VariableDeclarator": 1,
+ "outerIIFEBody": 1,
+ "MemberExpression": 1,
+ "FunctionDeclaration": {
+ "parameters": 1,
+ "body": 1
+ },
+ "FunctionExpression": {
+ "parameters": 1,
+ "body": 1
+ },
+ "CallExpression": {
+ "arguments": 1
+ },
+ "ArrayExpression": 1,
+ "ObjectExpression": 1,
+ "ImportDeclaration": 1,
+ "flatTernaryExpressions": false,
+ "ignoreComments": false,
+ "ignoredNodes": [
+ "TemplateLiteral *",
+ "JSXElement",
+ "JSXElement > *",
+ "JSXAttribute",
+ "JSXIdentifier",
+ "JSXNamespacedName",
+ "JSXMemberExpression",
+ "JSXSpreadAttribute",
+ "JSXExpressionContainer",
+ "JSXOpeningElement",
+ "JSXClosingElement",
+ "JSXFragment",
+ "JSXOpeningFragment",
+ "JSXClosingFragment",
+ "JSXText",
+ "JSXEmptyExpression",
+ "JSXSpreadChild"
+ ],
+ "offsetTernaryExpressions": true
+ }
+ ]
+ }
},
"browserslist": [
"> 1% in US",
diff --git a/src/App.vue b/src/App.vue
index c63d9e4..3753cbb 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -95,104 +95,104 @@ import { changeLocale } from '@/plugins/i18n'
import AuthenticationModal from '@/components/AuthenticationModal'
export default {
- name: 'App',
- components: {
- AuthenticationModal,
- SearchMenu
- },
- metaInfo () {
- return {
- title: this.$t('titles.homepage'),
- // all titles will be injected into this template
- titleTemplate: '%s - Piped'
- }
- },
+ name: 'App',
+ components: {
+ AuthenticationModal,
+ SearchMenu
+ },
+ metaInfo () {
+ return {
+ title: this.$t('titles.homepage'),
+ // all titles will be injected into this template
+ titleTemplate: '%s - Piped'
+ }
+ },
- data: () => ({
- languageOptions: [
- { value: 'zh_Hant', text: 'Chinese (Traditional)' },
- { value: 'en', text: 'English' },
- { value: 'fr', text: 'French' },
- { value: 'de', text: 'German' },
- { value: 'el', text: 'Greek' },
- { value: 'zh_Hans', text: 'Chinese (Simplified, only loads the font)' },
- { value: 'jp', text: 'Japanese (only loads fonts)' },
- { value: 'kr', text: 'Korean (only loads fonts)' },
- { value: 'cyrl', text: 'Cyrillic Languages (fonts)' },
- { value: 'lt', text: 'Lithuanian' },
- { value: 'ml', text: 'Malayalam' },
- { value: 'nb_NO', text: 'Norwegian Bokmål' },
- { value: 'tr', text: 'Turkish' },
- { value: 'bn_beng', text: 'Bengali (বাংলা)' }
- // Incomplete, DO NOT USE
- /* { value: 'bn_latn', text: 'Bengali (Latin)' }, */
- ].sort((a, b) => {
- return a.text.localeCompare(b.text)
- }),
- drawer: false
- }),
+ data: () => ({
+ languageOptions: [
+ { value: 'zh_Hant', text: 'Chinese (Traditional)' },
+ { value: 'en', text: 'English' },
+ { value: 'fr', text: 'French' },
+ { value: 'de', text: 'German' },
+ { value: 'el', text: 'Greek' },
+ { value: 'zh_Hans', text: 'Chinese (Simplified, only loads the font)' },
+ { value: 'jp', text: 'Japanese (only loads fonts)' },
+ { value: 'kr', text: 'Korean (only loads fonts)' },
+ { value: 'cyrl', text: 'Cyrillic Languages (fonts)' },
+ { value: 'lt', text: 'Lithuanian' },
+ { value: 'ml', text: 'Malayalam' },
+ { value: 'nb_NO', text: 'Norwegian Bokmål' },
+ { value: 'tr', text: 'Turkish' },
+ { value: 'bn_beng', text: 'Bengali (বাংলা)' }
+ // Incomplete, DO NOT USE
+ /* { value: 'bn_latn', text: 'Bengali (Latin)' }, */
+ ].sort((a, b) => {
+ return a.text.localeCompare(b.text)
+ }),
+ drawer: false
+ }),
- computed: {
- bgStyles () {
- return {
- backgroundColor: this.$vuetify.theme.dark ? '#282828' : '#fbf1c7'
- }
- },
+ computed: {
+ bgStyles () {
+ return {
+ backgroundColor: this.$vuetify.theme.dark ? '#282828' : '#fbf1c7'
+ }
+ },
- links () {
- const links = [
- {
- id: 'trending',
- name: 'titles.trending',
- to: '/trending'
- },
- {
- id: 'prefs',
- name: 'titles.preferences',
- to: '/preferences'
- },
- {
- id: 'watch-history',
- name: 'titles.history',
- to: '/watch-history'
- }
- ]
+ links () {
+ const links = [
+ {
+ id: 'trending',
+ name: 'titles.trending',
+ to: '/trending'
+ },
+ {
+ id: 'prefs',
+ name: 'titles.preferences',
+ to: '/preferences'
+ },
+ {
+ id: 'watch-history',
+ name: 'titles.history',
+ to: '/watch-history'
+ }
+ ]
- if (this.$store.getters['auth/isCurrentlyAuthenticated']) {
- links.splice(0, 0, {
- id: 'feed',
- name: 'titles.feed',
- to: '/feed'
- })
- }
+ if (this.$store.getters['auth/isCurrentlyAuthenticated']) {
+ links.splice(0, 0, {
+ id: 'feed',
+ name: 'titles.feed',
+ to: '/feed'
+ })
+ }
- return links
- }
- },
+ return links
+ }
+ },
- watch: {
- '$store.state.prefs.prefs.darkMode' (newVal) {
- this.$vuetify.theme.dark = newVal
- }
- },
+ watch: {
+ '$store.state.prefs.prefs.darkMode' (newVal) {
+ this.$vuetify.theme.dark = newVal
+ }
+ },
- methods: {
- changeLocale (lang) {
- return changeLocale(lang)
- },
+ methods: {
+ changeLocale (lang) {
+ return changeLocale(lang)
+ },
- toggleDarkMode () {
- this.$vuetify.theme.dark = !this.$vuetify.theme.dark
- this.$store.commit('prefs/setPrefs', {
- id: 'darkMode',
- value: this.$vuetify.theme.dark
- })
- }
- },
+ toggleDarkMode () {
+ this.$vuetify.theme.dark = !this.$vuetify.theme.dark
+ this.$store.commit('prefs/setPrefs', {
+ id: 'darkMode',
+ value: this.$vuetify.theme.dark
+ })
+ }
+ },
- created () {
- this.$store.dispatch('prefs/loadState')
- this.$store.dispatch('auth/initializeAuth')
- }
+ created () {
+ this.$store.dispatch('prefs/loadState')
+ this.$store.dispatch('auth/initializeAuth')
+ }
}
diff --git a/src/components/AuthenticationModal.vue b/src/components/AuthenticationModal.vue
index 88b294b..1e5cc95 100644
--- a/src/components/AuthenticationModal.vue
+++ b/src/components/AuthenticationModal.vue
@@ -47,50 +47,50 @@
import { AuthenticationError } from '@/store/authentication-store'
export default {
- name: 'AuthenticationModal',
- props: ['listMode'],
- data: () => ({
- error: null,
- requestInProgress: false,
- dialogOpen: false,
- username: '',
- password: ''
- }),
- methods: {
- async doCall (path) {
- this.error = null
- try {
- this.requestInProgress = true
- await this.$store.dispatch('auth/loginOrRegister', {
- path,
- username: this.username,
- password: this.password
- })
- this.dialogOpen = false
- } catch (e) {
- if (!(e instanceof AuthenticationError)) {
- throw e
- }
+ name: 'AuthenticationModal',
+ props: ['listMode'],
+ data: () => ({
+ error: null,
+ requestInProgress: false,
+ dialogOpen: false,
+ username: '',
+ password: ''
+ }),
+ methods: {
+ async doCall (path) {
+ this.error = null
+ try {
+ this.requestInProgress = true
+ await this.$store.dispatch('auth/loginOrRegister', {
+ path,
+ username: this.username,
+ password: this.password
+ })
+ this.dialogOpen = false
+ } catch (e) {
+ if (!(e instanceof AuthenticationError)) {
+ throw e
+ }
- this.error = e.message
- } finally {
- this.requestInProgress = false
- }
- },
+ this.error = e.message
+ } finally {
+ this.requestInProgress = false
+ }
+ },
- logOut () {
- this.$store.commit('auth/deleteAuthToken', {
- apiURL: this.$store.getters['prefs/apiUrl']
- })
- },
+ logOut () {
+ this.$store.commit('auth/deleteAuthToken', {
+ apiURL: this.$store.getters['prefs/apiUrl']
+ })
+ },
- login () {
- return this.doCall('login')
- },
+ login () {
+ return this.doCall('login')
+ },
- register () {
- return this.doCall('register')
- }
- }
+ register () {
+ return this.doCall('register')
+ }
+ }
}
diff --git a/src/components/ErrorHandler.vue b/src/components/ErrorHandler.vue
index 2f798fd..cc7fbf5 100644
--- a/src/components/ErrorHandler.vue
+++ b/src/components/ErrorHandler.vue
@@ -8,12 +8,12 @@
diff --git a/src/components/ExpandableDate.vue b/src/components/ExpandableDate.vue
index c03442e..eeb61ba 100644
--- a/src/components/ExpandableDate.vue
+++ b/src/components/ExpandableDate.vue
@@ -13,16 +13,16 @@
import { LibPiped } from '@/tools/libpiped'
export default {
- name: 'ExpandableDate',
- props: ['date'],
- computed: {
- timeAgo () {
- return LibPiped.timeAgo(this.date)
- },
+ name: 'ExpandableDate',
+ props: ['date'],
+ computed: {
+ timeAgo () {
+ return LibPiped.timeAgo(this.date)
+ },
- formattedDate () {
- return LibPiped.formatFullDate(this.date)
- }
- }
+ formattedDate () {
+ return LibPiped.formatFullDate(this.date)
+ }
+ }
}
diff --git a/src/components/GenericDisplayItem.vue b/src/components/GenericDisplayItem.vue
index 649f155..c1e188d 100644
--- a/src/components/GenericDisplayItem.vue
+++ b/src/components/GenericDisplayItem.vue
@@ -22,16 +22,16 @@
import { isNumber } from 'lodash-es'
export default {
- props: {
- item: Object,
- height: Number,
- width: Number,
- hideChannel: Boolean
- },
- methods: {
- isNumber (...args) {
- return isNumber(...args)
- }
- }
+ props: {
+ item: Object,
+ height: Number,
+ width: Number,
+ hideChannel: Boolean
+ },
+ methods: {
+ isNumber (...args) {
+ return isNumber(...args)
+ }
+ }
}
diff --git a/src/components/Player.vue b/src/components/Player.vue
index c9e6f16..fcb5c35 100644
--- a/src/components/Player.vue
+++ b/src/components/Player.vue
@@ -43,310 +43,310 @@ import { setupKeybindings } from '@/plugins/keybindings'
window.muxjs = muxjs
export default {
- props: {
- video: Object,
- sponsors: Object,
- skipToTime: Number,
- selectedAutoLoop: Boolean
- },
- data () {
- return {
- $player: null,
- $ui: null
- }
- },
- computed: {
- shouldAutoPlay () {
- return this.$store.getters['prefs/getPreferenceBoolean']('playerAutoPlay', true)
- },
+ props: {
+ video: Object,
+ sponsors: Object,
+ skipToTime: Number,
+ selectedAutoLoop: Boolean
+ },
+ data () {
+ return {
+ $player: null,
+ $ui: null
+ }
+ },
+ computed: {
+ shouldAutoPlay () {
+ return this.$store.getters['prefs/getPreferenceBoolean']('playerAutoPlay', true)
+ },
- preferredVideoCodecs () {
- const preferredVideoCodecs = []
- if (this.$refs.videoEl.canPlayType('video/mp4; codecs="av01.0.08M.08"') !== '') { preferredVideoCodecs.push('av01') }
- if (this.$refs.videoEl.canPlayType('video/webm; codecs="vp9"') !== '') preferredVideoCodecs.push('vp9')
- if (this.$refs.videoEl.canPlayType('video/mp4; codecs="avc1.4d401f"') !== '') { preferredVideoCodecs.push('avc1') }
- return preferredVideoCodecs
- }
- },
- methods: {
- getCurrentTime () {
- // UNCLEAN
- return this.$refs.videoEl ? this.$refs.videoEl.currentTime : undefined
- },
+ preferredVideoCodecs () {
+ const preferredVideoCodecs = []
+ if (this.$refs.videoEl.canPlayType('video/mp4; codecs="av01.0.08M.08"') !== '') { preferredVideoCodecs.push('av01') }
+ if (this.$refs.videoEl.canPlayType('video/webm; codecs="vp9"') !== '') preferredVideoCodecs.push('vp9')
+ if (this.$refs.videoEl.canPlayType('video/mp4; codecs="avc1.4d401f"') !== '') { preferredVideoCodecs.push('avc1') }
+ return preferredVideoCodecs
+ }
+ },
+ methods: {
+ getCurrentTime () {
+ // UNCLEAN
+ return this.$refs.videoEl ? this.$refs.videoEl.currentTime : undefined
+ },
- async loadVideo () {
- console.log('PIPED | LOADING VIDEO')
- const component = this
- const videoEl = this.$refs.videoEl
+ async loadVideo () {
+ console.log('PIPED | LOADING VIDEO')
+ const component = this
+ const videoEl = this.$refs.videoEl
- videoEl.setAttribute('poster', this.video.thumbnailUrl)
+ videoEl.setAttribute('poster', this.video.thumbnailUrl)
- if (this.skipToTime != null) videoEl.currentTime = this.skipToTime
+ if (this.skipToTime != null) videoEl.currentTime = this.skipToTime
- const noPrevPlayer = !this.$player
+ const noPrevPlayer = !this.$player
- const streams = []
+ const streams = []
- streams.push(...this.video.audioStreams)
- streams.push(...this.video.videoStreams)
+ streams.push(...this.video.audioStreams)
+ streams.push(...this.video.videoStreams)
- const MseSupport = window.MediaSource !== undefined
- const lbry = this.$store.getters['prefs/getPreferenceBoolean']('disableLBRY', true)
- ? null
- : this.video.videoStreams.filter(stream => stream.quality === 'LBRY')[0]
- let uri, mime
+ const MseSupport = window.MediaSource !== undefined
+ const lbry = this.$store.getters['prefs/getPreferenceBoolean']('disableLBRY', true)
+ ? null
+ : this.video.videoStreams.filter(stream => stream.quality === 'LBRY')[0]
+ let uri, mime
- if (this.video.livestream) {
- uri = this.video.hls
- mime = 'application/x-mpegURL'
- } else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
- if (!this.video.dash) {
- const dash = DashUtils.generate_dash_file_from_formats(
- streams,
- this.video.duration
- )
- uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash)
- } else {
- uri = this.video.dash
- }
- mime = 'application/dash+xml'
- } else if (lbry) {
- uri = lbry.url
- if (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true)) {
- const url = new URL(uri)
- url.searchParams.set('host', url.host)
- url.host = new URL(this.video.proxyUrl).host
- uri = url.toString()
- }
- mime = await fetch(uri, {
- method: 'HEAD'
- }).then(response => response.headers.get('Content-Type'))
- } else if (this.video.hls) {
- uri = this.video.hls
- mime = 'application/x-mpegURL'
- } else {
- uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url
- }
+ if (this.video.livestream) {
+ uri = this.video.hls
+ mime = 'application/x-mpegURL'
+ } else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
+ if (!this.video.dash) {
+ const dash = DashUtils.generate_dash_file_from_formats(
+ streams,
+ this.video.duration
+ )
+ uri = 'data:application/dash+xml;charset=utf-8;base64,' + btoa(dash)
+ } else {
+ uri = this.video.dash
+ }
+ mime = 'application/dash+xml'
+ } else if (lbry) {
+ uri = lbry.url
+ if (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true)) {
+ const url = new URL(uri)
+ url.searchParams.set('host', url.host)
+ url.host = new URL(this.video.proxyUrl).host
+ uri = url.toString()
+ }
+ mime = await fetch(uri, {
+ method: 'HEAD'
+ }).then(response => response.headers.get('Content-Type'))
+ } else if (this.video.hls) {
+ uri = this.video.hls
+ mime = 'application/x-mpegURL'
+ } else {
+ uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url
+ }
- if (noPrevPlayer) {
- shaka.polyfill.installAll()
+ if (noPrevPlayer) {
+ shaka.polyfill.installAll()
- const localPlayer = new shaka.Player(videoEl)
- const proxyHost = new URL(component.video.proxyUrl).host
+ const localPlayer = new shaka.Player(videoEl)
+ const proxyHost = new URL(component.video.proxyUrl).host
- localPlayer.getNetworkingEngine().registerRequestFilter((_type, request) => {
- const uri = request.uris[0]
- const url = new URL(uri)
- const headers = request.headers
- if (
- url.host.endsWith('.googlevideo.com') ||
+ localPlayer.getNetworkingEngine().registerRequestFilter((_type, request) => {
+ const uri = request.uris[0]
+ const url = new URL(uri)
+ const headers = request.headers
+ if (
+ url.host.endsWith('.googlevideo.com') ||
(url.host.endsWith('.lbryplayer.xyz') && (this.$store.getters['prefs/getPreferenceBoolean']('proxyLBRY', true) || headers.Range))
- ) {
- url.searchParams.set('host', url.host)
- url.host = proxyHost
- request.uris[0] = url.toString()
- }
+ ) {
+ url.searchParams.set('host', url.host)
+ url.host = proxyHost
+ request.uris[0] = url.toString()
+ }
- if (url.pathname === '/videoplayback') {
- if (headers.Range) {
- url.searchParams.set('range', headers.Range.split('=')[1])
- request.headers = {}
- request.uris[0] = url.toString()
- }
- }
- })
+ if (url.pathname === '/videoplayback') {
+ if (headers.Range) {
+ url.searchParams.set('range', headers.Range.split('=')[1])
+ request.headers = {}
+ request.uris[0] = url.toString()
+ }
+ }
+ })
- localPlayer.configure(
- 'streaming.bufferingGoal',
- this.$store.getters['prefs/getPreferenceNumber']('bufferGoal', 10)
- )
+ localPlayer.configure(
+ 'streaming.bufferingGoal',
+ this.$store.getters['prefs/getPreferenceNumber']('bufferGoal', 10)
+ )
- this.setPlayerAttrs(localPlayer, videoEl, uri, mime, shaka)
- } else this.setPlayerAttrs(this.$player, videoEl, uri, mime, shaka)
+ this.setPlayerAttrs(localPlayer, videoEl, uri, mime, shaka)
+ } else this.setPlayerAttrs(this.$player, videoEl, uri, mime, shaka)
- if (noPrevPlayer) {
- videoEl.addEventListener('timeupdate', (ev) => {
- if (this.sponsors && this.sponsors.segments) {
- const time = videoEl.currentTime
- this.sponsors.segments.forEach(segment => {
- if (!segment.skipped || this.selectedAutoLoop) {
- const end = segment.segment[1]
- if (time >= segment.segment[0] && time < end) {
- console.log('Skipped segment at ' + time)
- videoEl.currentTime = end
- segment.skipped = true
- }
- }
- })
- }
- this.$emit('timeupdate', ev)
- })
+ if (noPrevPlayer) {
+ videoEl.addEventListener('timeupdate', (ev) => {
+ if (this.sponsors && this.sponsors.segments) {
+ const time = videoEl.currentTime
+ this.sponsors.segments.forEach(segment => {
+ if (!segment.skipped || this.selectedAutoLoop) {
+ const end = segment.segment[1]
+ if (time >= segment.segment[0] && time < end) {
+ console.log('Skipped segment at ' + time)
+ videoEl.currentTime = end
+ segment.skipped = true
+ }
+ }
+ })
+ }
+ this.$emit('timeupdate', ev)
+ })
- videoEl.addEventListener('volumechange', () => {
- this.$store.commit('prefs/setPrefs', {
- id: 'volume',
- value: videoEl.volume
- })
- })
+ videoEl.addEventListener('volumechange', () => {
+ this.$store.commit('prefs/setPrefs', {
+ id: 'volume',
+ value: videoEl.volume
+ })
+ })
- videoEl.addEventListener('ratechange', () => {
- this.$store.commit('prefs/setPrefs', {
- id: 'rate',
- value: videoEl.playbackRate
- })
- })
+ videoEl.addEventListener('ratechange', () => {
+ this.$store.commit('prefs/setPrefs', {
+ id: 'rate',
+ value: videoEl.playbackRate
+ })
+ })
- videoEl.addEventListener('ended', () => {
- this.$emit('videoEnded')
- })
- }
+ videoEl.addEventListener('ended', () => {
+ this.$emit('videoEnded')
+ })
+ }
- // TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
- },
- setPlayerAttrs (localPlayer, videoEl, uri, mime, shaka) {
- if (!this.$ui) {
- this.$ui = new shaka.ui.Overlay(localPlayer, this.$refs.container, videoEl)
+ // TODO: Add sponsors on seekbar: https://github.com/ajayyy/SponsorBlock/blob/e39de9fd852adb9196e0358ed827ad38d9933e29/src/js-components/previewBar.ts#L12
+ },
+ setPlayerAttrs (localPlayer, videoEl, uri, mime, shaka) {
+ if (!this.$ui) {
+ this.$ui = new shaka.ui.Overlay(localPlayer, this.$refs.container, videoEl)
- const config = {
- overflowMenuButtons: ['quality', 'captions', 'picture_in_picture', 'playback_rate'],
- seekBarColors: {
- base: 'rgba(255, 255, 255, 0.3)',
- buffered: 'rgba(255, 255, 255, 0.54)',
- played: 'rgb(255, 0, 0)'
- }
- }
+ const config = {
+ overflowMenuButtons: ['quality', 'captions', 'picture_in_picture', 'playback_rate'],
+ seekBarColors: {
+ base: 'rgba(255, 255, 255, 0.3)',
+ buffered: 'rgba(255, 255, 255, 0.54)',
+ played: 'rgb(255, 0, 0)'
+ }
+ }
- this.$ui.configure(config)
- }
+ this.$ui.configure(config)
+ }
- const player = this.$ui.getControls().getPlayer()
+ const player = this.$ui.getControls().getPlayer()
- this.$player = player
+ this.$player = player
- const disableVideo = this.$store.getters['prefs/getPreferenceBoolean']('listen', false) && !this.video.livestream
+ const disableVideo = this.$store.getters['prefs/getPreferenceBoolean']('listen', false) && !this.video.livestream
- this.$player.configure({
- preferredVideoCodecs: this.preferredVideoCodecs,
- preferredAudioCodecs: ['opus', 'mp4a'],
- manifest: {
- disableVideo: disableVideo,
- hls: {
- useFullSegmentsForStartTime: true
- }
- },
- streaming: {
- useNativeHlsOnSafari: false
- }
- })
+ this.$player.configure({
+ preferredVideoCodecs: this.preferredVideoCodecs,
+ preferredAudioCodecs: ['opus', 'mp4a'],
+ manifest: {
+ disableVideo: disableVideo,
+ hls: {
+ useFullSegmentsForStartTime: true
+ }
+ },
+ streaming: {
+ useNativeHlsOnSafari: false
+ }
+ })
- const quality = this.$store.getters['prefs/getPreferenceNumber']('quality', 0)
- const qualityConds = quality > 0 && (this.video.audioStreams.length > 0 || this.video.livestream) && !disableVideo
- if (qualityConds) this.$player.configure('abr.enabled', false)
+ const quality = this.$store.getters['prefs/getPreferenceNumber']('quality', 0)
+ const qualityConds = quality > 0 && (this.video.audioStreams.length > 0 || this.video.livestream) && !disableVideo
+ if (qualityConds) this.$player.configure('abr.enabled', false)
- player.load(uri, 0, mime).then(() => {
- if (qualityConds) {
- let leastDiff = Number.MAX_VALUE
- let bestStream = null
- player
- .getVariantTracks()
- .sort((a, b) => a.bandwidth - b.bandwidth)
- .forEach(stream => {
- const diff = Math.abs(quality - stream.height)
- if (diff < leastDiff) {
- leastDiff = diff
- bestStream = stream
- }
- })
- player.selectVariantTrack(bestStream)
- }
+ player.load(uri, 0, mime).then(() => {
+ if (qualityConds) {
+ let leastDiff = Number.MAX_VALUE
+ let bestStream = null
+ player
+ .getVariantTracks()
+ .sort((a, b) => a.bandwidth - b.bandwidth)
+ .forEach(stream => {
+ const diff = Math.abs(quality - stream.height)
+ if (diff < leastDiff) {
+ leastDiff = diff
+ bestStream = stream
+ }
+ })
+ player.selectVariantTrack(bestStream)
+ }
- this.video.subtitles.forEach(subtitle => {
- player.addTextTrackAsync(
- subtitle.url,
- subtitle.code,
- 'SUBTITLE',
- subtitle.mimeType,
- null,
- subtitle.name
- )
- })
- videoEl.volume = this.$store.getters['prefs/getPreferenceNumber']('volume', 1)
- player.trickPlay(this.$store.getters['prefs/getPreferenceNumber']('rate', 1))
- })
- }
- },
+ this.video.subtitles.forEach(subtitle => {
+ player.addTextTrackAsync(
+ subtitle.url,
+ subtitle.code,
+ 'SUBTITLE',
+ subtitle.mimeType,
+ null,
+ subtitle.name
+ )
+ })
+ videoEl.volume = this.$store.getters['prefs/getPreferenceNumber']('volume', 1)
+ player.trickPlay(this.$store.getters['prefs/getPreferenceNumber']('rate', 1))
+ })
+ }
+ },
- watch: {
- 'video.videoId' () {
- this.loadVideo()
- }
- },
+ watch: {
+ 'video.videoId' () {
+ this.loadVideo()
+ }
+ },
- mounted () {
- this.loadVideo()
- const self = this
- const videoEl = self.$refs.videoEl
- const onSpace = (e) => {
- if (videoEl.paused) videoEl.play()
- else videoEl.pause()
- e.preventDefault()
- }
+ mounted () {
+ this.loadVideo()
+ const self = this
+ const videoEl = self.$refs.videoEl
+ const onSpace = (e) => {
+ if (videoEl.paused) videoEl.play()
+ else videoEl.pause()
+ e.preventDefault()
+ }
- this.unsubToKeybindings = setupKeybindings(window, {
- f (e) {
- if (document.fullscreenElement) document.exitFullscreen()
- else self.$refs.container.requestFullscreen()
- e.preventDefault()
- },
- m (e) {
- videoEl.muted = !videoEl.muted
- e.preventDefault()
- },
- j (e) {
- videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0)
- e.preventDefault()
- },
- l (e) {
- videoEl.currentTime = videoEl.currentTime + 15
- e.preventDefault()
- },
- c (e) {
- self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible())
- e.preventDefault()
- },
- Space: onSpace,
- k: onSpace,
- ArrowUp (e) {
- videoEl.volume = Math.min(videoEl.volume + 0.05, 1)
- e.preventDefault()
- },
- ArrowDown (e) {
- videoEl.volume = Math.max(videoEl.volume - 0.05, 0)
- e.preventDefault()
- },
- ArrowLeft (e) {
- videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0)
- e.preventDefault()
- },
- ArrowRight (e) {
- videoEl.currentTime = videoEl.currentTime + 5
- e.preventDefault()
- }
- })
- },
+ this.unsubToKeybindings = setupKeybindings(window, {
+ f (e) {
+ if (document.fullscreenElement) document.exitFullscreen()
+ else self.$refs.container.requestFullscreen()
+ e.preventDefault()
+ },
+ m (e) {
+ videoEl.muted = !videoEl.muted
+ e.preventDefault()
+ },
+ j (e) {
+ videoEl.currentTime = Math.max(videoEl.currentTime - 15, 0)
+ e.preventDefault()
+ },
+ l (e) {
+ videoEl.currentTime = videoEl.currentTime + 15
+ e.preventDefault()
+ },
+ c (e) {
+ self.$player.setTextTrackVisibility(!self.$player.isTextTrackVisible())
+ e.preventDefault()
+ },
+ Space: onSpace,
+ k: onSpace,
+ ArrowUp (e) {
+ videoEl.volume = Math.min(videoEl.volume + 0.05, 1)
+ e.preventDefault()
+ },
+ ArrowDown (e) {
+ videoEl.volume = Math.max(videoEl.volume - 0.05, 0)
+ e.preventDefault()
+ },
+ ArrowLeft (e) {
+ videoEl.currentTime = Math.max(videoEl.currentTime - 5, 0)
+ e.preventDefault()
+ },
+ ArrowRight (e) {
+ videoEl.currentTime = videoEl.currentTime + 5
+ e.preventDefault()
+ }
+ })
+ },
- beforeDestroy () {
- if (this.$ui) {
- this.$ui.destroy()
- this.$ui = undefined
- this.$player = undefined
- }
- if (this.$player) {
- this.$player.destroy()
- this.$player = undefined
- }
- this.unsubToKeybindings()
- this.$refs.container.querySelectorAll('div').forEach(node => node.remove())
- }
+ beforeDestroy () {
+ if (this.$ui) {
+ this.$ui.destroy()
+ this.$ui = undefined
+ this.$player = undefined
+ }
+ if (this.$player) {
+ this.$player.destroy()
+ this.$player = undefined
+ }
+ this.unsubToKeybindings()
+ this.$refs.container.querySelectorAll('div').forEach(node => node.remove())
+ }
}
diff --git a/src/components/SubscriptionButton.vue b/src/components/SubscriptionButton.vue
index 0effb02..9f446db 100644
--- a/src/components/SubscriptionButton.vue
+++ b/src/components/SubscriptionButton.vue
@@ -9,52 +9,52 @@
diff --git a/src/components/VideoComment.vue b/src/components/VideoComment.vue
index 5d8eb14..731a90b 100644
--- a/src/components/VideoComment.vue
+++ b/src/components/VideoComment.vue
@@ -37,47 +37,47 @@ import { LibPiped } from '@/tools/libpiped'
import marked from 'marked'
export default {
- name: 'VideoComment',
- props: ['comment', 'video', 'subComment'],
- data: () => ({
- requestInProgress: false,
- showChildComments: true,
- childComments: []
- }),
- methods: {
- numberFormat (...args) {
- return LibPiped.numberFormat(...args)
- },
- async loadReplies () {
- let nextpage = null
- this.requestInProgress = true
+ name: 'VideoComment',
+ props: ['comment', 'video', 'subComment'],
+ data: () => ({
+ requestInProgress: false,
+ showChildComments: true,
+ childComments: []
+ }),
+ methods: {
+ numberFormat (...args) {
+ return LibPiped.numberFormat(...args)
+ },
+ async loadReplies () {
+ let nextpage = null
+ this.requestInProgress = true
- while (true) {
- const replies = await this.$store.dispatch('auth/makeRequest', {
- method: 'GET',
- path: '/nextpage/comments/' + this.video.videoId,
- params: {
- nextpage: nextpage || this.comment.repliesPage
- }
- })
+ while (true) {
+ const replies = await this.$store.dispatch('auth/makeRequest', {
+ method: 'GET',
+ path: '/nextpage/comments/' + this.video.videoId,
+ params: {
+ nextpage: nextpage || this.comment.repliesPage
+ }
+ })
- this.childComments = this.childComments.concat(replies.comments)
- if (replies.nextpage) {
- nextpage = replies.nextpage
- } else {
- break
- }
- }
- this.requestInProgress = false
- }
- },
+ this.childComments = this.childComments.concat(replies.comments)
+ if (replies.nextpage) {
+ nextpage = replies.nextpage
+ } else {
+ break
+ }
+ }
+ this.requestInProgress = false
+ }
+ },
- computed: {
- renderedCommentTxt () {
- return LibPiped.purifyHTML(marked.parseInline(this.comment.commentText, {
- breaks: true
- }))
- }
- }
+ computed: {
+ renderedCommentTxt () {
+ return LibPiped.purifyHTML(marked.parseInline(this.comment.commentText, {
+ breaks: true
+ }))
+ }
+ }
}
diff --git a/src/components/VideoItem.vue b/src/components/VideoItem.vue
index 31c45dd..673cce2 100644
--- a/src/components/VideoItem.vue
+++ b/src/components/VideoItem.vue
@@ -26,62 +26,62 @@ import { LibPiped } from '@/tools/libpiped'
import { findLastWatch } from '@/store/watched-videos-db'
export default {
- name: 'VideoItem',
- props: {
- video: Object,
- height: Number,
- width: Number,
- hideChannel: Boolean,
- maxHeight: Boolean,
- srcProgress: Number
- },
- data: () => ({
- alreadyWatched: false,
- progress: 0
- }),
- mounted () {
- this.findIfVideoWatched()
- },
- watch: {
- 'video.videoId': 'findIfVideoWatched',
- 'video.url': 'findIfVideoWatched',
- srcProgress: 'findIfVideoWatched'
- },
+ name: 'VideoItem',
+ props: {
+ video: Object,
+ height: Number,
+ width: Number,
+ hideChannel: Boolean,
+ maxHeight: Boolean,
+ srcProgress: Number
+ },
+ data: () => ({
+ alreadyWatched: false,
+ progress: 0
+ }),
+ mounted () {
+ this.findIfVideoWatched()
+ },
+ watch: {
+ 'video.videoId': 'findIfVideoWatched',
+ 'video.url': 'findIfVideoWatched',
+ srcProgress: 'findIfVideoWatched'
+ },
- methods: {
- async findIfVideoWatched () {
- // if it has source progress, it's already seen
- if (this.srcProgress) {
- this.alreadyWatched = true
- this.progress = this.srcProgress
- return
- }
+ methods: {
+ async findIfVideoWatched () {
+ // if it has source progress, it's already seen
+ if (this.srcProgress) {
+ this.alreadyWatched = true
+ this.progress = this.srcProgress
+ return
+ }
- let videoId
- if (this.video.videoId) {
- videoId = this.video.videoId
- } else {
- videoId = LibPiped.determineVideoIdFromPath(this.video.url)
- }
+ let videoId
+ if (this.video.videoId) {
+ videoId = this.video.videoId
+ } else {
+ videoId = LibPiped.determineVideoIdFromPath(this.video.url)
+ }
- if (videoId) {
- const lastVideo = await findLastWatch(videoId)
- if (lastVideo != null) {
- this.alreadyWatched = true
- this.progress = lastVideo.progressPcnt
- } else {
- this.alreadyWatched = false
- }
- }
- },
+ if (videoId) {
+ const lastVideo = await findLastWatch(videoId)
+ if (lastVideo != null) {
+ this.alreadyWatched = true
+ this.progress = lastVideo.progressPcnt
+ } else {
+ this.alreadyWatched = false
+ }
+ }
+ },
- numberFormat (...args) {
- return LibPiped.numberFormat(...args)
- },
+ numberFormat (...args) {
+ return LibPiped.numberFormat(...args)
+ },
- timeFormat (...args) {
- return LibPiped.timeFormat(...args)
- }
- }
+ timeFormat (...args) {
+ return LibPiped.timeFormat(...args)
+ }
+ }
}
diff --git a/src/components/VideoRedirect.vue b/src/components/VideoRedirect.vue
index 859714b..1080f6b 100644
--- a/src/components/VideoRedirect.vue
+++ b/src/components/VideoRedirect.vue
@@ -4,14 +4,14 @@
diff --git a/src/main.js b/src/main.js
index eadb6be..76cc9de 100644
--- a/src/main.js
+++ b/src/main.js
@@ -14,9 +14,9 @@ import './registerServiceWorker'
Vue.config.productionTip = false
new Vue({
- vuetify,
- i18n,
- store,
- router,
- render: h => h(App)
+ vuetify,
+ i18n,
+ store,
+ router,
+ render: h => h(App)
}).$mount('#app')
diff --git a/src/plugins/i18n.js b/src/plugins/i18n.js
index af25ae2..83febaf 100644
--- a/src/plugins/i18n.js
+++ b/src/plugins/i18n.js
@@ -6,93 +6,93 @@ import ENTranslations from '@/translations/en.json'
Vue.use(VueI18n)
const messages = {
- en: ENTranslations
+ en: ENTranslations
}
export const i18n = new VueI18n({
- locale: 'en', // set default locale
- fallbackLocale: 'en',
- messages
+ locale: 'en', // set default locale
+ fallbackLocale: 'en',
+ messages
})
async function syncStylesPerLanguage (locale) {
- switch (locale) {
- // All the latin languages
- case 'de':
- case 'en':
- case 'el':
- case 'fr':
- case 'lt':
- case 'ml':
- case 'tr':
- case 'bn_latn':
- // Don't need to import fonts because Latin fonts are always loaded
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('latin')
- break
- // Bengali script
- case 'bn_beng':
- await import('@fontsource/hind-siliguri/bengali.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('bengali')
- break
- // Other languages
- // NOTE: if you are a native speaker & want to see a different font, just email me or join the channel
- case 'zh_Hant':
- await import('@fontsource/noto-sans-tc/chinese-traditional.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('traditional-chinese')
- break
- case 'zh_Hans':
- await import('@fontsource/noto-sans-sc/chinese-simplified.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('simplified-chinese')
- break
- case 'jp':
- await import('@fontsource/noto-sans-jp/japanese.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('japanese')
- break
- case 'kr':
- await import('@fontsource/noto-sans-kr/korean.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('korean')
- break
- case 'cyrl':
- await import('@fontsource/nunito-sans/cyrillic.css')
- document.body.classList.remove(...document.body.classList)
- document.body.classList.add('cyrillic')
- break
- }
+ switch (locale) {
+ // All the latin languages
+ case 'de':
+ case 'en':
+ case 'el':
+ case 'fr':
+ case 'lt':
+ case 'ml':
+ case 'tr':
+ case 'bn_latn':
+ // Don't need to import fonts because Latin fonts are always loaded
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('latin')
+ break
+ // Bengali script
+ case 'bn_beng':
+ await import('@fontsource/hind-siliguri/bengali.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('bengali')
+ break
+ // Other languages
+ // NOTE: if you are a native speaker & want to see a different font, just email me or join the channel
+ case 'zh_Hant':
+ await import('@fontsource/noto-sans-tc/chinese-traditional.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('traditional-chinese')
+ break
+ case 'zh_Hans':
+ await import('@fontsource/noto-sans-sc/chinese-simplified.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('simplified-chinese')
+ break
+ case 'jp':
+ await import('@fontsource/noto-sans-jp/japanese.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('japanese')
+ break
+ case 'kr':
+ await import('@fontsource/noto-sans-kr/korean.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('korean')
+ break
+ case 'cyrl':
+ await import('@fontsource/nunito-sans/cyrillic.css')
+ document.body.classList.remove(...document.body.classList)
+ document.body.classList.add('cyrillic')
+ break
+ }
}
export async function loadLocale (locale) {
- if (i18n.availableLocales.includes(locale)) {
- return
- }
- // load locale messages with dynamic import
- const messages = await import(/* webpackChunkName: "locale-[request]" */ `@/translations/${locale}.json`)
+ if (i18n.availableLocales.includes(locale)) {
+ return
+ }
+ // load locale messages with dynamic import
+ const messages = await import(/* webpackChunkName: "locale-[request]" */ `@/translations/${locale}.json`)
- // set locale and locale message
- i18n.setLocaleMessage(locale, messages.default)
+ // set locale and locale message
+ i18n.setLocaleMessage(locale, messages.default)
- return Vue.nextTick()
+ return Vue.nextTick()
}
export async function changeLocale (lang) {
- await loadLocale(lang)
- await syncStylesPerLanguage(lang)
- i18n.locale = lang
- window.localStorage.setItem('LOCALE', lang)
+ await loadLocale(lang)
+ await syncStylesPerLanguage(lang)
+ i18n.locale = lang
+ window.localStorage.setItem('LOCALE', lang)
}
function initializeLocalLocale () {
- let lang = window.localStorage.getItem('LOCALE')
- if (lang == null) {
- // Default language
- lang = 'en'
- }
- changeLocale(lang).catch(e => console.error(e))
+ let lang = window.localStorage.getItem('LOCALE')
+ if (lang == null) {
+ // Default language
+ lang = 'en'
+ }
+ changeLocale(lang).catch(e => console.error(e))
}
initializeLocalLocale()
diff --git a/src/plugins/keybindings.js b/src/plugins/keybindings.js
index f7534d5..52b901d 100644
--- a/src/plugins/keybindings.js
+++ b/src/plugins/keybindings.js
@@ -1,19 +1,19 @@
import tinykeys from 'tinykeys'
export function setupKeybindings (el, bindings, ...extra) {
- const newBindings = {}
- for (const [k, f] of Object.entries(bindings)) {
- newBindings[k] = (ev, ...args) => {
- const active = ev.target
- const enteringText = active instanceof HTMLElement &&
+ const newBindings = {}
+ for (const [k, f] of Object.entries(bindings)) {
+ newBindings[k] = (ev, ...args) => {
+ const active = ev.target
+ const enteringText = active instanceof HTMLElement &&
(active.isContentEditable ||
active.tagName === 'INPUT' ||
active.tagName === 'TEXTAREA' ||
active.tagName === 'SELECT'
)
- if (!enteringText) return f(ev, ...args)
- }
- }
+ if (!enteringText) return f(ev, ...args)
+ }
+ }
- return tinykeys(el, newBindings, ...extra)
+ return tinykeys(el, newBindings, ...extra)
}
diff --git a/src/plugins/vuetify.js b/src/plugins/vuetify.js
index 9a8047a..fb06187 100644
--- a/src/plugins/vuetify.js
+++ b/src/plugins/vuetify.js
@@ -4,41 +4,41 @@ import Vuetify from 'vuetify/lib/framework'
Vue.use(Vuetify)
export default new Vuetify({
- theme: {
- // There's apparently a race condition that causes components being rendered shortly before dark mode initialization unable to change to dark mode
- dark: (() => {
- try {
- const preferences = JSON.parse(window.localStorage.getItem('PREFERENCES'))
- return preferences.darkMode
- } catch (e) {
- return false
- }
- })(),
- themes: {
- light: {
- primary: '#458588',
- secondary: '#689d6a',
- accent: '#af3a03',
- error: '#cc241d',
- warning: '#d79921',
- info: '#458588',
- success: '#98971a',
+ theme: {
+ // There's apparently a race condition that causes components being rendered shortly before dark mode initialization unable to change to dark mode
+ dark: (() => {
+ try {
+ const preferences = JSON.parse(window.localStorage.getItem('PREFERENCES'))
+ return preferences.darkMode
+ } catch (e) {
+ return false
+ }
+ })(),
+ themes: {
+ light: {
+ primary: '#458588',
+ secondary: '#689d6a',
+ accent: '#af3a03',
+ error: '#cc241d',
+ warning: '#d79921',
+ info: '#458588',
+ success: '#98971a',
- bgOne: '#fbf1c7',
- bgTwo: '#ebdbb2'
- },
- dark: {
- primary: '#458588',
- secondary: '#689d6a',
- accent: '#af3a03',
- error: '#cc241d',
- warning: '#d79921',
- info: '#458588',
- success: '#98971a',
+ bgOne: '#fbf1c7',
+ bgTwo: '#ebdbb2'
+ },
+ dark: {
+ primary: '#458588',
+ secondary: '#689d6a',
+ accent: '#af3a03',
+ error: '#cc241d',
+ warning: '#d79921',
+ info: '#458588',
+ success: '#98971a',
- bgOne: '#282828',
- bgTwo: '#3c3836'
- }
- }
- }
+ bgOne: '#282828',
+ bgTwo: '#3c3836'
+ }
+ }
+ }
})
diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js
index 8c0c65d..f89ec91 100644
--- a/src/registerServiceWorker.js
+++ b/src/registerServiceWorker.js
@@ -3,31 +3,31 @@
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
- register(`${process.env.BASE_URL}service-worker.js`, {
- ready () {
- console.log(
- 'App is being served from cache by a service worker.\n' +
+ register(`${process.env.BASE_URL}service-worker.js`, {
+ ready () {
+ console.log(
+ 'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
- )
- },
- registered () {
- console.log('Service worker has been registered.')
- },
- cached () {
- console.log('Content has been cached for offline use.')
- },
- updatefound () {
- console.log('New content is downloading.')
- },
- updated () {
- console.log('New content is available; please refresh.')
- window.location.reload()
- },
- offline () {
- console.log('No internet connection found. App is running in offline mode.')
- },
- error (error) {
- console.error('Error during service worker registration:', error)
- }
- })
+ )
+ },
+ registered () {
+ console.log('Service worker has been registered.')
+ },
+ cached () {
+ console.log('Content has been cached for offline use.')
+ },
+ updatefound () {
+ console.log('New content is downloading.')
+ },
+ updated () {
+ console.log('New content is available; please refresh.')
+ window.location.reload()
+ },
+ offline () {
+ console.log('No internet connection found. App is running in offline mode.')
+ },
+ error (error) {
+ console.error('Error during service worker registration:', error)
+ }
+ })
}
diff --git a/src/router/index.js b/src/router/index.js
index 9707080..efd1fc0 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -8,64 +8,64 @@ Vue.use(VueMeta)
Vue.use(VueRouter)
const routes = [
- {
- path: '/',
- name: 'Homepage',
- component: FeedPage
- },
- {
- path: '/trending',
- name: 'Trending',
- component: FeedPage
- },
- {
- path: '/feed',
- name: 'Feed',
- component: FeedPage
- },
- {
- path: '/preferences',
- name: 'Preferences',
- component: () => import('@/routes/Preferences')
- },
- {
- path: '/subscriptions',
- component: () => import('@/routes/Subscriptions')
- },
- {
- path: '/results',
- name: 'SearchResults',
- component: () => import('@/routes/SearchResults')
- },
- {
- path: '/playlist',
- name: 'Playlist',
- component: () => import('@/routes/Playlist')
- },
- {
- path: '/:path(v|w|embed|shorts|watch)/:v?',
- name: 'WatchVideo',
- component: () => import('@/routes/WatchVideo')
- },
- {
- path: '/:path(channel|user|c)/:channelId/:videos?',
- name: 'Channel',
- component: () => import('@/routes/Channel')
- },
- {
- path: '/:videoId([a-zA-Z0-9_-]{11})',
- component: () => import('@/components/VideoRedirect')
- },
- {
- path: '/watch-history',
- component: () => import('@/routes/WatchHistory')
- }
+ {
+ path: '/',
+ name: 'Homepage',
+ component: FeedPage
+ },
+ {
+ path: '/trending',
+ name: 'Trending',
+ component: FeedPage
+ },
+ {
+ path: '/feed',
+ name: 'Feed',
+ component: FeedPage
+ },
+ {
+ path: '/preferences',
+ name: 'Preferences',
+ component: () => import('@/routes/Preferences')
+ },
+ {
+ path: '/subscriptions',
+ component: () => import('@/routes/Subscriptions')
+ },
+ {
+ path: '/results',
+ name: 'SearchResults',
+ component: () => import('@/routes/SearchResults')
+ },
+ {
+ path: '/playlist',
+ name: 'Playlist',
+ component: () => import('@/routes/Playlist')
+ },
+ {
+ path: '/:path(v|w|embed|shorts|watch)/:v?',
+ name: 'WatchVideo',
+ component: () => import('@/routes/WatchVideo')
+ },
+ {
+ path: '/:path(channel|user|c)/:channelId/:videos?',
+ name: 'Channel',
+ component: () => import('@/routes/Channel')
+ },
+ {
+ path: '/:videoId([a-zA-Z0-9_-]{11})',
+ component: () => import('@/components/VideoRedirect')
+ },
+ {
+ path: '/watch-history',
+ component: () => import('@/routes/WatchHistory')
+ }
]
const router = new VueRouter({
- mode: 'history',
- base: process.env.BASE_URL,
- routes
+ mode: 'history',
+ base: process.env.BASE_URL,
+ routes
})
export default router
diff --git a/src/routes/Channel.vue b/src/routes/Channel.vue
index 485127c..f2443a2 100644
--- a/src/routes/Channel.vue
+++ b/src/routes/Channel.vue
@@ -42,103 +42,103 @@ import SubscriptionButton from '@/components/SubscriptionButton'
import { LibPiped } from '@/tools/libpiped'
export default {
- data () {
- return {
- channel: null
- }
- },
- metaInfo () {
- const title = this.channel ? this.channel.name : 'Loading'
+ data () {
+ return {
+ channel: null
+ }
+ },
+ metaInfo () {
+ const title = this.channel ? this.channel.name : 'Loading'
- return {
- title,
- meta: this.channel && [
- {
- name: 'twitter:title',
- content: this.channel.name
- },
- {
- name: 'twitter:description',
- content: this.channel.description
- },
- {
- property: 'og:type',
- content: 'profile'
- },
- {
- property: 'og:title',
- content: this.channel.name
- },
- {
- property: 'og:profile:username',
- content: this.channel.name
- },
- {
- property: 'og:description',
- content: this.channel.description
- },
- {
- name: 'description',
- content: this.channel.description
- },
- {
- property: 'og:image',
- content: this.channel.bannerUrl
- },
- {
- name: 'twitter:image',
- content: this.channel.bannerUrl
- }
- ]
- }
- },
+ return {
+ title,
+ meta: this.channel && [
+ {
+ name: 'twitter:title',
+ content: this.channel.name
+ },
+ {
+ name: 'twitter:description',
+ content: this.channel.description
+ },
+ {
+ property: 'og:type',
+ content: 'profile'
+ },
+ {
+ property: 'og:title',
+ content: this.channel.name
+ },
+ {
+ property: 'og:profile:username',
+ content: this.channel.name
+ },
+ {
+ property: 'og:description',
+ content: this.channel.description
+ },
+ {
+ name: 'description',
+ content: this.channel.description
+ },
+ {
+ property: 'og:image',
+ content: this.channel.bannerUrl
+ },
+ {
+ name: 'twitter:image',
+ content: this.channel.bannerUrl
+ }
+ ]
+ }
+ },
- mounted () {
- this.fetchChannel()
- },
- watch: {
- '$route.params.channelId': 'fetchChannel'
- },
- methods: {
- async fetchChannel () {
- this.channel = await this.$store.dispatch('auth/makeRequest', {
- path: '/' + this.$route.params.path + '/' + this.$route.params.channelId
- })
- },
+ mounted () {
+ this.fetchChannel()
+ },
+ watch: {
+ '$route.params.channelId': 'fetchChannel'
+ },
+ methods: {
+ async fetchChannel () {
+ this.channel = await this.$store.dispatch('auth/makeRequest', {
+ path: '/' + this.$route.params.path + '/' + this.$route.params.channelId
+ })
+ },
- onRelatedStreamsEndIntersect (entries) {
- if (entries[0].isIntersecting) {
- this.fetchMoreVideos()
- }
- },
+ onRelatedStreamsEndIntersect (entries) {
+ if (entries[0].isIntersecting) {
+ this.fetchMoreVideos()
+ }
+ },
- fetchMoreVideos () {
- this.$store.dispatch('auth/makeRequest', {
- path: '/nextpage/channel/' + this.channel.id,
- params: {
- nextpage: this.channel.nextpage
- }
- }).then(j => {
- this.channel.relatedStreams = this.channel.relatedStreams.concat(j.relatedStreams)
- this.channel.nextpage = j.nextpage
- })
- }
- },
- computed: {
- renderedDescription () {
- return LibPiped.purifyHTML(marked.parseInline(this.channel.description, {
- breaks: true
- }))
- },
+ fetchMoreVideos () {
+ this.$store.dispatch('auth/makeRequest', {
+ path: '/nextpage/channel/' + this.channel.id,
+ params: {
+ nextpage: this.channel.nextpage
+ }
+ }).then(j => {
+ this.channel.relatedStreams = this.channel.relatedStreams.concat(j.relatedStreams)
+ this.channel.nextpage = j.nextpage
+ })
+ }
+ },
+ computed: {
+ renderedDescription () {
+ return LibPiped.purifyHTML(marked.parseInline(this.channel.description, {
+ breaks: true
+ }))
+ },
- chunkedByFour () {
- return _chunk(this.channel.relatedStreams, 4)
- }
- },
- components: {
- SubscriptionButton,
- ErrorHandler,
- VideoItem
- }
+ chunkedByFour () {
+ return _chunk(this.channel.relatedStreams, 4)
+ }
+ },
+ components: {
+ SubscriptionButton,
+ ErrorHandler,
+ VideoItem
+ }
}
diff --git a/src/routes/FeedPage.vue b/src/routes/FeedPage.vue
index 8e39db6..83b7de3 100644
--- a/src/routes/FeedPage.vue
+++ b/src/routes/FeedPage.vue
@@ -28,97 +28,97 @@ import { chunk as _chunk } from 'lodash-es'
import VideoItem from '@/components/VideoItem.vue'
export default {
- data () {
- return {
- feedName: 'trending',
- videos: [],
- error: null,
- errorIsJSON: false
- }
- },
+ data () {
+ return {
+ feedName: 'trending',
+ videos: [],
+ error: null,
+ errorIsJSON: false
+ }
+ },
- metaInfo () {
- return {
- title: this.$t('titles.' + this.feedName)
- }
- },
+ metaInfo () {
+ return {
+ title: this.$t('titles.' + this.feedName)
+ }
+ },
- watch: {
- '$route.path': 'fetchData'
- },
- mounted () {
- this.fetchData()
- },
- methods: {
- async fetchData () {
- const region = this.$store.getters['prefs/getPreference']('region', 'US')
- const selectedHomepage = this.$store.getters['prefs/getPreference']('homepage', 'trending')
- let path
+ watch: {
+ '$route.path': 'fetchData'
+ },
+ mounted () {
+ this.fetchData()
+ },
+ methods: {
+ async fetchData () {
+ const region = this.$store.getters['prefs/getPreference']('region', 'US')
+ const selectedHomepage = this.$store.getters['prefs/getPreference']('homepage', 'trending')
+ let path
- switch (this.$route.path) {
- case '/':
- if (selectedHomepage === 'trending') {
- path = '/trending'
- this.feedName = 'trending'
- } else {
- path = '/feed'
- this.feedName = 'feed'
- }
- break
- case '/trending':
- path = '/trending'
- this.feedName = 'trending'
- break
- case '/feed':
- path = '/feed'
- this.feedName = 'feed'
- break
- }
+ switch (this.$route.path) {
+ case '/':
+ if (selectedHomepage === 'trending') {
+ path = '/trending'
+ this.feedName = 'trending'
+ } else {
+ path = '/feed'
+ this.feedName = 'feed'
+ }
+ break
+ case '/trending':
+ path = '/trending'
+ this.feedName = 'trending'
+ break
+ case '/feed':
+ path = '/feed'
+ this.feedName = 'feed'
+ break
+ }
- try {
- this.error = null
- this.videos = await this.$store.dispatch('auth/makeRequest', {
- path,
- params: {
- region: region
- },
- tokenInParams: true
- })
- } catch (e) {
- if (e.isAxiosError) {
- const rData = e.response.data
- if (rData.message === 'Could not get Trending name') {
- this.error = 'errors.trendingFetchError'
- } else {
- this.error = rData
- this.errorIsJSON = true
- }
- } else {
- throw e
- }
- }
- }
- },
- computed: {
- rowSize () {
- return this.$store.getters['prefs/getPreferenceNumber']('feedColumns', 4)
- },
+ try {
+ this.error = null
+ this.videos = await this.$store.dispatch('auth/makeRequest', {
+ path,
+ params: {
+ region: region
+ },
+ tokenInParams: true
+ })
+ } catch (e) {
+ if (e.isAxiosError) {
+ const rData = e.response.data
+ if (rData.message === 'Could not get Trending name') {
+ this.error = 'errors.trendingFetchError'
+ } else {
+ this.error = rData
+ this.errorIsJSON = true
+ }
+ } else {
+ throw e
+ }
+ }
+ }
+ },
+ computed: {
+ rowSize () {
+ return this.$store.getters['prefs/getPreferenceNumber']('feedColumns', 4)
+ },
- columnClass () {
- if (!this.$vuetify.breakpoint.mdAndUp) {
- return
- }
- return 'span-col-' + (60 / this.rowSize) + ' mb-4'
- },
+ columnClass () {
+ if (!this.$vuetify.breakpoint.mdAndUp) {
+ return
+ }
+ return 'span-col-' + (60 / this.rowSize) + ' mb-4'
+ },
- splitIntoRows () {
- return _chunk(this.videos, this.rowSize)
- }
- },
- components: {
- VideoItem,
- JSONViewer: () => import('vue-json-viewer')
- }
+ splitIntoRows () {
+ return _chunk(this.videos, this.rowSize)
+ }
+ },
+ components: {
+ VideoItem,
+ JSONViewer: () => import('vue-json-viewer')
+ }
}
diff --git a/src/routes/Playlist.vue b/src/routes/Playlist.vue
index 3d63ba7..c486756 100644
--- a/src/routes/Playlist.vue
+++ b/src/routes/Playlist.vue
@@ -47,41 +47,41 @@ import VideoItem from '@/components/VideoItem.vue'
import { chunk as _chunk } from 'lodash-es'
export default {
- data () {
- return {
- playlist: null
- }
- },
- metaInfo () {
- return { title: this.playlist ? this.playlist.name : 'Loading' }
- },
+ data () {
+ return {
+ playlist: null
+ }
+ },
+ metaInfo () {
+ return { title: this.playlist ? this.playlist.name : 'Loading' }
+ },
- mounted () {
- this.getPlaylistData()
- },
- computed: {
- getRssUrl () {
- return this.$store.getters['prefs/apiUrl'] + '/rss/playlists/' + this.$route.query.list
- },
+ mounted () {
+ this.getPlaylistData()
+ },
+ computed: {
+ getRssUrl () {
+ return this.$store.getters['prefs/apiUrl'] + '/rss/playlists/' + this.$route.query.list
+ },
- chunkedByFour () {
- return _chunk(this.playlist.relatedStreams, 4)
- }
- },
- methods: {
- async fetchPlaylist () {
- return this.$store.dispatch('auth/makeRequest', {
- path: '/playlists/' + this.$route.query.list
- })
- },
- async getPlaylistData () {
- this.fetchPlaylist()
- .then(data => (this.playlist = data))
- }
- },
- components: {
- ErrorHandler,
- VideoItem
- }
+ chunkedByFour () {
+ return _chunk(this.playlist.relatedStreams, 4)
+ }
+ },
+ methods: {
+ async fetchPlaylist () {
+ return this.$store.dispatch('auth/makeRequest', {
+ path: '/playlists/' + this.$route.query.list
+ })
+ },
+ async getPlaylistData () {
+ this.fetchPlaylist()
+ .then(data => (this.playlist = data))
+ }
+ },
+ components: {
+ ErrorHandler,
+ VideoItem
+ }
}
diff --git a/src/routes/Preferences.vue b/src/routes/Preferences.vue
index 8a20029..823bd48 100644
--- a/src/routes/Preferences.vue
+++ b/src/routes/Preferences.vue
@@ -38,196 +38,196 @@ import EnglishNames from 'i18n-iso-countries/langs/en.json'
Countries.registerLocale(EnglishNames)
export default {
- data () {
- return {
- instances: [],
+ data () {
+ return {
+ instances: [],
- options: [
- {
- id: 'playerAutoplay',
- type: 'bool',
- default: true
- },
- {
- id: 'listen',
- type: 'bool',
- default: false
- },
- {
- id: 'disableLBRY',
- type: 'bool',
- default: true
- },
- {
- id: 'proxyLBRY',
- type: 'bool',
- default: true
- },
- {
- id: 'skipToLastPoint',
- type: 'bool',
- default: true
- },
- {
- id: 'homepage',
- type: 'select',
- label: 'Default Homepage',
- default: 'trending',
- options: [
- {
- text: 'Trending',
- value: 'trending'
- },
- {
- text: 'Feed',
- value: 'feed'
- }
- ]
- },
- {
- id: 'feedColumns',
- type: 'select',
- label: 'Amount of Columns on Feed Pages',
- default: 4,
- options: [
- {
- text: '4 columns',
- value: 4
- },
- {
- text: '5 columns',
- value: 5
- },
- {
- text: '6 columns',
- value: 6
- }
- ]
- },
- {
- id: 'quality',
- type: 'select',
- default: 0,
- options: [
- {
- text: 'Auto',
- value: 0
- },
- ...([144, 240, 360, 480, 720, 1080, 1440, 2160, 4320].map(i => ({
- text: i.toString() + 'p',
- value: i
- })))
- ]
- },
- {
- id: 'bufferGoal',
- type: 'number',
- default: 10
- },
- {
- id: 'sponsorblock',
- type: 'bool',
- default: true
- },
- {
- id: 'selectedSkip',
- type: 'select',
- label: 'Selected Segments to Skip',
- multi: true,
- default: ['sponsor', 'interaction', 'selfpromo', 'music_offtopic'],
- options: [
- { text: 'Sponsor Segments ', value: 'sponsors' },
- { text: 'Intermission/Intro Animation Segments', value: 'intro' },
- { text: 'Endcards/Credits Segments', value: 'outro' },
- { text: 'Preview/Recap Segments', value: 'preview' },
- { text: 'Interaction Reminder (Subscribe) Segments', value: 'interaction' },
- { text: 'Unpaid/Self Promotion Segments', value: 'selfpromo' },
- { text: 'Music: Non-Music Segments', value: 'music_offtopic' }
- ]
- },
- {
- id: 'region',
- type: 'select',
- label: 'Country',
- default: 'US',
- options: Object.entries(Countries.getNames('en', { select: 'official' })).map(([code, name]) => ({
- text: name,
- value: code
- }))
- }
- ],
- tableHeaders: [
- {
- text: 'Name',
- value: 'name'
- },
- {
- text: 'API URL',
- value: 'apiurl'
- },
- {
- text: 'Locations',
- value: 'locations'
- },
- {
- text: 'CDN enabled?',
- value: 'cdn'
- }
- ]
- }
- },
- metaInfo () {
- return {
- title: this.$t('titles.preferences')
- }
- },
+ options: [
+ {
+ id: 'playerAutoplay',
+ type: 'bool',
+ default: true
+ },
+ {
+ id: 'listen',
+ type: 'bool',
+ default: false
+ },
+ {
+ id: 'disableLBRY',
+ type: 'bool',
+ default: true
+ },
+ {
+ id: 'proxyLBRY',
+ type: 'bool',
+ default: true
+ },
+ {
+ id: 'skipToLastPoint',
+ type: 'bool',
+ default: true
+ },
+ {
+ id: 'homepage',
+ type: 'select',
+ label: 'Default Homepage',
+ default: 'trending',
+ options: [
+ {
+ text: 'Trending',
+ value: 'trending'
+ },
+ {
+ text: 'Feed',
+ value: 'feed'
+ }
+ ]
+ },
+ {
+ id: 'feedColumns',
+ type: 'select',
+ label: 'Amount of Columns on Feed Pages',
+ default: 4,
+ options: [
+ {
+ text: '4 columns',
+ value: 4
+ },
+ {
+ text: '5 columns',
+ value: 5
+ },
+ {
+ text: '6 columns',
+ value: 6
+ }
+ ]
+ },
+ {
+ id: 'quality',
+ type: 'select',
+ default: 0,
+ options: [
+ {
+ text: 'Auto',
+ value: 0
+ },
+ ...([144, 240, 360, 480, 720, 1080, 1440, 2160, 4320].map(i => ({
+ text: i.toString() + 'p',
+ value: i
+ })))
+ ]
+ },
+ {
+ id: 'bufferGoal',
+ type: 'number',
+ default: 10
+ },
+ {
+ id: 'sponsorblock',
+ type: 'bool',
+ default: true
+ },
+ {
+ id: 'selectedSkip',
+ type: 'select',
+ label: 'Selected Segments to Skip',
+ multi: true,
+ default: ['sponsor', 'interaction', 'selfpromo', 'music_offtopic'],
+ options: [
+ { text: 'Sponsor Segments ', value: 'sponsors' },
+ { text: 'Intermission/Intro Animation Segments', value: 'intro' },
+ { text: 'Endcards/Credits Segments', value: 'outro' },
+ { text: 'Preview/Recap Segments', value: 'preview' },
+ { text: 'Interaction Reminder (Subscribe) Segments', value: 'interaction' },
+ { text: 'Unpaid/Self Promotion Segments', value: 'selfpromo' },
+ { text: 'Music: Non-Music Segments', value: 'music_offtopic' }
+ ]
+ },
+ {
+ id: 'region',
+ type: 'select',
+ label: 'Country',
+ default: 'US',
+ options: Object.entries(Countries.getNames('en', { select: 'official' })).map(([code, name]) => ({
+ text: name,
+ value: code
+ }))
+ }
+ ],
+ tableHeaders: [
+ {
+ text: 'Name',
+ value: 'name'
+ },
+ {
+ text: 'API URL',
+ value: 'apiurl'
+ },
+ {
+ text: 'Locations',
+ value: 'locations'
+ },
+ {
+ text: 'CDN enabled?',
+ value: 'cdn'
+ }
+ ]
+ }
+ },
+ metaInfo () {
+ return {
+ title: this.$t('titles.preferences')
+ }
+ },
- mounted () {
- if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} })
+ mounted () {
+ if (Object.keys(this.$route.query).length > 0) this.$router.replace({ query: {} })
- fetch('https://raw.githubusercontent.com/wiki/TeamPiped/Piped-Frontend/Instances.md')
- .then(resp => resp.text())
- .then(body => {
- let skipped = 0
- const lines = body.split('\n')
- lines.forEach(line => {
- const split = line.split('|')
- if (split.length === 5) {
- if (skipped < 2) {
- skipped++
- return
- }
- this.instances.push({
- name: split[0].trim(),
- apiurl: split[1].trim(),
- locations: split[2].trim(),
- cdn: split[3].trim()
- })
- }
- })
- this.options.push({
- id: 'instance',
- type: 'select',
- default: this.$store.getters['prefs/apiUrl'],
- label: 'Instance',
- options: this.instances.map(i => ({
- text: i.name,
- value: i.apiurl
- }))
- })
- })
- },
- methods: {
- setValue (k, v) {
- this.$store.commit('prefs/setPrefs', {
- id: k,
- value: v
- })
- },
+ fetch('https://raw.githubusercontent.com/wiki/TeamPiped/Piped-Frontend/Instances.md')
+ .then(resp => resp.text())
+ .then(body => {
+ let skipped = 0
+ const lines = body.split('\n')
+ lines.forEach(line => {
+ const split = line.split('|')
+ if (split.length === 5) {
+ if (skipped < 2) {
+ skipped++
+ return
+ }
+ this.instances.push({
+ name: split[0].trim(),
+ apiurl: split[1].trim(),
+ locations: split[2].trim(),
+ cdn: split[3].trim()
+ })
+ }
+ })
+ this.options.push({
+ id: 'instance',
+ type: 'select',
+ default: this.$store.getters['prefs/apiUrl'],
+ label: 'Instance',
+ options: this.instances.map(i => ({
+ text: i.name,
+ value: i.apiurl
+ }))
+ })
+ })
+ },
+ methods: {
+ setValue (k, v) {
+ this.$store.commit('prefs/setPrefs', {
+ id: k,
+ value: v
+ })
+ },
- sslScore (url) {
- return 'https://www.ssllabs.com/ssltest/analyze.html?d=' + new URL(url).host + '&latest'
- }
- }
+ sslScore (url) {
+ return 'https://www.ssllabs.com/ssltest/analyze.html?d=' + new URL(url).host + '&latest'
+ }
+ }
}
diff --git a/src/routes/SearchMenu.vue b/src/routes/SearchMenu.vue
index 9a4cd4b..e5e15e3 100644
--- a/src/routes/SearchMenu.vue
+++ b/src/routes/SearchMenu.vue
@@ -20,66 +20,66 @@
import { setupKeybindings } from '@/plugins/keybindings'
export default {
- name: 'SearchMenu',
- data () {
- return {
- selected: 0,
- select: '',
- searchText: '',
- requestInProgress: false,
- searchSuggestions: []
- }
- },
- watch: {
- searchText (val) {
- if (val && val !== this.select) {
- this.refreshSuggestions()
- }
- },
+ name: 'SearchMenu',
+ data () {
+ return {
+ selected: 0,
+ select: '',
+ searchText: '',
+ requestInProgress: false,
+ searchSuggestions: []
+ }
+ },
+ watch: {
+ searchText (val) {
+ if (val && val !== this.select) {
+ this.refreshSuggestions()
+ }
+ },
- async select (val) {
- if (val === '') {
- return
- }
- try {
- await this.$router.push({
- name: 'SearchResults',
- query: { search_query: val }
- })
- } catch (e) {
- console.log('???', e)
- } finally {
- this.select = ''
- this.searchText = ''
- }
- }
- },
+ async select (val) {
+ if (val === '') {
+ return
+ }
+ try {
+ await this.$router.push({
+ name: 'SearchResults',
+ query: { search_query: val }
+ })
+ } catch (e) {
+ console.log('???', e)
+ } finally {
+ this.select = ''
+ this.searchText = ''
+ }
+ }
+ },
- methods: {
- async refreshSuggestions () {
- this.requestInProgress = true
- this.searchSuggestions = await this.$store.dispatch('auth/makeRequest', {
- path: '/suggestions',
- params: {
- query: this.searchText
- }
- })
- this.searchSuggestions.unshift(this.searchText)
- this.requestInProgress = false
- }
- },
+ methods: {
+ async refreshSuggestions () {
+ this.requestInProgress = true
+ this.searchSuggestions = await this.$store.dispatch('auth/makeRequest', {
+ path: '/suggestions',
+ params: {
+ query: this.searchText
+ }
+ })
+ this.searchSuggestions.unshift(this.searchText)
+ this.requestInProgress = false
+ }
+ },
- mounted () {
- this.unsubToKeybindings = setupKeybindings(window, {
- '/': (e) => {
- this.$refs.searchMenu.focus()
- e.preventDefault()
- }
- })
- },
+ mounted () {
+ this.unsubToKeybindings = setupKeybindings(window, {
+ '/': (e) => {
+ this.$refs.searchMenu.focus()
+ e.preventDefault()
+ }
+ })
+ },
- beforeDestroy () {
- this.unsubToKeybindings()
- }
+ beforeDestroy () {
+ this.unsubToKeybindings()
+ }
}
diff --git a/src/routes/SearchResults.vue b/src/routes/SearchResults.vue
index cd3a230..a39d2b6 100644
--- a/src/routes/SearchResults.vue
+++ b/src/routes/SearchResults.vue
@@ -31,115 +31,115 @@ import VideoItem from '@/components/VideoItem'
import GenericDisplayItem from '@/components/GenericDisplayItem'
export default {
- components: { GenericDisplayItem, VideoItem },
- data () {
- return {
- results: null,
- availableFilters: [
- 'all',
- 'videos',
- 'channels',
- 'playlists',
- 'music_songs',
- 'music_videos',
- 'music_albums',
- 'music_playlists'
- ],
- selectedFilter: 'all'
- }
- },
- metaInfo () {
- return {
- title: this.$route.query.search_query
- }
- },
+ components: { GenericDisplayItem, VideoItem },
+ data () {
+ return {
+ results: null,
+ availableFilters: [
+ 'all',
+ 'videos',
+ 'channels',
+ 'playlists',
+ 'music_songs',
+ 'music_videos',
+ 'music_albums',
+ 'music_playlists'
+ ],
+ selectedFilter: 'all'
+ }
+ },
+ metaInfo () {
+ return {
+ title: this.$route.query.search_query
+ }
+ },
- mounted () {
- this.updateResults()
- },
- computed: {
- chunkedByFour () {
- return _chunk(this.results.items, 4)
- }
- },
- watch: {
- selectedFilter () {
- this.updateResults()
- },
+ mounted () {
+ this.updateResults()
+ },
+ computed: {
+ chunkedByFour () {
+ return _chunk(this.results.items, 4)
+ }
+ },
+ watch: {
+ selectedFilter () {
+ this.updateResults()
+ },
- // For history navigation
- '$route.query.search_query' () {
- this.updateResults()
- }
- },
+ // For history navigation
+ '$route.query.search_query' () {
+ this.updateResults()
+ }
+ },
- methods: {
- rationalizeSearchResult (sr) {
- let type
- // This seriously can't be the best solution
- if (sr.url.startsWith('/watch')) {
- type = 'VIDEO'
- } else if (sr.url.startsWith('/playlist')) {
- type = 'PLAYLIST'
- } else if (sr.url.startsWith('/channel')) {
- type = 'CHANNEL'
- } else {
- console.warn('WARNING: UNKNOWN VIDEO URL TYPE FOUND:', sr.url)
- type = 'VIDEO'
- }
+ methods: {
+ rationalizeSearchResult (sr) {
+ let type
+ // This seriously can't be the best solution
+ if (sr.url.startsWith('/watch')) {
+ type = 'VIDEO'
+ } else if (sr.url.startsWith('/playlist')) {
+ type = 'PLAYLIST'
+ } else if (sr.url.startsWith('/channel')) {
+ type = 'CHANNEL'
+ } else {
+ console.warn('WARNING: UNKNOWN VIDEO URL TYPE FOUND:', sr.url)
+ type = 'VIDEO'
+ }
- return {
- title: sr.name,
- type,
- uploaderName: sr.uploader,
- uploadedDate: sr.uploadDate,
- ...sr
- }
- },
+ return {
+ title: sr.name,
+ type,
+ uploaderName: sr.uploader,
+ uploadedDate: sr.uploadDate,
+ ...sr
+ }
+ },
- async fetchResults () {
- return this.$store.dispatch('auth/makeRequest', {
- path: 'search',
- params: {
- q: this.$route.query.search_query,
- filter: this.selectedFilter
- }
- })
- },
+ async fetchResults () {
+ return this.$store.dispatch('auth/makeRequest', {
+ path: 'search',
+ params: {
+ q: this.$route.query.search_query,
+ filter: this.selectedFilter
+ }
+ })
+ },
- numberFormat (...args) {
- return LibPiped.numberFormat(...args)
- },
+ numberFormat (...args) {
+ return LibPiped.numberFormat(...args)
+ },
- timeFormat (...args) {
- return LibPiped.timeFormat(...args)
- },
- async updateResults () {
- this.results = this.fetchResults().then(json => {
- json.items = json.items.map(this.rationalizeSearchResult)
- this.results = json
- })
- },
- onSearchResultsEndIntersect (entries) {
- if (entries[0].isIntersecting) {
- this.fetchMoreResults()
- }
- },
+ timeFormat (...args) {
+ return LibPiped.timeFormat(...args)
+ },
+ async updateResults () {
+ this.results = this.fetchResults().then(json => {
+ json.items = json.items.map(this.rationalizeSearchResult)
+ this.results = json
+ })
+ },
+ onSearchResultsEndIntersect (entries) {
+ if (entries[0].isIntersecting) {
+ this.fetchMoreResults()
+ }
+ },
- fetchMoreResults () {
- this.$store.dispatch('auth/makeRequest', {
- path: '/nextpage/search',
- params: {
- nextpage: this.results.nextpage,
- q: this.$route.query.search_query,
- filter: this.selectedFilter
- }
- }).then(json => {
- this.results.nextpage = json.nextpage
- this.results.id = json.id
- this.results.items = this.results.items.concat(json.items.map(this.rationalizeSearchResult))
- })
- }
- }
+ fetchMoreResults () {
+ this.$store.dispatch('auth/makeRequest', {
+ path: '/nextpage/search',
+ params: {
+ nextpage: this.results.nextpage,
+ q: this.$route.query.search_query,
+ filter: this.selectedFilter
+ }
+ }).then(json => {
+ this.results.nextpage = json.nextpage
+ this.results.id = json.id
+ this.results.items = this.results.items.concat(json.items.map(this.rationalizeSearchResult))
+ })
+ }
+ }
}
diff --git a/src/routes/Subscriptions.vue b/src/routes/Subscriptions.vue
index fdce3a3..d7e1ea7 100644
--- a/src/routes/Subscriptions.vue
+++ b/src/routes/Subscriptions.vue
@@ -23,33 +23,33 @@ import SubscriptionButton from '@/components/SubscriptionButton'
import { LibPiped } from '@/tools/libpiped'
export default {
- name: 'Subscriptions',
- components: { SubscriptionButton },
- data: () => ({
- loaded: false,
- data: null
- }),
- methods: {
- async loadData () {
- if (!this.$store.getters['auth/isCurrentlyAuthenticated']) {
- await this.$router.replace({
- path: '/'
- })
- return
- }
- const resp = await this.$store.dispatch('auth/makeRequest', {
- method: 'GET',
- path: '/subscriptions'
- })
- this.loaded = true
- this.data = _chunk(resp.map(r => {
- r.id = LibPiped.determineVideoIdFromChannelURL(r.url)
- return r
- }), 6)
- }
- },
- mounted () {
- this.loadData()
- }
+ name: 'Subscriptions',
+ components: { SubscriptionButton },
+ data: () => ({
+ loaded: false,
+ data: null
+ }),
+ methods: {
+ async loadData () {
+ if (!this.$store.getters['auth/isCurrentlyAuthenticated']) {
+ await this.$router.replace({
+ path: '/'
+ })
+ return
+ }
+ const resp = await this.$store.dispatch('auth/makeRequest', {
+ method: 'GET',
+ path: '/subscriptions'
+ })
+ this.loaded = true
+ this.data = _chunk(resp.map(r => {
+ r.id = LibPiped.determineVideoIdFromChannelURL(r.url)
+ return r
+ }), 6)
+ }
+ },
+ mounted () {
+ this.loadData()
+ }
}
diff --git a/src/routes/WatchHistory.vue b/src/routes/WatchHistory.vue
index 7186a1e..fe8e83e 100644
--- a/src/routes/WatchHistory.vue
+++ b/src/routes/WatchHistory.vue
@@ -30,52 +30,52 @@ import VideoItem from '@/components/VideoItem'
import ExpandableDate from '@/components/ExpandableDate'
export default {
- components: {
- ExpandableDate,
- VideoItem
- },
- data: () => ({
- loaded: false,
- data: null,
- unfinishedVideosSwitch: false
- }),
+ components: {
+ ExpandableDate,
+ VideoItem
+ },
+ data: () => ({
+ loaded: false,
+ data: null,
+ unfinishedVideosSwitch: false
+ }),
- metaInfo () {
- return {
- title: this.$t('titles.history')
- }
- },
+ metaInfo () {
+ return {
+ title: this.$t('titles.history')
+ }
+ },
- methods: {
- async loadData (onlyUnfinished = false) {
- if (!onlyUnfinished) {
- this.data = await getWatchedVideos()
- } else {
- this.data = await getUnfinishedVideos()
- }
- this.loaded = true
- },
+ methods: {
+ async loadData (onlyUnfinished = false) {
+ if (!onlyUnfinished) {
+ this.data = await getWatchedVideos()
+ } else {
+ this.data = await getUnfinishedVideos()
+ }
+ this.loaded = true
+ },
- async deleteWatchHistory () {
- await deleteWatchedVideos()
- await this.loadData()
- }
- },
+ async deleteWatchHistory () {
+ await deleteWatchedVideos()
+ await this.loadData()
+ }
+ },
- watch: {
- unfinishedVideosSwitch () {
- this.loadData(this.unfinishedVideosSwitch)
- }
- },
+ watch: {
+ unfinishedVideosSwitch () {
+ this.loadData(this.unfinishedVideosSwitch)
+ }
+ },
- computed: {
- chunkedByFour () {
- return _chunk(this.data, 4)
- }
- },
+ computed: {
+ chunkedByFour () {
+ return _chunk(this.data, 4)
+ }
+ },
- mounted () {
- this.loadData().catch(e => console.error(e))
- }
+ mounted () {
+ this.loadData().catch(e => console.error(e))
+ }
}
diff --git a/src/routes/WatchVideo.vue b/src/routes/WatchVideo.vue
index 403dca7..b0ae970 100644
--- a/src/routes/WatchVideo.vue
+++ b/src/routes/WatchVideo.vue
@@ -105,228 +105,228 @@ import SubscriptionButton from '@/components/SubscriptionButton'
import ExpandableDate from '@/components/ExpandableDate'
export default {
- name: 'WatchVideo',
- data () {
- return {
- loaded: false,
- video: {
- title: 'Loading ...'
- },
- sponsors: null,
- selectedAutoLoop: false,
- showDesc: true,
- comments: null,
- channelId: null,
+ name: 'WatchVideo',
+ data () {
+ return {
+ loaded: false,
+ video: {
+ title: 'Loading ...'
+ },
+ sponsors: null,
+ selectedAutoLoop: false,
+ showDesc: true,
+ comments: null,
+ channelId: null,
- dbID: null,
- lastWatch: null
- }
- },
- metaInfo () {
- return {
- title: this.video.title,
- meta: [
- {
- name: 'twitter:title',
- content: this.video.title
- },
- {
- name: 'twitter:description',
- content: this.video.description
- },
- {
- property: 'og:type',
- content: 'video'
- },
- {
- property: 'og:title',
- content: this.video.title
- },
- {
- property: 'og:description',
- content: this.video.description
- },
- {
- name: 'description',
- content: this.video.description
- },
- {
- property: 'og:image',
- content: this.video.thumbnailUrl
- },
- {
- name: 'twitter:image',
- content: this.video.thumbnailUrl
- }
- ]
- }
- },
+ dbID: null,
+ lastWatch: null
+ }
+ },
+ metaInfo () {
+ return {
+ title: this.video.title,
+ meta: [
+ {
+ name: 'twitter:title',
+ content: this.video.title
+ },
+ {
+ name: 'twitter:description',
+ content: this.video.description
+ },
+ {
+ property: 'og:type',
+ content: 'video'
+ },
+ {
+ property: 'og:title',
+ content: this.video.title
+ },
+ {
+ property: 'og:description',
+ content: this.video.description
+ },
+ {
+ name: 'description',
+ content: this.video.description
+ },
+ {
+ property: 'og:image',
+ content: this.video.thumbnailUrl
+ },
+ {
+ name: 'twitter:image',
+ content: this.video.thumbnailUrl
+ }
+ ]
+ }
+ },
- mounted () {
- this.initialize()
- },
- watch: {
- '$route.query.v': function (v) {
- if (v) {
- window.scrollTo(0, 0)
- }
- this.initialize()
- }
- },
- methods: {
- initialize () {
- this.getVideoData()
- this.getSponsors()
- if (this.$store.getters['prefs/getPreferenceBoolean']('comments', true)) this.getComments()
- },
+ mounted () {
+ this.initialize()
+ },
+ watch: {
+ '$route.query.v': function (v) {
+ if (v) {
+ window.scrollTo(0, 0)
+ }
+ this.initialize()
+ }
+ },
+ methods: {
+ initialize () {
+ this.getVideoData()
+ this.getSponsors()
+ if (this.$store.getters['prefs/getPreferenceBoolean']('comments', true)) this.getComments()
+ },
- videoEnded () {
- if (!this.selectedAutoLoop && this.isAutoplayEnabled && this.video.relatedStreams[0]) {
- this.$router.push({
- name: 'WatchVideo',
- query: {
- v: LibPiped.determineVideoIdFromPath(this.video.relatedStreams[0].url)
- }
- })
- }
- },
+ videoEnded () {
+ if (!this.selectedAutoLoop && this.isAutoplayEnabled && this.video.relatedStreams[0]) {
+ this.$router.push({
+ name: 'WatchVideo',
+ query: {
+ v: LibPiped.determineVideoIdFromPath(this.video.relatedStreams[0].url)
+ }
+ })
+ }
+ },
- onYTClick () {
- const time = this.$refs.player.getCurrentTime()
+ onYTClick () {
+ const time = this.$refs.player.getCurrentTime()
- const url = new URL('https://youtube.com/watch')
- url.searchParams.set('v', this.videoId)
- if (Number.isFinite(time)) {
- url.searchParams.set('t', time.toFixed(0))
- }
- window.location.href = url.href
- },
+ const url = new URL('https://youtube.com/watch')
+ url.searchParams.set('v', this.videoId)
+ if (Number.isFinite(time)) {
+ url.searchParams.set('t', time.toFixed(0))
+ }
+ window.location.href = url.href
+ },
- numberFormat (...args) {
- return LibPiped.numberFormat(...args)
- },
+ numberFormat (...args) {
+ return LibPiped.numberFormat(...args)
+ },
- addCommas (...args) {
- return LibPiped.addCommas(...args)
- },
+ addCommas (...args) {
+ return LibPiped.addCommas(...args)
+ },
- fetchVideo () {
- return this.$store.dispatch('auth/makeRequest', {
- method: 'GET',
- path: '/streams/' + this.videoId
- })
- },
+ fetchVideo () {
+ return this.$store.dispatch('auth/makeRequest', {
+ method: 'GET',
+ path: '/streams/' + this.videoId
+ })
+ },
- async getSponsors () {
- if (!this.$store.getters['prefs/getPreference']('sponsorblock', true)) {
- return
- }
- this.sponsors = await this.$store.dispatch('auth/makeRequest', {
- path: '/sponsors/' + this.videoId,
- params: {
- category: JSON.stringify(this.$store.getters['prefs/getPreference']('selectedSkip', ['sponsor', 'interaction', 'selfpromo', 'music_offtopic']))
- }
- })
- },
- fetchComments () {
- return this.$store.dispatch('auth/makeRequest', {
- path: '/comments/' + this.videoId
- })
- },
+ async getSponsors () {
+ if (!this.$store.getters['prefs/getPreference']('sponsorblock', true)) {
+ return
+ }
+ this.sponsors = await this.$store.dispatch('auth/makeRequest', {
+ path: '/sponsors/' + this.videoId,
+ params: {
+ category: JSON.stringify(this.$store.getters['prefs/getPreference']('selectedSkip', ['sponsor', 'interaction', 'selfpromo', 'music_offtopic']))
+ }
+ })
+ },
+ fetchComments () {
+ return this.$store.dispatch('auth/makeRequest', {
+ path: '/comments/' + this.videoId
+ })
+ },
- onCommentsProgressIntersect (entries) {
- if (entries[0].isIntersecting) {
- this.fetchMoreComments()
- }
- },
+ onCommentsProgressIntersect (entries) {
+ if (entries[0].isIntersecting) {
+ this.fetchMoreComments()
+ }
+ },
- fetchMoreComments () {
- this.$store.dispatch('auth/makeRequest', {
- path: '/nextpage/comments/' + this.videoId,
- params: {
- nextpage: this.comments.nextpage
- }
- }).then(json => {
- this.comments.nextpage = json.nextpage
- this.comments.comments = this.comments.comments.concat(json.comments)
- })
- },
+ fetchMoreComments () {
+ this.$store.dispatch('auth/makeRequest', {
+ path: '/nextpage/comments/' + this.videoId,
+ params: {
+ nextpage: this.comments.nextpage
+ }
+ }).then(json => {
+ this.comments.nextpage = json.nextpage
+ this.comments.comments = this.comments.comments.concat(json.comments)
+ })
+ },
- onAutoplayChg (ev) {
- this.$store.commit('prefs/setPrefs', {
- id: 'autoplay',
- value: ev
- })
- },
+ onAutoplayChg (ev) {
+ this.$store.commit('prefs/setPrefs', {
+ id: 'autoplay',
+ value: ev
+ })
+ },
- async getVideoData () {
- try {
- this.lastWatch = await findLastWatch(this.videoId)
- } catch (e) {
- console.error('Errored while finding last watched', e)
- }
+ async getVideoData () {
+ try {
+ this.lastWatch = await findLastWatch(this.videoId)
+ } catch (e) {
+ console.error('Errored while finding last watched', e)
+ }
- const video = await this.fetchVideo()
- video.videoId = this.videoId
- video.url = this.$route.fullPath
- this.video = video
- this.loaded = true
+ const video = await this.fetchVideo()
+ video.videoId = this.videoId
+ video.url = this.$route.fullPath
+ this.video = video
+ this.loaded = true
- if (this.video.error) {
- return
- }
- this.channelId = this.video.uploaderUrl.split('/')[2]
+ if (this.video.error) {
+ return
+ }
+ this.channelId = this.video.uploaderUrl.split('/')[2]
- this.video.description = LibPiped.purifyHTML(
- this.video.description
- .replaceAll('http://www.youtube.com', '')
- .replaceAll('https://www.youtube.com', '')
- .replaceAll('\n', '
')
- )
- this.dbID = await addWatchedVideo(video)
- },
- getComments () {
- this.fetchComments().then(data => (this.comments = data))
- },
+ this.video.description = LibPiped.purifyHTML(
+ this.video.description
+ .replaceAll('http://www.youtube.com', '')
+ .replaceAll('https://www.youtube.com', '')
+ .replaceAll('\n', '
')
+ )
+ this.dbID = await addWatchedVideo(video)
+ },
+ getComments () {
+ this.fetchComments().then(data => (this.comments = data))
+ },
- onTimeUpdate: debounce(function onTimeUpdate (e) {
- if (this.dbID == null || !this.$refs.player) {
- return
- }
- return updateWatchedVideoProgress(this.dbID, this.$refs.player.getCurrentTime(), this.video.duration)
- }, 500)
- },
- computed: {
- isAutoplayEnabled () {
- return this.$store.getters['prefs/getPreferenceBoolean']('autoplay', false)
- },
- videoId () {
- return this.$route.query.v || this.$route.params.v
- },
- skipToTime () {
- // 't' in $route.query ? Number($route.query.t) : (lastWatch.progress ? lastWatch.progress : undefined)
- // 1st Priority - t in query
- // 2nd Priority - Last Watched Progress, if enabled
- if ('t' in this.$route.query) {
- return Number(this.$route.query.t)
- } else if (this.lastWatch && this.lastWatch.progress != null && this.lastWatch.progress !== 0 && this.$store.getters['prefs/getPreferenceBoolean']('skipToLastPoint', true)) {
- return this.lastWatch.progress
- } else {
- return undefined
- }
- },
+ onTimeUpdate: debounce(function onTimeUpdate (e) {
+ if (this.dbID == null || !this.$refs.player) {
+ return
+ }
+ return updateWatchedVideoProgress(this.dbID, this.$refs.player.getCurrentTime(), this.video.duration)
+ }, 500)
+ },
+ computed: {
+ isAutoplayEnabled () {
+ return this.$store.getters['prefs/getPreferenceBoolean']('autoplay', false)
+ },
+ videoId () {
+ return this.$route.query.v || this.$route.params.v
+ },
+ skipToTime () {
+ // 't' in $route.query ? Number($route.query.t) : (lastWatch.progress ? lastWatch.progress : undefined)
+ // 1st Priority - t in query
+ // 2nd Priority - Last Watched Progress, if enabled
+ if ('t' in this.$route.query) {
+ return Number(this.$route.query.t)
+ } else if (this.lastWatch && this.lastWatch.progress != null && this.lastWatch.progress !== 0 && this.$store.getters['prefs/getPreferenceBoolean']('skipToLastPoint', true)) {
+ return this.lastWatch.progress
+ } else {
+ return undefined
+ }
+ },
- lastWatchDurationH () {
- return LibPiped.timeFormat(this.lastWatch.progress)
- }
- },
- components: {
- ExpandableDate,
- SubscriptionButton,
- VideoComment,
- Player,
- VideoItem,
- ErrorHandler
- }
+ lastWatchDurationH () {
+ return LibPiped.timeFormat(this.lastWatch.progress)
+ }
+ },
+ components: {
+ ExpandableDate,
+ SubscriptionButton,
+ VideoComment,
+ Player,
+ VideoItem,
+ ErrorHandler
+ }
}
diff --git a/src/store/authentication-store.js b/src/store/authentication-store.js
index 0bb5a91..92a48c5 100644
--- a/src/store/authentication-store.js
+++ b/src/store/authentication-store.js
@@ -2,136 +2,136 @@ import axios from 'axios'
import { isPlainObject as _isPlainObject, set as _set } from 'lodash-es'
export class AuthenticationError extends Error {
- constructor (message) {
- super()
- this.message = message
- }
+ constructor (message) {
+ super()
+ this.message = message
+ }
}
const AuthenticationStore = {
- namespaced: true,
+ namespaced: true,
- state: () => ({
- authStateByInstance: {
- // isAuthenticated: Boolean
- // authToken: String?
- }
- }),
+ state: () => ({
+ authStateByInstance: {
+ // isAuthenticated: Boolean
+ // authToken: String?
+ }
+ }),
- mutations: {
- replaceAuth (state, data) {
- state.authStateByInstance = data
- },
+ mutations: {
+ replaceAuth (state, data) {
+ state.authStateByInstance = data
+ },
- setAuthToken (state, { apiURL, token }) {
- _set(state.authStateByInstance, [apiURL], {
- isAuthenticated: true,
- authToken: token
- })
- window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
- },
+ setAuthToken (state, { apiURL, token }) {
+ _set(state.authStateByInstance, [apiURL], {
+ isAuthenticated: true,
+ authToken: token
+ })
+ window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
+ },
- deleteAuthToken (state, { apiURL }) {
- _set(state.authStateByInstance, [apiURL], {
- isAuthenticated: false
- })
- window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
- }
- },
+ deleteAuthToken (state, { apiURL }) {
+ _set(state.authStateByInstance, [apiURL], {
+ isAuthenticated: false
+ })
+ window.localStorage.setItem('AUTH', JSON.stringify(state.authStateByInstance))
+ }
+ },
- getters: {
- isCurrentlyAuthenticated (state, getters, rootState, rootGetters) {
- const s = state.authStateByInstance[rootGetters['prefs/apiUrl']]
- return s ? (s.isAuthenticated === true) : false
- },
+ getters: {
+ isCurrentlyAuthenticated (state, getters, rootState, rootGetters) {
+ const s = state.authStateByInstance[rootGetters['prefs/apiUrl']]
+ return s ? (s.isAuthenticated === true) : false
+ },
- authToken (state, getters, rootState, rootGetters) {
- const authState = state.authStateByInstance[rootGetters['prefs/apiUrl']] || {}
- return authState.authToken
- }
- },
+ authToken (state, getters, rootState, rootGetters) {
+ const authState = state.authStateByInstance[rootGetters['prefs/apiUrl']] || {}
+ return authState.authToken
+ }
+ },
- actions: {
- initializeAuth ({ commit }) {
- const data = window.localStorage.getItem('AUTH')
- if (data != null) {
- commit('replaceAuth', JSON.parse(data))
- }
- },
+ actions: {
+ initializeAuth ({ commit }) {
+ const data = window.localStorage.getItem('AUTH')
+ if (data != null) {
+ commit('replaceAuth', JSON.parse(data))
+ }
+ },
- async loginOrRegister ({ commit, rootGetters }, { path, username, password }) {
- const apiURL = rootGetters['prefs/apiUrl']
- const { data: resp } = await axios({
- method: 'POST',
- baseURL: apiURL,
- url: '/' + path,
- data: {
- username,
- password
- }
- })
+ async loginOrRegister ({ commit, rootGetters }, { path, username, password }) {
+ const apiURL = rootGetters['prefs/apiUrl']
+ const { data: resp } = await axios({
+ method: 'POST',
+ baseURL: apiURL,
+ url: '/' + path,
+ data: {
+ username,
+ password
+ }
+ })
- if ('error' in resp) {
- throw new AuthenticationError(resp.error)
- }
+ if ('error' in resp) {
+ throw new AuthenticationError(resp.error)
+ }
- commit('setAuthToken', {
- apiURL,
- token: resp.token
- })
- },
+ commit('setAuthToken', {
+ apiURL,
+ token: resp.token
+ })
+ },
- async makeRequest ({
- commit,
- state,
- rootGetters
- }, {
- path,
- method,
- data,
- params,
- tokenInParams = false
- }) {
- const APIURL = rootGetters['prefs/apiUrl']
- const AuthState = state.authStateByInstance[APIURL] || {}
+ async makeRequest ({
+ commit,
+ state,
+ rootGetters
+ }, {
+ path,
+ method,
+ data,
+ params,
+ tokenInParams = false
+ }) {
+ const APIURL = rootGetters['prefs/apiUrl']
+ const AuthState = state.authStateByInstance[APIURL] || {}
- if (AuthState.isAuthenticated && tokenInParams) {
- if (_isPlainObject(params)) {
- params.authToken = AuthState.authToken
- } else {
- params = {
- authToken: AuthState.authToken
- }
- }
- }
+ if (AuthState.isAuthenticated && tokenInParams) {
+ if (_isPlainObject(params)) {
+ params.authToken = AuthState.authToken
+ } else {
+ params = {
+ authToken: AuthState.authToken
+ }
+ }
+ }
- const { data: resp } = await axios({
- baseURL: APIURL,
- method,
- url: path,
- params,
- data,
- headers: AuthState.isAuthenticated
- ? {
- Authorization: AuthState.authToken
- }
- : undefined
- })
+ const { data: resp } = await axios({
+ baseURL: APIURL,
+ method,
+ url: path,
+ params,
+ data,
+ headers: AuthState.isAuthenticated
+ ? {
+ Authorization: AuthState.authToken
+ }
+ : undefined
+ })
- return resp
- }
- }
+ return resp
+ }
+ }
}
function initializeAuthEvents (store) {
- window.addEventListener('storage', (storageEv) => {
- if (storageEv.key === 'AUTH') {
- store.commit('auth/replaceAuth', JSON.parse(storageEv.newValue))
- }
- })
+ window.addEventListener('storage', (storageEv) => {
+ if (storageEv.key === 'AUTH') {
+ store.commit('auth/replaceAuth', JSON.parse(storageEv.newValue))
+ }
+ })
}
export {
- AuthenticationStore,
- initializeAuthEvents
+ AuthenticationStore,
+ initializeAuthEvents
}
diff --git a/src/store/index.js b/src/store/index.js
index ead76ad..ca76c8d 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -7,10 +7,10 @@ import { AuthenticationStore, initializeAuthEvents } from '@/store/authenticatio
Vue.use(Vuex)
const store = new Vuex.Store({
- modules: {
- prefs: PrefsStore,
- auth: AuthenticationStore
- }
+ modules: {
+ prefs: PrefsStore,
+ auth: AuthenticationStore
+ }
})
initializePrefEvents(store)
diff --git a/src/store/prefs-store.js b/src/store/prefs-store.js
index 8c943ed..6f4ed2e 100644
--- a/src/store/prefs-store.js
+++ b/src/store/prefs-store.js
@@ -1,93 +1,93 @@
import { get as _get, isString, set as _set } from 'lodash-es'
const PrefsStore = {
- namespaced: true,
- state: () => ({
- // Declaring boolean values here because v-simple-checkbox has a bug (???) which renders it unable to have internal state
- prefs: {
- darkMode: false,
- playerAutoplay: true,
- autoplay: false,
- listen: false,
- disableLBRY: true,
- proxyLBRY: true,
- sponsorblock: true,
- skipToLastPoint: true
- }
- }),
- mutations: {
- setPrefs (state, {
- id,
- value
- }) {
- _set(state.prefs, id, value)
- window.localStorage.setItem('PREFERENCES', JSON.stringify(state.prefs))
- },
+ namespaced: true,
+ state: () => ({
+ // Declaring boolean values here because v-simple-checkbox has a bug (???) which renders it unable to have internal state
+ prefs: {
+ darkMode: false,
+ playerAutoplay: true,
+ autoplay: false,
+ listen: false,
+ disableLBRY: true,
+ proxyLBRY: true,
+ sponsorblock: true,
+ skipToLastPoint: true
+ }
+ }),
+ mutations: {
+ setPrefs (state, {
+ id,
+ value
+ }) {
+ _set(state.prefs, id, value)
+ window.localStorage.setItem('PREFERENCES', JSON.stringify(state.prefs))
+ },
- replacePrefs (state, nextPrefs) {
- state.prefs = nextPrefs
- }
- },
- actions: {
- loadState ({ commit }) {
- try {
- const jv = window.localStorage.getItem('PREFERENCES')
- if (isString(jv) && jv.length !== 0) {
- const p = JSON.parse(jv)
- commit('replacePrefs', p)
- }
- } catch (e) {
- console.log('Error:', e)
- }
- }
- },
+ replacePrefs (state, nextPrefs) {
+ state.prefs = nextPrefs
+ }
+ },
+ actions: {
+ loadState ({ commit }) {
+ try {
+ const jv = window.localStorage.getItem('PREFERENCES')
+ if (isString(jv) && jv.length !== 0) {
+ const p = JSON.parse(jv)
+ commit('replacePrefs', p)
+ }
+ } catch (e) {
+ console.log('Error:', e)
+ }
+ }
+ },
- getters: {
- getPreference: state => (id, default_) => {
- return _get(state.prefs, id, default_)
- },
+ getters: {
+ getPreference: state => (id, default_) => {
+ return _get(state.prefs, id, default_)
+ },
- getPreferenceBoolean: (_, getters) => (...args) => {
- const v = getters.getPreference(...args)
+ getPreferenceBoolean: (_, getters) => (...args) => {
+ const v = getters.getPreference(...args)
- switch (v) {
- case 'true':
- case true:
- case '1':
- case 'on':
- case 'yes':
- return true
- default:
- return false
- }
- },
+ switch (v) {
+ case 'true':
+ case true:
+ case '1':
+ case 'on':
+ case 'yes':
+ return true
+ default:
+ return false
+ }
+ },
- getPreferenceNumber: (_, getters) => (id, default_) => {
- const v = getters.getPreference(id, default_)
+ getPreferenceNumber: (_, getters) => (id, default_) => {
+ const v = getters.getPreference(id, default_)
- const n = Number(v)
- if (!Number.isFinite(n)) {
- return default_
- } else {
- return n
- }
- },
+ const n = Number(v)
+ if (!Number.isFinite(n)) {
+ return default_
+ } else {
+ return n
+ }
+ },
- apiUrl (state, getters) {
- return getters.getPreference('instance', process.env.VUE_APP_PIPED_URL || 'https://pipedapi.kavin.rocks')
- }
- }
+ apiUrl (state, getters) {
+ return getters.getPreference('instance', process.env.VUE_APP_PIPED_URL || 'https://pipedapi.kavin.rocks')
+ }
+ }
}
function initializePrefEvents (store) {
- window.addEventListener('storage', (storageEv) => {
- if (storageEv.key === 'PREFERENCES') {
- store.commit('prefs/replacePrefs', JSON.parse(storageEv.newValue))
- }
- })
+ window.addEventListener('storage', (storageEv) => {
+ if (storageEv.key === 'PREFERENCES') {
+ store.commit('prefs/replacePrefs', JSON.parse(storageEv.newValue))
+ }
+ })
}
export {
- PrefsStore,
- initializePrefEvents
+ PrefsStore,
+ initializePrefEvents
}
diff --git a/src/store/watched-videos-db.js b/src/store/watched-videos-db.js
index b3db21f..1b432ec 100644
--- a/src/store/watched-videos-db.js
+++ b/src/store/watched-videos-db.js
@@ -3,39 +3,39 @@ import Dexie from 'dexie'
export const PMDB = new Dexie('PipedMaterialDB')
PMDB.version(3).stores({
- watchedVideos: '++id,videoId,progressPcnt,timestamp'
+ watchedVideos: '++id,videoId,progressPcnt,timestamp'
})
export async function addWatchedVideo (videoObj) {
- return PMDB.watchedVideos.add({
- videoId: videoObj.videoId,
- video: videoObj,
- progress: 0,
- progressPcnt: 0,
- timestamp: new Date()
- })
+ return PMDB.watchedVideos.add({
+ videoId: videoObj.videoId,
+ video: videoObj,
+ progress: 0,
+ progressPcnt: 0,
+ timestamp: new Date()
+ })
}
export function updateWatchedVideoProgress (videoID, prog, dur) {
- return PMDB.watchedVideos.update(videoID, {
- progress: prog,
- progressPcnt: Math.min((prog / dur) * 100, 100)
- })
+ return PMDB.watchedVideos.update(videoID, {
+ progress: prog,
+ progressPcnt: Math.min((prog / dur) * 100, 100)
+ })
}
export function findLastWatch (videoId) {
- return PMDB.watchedVideos.where('videoId').equals(videoId).last()
+ return PMDB.watchedVideos.where('videoId').equals(videoId).last()
}
export function getWatchedVideos () {
- return PMDB.watchedVideos.orderBy('timestamp').reverse().toArray()
+ return PMDB.watchedVideos.orderBy('timestamp').reverse().toArray()
}
export function getUnfinishedVideos () {
- // Shaka never fires the last event, thus the last percent turns out to be around 99.95 or 99.98
- return PMDB.watchedVideos.where('progressPcnt').below(99.9).reverse().sortBy('timestamp')
+ // Shaka never fires the last event, thus the last percent turns out to be around 99.95 or 99.98
+ return PMDB.watchedVideos.where('progressPcnt').below(99.9).reverse().sortBy('timestamp')
}
export function deleteWatchedVideos () {
- return PMDB.watchedVideos.clear()
+ return PMDB.watchedVideos.clear()
}
diff --git a/src/tools/DashUtils.js b/src/tools/DashUtils.js
index a225684..471c6cc 100644
--- a/src/tools/DashUtils.js
+++ b/src/tools/DashUtils.js
@@ -3,185 +3,185 @@
import xml from 'xml-js'
const DashUtils = {
- generate_dash_file_from_formats (VideoFormats, VideoLength) {
- const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength)
- return xml.json2xml(generatedJSON)
- },
- generate_xmljs_json_from_data (VideoFormatArray, VideoLength) {
- return {
- declaration: {
- attributes: {
- version: '1.0',
- encoding: 'utf-8'
- }
- },
- elements: [
- {
- type: 'element',
- name: 'MPD',
- attributes: {
- xmlns: 'urn:mpeg:dash:schema:mpd:2011',
- profiles: 'urn:mpeg:dash:profile:full:2011',
- minBufferTime: 'PT1.5S',
- type: 'static',
- mediaPresentationDuration: `PT${VideoLength}S`
- },
- elements: [
- {
- type: 'element',
- name: 'Period',
- elements: this.generate_adaptation_set(VideoFormatArray)
- }
- ]
- }
- ]
- }
- },
- generate_adaptation_set (VideoFormatArray) {
- const adaptationSets = []
- const mimeTypes = []
- const mimeObjects = [[]]
- // sort the formats by mime types
- VideoFormatArray.forEach(videoFormat => {
- // the dual formats should not be used
- if (videoFormat.mimeType.indexOf('video') !== -1 && !videoFormat.videoOnly) {
- return
- }
- // if these properties are not available, then we skip it because we cannot set these properties
- // if (!(videoFormat.hasOwnProperty('initRange') && videoFormat.hasOwnProperty('indexRange'))) {
- // return
- // }
- const mimeType = videoFormat.mimeType
- const mimeTypeIndex = mimeTypes.indexOf(mimeType)
- if (mimeTypeIndex > -1) {
- mimeObjects[mimeTypeIndex].push(videoFormat)
- } else {
- mimeTypes.push(mimeType)
- mimeObjects.push([])
- mimeObjects[mimeTypes.length - 1].push(videoFormat)
- }
- })
- // for each MimeType generate a new Adaptation set with Representations as sub elements
- for (let i = 0; i < mimeTypes.length; i++) {
- let isVideoFormat = false
- const adapSet = {
- type: 'element',
- name: 'AdaptationSet',
- attributes: {
- id: i,
- mimeType: mimeTypes[i],
- startWithSAP: '1',
- subsegmentAlignment: 'true'
- },
- elements: []
- }
- if (!mimeTypes[i].includes('audio')) {
- adapSet.attributes.scanType = 'progressive'
- isVideoFormat = true
- }
- mimeObjects[i].forEach(format => {
- if (isVideoFormat) {
- adapSet.elements.push(this.generate_representation_video(format))
- } else {
- adapSet.elements.push(this.generate_representation_audio(format))
- }
- })
- adaptationSets.push(adapSet)
- }
- return adaptationSets
- },
- generate_representation_audio (Format) {
- return {
- type: 'element',
- name: 'Representation',
- attributes: {
- id: Format.itag,
- codecs: Format.codec,
- bandwidth: Format.bitrate
- },
- elements: [
- {
- type: 'element',
- name: 'AudioChannelConfiguration',
- attributes: {
- schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
- value: '2'
- }
- },
- {
- type: 'element',
- name: 'BaseURL',
- elements: [
- {
- type: 'text',
- text: Format.url
- }
- ]
- },
- {
- type: 'element',
- name: 'SegmentBase',
- attributes: {
- indexRange: `${Format.indexStart}-${Format.indexEnd}`
- },
- elements: [
- {
- type: 'element',
- name: 'Initialization',
- attributes: {
- range: `${Format.initStart}-${Format.initEnd}`
- }
- }
- ]
- }
- ]
- }
- },
- generate_representation_video (Format) {
- return {
- type: 'element',
- name: 'Representation',
- attributes: {
- id: Format.itag,
- codecs: Format.codec,
- bandwidth: Format.bitrate,
- width: Format.width,
- height: Format.height,
- maxPlayoutRate: '1',
- frameRate: Format.fps
- },
- elements: [
- {
- type: 'element',
- name: 'BaseURL',
- elements: [
- {
- type: 'text',
- text: Format.url
- }
- ]
- },
- {
- type: 'element',
- name: 'SegmentBase',
- attributes: {
- indexRange: `${Format.indexStart}-${Format.indexEnd}`
- },
- elements: [
- {
- type: 'element',
- name: 'Initialization',
- attributes: {
- range: `${Format.initStart}-${Format.initEnd}`
- }
- }
- ]
- }
- ]
- }
- }
+ generate_dash_file_from_formats (VideoFormats, VideoLength) {
+ const generatedJSON = this.generate_xmljs_json_from_data(VideoFormats, VideoLength)
+ return xml.json2xml(generatedJSON)
+ },
+ generate_xmljs_json_from_data (VideoFormatArray, VideoLength) {
+ return {
+ declaration: {
+ attributes: {
+ version: '1.0',
+ encoding: 'utf-8'
+ }
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'MPD',
+ attributes: {
+ xmlns: 'urn:mpeg:dash:schema:mpd:2011',
+ profiles: 'urn:mpeg:dash:profile:full:2011',
+ minBufferTime: 'PT1.5S',
+ type: 'static',
+ mediaPresentationDuration: `PT${VideoLength}S`
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'Period',
+ elements: this.generate_adaptation_set(VideoFormatArray)
+ }
+ ]
+ }
+ ]
+ }
+ },
+ generate_adaptation_set (VideoFormatArray) {
+ const adaptationSets = []
+ const mimeTypes = []
+ const mimeObjects = [[]]
+ // sort the formats by mime types
+ VideoFormatArray.forEach(videoFormat => {
+ // the dual formats should not be used
+ if (videoFormat.mimeType.indexOf('video') !== -1 && !videoFormat.videoOnly) {
+ return
+ }
+ // if these properties are not available, then we skip it because we cannot set these properties
+ // if (!(videoFormat.hasOwnProperty('initRange') && videoFormat.hasOwnProperty('indexRange'))) {
+ // return
+ // }
+ const mimeType = videoFormat.mimeType
+ const mimeTypeIndex = mimeTypes.indexOf(mimeType)
+ if (mimeTypeIndex > -1) {
+ mimeObjects[mimeTypeIndex].push(videoFormat)
+ } else {
+ mimeTypes.push(mimeType)
+ mimeObjects.push([])
+ mimeObjects[mimeTypes.length - 1].push(videoFormat)
+ }
+ })
+ // for each MimeType generate a new Adaptation set with Representations as sub elements
+ for (let i = 0; i < mimeTypes.length; i++) {
+ let isVideoFormat = false
+ const adapSet = {
+ type: 'element',
+ name: 'AdaptationSet',
+ attributes: {
+ id: i,
+ mimeType: mimeTypes[i],
+ startWithSAP: '1',
+ subsegmentAlignment: 'true'
+ },
+ elements: []
+ }
+ if (!mimeTypes[i].includes('audio')) {
+ adapSet.attributes.scanType = 'progressive'
+ isVideoFormat = true
+ }
+ mimeObjects[i].forEach(format => {
+ if (isVideoFormat) {
+ adapSet.elements.push(this.generate_representation_video(format))
+ } else {
+ adapSet.elements.push(this.generate_representation_audio(format))
+ }
+ })
+ adaptationSets.push(adapSet)
+ }
+ return adaptationSets
+ },
+ generate_representation_audio (Format) {
+ return {
+ type: 'element',
+ name: 'Representation',
+ attributes: {
+ id: Format.itag,
+ codecs: Format.codec,
+ bandwidth: Format.bitrate
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'AudioChannelConfiguration',
+ attributes: {
+ schemeIdUri: 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011',
+ value: '2'
+ }
+ },
+ {
+ type: 'element',
+ name: 'BaseURL',
+ elements: [
+ {
+ type: 'text',
+ text: Format.url
+ }
+ ]
+ },
+ {
+ type: 'element',
+ name: 'SegmentBase',
+ attributes: {
+ indexRange: `${Format.indexStart}-${Format.indexEnd}`
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'Initialization',
+ attributes: {
+ range: `${Format.initStart}-${Format.initEnd}`
+ }
+ }
+ ]
+ }
+ ]
+ }
+ },
+ generate_representation_video (Format) {
+ return {
+ type: 'element',
+ name: 'Representation',
+ attributes: {
+ id: Format.itag,
+ codecs: Format.codec,
+ bandwidth: Format.bitrate,
+ width: Format.width,
+ height: Format.height,
+ maxPlayoutRate: '1',
+ frameRate: Format.fps
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'BaseURL',
+ elements: [
+ {
+ type: 'text',
+ text: Format.url
+ }
+ ]
+ },
+ {
+ type: 'element',
+ name: 'SegmentBase',
+ attributes: {
+ indexRange: `${Format.indexStart}-${Format.indexEnd}`
+ },
+ elements: [
+ {
+ type: 'element',
+ name: 'Initialization',
+ attributes: {
+ range: `${Format.initStart}-${Format.initEnd}`
+ }
+ }
+ ]
+ }
+ ]
+ }
+ }
}
export {
- DashUtils
+ DashUtils
}
diff --git a/src/tools/libpiped.js b/src/tools/libpiped.js
index 88d27d9..a1a81ae 100644
--- a/src/tools/libpiped.js
+++ b/src/tools/libpiped.js
@@ -7,74 +7,86 @@ TimeAgo.addDefaultLocale(en)
const timeAgo = new TimeAgo('en-US')
class LibPiped {
- intlDTF = new Intl.DateTimeFormat([], {
- dateStyle: 'full',
- timeStyle: 'full'
- })
+ intlDTF = new Intl.DateTimeFormat([], {
+ dateStyle: 'full',
+ timeStyle: 'full'
+ })
- pad (num, size) {
- return ('000' + num).slice(size * -1)
- }
+ pad (num, size) {
+ return ('000' + num).slice(size * -1)
+ }
- timeFormat (duration) {
- const time = parseFloat(duration).toFixed(3)
- const hours = Math.floor(time / 60 / 60)
- const minutes = Math.floor(time / 60) % 60
- const seconds = Math.floor(time - minutes * 60)
+ timeFormat (duration) {
+ const time = parseFloat(duration).toFixed(3)
+ const hours = Math.floor(time / 60 / 60)
+ const minutes = Math.floor(time / 60) % 60
+ const seconds = Math.floor(time - minutes * 60)
- let str = ''
+ let str = ''
- if (hours > 0) str += hours + ':'
+ if (hours > 0) str += hours + ':'
- str += this.pad(minutes, 2) + ':' + this.pad(seconds, 2)
+ str += this.pad(minutes, 2) + ':' + this.pad(seconds, 2)
- return str
- }
+ return str
+ }
- formatFullDate (date) {
- return this.intlDTF.format(date)
- }
+ formatFullDate (date) {
+ return this.intlDTF.format(date)
+ }
- numberFormat (num) {
- const digits = 2
- const si = [
- { value: 1, symbol: '' },
- { value: 1e3, symbol: 'K' },
- { value: 1e6, symbol: 'M' },
- { value: 1e9, symbol: 'B' }
- ]
- const rx = /\.0+$|(\.[0-9]*[1-9])0+$/
- let i
- for (i = si.length - 1; i > 0; i--) {
- if (num >= si[i].value) {
- break
- }
- }
- return (num / si[i].value).toFixed(digits).replace(rx, '$1') + si[i].symbol
- }
+ numberFormat (num) {
+ const digits = 2
+ const si = [
+ {
+ value: 1,
+ symbol: ''
+ },
+ {
+ value: 1e3,
+ symbol: 'K'
+ },
+ {
+ value: 1e6,
+ symbol: 'M'
+ },
+ {
+ value: 1e9,
+ symbol: 'B'
+ }
+ ]
+ const rx = /\.0+$|(\.[0-9]*[1-9])0+$/
+ let i
+ for (i = si.length - 1; i > 0; i--) {
+ if (num >= si[i].value) {
+ break
+ }
+ }
+ return (num / si[i].value).toFixed(digits).replace(rx, '$1') + si[i].symbol
+ }
- addCommas (num) {
- num = parseInt(num)
- return num.toLocaleString('en-US')
- }
+ addCommas (num) {
+ num = parseInt(num)
+ return num.toLocaleString('en-US')
+ }
- purifyHTML (original) {
- return DOMPurify.sanitize(original)
- }
+ purifyHTML (original) {
+ return DOMPurify.sanitize(original)
+ }
- timeAgo (time) {
- return timeAgo.format(time)
- }
+ timeAgo (time) {
+ return timeAgo.format(time)
+ }
- determineVideoIdFromPath (path) {
- const loc = new URL(path, 'http://localhost')
- return loc.searchParams.get('v')
- }
+ determineVideoIdFromPath (path) {
+ const loc = new URL(path, 'http://localhost')
+ return loc.searchParams.get('v')
+ }
- determineVideoIdFromChannelURL (path) {
- const pathParts = path.split('/')
- return pathParts[2]
- }
+ determineVideoIdFromChannelURL (path) {
+ const pathParts = path.split('/')
+ return pathParts[2]
+ }
}
const lp = new LibPiped()
diff --git a/vue.config.js b/vue.config.js
index 89de98f..6f8637d 100644
--- a/vue.config.js
+++ b/vue.config.js
@@ -1,26 +1,26 @@
module.exports = {
- transpileDependencies: [
- 'vuetify'
- ],
- pwa: {
- name: 'Piped Material',
- themeColor: '#458588',
- msTileColor: '',
- appleMobileWebAppCapable: 'yes',
- appleMobileWebAppStatusBarStyle: 'black',
+ transpileDependencies: [
+ 'vuetify'
+ ],
+ pwa: {
+ name: 'Piped Material',
+ themeColor: '#458588',
+ msTileColor: '',
+ appleMobileWebAppCapable: 'yes',
+ appleMobileWebAppStatusBarStyle: 'black',
- workboxOptions: {
- navigateFallback: 'index.html',
- skipWaiting: true,
- importWorkboxFrom: 'local',
- runtimeCaching: [
- {
- urlPattern: /\.(?:png|svg|ico)$/,
- handler: 'CacheFirst'
- }
- ]
- }
- },
+ workboxOptions: {
+ navigateFallback: 'index.html',
+ skipWaiting: true,
+ importWorkboxFrom: 'local',
+ runtimeCaching: [
+ {
+ urlPattern: /\.(?:png|svg|ico)$/,
+ handler: 'CacheFirst'
+ }
+ ]
+ }
+ },
- lintOnSave: false
+ lintOnSave: false
}