1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

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
This commit is contained in:
Viperinius 2022-06-26 01:10:20 +02:00
parent 70284199a3
commit 49f1f1fae3
4 changed files with 130 additions and 7 deletions

View file

@ -21,6 +21,7 @@
<div class="flex flex-direction-row align-items-center">
<div class="osdTextContainer startTimeText osdPositionText" style="margin: 0 .25em 0 0;"></div>
<div class="sliderContainer flex-grow" style="margin: .5em 0 .25em;">
<div class="sliderChapterMarkContainer hide"></div>
<input type="range" step=".01" min="0" max="100" value="0" is="emby-slider" class="osdPositionSlider" data-slider-keep-progress="true" />
</div>
<div class="osdTextContainer endTimeText osdDurationText" style="margin: 0 0 0 .25em;"></div>

View file

@ -1574,6 +1574,28 @@ import { setBackdropTransparency, TRANSPARENCY_LEVEL } from '../../../components
return '<h1 class="sliderBubbleText">' + datetime.getDisplayRunningTime(ticks) + '</h1>';
};
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);
});

View file

@ -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 = '<span class="material-icons label sliderChapterMark" aria-hidden="true"></span>';
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
});

View file

@ -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;
}