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 c76a1714ae..278b1d4d4a 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 d5da82b740..baaec52ff2 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 0bb20270e6..04aa7c4778 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 f7503d4fd5..cd07533efd 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; +}