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:
parent
70284199a3
commit
49f1f1fae3
4 changed files with 130 additions and 7 deletions
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue