From 2133a10efa112945a01f44ceb302605beff02cd0 Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 19 Aug 2020 22:39:44 -0400 Subject: [PATCH] Implement History and jump to last watched time progress --- src/renderer/App.js | 1 + .../components/ft-list-video/ft-list-video.js | 106 ++++++++++++++++-- .../ft-list-video/ft-list-video.vue | 6 +- .../ft-video-player/ft-video-player.js | 4 + .../player-settings/player-settings.vue | 1 - src/renderer/router/index.js | 1 + src/renderer/sass-partials/_ft-list-item.sass | 15 +++ src/renderer/store/modules/history.js | 86 ++++++++++++++ src/renderer/views/History/History.js | 46 +++++++- src/renderer/views/History/History.vue | 29 ++++- src/renderer/views/Watch/Watch.js | 70 +++++++++++- src/renderer/views/Watch/Watch.vue | 1 + static/locales/en-US.yaml | 5 + 13 files changed, 357 insertions(+), 14 deletions(-) create mode 100644 src/renderer/store/modules/history.js diff --git a/src/renderer/App.js b/src/renderer/App.js index 63375810..d74d3262 100644 --- a/src/renderer/App.js +++ b/src/renderer/App.js @@ -31,6 +31,7 @@ export default Vue.extend({ }, mounted: function () { this.$store.dispatch('grabUserSettings') + this.$store.dispatch('grabHistory') this.$store.commit('setUsingElectron', useElectron) this.checkThemeSettings() this.checkLocale() diff --git a/src/renderer/components/ft-list-video/ft-list-video.js b/src/renderer/components/ft-list-video/ft-list-video.js index f9c59e37..e7622555 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.js +++ b/src/renderer/components/ft-list-video/ft-list-video.js @@ -37,11 +37,13 @@ export default Vue.extend({ duration: '', description: '', watched: false, - progressPercentage: 0, + watchProgress: 0, + publishedText: '', isLive: false, isFavorited: false, hideViews: false, optionsValues: [ + 'history', 'openYoutube', 'copyYoutube', 'openYoutubeEmbed', @@ -56,6 +58,10 @@ export default Vue.extend({ return this.$store.getters.getUsingElectron }, + historyCache: function () { + return this.$store.getters.getHistoryCache + }, + listType: function () { return this.$store.getters.getListType }, @@ -72,6 +78,12 @@ export default Vue.extend({ return this.$store.getters.getInvidiousInstance }, + inHistory: function () { + // When in the history page, showing relative dates isn't very useful. + // We want to show the exact date instead + return this.$router.currentRoute.name === 'history' + }, + invidiousUrl: function () { return `${this.invidiousInstance}/watch?v=${this.id}` }, @@ -84,8 +96,12 @@ export default Vue.extend({ return `https://www.youtube-nocookie.com/embed/${this.id}` }, + progressPercentage: function () { + return (this.watchProgress / this.data.lengthSeconds) * 100 + }, + optionsNames: function () { - return [ + const names = [ this.$t('Video.Open in YouTube'), this.$t('Video.Copy YouTube Link'), this.$t('Video.Open YouTube Embedded Player'), @@ -93,6 +109,14 @@ export default Vue.extend({ this.$t('Video.Open in Invidious'), this.$t('Video.Copy Invidious Link') ] + + if (this.watched) { + names.unshift(this.$t('Video.Remove From History')) + } else { + names.unshift(this.$t('Video.Mark As Watched')) + } + + return names }, thumbnail: function () { @@ -128,6 +152,8 @@ export default Vue.extend({ } else { this.parseLocalData() } + + this.checkIfWatched() }, methods: { toggleSave: function () { @@ -139,6 +165,13 @@ export default Vue.extend({ console.log(option) switch (option) { + case 'history': + if (this.watched) { + this.removeFromWatched() + } else { + this.markAsWatched() + } + break case 'copyYoutube': navigator.clipboard.writeText(this.youtubeUrl) break @@ -213,7 +246,7 @@ export default Vue.extend({ this.isLive = this.data.liveNow this.viewCount = this.data.viewCount - if (typeof (this.data.publishedText) !== 'undefined') { + if (typeof (this.data.publishedText) !== 'undefined' && !this.isLive) { // produces a string according to the template in the locales string this.toLocalePublicationString({ publishText: this.data.publishedText, @@ -221,7 +254,7 @@ export default Vue.extend({ timeStrings: this.$t('Video.Published'), liveStreamString: this.$t('Video.Watching'), upcomingString: this.$t('Video.Published.Upcoming'), - isLive: this.data.live, + isLive: this.isLive, isUpcoming: this.data.isUpcoming }).then((data) => { this.uploadedTime = data @@ -253,7 +286,7 @@ export default Vue.extend({ this.channelId = this.data.ucid this.viewCount = this.data.views - // Data is returned as a literal string names 'undefined' + // Data is returned as a literal string named 'undefined' if (this.data.length_seconds !== 'undefined') { this.duration = this.calculateVideoDuration(parseInt(this.data.length_seconds)) } @@ -265,7 +298,7 @@ export default Vue.extend({ this.channelId = this.channelId.replace('https://www.youtube.com/channel/', '') } - if (typeof (this.data.uploaded_at) !== 'undefined') { + if (typeof (this.data.uploaded_at) !== 'undefined' && !this.data.live) { this.toLocalePublicationString({ publishText: this.data.uploaded_at, templateString: this.$t('Video.Publicationtemplate'), @@ -293,8 +326,67 @@ export default Vue.extend({ this.isLive = this.data.live }, + + checkIfWatched: function () { + const historyIndex = this.historyCache.findIndex((video) => { + return video.videoId === this.id + }) + + if (historyIndex !== -1) { + this.watched = true + this.watchProgress = this.historyCache[historyIndex].watchProgress + + if (this.historyCache[historyIndex].published !== '') { + const videoPublished = this.historyCache[historyIndex].published + const videoPublishedDate = new Date(videoPublished) + this.publishedText = videoPublishedDate.toLocaleDateString() + } else { + this.publishedText = '' + } + } + }, + + markAsWatched: function () { + const videoData = { + videoId: this.id, + title: this.title, + author: this.channelName, + authorId: this.channelId, + published: '', + description: this.description, + viewCount: this.viewCount, + lengthSeconds: this.data.lengthSeconds, + watchProgress: 0, + timeWatched: new Date().getTime(), + isLive: false, + paid: false, + type: 'video' + } + + this.updateHistory(videoData) + + this.showToast({ + message: this.$t('Video.Video has been marked as watched') + }) + + this.watched = true + }, + + removeFromWatched: function () { + this.removeFromHistory(this.id) + + this.showToast({ + message: this.$t('Video.Video has been removed from your history') + }) + + this.watched = false + }, + ...mapActions([ - 'toLocalePublicationString' + 'showToast', + 'toLocalePublicationString', + 'updateHistory', + 'removeFromHistory' ]) } }) diff --git a/src/renderer/components/ft-list-video/ft-list-video.vue b/src/renderer/components/ft-list-video/ft-list-video.vue index e7222f42..cf238368 100644 --- a/src/renderer/components/ft-list-video/ft-list-video.vue +++ b/src/renderer/components/ft-list-video/ft-list-video.vue @@ -84,9 +84,13 @@ {{ $t("Video.View") }} {{ $t("Video.Views").toLowerCase() }} • {{ uploadedTime }} + • {{ publishedText }}
{ + return state.historyCache + } +} + +const actions = { + grabHistory ({ commit }) { + historyDb.find({}).sort({ + timeWatched: -1 + }).exec((err, results) => { + if (err) { + console.log(err) + return + } + commit('setHistoryCache', results) + }) + }, + + updateHistory ({ dispatch }, videoData) { + historyDb.update({ videoId: videoData.videoId }, videoData, { upsert: true }, (err, numReplaced) => { + if (!err) { + dispatch('grabHistory') + } + }) + }, + + removeFromHistory ({ dispatch }, videoId) { + historyDb.remove({ videoId: videoId }, (err, numReplaced) => { + if (!err) { + dispatch('grabHistory') + } + }) + }, + + updateWatchProgress ({ dispatch }, videoData) { + historyDb.update({ videoId: videoData.videoId }, { $set: { watchProgress: videoData.watchProgress } }, { upsert: true }, (err, numReplaced) => { + if (!err) { + dispatch('grabHistory') + } + }) + } +} + +const mutations = { + setHistoryCache (state, historyCache) { + state.historyCache = historyCache + } +} + +export default { + state, + getters, + actions, + mutations +} diff --git a/src/renderer/views/History/History.js b/src/renderer/views/History/History.js index bca1dfdb..90414add 100644 --- a/src/renderer/views/History/History.js +++ b/src/renderer/views/History/History.js @@ -1,15 +1,59 @@ import Vue from 'vue' +import FtLoader from '../../components/ft-loader/ft-loader.vue' import FtCard from '../../components/ft-card/ft-card.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtElementList from '../../components/ft-element-list/ft-element-list.vue' +import FtButton from '../../components/ft-button/ft-button.vue' export default Vue.extend({ name: 'History', components: { + 'ft-loader': FtLoader, 'ft-card': FtCard, 'ft-flex-box': FtFlexBox, - 'ft-element-list': FtElementList + 'ft-element-list': FtElementList, + 'ft-button': FtButton + }, + data: function () { + return { + isLoading: false, + dataLimit: 100 + } + }, + computed: { + historyCache: function () { + return this.$store.getters.getHistoryCache + }, + + activeData: function () { + if (this.historyCache.length < this.dataLimit) { + return this.historyCache + } else { + return this.historyCache.slice(0, this.dataLimit) + } + } + }, + watch: { + historyCache() { + this.isLoading = true + setTimeout(() => { + this.isLoading = false + }, 100) + } }, mounted: function () { + console.log(this.historyCache) + + const limit = sessionStorage.getItem('historyLimit') + + if (limit !== null) { + this.dataLimit = limit + } + }, + methods: { + increaseLimit: function () { + this.dataLimit += 100 + sessionStorage.setItem('historyLimit', this.dataLimit) + } } }) diff --git a/src/renderer/views/History/History.vue b/src/renderer/views/History/History.vue index 24b5cf42..2507f326 100644 --- a/src/renderer/views/History/History.vue +++ b/src/renderer/views/History/History.vue @@ -1,12 +1,35 @@ diff --git a/src/renderer/views/Watch/Watch.js b/src/renderer/views/Watch/Watch.js index 7ba0bafd..ac63c2ec 100644 --- a/src/renderer/views/Watch/Watch.js +++ b/src/renderer/views/Watch/Watch.js @@ -47,6 +47,7 @@ export default Vue.extend({ videoViewCount: 0, videoLikeCount: 0, videoDislikeCount: 0, + videoLengthSeconds: 0, channelName: '', channelThumbnail: '', channelId: '', @@ -72,6 +73,14 @@ export default Vue.extend({ return this.$store.getters.getUsingElectron }, + historyCache: function () { + return this.$store.getters.getHistoryCache + }, + + rememberHistory: function () { + return this.$store.getters.getRememberHistory + }, + backendPreference: function () { return this.$store.getters.getBackendPreference }, @@ -276,6 +285,7 @@ export default Vue.extend({ this.activeSourceList = this.videoSourceList } } else { + this.videoLengthSeconds = parseInt(result.videoDetails.lengthSeconds) this.videoSourceList = result.player_response.streamingData.formats this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => { @@ -395,6 +405,7 @@ export default Vue.extend({ } else if (this.forceLocalBackendForLegacy) { this.getLegacyFormats() } else { + this.videoLengthSeconds = result.lengthSeconds this.videoSourceList = result.formatStreams.reverse() this.audioSourceList = result.adaptiveFormats.filter((format) => { @@ -441,6 +452,46 @@ export default Vue.extend({ }) }, + addToHistory: function (watchProgress) { + const videoData = { + videoId: this.videoId, + title: this.videoTitle, + author: this.channelName, + authorId: this.channelId, + published: this.videoPublished, + description: this.videoDescription, + viewCount: this.videoViewCount, + lengthSeconds: this.videoLengthSeconds, + watchProgress: watchProgress, + timeWatched: new Date().getTime(), + isLive: false, + paid: false, + type: 'video' + } + + this.updateHistory(videoData) + }, + + checkIfWatched: function () { + const historyIndex = this.historyCache.findIndex((video) => { + return video.videoId === this.videoId + }) + + console.log(historyIndex) + + if (historyIndex !== -1 && !this.isLive) { + console.log(this.historyCache[historyIndex]) + const watchProgress = this.historyCache[historyIndex].watchProgress + this.$refs.videoPlayer.player.currentTime(watchProgress) + } + + if (this.rememberHistory && historyIndex !== -1) { + this.addToHistory(this.historyCache[historyIndex].watchProgress) + } else if (this.rememberHistory) { + this.addToHistory(0) + } + }, + checkIfPlaylist: function () { if (typeof (this.$route.query) !== 'undefined') { this.playlistId = this.$route.query.playlistId @@ -630,7 +681,24 @@ export default Vue.extend({ ...mapActions([ 'showToast', - 'buildVTTFileLocally' + 'buildVTTFileLocally', + 'updateHistory', + 'updateWatchProgress' ]) + }, + beforeRouteLeave: function (to, from, next) { + if (this.rememberHistory) { + const currentTime = this.$refs.videoPlayer.player.currentTime() + console.log(currentTime) + const payload = { + videoId: this.videoId, + watchProgress: currentTime + } + + console.log('update watch progress') + this.updateWatchProgress(payload) + } + + next() } }) diff --git a/src/renderer/views/Watch/Watch.vue b/src/renderer/views/Watch/Watch.vue index 95ca9ab4..298347f9 100644 --- a/src/renderer/views/Watch/Watch.vue +++ b/src/renderer/views/Watch/Watch.vue @@ -23,6 +23,7 @@ :thumbnail="thumbnail" class="videoPlayer" :class="{ theatrePlayer: useTheatreMode }" + @ready="checkIfWatched" @ended="handleVideoEnded" @error="handleVideoError" /> diff --git a/static/locales/en-US.yaml b/static/locales/en-US.yaml index ad60dcc2..821604bd 100644 --- a/static/locales/en-US.yaml +++ b/static/locales/en-US.yaml @@ -95,6 +95,7 @@ History: # On History Page History: History Watch History: Watch History + Your history list is currently empty.: Your history list is currently empty. Settings: # On Settings Page Settings: Settings @@ -264,6 +265,10 @@ Channel: Channel Description: Channel Description Featured Channels: Featured Channels Video: + Mark As Watched: Mark As Watched + Remove From History: Remove From History + Video has been marked as watched: Video has been marked as watched + Video has been removed from your history: Video has been removed from your history Open in YouTube: Open in YouTube Copy YouTube Link: Copy YouTube Link Open YouTube Embedded Player: Open YouTube Embedded Player