Implement History and jump to last watched time progress

This commit is contained in:
Preston 2020-08-19 22:39:44 -04:00
parent 6e5e1e0542
commit 2133a10efa
13 changed files with 357 additions and 14 deletions

View File

@ -31,6 +31,7 @@ export default Vue.extend({
}, },
mounted: function () { mounted: function () {
this.$store.dispatch('grabUserSettings') this.$store.dispatch('grabUserSettings')
this.$store.dispatch('grabHistory')
this.$store.commit('setUsingElectron', useElectron) this.$store.commit('setUsingElectron', useElectron)
this.checkThemeSettings() this.checkThemeSettings()
this.checkLocale() this.checkLocale()

View File

@ -37,11 +37,13 @@ export default Vue.extend({
duration: '', duration: '',
description: '', description: '',
watched: false, watched: false,
progressPercentage: 0, watchProgress: 0,
publishedText: '',
isLive: false, isLive: false,
isFavorited: false, isFavorited: false,
hideViews: false, hideViews: false,
optionsValues: [ optionsValues: [
'history',
'openYoutube', 'openYoutube',
'copyYoutube', 'copyYoutube',
'openYoutubeEmbed', 'openYoutubeEmbed',
@ -56,6 +58,10 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron return this.$store.getters.getUsingElectron
}, },
historyCache: function () {
return this.$store.getters.getHistoryCache
},
listType: function () { listType: function () {
return this.$store.getters.getListType return this.$store.getters.getListType
}, },
@ -72,6 +78,12 @@ export default Vue.extend({
return this.$store.getters.getInvidiousInstance 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 () { invidiousUrl: function () {
return `${this.invidiousInstance}/watch?v=${this.id}` return `${this.invidiousInstance}/watch?v=${this.id}`
}, },
@ -84,8 +96,12 @@ export default Vue.extend({
return `https://www.youtube-nocookie.com/embed/${this.id}` return `https://www.youtube-nocookie.com/embed/${this.id}`
}, },
progressPercentage: function () {
return (this.watchProgress / this.data.lengthSeconds) * 100
},
optionsNames: function () { optionsNames: function () {
return [ const names = [
this.$t('Video.Open in YouTube'), this.$t('Video.Open in YouTube'),
this.$t('Video.Copy YouTube Link'), this.$t('Video.Copy YouTube Link'),
this.$t('Video.Open YouTube Embedded Player'), this.$t('Video.Open YouTube Embedded Player'),
@ -93,6 +109,14 @@ export default Vue.extend({
this.$t('Video.Open in Invidious'), this.$t('Video.Open in Invidious'),
this.$t('Video.Copy Invidious Link') 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 () { thumbnail: function () {
@ -128,6 +152,8 @@ export default Vue.extend({
} else { } else {
this.parseLocalData() this.parseLocalData()
} }
this.checkIfWatched()
}, },
methods: { methods: {
toggleSave: function () { toggleSave: function () {
@ -139,6 +165,13 @@ export default Vue.extend({
console.log(option) console.log(option)
switch (option) { switch (option) {
case 'history':
if (this.watched) {
this.removeFromWatched()
} else {
this.markAsWatched()
}
break
case 'copyYoutube': case 'copyYoutube':
navigator.clipboard.writeText(this.youtubeUrl) navigator.clipboard.writeText(this.youtubeUrl)
break break
@ -213,7 +246,7 @@ export default Vue.extend({
this.isLive = this.data.liveNow this.isLive = this.data.liveNow
this.viewCount = this.data.viewCount 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 // produces a string according to the template in the locales string
this.toLocalePublicationString({ this.toLocalePublicationString({
publishText: this.data.publishedText, publishText: this.data.publishedText,
@ -221,7 +254,7 @@ export default Vue.extend({
timeStrings: this.$t('Video.Published'), timeStrings: this.$t('Video.Published'),
liveStreamString: this.$t('Video.Watching'), liveStreamString: this.$t('Video.Watching'),
upcomingString: this.$t('Video.Published.Upcoming'), upcomingString: this.$t('Video.Published.Upcoming'),
isLive: this.data.live, isLive: this.isLive,
isUpcoming: this.data.isUpcoming isUpcoming: this.data.isUpcoming
}).then((data) => { }).then((data) => {
this.uploadedTime = data this.uploadedTime = data
@ -253,7 +286,7 @@ export default Vue.extend({
this.channelId = this.data.ucid this.channelId = this.data.ucid
this.viewCount = this.data.views 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') { if (this.data.length_seconds !== 'undefined') {
this.duration = this.calculateVideoDuration(parseInt(this.data.length_seconds)) 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/', '') 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({ this.toLocalePublicationString({
publishText: this.data.uploaded_at, publishText: this.data.uploaded_at,
templateString: this.$t('Video.Publicationtemplate'), templateString: this.$t('Video.Publicationtemplate'),
@ -293,8 +326,67 @@ export default Vue.extend({
this.isLive = this.data.live 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([ ...mapActions([
'toLocalePublicationString' 'showToast',
'toLocalePublicationString',
'updateHistory',
'removeFromHistory'
]) ])
} }
}) })

View File

@ -84,9 +84,13 @@
<span v-if="viewCount === 1">{{ $t("Video.View") }}</span> <span v-if="viewCount === 1">{{ $t("Video.View") }}</span>
<span v-else-if="parsedViewCount !== ''">{{ $t("Video.Views").toLowerCase() }}</span> <span v-else-if="parsedViewCount !== ''">{{ $t("Video.Views").toLowerCase() }}</span>
<span <span
v-if="uploadedTime !== '' && !isLive" v-if="uploadedTime !== '' && !isLive && !inHistory"
class="uploadedTime" class="uploadedTime"
> {{ uploadedTime }}</span> > {{ uploadedTime }}</span>
<span
v-if="inHistory"
class="uploadedTime"
> {{ publishedText }}</span>
<span <span
v-if="isLive" v-if="isLive"
class="viewCount" class="viewCount"

View File

@ -257,6 +257,10 @@ export default Vue.extend({
const v = this const v = this
this.player.on('ready', function () {
v.$emit('ready')
})
this.player.on('ended', function () { this.player.on('ended', function () {
v.$emit('ended') v.$emit('ended')
}) })

View File

@ -10,7 +10,6 @@
<div class="switchColumnGrid"> <div class="switchColumnGrid">
<div class="switchColumn"> <div class="switchColumn">
<ft-toggle-switch <ft-toggle-switch
v-if="false"
label="Remember History" label="Remember History"
:compact="true" :compact="true"
:default-value="rememberHistory" :default-value="rememberHistory"

View File

@ -58,6 +58,7 @@ const router = new Router({
}, },
{ {
path: '/history', path: '/history',
name: 'history',
meta: { meta: {
title: 'History', title: 'History',
icon: 'fa-home' icon: 'fa-home'

View File

@ -38,6 +38,14 @@ $thumbnail-overlay-opacity: 0.85
width: 163px width: 163px
height: auto height: auto
.videoWatched
position: absolute
padding: 2px
opacity: $thumbnail-overlay-opacity
color: var(--primary-text-color)
background-color: var(--card-bg-color)
pointer-events: none
.videoDuration .videoDuration
position: absolute position: absolute
bottom: 4px bottom: 4px
@ -66,6 +74,13 @@ $thumbnail-overlay-opacity: 0.85
font-size: 17px font-size: 17px
opacity: $thumbnail-overlay-opacity opacity: $thumbnail-overlay-opacity
.watchedProgressBar
height: 2px
position: absolute
bottom: 0px
background-color: var(--primary-color)
z-index: 2
.videoCountContainer .videoCountContainer
position: absolute position: absolute
right: 0 right: 0

View File

@ -0,0 +1,86 @@
import Datastore from 'nedb'
let dbLocation
if (window && window.process && window.process.type === 'renderer') {
// Electron is being used
/* let dbLocation = localStorage.getItem('dbLocation')
if (dbLocation === null) {
const electron = require('electron')
dbLocation = electron.remote.app.getPath('userData')
} */
const electron = require('electron')
dbLocation = electron.remote.app.getPath('userData')
dbLocation = dbLocation + '/history.db'
} else {
dbLocation = 'history.db'
}
const historyDb = new Datastore({
filename: dbLocation,
autoload: true
})
const state = {
historyCache: []
}
const getters = {
getHistoryCache: () => {
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
}

View File

@ -1,15 +1,59 @@
import Vue from 'vue' import Vue from 'vue'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue' import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue' import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.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({ export default Vue.extend({
name: 'History', name: 'History',
components: { components: {
'ft-loader': FtLoader,
'ft-card': FtCard, 'ft-card': FtCard,
'ft-flex-box': FtFlexBox, '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 () { 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)
}
} }
}) })

View File

@ -1,12 +1,35 @@
<template> <template>
<div> <div>
<ft-card class="card"> <ft-loader
v-if="isLoading"
:fullscreen="true"
/>
<ft-card
v-else
class="card"
>
<h3>{{ $t("History.History") }}</h3> <h3>{{ $t("History.History") }}</h3>
<ft-flex-box> <ft-flex-box
v-if="activeData.length === 0"
>
<p class="message"> <p class="message">
{{ $t("This part of the app is not ready yet. Come back later when progress has been made.") }} {{ $t("History['Your history list is currently empty.']") }}
</p> </p>
</ft-flex-box> </ft-flex-box>
<ft-element-list
v-else
:data="activeData"
/>
<ft-flex-box
v-if="activeData.length < historyCache.length"
>
<ft-button
label="Load More"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="increaseLimit"
/>
</ft-flex-box>
</ft-card> </ft-card>
</div> </div>
</template> </template>

View File

@ -47,6 +47,7 @@ export default Vue.extend({
videoViewCount: 0, videoViewCount: 0,
videoLikeCount: 0, videoLikeCount: 0,
videoDislikeCount: 0, videoDislikeCount: 0,
videoLengthSeconds: 0,
channelName: '', channelName: '',
channelThumbnail: '', channelThumbnail: '',
channelId: '', channelId: '',
@ -72,6 +73,14 @@ export default Vue.extend({
return this.$store.getters.getUsingElectron return this.$store.getters.getUsingElectron
}, },
historyCache: function () {
return this.$store.getters.getHistoryCache
},
rememberHistory: function () {
return this.$store.getters.getRememberHistory
},
backendPreference: function () { backendPreference: function () {
return this.$store.getters.getBackendPreference return this.$store.getters.getBackendPreference
}, },
@ -276,6 +285,7 @@ export default Vue.extend({
this.activeSourceList = this.videoSourceList this.activeSourceList = this.videoSourceList
} }
} else { } else {
this.videoLengthSeconds = parseInt(result.videoDetails.lengthSeconds)
this.videoSourceList = result.player_response.streamingData.formats this.videoSourceList = result.player_response.streamingData.formats
this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => { this.audioSourceList = result.player_response.streamingData.adaptiveFormats.filter((format) => {
@ -395,6 +405,7 @@ export default Vue.extend({
} else if (this.forceLocalBackendForLegacy) { } else if (this.forceLocalBackendForLegacy) {
this.getLegacyFormats() this.getLegacyFormats()
} else { } else {
this.videoLengthSeconds = result.lengthSeconds
this.videoSourceList = result.formatStreams.reverse() this.videoSourceList = result.formatStreams.reverse()
this.audioSourceList = result.adaptiveFormats.filter((format) => { 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 () { checkIfPlaylist: function () {
if (typeof (this.$route.query) !== 'undefined') { if (typeof (this.$route.query) !== 'undefined') {
this.playlistId = this.$route.query.playlistId this.playlistId = this.$route.query.playlistId
@ -630,7 +681,24 @@ export default Vue.extend({
...mapActions([ ...mapActions([
'showToast', '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()
} }
}) })

View File

@ -23,6 +23,7 @@
:thumbnail="thumbnail" :thumbnail="thumbnail"
class="videoPlayer" class="videoPlayer"
:class="{ theatrePlayer: useTheatreMode }" :class="{ theatrePlayer: useTheatreMode }"
@ready="checkIfWatched"
@ended="handleVideoEnded" @ended="handleVideoEnded"
@error="handleVideoError" @error="handleVideoError"
/> />

View File

@ -95,6 +95,7 @@ History:
# On History Page # On History Page
History: History History: History
Watch History: Watch History Watch History: Watch History
Your history list is currently empty.: Your history list is currently empty.
Settings: Settings:
# On Settings Page # On Settings Page
Settings: Settings Settings: Settings
@ -264,6 +265,10 @@ Channel:
Channel Description: Channel Description Channel Description: Channel Description
Featured Channels: Featured Channels Featured Channels: Featured Channels
Video: 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 Open in YouTube: Open in YouTube
Copy YouTube Link: Copy YouTube Link Copy YouTube Link: Copy YouTube Link
Open YouTube Embedded Player: Open YouTube Embedded Player Open YouTube Embedded Player: Open YouTube Embedded Player