Implement History and jump to last watched time progress
This commit is contained in:
parent
6e5e1e0542
commit
2133a10efa
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -84,9 +84,13 @@
|
|||
<span v-if="viewCount === 1">{{ $t("Video.View") }}</span>
|
||||
<span v-else-if="parsedViewCount !== ''">{{ $t("Video.Views").toLowerCase() }}</span>
|
||||
<span
|
||||
v-if="uploadedTime !== '' && !isLive"
|
||||
v-if="uploadedTime !== '' && !isLive && !inHistory"
|
||||
class="uploadedTime"
|
||||
>• {{ uploadedTime }}</span>
|
||||
<span
|
||||
v-if="inHistory"
|
||||
class="uploadedTime"
|
||||
>• {{ publishedText }}</span>
|
||||
<span
|
||||
v-if="isLive"
|
||||
class="viewCount"
|
||||
|
|
|
@ -257,6 +257,10 @@ export default Vue.extend({
|
|||
|
||||
const v = this
|
||||
|
||||
this.player.on('ready', function () {
|
||||
v.$emit('ready')
|
||||
})
|
||||
|
||||
this.player.on('ended', function () {
|
||||
v.$emit('ended')
|
||||
})
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
<div class="switchColumnGrid">
|
||||
<div class="switchColumn">
|
||||
<ft-toggle-switch
|
||||
v-if="false"
|
||||
label="Remember History"
|
||||
:compact="true"
|
||||
:default-value="rememberHistory"
|
||||
|
|
|
@ -58,6 +58,7 @@ const router = new Router({
|
|||
},
|
||||
{
|
||||
path: '/history',
|
||||
name: 'history',
|
||||
meta: {
|
||||
title: 'History',
|
||||
icon: 'fa-home'
|
||||
|
|
|
@ -38,6 +38,14 @@ $thumbnail-overlay-opacity: 0.85
|
|||
width: 163px
|
||||
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
|
||||
position: absolute
|
||||
bottom: 4px
|
||||
|
@ -66,6 +74,13 @@ $thumbnail-overlay-opacity: 0.85
|
|||
font-size: 17px
|
||||
opacity: $thumbnail-overlay-opacity
|
||||
|
||||
.watchedProgressBar
|
||||
height: 2px
|
||||
position: absolute
|
||||
bottom: 0px
|
||||
background-color: var(--primary-color)
|
||||
z-index: 2
|
||||
|
||||
.videoCountContainer
|
||||
position: absolute
|
||||
right: 0
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,12 +1,35 @@
|
|||
<template>
|
||||
<div>
|
||||
<ft-card class="card">
|
||||
<ft-loader
|
||||
v-if="isLoading"
|
||||
:fullscreen="true"
|
||||
/>
|
||||
<ft-card
|
||||
v-else
|
||||
class="card"
|
||||
>
|
||||
<h3>{{ $t("History.History") }}</h3>
|
||||
<ft-flex-box>
|
||||
<ft-flex-box
|
||||
v-if="activeData.length === 0"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
:thumbnail="thumbnail"
|
||||
class="videoPlayer"
|
||||
:class="{ theatrePlayer: useTheatreMode }"
|
||||
@ready="checkIfWatched"
|
||||
@ended="handleVideoEnded"
|
||||
@error="handleVideoError"
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue