From 49f1f1fae3d8615f696eda6a91a4e41d5f56e1fd Mon Sep 17 00:00:00 2001 From: Viperinius Date: Sun, 26 Jun 2022 01:10:20 +0200 Subject: [PATCH] Add chapter markings to video player timeline. These labels show the start of each chapter when interacting with the slider the same way that activates the slider bubble. They follow the same color scheme as the slider (watched chapters turn blue). Inspired by https://features.jellyfin.org/posts/397/chapter-markers-in-timeline --- src/controllers/playback/video/index.html | 1 + src/controllers/playback/video/index.js | 22 ++++++ src/elements/emby-slider/emby-slider.js | 94 +++++++++++++++++++++-- src/elements/emby-slider/emby-slider.scss | 20 +++++ 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html index c76a1714a..278b1d4d4 100644 --- a/src/controllers/playback/video/index.html +++ b/src/controllers/playback/video/index.html @@ -21,6 +21,7 @@
+
diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index d5da82b74..baaec52ff 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -1574,6 +1574,28 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components return '

' + datetime.getDisplayRunningTime(ticks) + '

'; }; + nowPlayingPositionSlider.getChapterFractions = function () { + showOsd(); + + const item = currentItem; + + if (item && item.Chapters && item.Chapters.length) { + const chapterFractions = []; + const runtimeDuration = item.RunTimeTicks; + + for (let i = 0, length = item.Chapters.length; i < length; i++) { + const currentChapter = item.Chapters[i]; + + const fraction = currentChapter.StartPositionTicks / runtimeDuration; + chapterFractions.push(fraction); + } + + return chapterFractions; + } + + return []; + }; + view.querySelector('.btnPreviousTrack').addEventListener('click', function () { playbackManager.previousTrack(currentPlayer); }); diff --git a/src/elements/emby-slider/emby-slider.js b/src/elements/emby-slider/emby-slider.js index 0bb20270e..04aa7c477 100644 --- a/src/elements/emby-slider/emby-slider.js +++ b/src/elements/emby-slider/emby-slider.js @@ -102,6 +102,13 @@ import '../emby-input/emby-input'; fraction *= 100; backgroundLower.style.width = fraction + '%'; } + + if (range.chapterMarkContainerElement) { + if (!range.triedChapterMarksAdd) { + addChapterMarks(range); + } + updateChapterMarks(range, value); + } }); } @@ -130,6 +137,57 @@ import '../emby-input/emby-input'; }); } + function setChapterMark(range, valueChapterMark, chapterMark, valueProgress) { + requestAnimationFrame(function () { + const bubbleTrackRect = range.sliderBubbleTrack.getBoundingClientRect(); + const chapterMarkRect = chapterMark.getBoundingClientRect(); + + if (!bubbleTrackRect.width || !chapterMarkRect.width) { + // width is not set, most probably because the OSD is currently hidden + return; + } + + let chapterMarkPos = (bubbleTrackRect.width * valueChapterMark / 100) - chapterMarkRect.width / 2; + chapterMarkPos = Math.min(Math.max(chapterMarkPos, - chapterMarkRect.width / 2), bubbleTrackRect.width - chapterMarkRect.width / 2); + + chapterMark.style.left = chapterMarkPos + 'px'; + + if (valueProgress >= valueChapterMark) { + chapterMark.classList.remove('unwatched'); + chapterMark.classList.add('watched'); + } else { + chapterMark.classList.add('unwatched'); + chapterMark.classList.remove('watched'); + } + }); + } + + function updateChapterMarks(range, currentValue) { + if (range.chapterFractions && range.chapterFractions.length && range.chapterMarkElements && range.chapterMarkElements.length) { + for (let i = 0, length = range.chapterMarkElements.length; i < length; i++) { + if (range.chapterFractions.length > i) { + setChapterMark(range, mapFractionToValue(range, range.chapterFractions[i]), range.chapterMarkElements[i], currentValue); + } + } + } + } + + function addChapterMarks(range) { + range.chapterFractions = []; + if (range.getChapterFractions) { + range.chapterFractions = range.getChapterFractions(); + } + + const htmlToInsert = ''; + + range.chapterFractions.forEach(() => { + range.chapterMarkContainerElement.insertAdjacentHTML('beforeend', htmlToInsert); + }); + + range.chapterMarkElements = range.chapterMarkContainerElement.querySelectorAll('.sliderChapterMark'); + range.triedChapterMarksAdd = true; + } + EmbySliderPrototype.attachedCallback = function () { if (this.getAttribute('data-embyslider') === 'true') { return; @@ -187,7 +245,13 @@ import '../emby-input/emby-input'; this.backgroundUpper = containerElement.querySelector('.mdl-slider-background-upper'); const sliderBubble = containerElement.querySelector('.sliderBubble'); - let hasHideClass = sliderBubble.classList.contains('hide'); + let hasHideClassBubble = sliderBubble.classList.contains('hide'); + + this.chapterMarkContainerElement = containerElement.querySelector('.sliderChapterMarkContainer'); + let hasHideClassChapterMarkContainer = false; + if (this.chapterMarkContainerElement) { + hasHideClassChapterMarkContainer = this.chapterMarkContainerElement.classList.contains('hide'); + } dom.addEventListener(this, 'input', function () { this.dragging = true; @@ -199,9 +263,14 @@ import '../emby-input/emby-input'; const bubbleValue = mapValueToFraction(this, this.value) * 100; updateBubble(this, bubbleValue, sliderBubble); - if (hasHideClass) { + if (hasHideClassBubble) { sliderBubble.classList.remove('hide'); - hasHideClass = false; + hasHideClassBubble = false; + } + + if (hasHideClassChapterMarkContainer) { + this.chapterMarkContainerElement.classList.remove('hide'); + hasHideClassChapterMarkContainer = false; } }, { passive: true @@ -215,7 +284,10 @@ import '../emby-input/emby-input'; } sliderBubble.classList.add('hide'); - hasHideClass = true; + hasHideClassBubble = true; + + this.chapterMarkContainerElement.classList.add('hide'); + hasHideClassChapterMarkContainer = true; }, { passive: true }); @@ -227,9 +299,14 @@ import '../emby-input/emby-input'; updateBubble(this, bubbleValue, sliderBubble); - if (hasHideClass) { + if (hasHideClassBubble) { sliderBubble.classList.remove('hide'); - hasHideClass = false; + hasHideClassBubble = false; + } + + if (hasHideClassChapterMarkContainer) { + this.chapterMarkContainerElement.classList.remove('hide'); + hasHideClassChapterMarkContainer = false; } } }, { @@ -239,7 +316,10 @@ import '../emby-input/emby-input'; /* eslint-disable-next-line compat/compat */ dom.addEventListener(this, (window.PointerEvent ? 'pointerleave' : 'mouseleave'), function () { sliderBubble.classList.add('hide'); - hasHideClass = true; + hasHideClassBubble = true; + + this.chapterMarkContainerElement.classList.add('hide'); + hasHideClassChapterMarkContainer = true; }, { passive: true }); diff --git a/src/elements/emby-slider/emby-slider.scss b/src/elements/emby-slider/emby-slider.scss index f7503d4fd..cd07533ef 100644 --- a/src/elements/emby-slider/emby-slider.scss +++ b/src/elements/emby-slider/emby-slider.scss @@ -245,3 +245,23 @@ display: block; margin-bottom: 0.25em; } + +.sliderChapterMarkContainer { + position: absolute; + left: 0; + right: 0; + margin: 0 0.54em; /* half of slider thumb size */ +} + +.sliderChapterMark { + position: absolute; + transform: translate3d(0, -100%, 0) rotate(90deg); +} + +.sliderChapterMark.unwatched { + color: rgba(255, 255, 255, 0.3); +} + +.sliderChapterMark.watched { + color: #00a4dc; +}