From bc886af7262093b2743116091ae1d23cca583f04 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 29 Sep 2022 22:01:54 +0200 Subject: [PATCH] Implement chapters (#2224) * Implement chapters * Generate chapters locally for the Invidious API backend * Performance improvements * More performance improvements * Improve chapters appearance and add compact mode for Invidious * Update UI while seeking instead of afterwards * Invidious extract chapters with range timestamps properly and duplicate chapters * Minor code improvement * Add accessibility labels and keyboard navigation * Add chapter markers * Fix missing newline at the bottom of ft-video-player.css * Fix marker placement --- .../distraction-settings.js | 11 +- .../distraction-settings.vue | 6 + .../ft-video-player/ft-video-player.css | 21 ++- .../ft-video-player/ft-video-player.js | 27 +++- .../watch-video-chapters.css | 83 +++++++++++ .../watch-video-chapters.js | 72 ++++++++++ .../watch-video-chapters.vue | 66 +++++++++ src/renderer/main.js | 2 + src/renderer/store/modules/settings.js | 1 + src/renderer/views/Watch/Watch.js | 134 ++++++++++++++++++ src/renderer/views/Watch/Watch.vue | 11 ++ static/locales/en-US.yaml | 6 + 12 files changed, 431 insertions(+), 9 deletions(-) create mode 100644 src/renderer/components/watch-video-chapters/watch-video-chapters.css create mode 100644 src/renderer/components/watch-video-chapters/watch-video-chapters.js create mode 100644 src/renderer/components/watch-video-chapters/watch-video-chapters.vue diff --git a/src/renderer/components/distraction-settings/distraction-settings.js b/src/renderer/components/distraction-settings/distraction-settings.js index de6963ce..2aecd3c0 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.js +++ b/src/renderer/components/distraction-settings/distraction-settings.js @@ -57,8 +57,14 @@ export default Vue.extend({ hideLiveStreams: function() { return this.$store.getters.getHideLiveStreams }, - hideSharingActions: function() { + hideSharingActions: function () { return this.$store.getters.getHideSharingActions + }, + backendPreference: function () { + return this.$store.getters.getBackendPreference + }, + hideChapters: function () { + return this.$store.getters.getHideChapters } }, methods: { @@ -86,7 +92,8 @@ export default Vue.extend({ 'updateHideVideoDescription', 'updateHideComments', 'updateHideLiveStreams', - 'updateHideSharingActions' + 'updateHideSharingActions', + 'updateHideChapters' ]) } }) diff --git a/src/renderer/components/distraction-settings/distraction-settings.vue b/src/renderer/components/distraction-settings/distraction-settings.vue index d6f56a07..724a5211 100644 --- a/src/renderer/components/distraction-settings/distraction-settings.vue +++ b/src/renderer/components/distraction-settings/distraction-settings.vue @@ -46,6 +46,12 @@ :default-value="hideSharingActions" @change="updateHideSharingActions" /> +
{ return [] } } }, data: function () { @@ -430,6 +434,10 @@ export default Vue.extend({ // https://github.com/videojs/video.js/blob/v7.13.3/docs/guides/components.md#default-component-tree this.player.controlBar.progressControl.seekBar.playProgressBar.removeChild('timeTooltip') + if (this.chapters.length > 0) { + this.chapters.forEach(this.addChapterMarker) + } + if (this.useSponsorBlock) { this.initializeSponsorBlock() } @@ -523,6 +531,7 @@ export default Vue.extend({ this.playerStats = this.player.tech({ IWillNotUseThisInPlugins: true }).vhs.stats this.updateStatsContent() } + this.$emit('timeupdate') }) this.player.textTrackSettings.on('modalclose', (_) => { @@ -621,16 +630,22 @@ export default Vue.extend({ }, addSponsorBlockMarker(marker) { - const markerDiv = videojs.dom.createEl('div', {}, {}) + const markerDiv = videojs.dom.createEl('div') + markerDiv.title = this.sponsorBlockTranslatedCategory(marker.category) markerDiv.className = `sponsorBlockMarker main${this.sponsorSkips.categoryData[marker.category].color}` - markerDiv.style.height = '100%' - markerDiv.style.position = 'absolute' - markerDiv.style.opacity = '0.6' - markerDiv.style['background-color'] = marker.color markerDiv.style.width = (marker.duration / this.lengthSeconds) * 100 + '%' markerDiv.style.marginLeft = (marker.time / this.lengthSeconds) * 100 + '%' - markerDiv.title = this.sponsorBlockTranslatedCategory(marker.category) + + this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv) + }, + + addChapterMarker(chapter) { + const markerDiv = videojs.dom.createEl('div') + + markerDiv.title = chapter.title + markerDiv.className = 'chapterMarker' + markerDiv.style.marginLeft = (chapter.startSeconds / this.lengthSeconds) * 100 - 0.5 + '%' this.player.el().querySelector('.vjs-progress-holder').appendChild(markerDiv) }, diff --git a/src/renderer/components/watch-video-chapters/watch-video-chapters.css b/src/renderer/components/watch-video-chapters/watch-video-chapters.css new file mode 100644 index 00000000..d95bde01 --- /dev/null +++ b/src/renderer/components/watch-video-chapters/watch-video-chapters.css @@ -0,0 +1,83 @@ +.videoChapters { + overflow-y: hidden; +} + +.chaptersTitle { + margin-top: 10px; + margin-bottom: 0; + cursor: pointer; +} + +.currentChapter { + font-size: 15px; +} + +.chaptersWrapper { + margin-top: 15px; + max-height: 250px; + overflow-y: scroll; + display: flex; + flex-direction: column; + gap: 8px; +} + +.chaptersWrapper.compact { + max-height: 200px; +} + +.chaptersChevron { + vertical-align: middle; +} + +.chaptersChevron.open { + margin-left: 4px; +} + +.chapter { + display: grid; + grid-template-areas: + 'thumbnail title' + 'thumbnail timestamp'; + grid-template-columns: auto 1fr; + grid-template-rows: min(auto, 2fr) 1fr; + column-gap: 10px; + justify-items: start; + cursor: pointer; + font-size: 15px; +} + +.chaptersWrapper.compact .chapter { + display: flex; + flex-direction: row; +} + +.chapterThumbnail { + grid-area: thumbnail; + width: 130px; + height: auto; + margin: 3px; +} + +.chapter.current .chapterThumbnail { + border: solid 3px var(--accent-color); + margin: 0; +} + +.chapterTitle { + grid-area: title; + align-self: center; + margin: 0; +} + +.chapter.current .chapterTitle { + font-weight: bold; +} + +.chapterTimestamp { + grid-area: timestamp; + align-self: flex-start; + padding: 3px 4px; + border-radius: 5px; + background-color: var(--accent-color); + color: var(--text-with-accent-color); +} diff --git a/src/renderer/components/watch-video-chapters/watch-video-chapters.js b/src/renderer/components/watch-video-chapters/watch-video-chapters.js new file mode 100644 index 00000000..36a88bc4 --- /dev/null +++ b/src/renderer/components/watch-video-chapters/watch-video-chapters.js @@ -0,0 +1,72 @@ +import Vue from 'vue' +import FtCard from '../ft-card/ft-card.vue' + +export default Vue.extend({ + name: 'WatchVideoChapters', + components: { + 'ft-card': FtCard + }, + props: { + compact: { + type: Boolean, + default: false + }, + chapters: { + type: Array, + required: true + }, + currentChapterIndex: { + type: Number, + required: true + } + }, + data: function () { + return { + showChapters: false, + currentIndex: 0 + } + }, + computed: { + currentTitle: function () { + return this.chapters[this.currentIndex].title + } + }, + watch: { + currentChapterIndex: function (value) { + if (this.currentIndex !== value) { + this.currentIndex = value + } + } + }, + mounted: function () { + this.currentIndex = this.currentChapterIndex + }, + methods: { + changeChapter: function(index) { + this.currentIndex = index + this.$emit('timestamp-event', this.chapters[index].startSeconds) + }, + + navigateChapters(direction) { + const chapterElements = Array.from(this.$refs.chaptersWrapper.children) + const focusedIndex = chapterElements.indexOf(document.activeElement) + + let newIndex = focusedIndex + if (direction === 'up') { + if (focusedIndex === 0) { + newIndex = chapterElements.length - 1 + } else { + newIndex-- + } + } else { + if (focusedIndex === chapterElements.length - 1) { + newIndex = 0 + } else { + newIndex++ + } + } + + chapterElements[newIndex].focus() + } + } +}) diff --git a/src/renderer/components/watch-video-chapters/watch-video-chapters.vue b/src/renderer/components/watch-video-chapters/watch-video-chapters.vue new file mode 100644 index 00000000..7554d78f --- /dev/null +++ b/src/renderer/components/watch-video-chapters/watch-video-chapters.vue @@ -0,0 +1,66 @@ + + +