From fd26b155785f786e993ed435dcc5dcbbdd8ac79d Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Tue, 21 Nov 2023 01:05:10 +0100 Subject: [PATCH 1/3] add chapter selection to player osd --- src/controllers/playback/video/index.html | 4 ++ src/controllers/playback/video/index.js | 78 +++++++++++++++++++++-- src/scripts/datetime.js | 11 +++- src/strings/en-us.json | 4 +- 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html index d9de34d308..4414672b60 100644 --- a/src/controllers/playback/video/index.html +++ b/src/controllers/playback/video/index.html @@ -73,6 +73,10 @@ + + diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 2cb099856b..35f0fa7afe 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -146,6 +146,25 @@ export default function (view) { btnUserRating.classList.add('hide'); btnUserRating.setItem(null); } + + if (currentItem.Chapters) { + const chapters = currentItem.Chapters; + // make sure all displayed chapter numbers and timestamps have the same length for a cleaner look + const chapterNumberPadLength = `${chapters.length}`.length; + const maxChapterStartTime = Math.max(...chapters.map(chpt => chpt.StartPositionTicks)); + const durationStringMaxLength = datetime.getDisplayRunningTime(maxChapterStartTime, true).length; + + chapterSelectionOptions = chapters.map((chapter, index) => { + const chapterNumber = `${index + 1}`.padStart(chapterNumberPadLength, '0'); + const chapterName = chapter.Name || `${globalize.translate('Chapter')} ${chapterNumber}`; + + return { + name: `${chapterNumber}: ${chapterName}`, + asideText: `[${datetime.getDisplayRunningTime(chapter.StartPositionTicks, true).padStart(durationStringMaxLength, '0')}]`, + id: chapter.StartPositionTicks + }; + }); + } } function getDisplayTimeWithoutAmPm(date, showSeconds) { @@ -186,6 +205,7 @@ export default function (view) { nowPlayingPositionSlider.disabled = true; btnFastForward.disabled = true; btnRewind.disabled = true; + view.querySelector('.btnChapters').classList.add('hide'); view.querySelector('.btnSubtitles').classList.add('hide'); view.querySelector('.btnAudio').classList.add('hide'); view.querySelector('.osdTitle').innerHTML = ''; @@ -200,6 +220,12 @@ export default function (view) { btnFastForward.disabled = false; btnRewind.disabled = false; + if (currentItem.Chapters?.length) { + view.querySelector('.btnChapters').classList.remove('hide'); + } else { + view.querySelector('.btnChapters').classList.add('hide'); + } + if (playbackManager.subtitleTracks(player).length) { view.querySelector('.btnSubtitles').classList.remove('hide'); toggleSubtitleSync(); @@ -1141,6 +1167,43 @@ export default function (view) { }); } + function showChapterSelection() { + // At the moment Jellyfin doesn't support most of MKV's chapter features (hidden- and standard-flags, multiple Editions, linked chapters, sub-chapters, multi language support, ...). + // For MKV files that use one or more of these features it's not guaranteed that chapters are correctly ordered or displayed. + // If support of one of these features is added in the future, it's maybe necessary to adjust the chapter handling. + + const player = currentPlayer; + const currentTicks = playbackManager.getCurrentTicks(player); + + const menuItems = chapterSelectionOptions.map((chapter, index) => { + return { + ...chapter, + selected: currentTicks >= chapter.id // the id is equal to StartPositionTicks + && (chapterSelectionOptions[index + 1] == null + || currentTicks < chapterSelectionOptions[index + 1].id) + }; + }); + + const positionTo = this; + + import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => { + mouseWheelVolumeControlDisabled = true; // prevent scrolling through list via mouse wheel to change volume + actionsheet.show({ + title: globalize.translate('Chapters'), + items: menuItems, + positionTo: positionTo, + scrollY: true + }).then( + chapterStartPositionTicks => playbackManager.seek(chapterStartPositionTicks, player) + ).finally(() => { + resetIdle(); + mouseWheelVolumeControlDisabled = false; + }); + + setTimeout(resetIdle, 0); + }); + } + function toggleSubtitleSync(action) { const player = currentPlayer; if (subtitleSyncOverlay) { @@ -1331,11 +1394,13 @@ export default function (view) { } function onWheel(e) { - if (e.deltaY < 0) { - playbackManager.volumeUp(currentPlayer); - } - if (e.deltaY > 0) { - playbackManager.volumeDown(currentPlayer); + if (!mouseWheelVolumeControlDisabled) { + if (e.deltaY < 0) { + playbackManager.volumeUp(currentPlayer); + } + if (e.deltaY > 0) { + playbackManager.volumeDown(currentPlayer); + } } } @@ -1455,6 +1520,8 @@ export default function (view) { let programEndDateMs = 0; let playbackStartTimeTicks = 0; let subtitleSyncOverlay; + let chapterSelectionOptions = []; + let mouseWheelVolumeControlDisabled = false; const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider'); const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer'); const nowPlayingPositionSlider = view.querySelector('.osdPositionSlider'); @@ -1757,6 +1824,7 @@ export default function (view) { }); view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection); view.querySelector('.btnSubtitles').addEventListener('click', showSubtitleTrackSelection); + view.querySelector('.btnChapters').addEventListener('click', showChapterSelection); // HACK: Remove `emby-button` from the rating button to make it look like the other buttons view.querySelector('.btnUserRating').classList.remove('emby-button'); diff --git a/src/scripts/datetime.js b/src/scripts/datetime.js index f1243a5076..6ebee13a9b 100644 --- a/src/scripts/datetime.js +++ b/src/scripts/datetime.js @@ -71,7 +71,12 @@ export function getDisplayDuration(ticks) { return result.join(' '); } -export function getDisplayRunningTime(ticks) { +/** + * Return a string in h:mm:ss format for the running time. + * @param {number} ticks - Running ime in ticks. + * @param {boolean} showZeroValues - If true, hours and minutes are always visible in resulting string, even if 0 + */ +export function getDisplayRunningTime(ticks, showZeroValues = false) { const ticksPerHour = 36000000000; const ticksPerMinute = 600000000; const ticksPerSecond = 10000000; @@ -81,7 +86,7 @@ export function getDisplayRunningTime(ticks) { let hours = ticks / ticksPerHour; hours = Math.floor(hours); - if (hours) { + if (hours || showZeroValues) { parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale())); } @@ -92,7 +97,7 @@ export function getDisplayRunningTime(ticks) { ticks -= (minutes * ticksPerMinute); - if (minutes < 10 && hours) { + if (minutes < 10 && (hours || showZeroValues)) { minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale()); } else { minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale()); diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 464521d275..5fbbb9ec6f 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1731,5 +1731,7 @@ "MachineTranslated": "Machine Translated", "ForeignPartsOnly": "Forced/Foreign parts only", "HearingImpairedShort": "HI/SDH", - "LabelIsHearingImpaired": "For hearing impaired (SDH)" + "LabelIsHearingImpaired": "For hearing impaired (SDH)", + "Chapter": "Chapter", + "Chapters": "Chapters" } From beb1bfd6c04ccd1965dfc81e966fdaefbced3b4c Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Tue, 21 Nov 2023 23:53:55 +0100 Subject: [PATCH 2/3] fix text truncation on actionSheetMenItem when scrollbar is visible --- src/components/actionSheet/actionSheet.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/actionSheet/actionSheet.scss b/src/components/actionSheet/actionSheet.scss index 9a9234ffcf..f7656f24f8 100644 --- a/src/components/actionSheet/actionSheet.scss +++ b/src/components/actionSheet/actionSheet.scss @@ -88,6 +88,11 @@ display: flex; flex-direction: column; width: 100%; + + /* to prevent truncation of the displayed text when the scrollbar is visible */ + .actionSheetMenuItem { + margin-right: 10px; + } } .actionSheetScroller-tv { From b39889b34445f663377534d18e55e412147fdfe5 Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Sun, 19 May 2024 21:43:10 +0200 Subject: [PATCH 3/3] fix: 'ActionSheet closed without resolving' error prevent error when ActionSheet dialog is closed without selection --- src/controllers/playback/video/index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 5ebedb0b19..a32a43984e 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -982,6 +982,8 @@ export default function (view) { stats: true, suboffset: showSubOffset, onOption: onSettingsOption + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1055,6 +1057,8 @@ export default function (view) { if (index !== currentIndex) { playbackManager.setAudioStreamIndex(index, player); } + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1102,10 +1106,11 @@ export default function (view) { playbackManager.setSecondarySubtitleStreamIndex(index, player); } } - }) - .finally(() => { - resetIdle(); - }); + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error + }).finally(() => { + resetIdle(); + }); setTimeout(resetIdle, 0); } @@ -1182,6 +1187,8 @@ export default function (view) { } toggleSubtitleSync(); + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1217,7 +1224,9 @@ export default function (view) { scrollY: true }).then( chapterStartPositionTicks => playbackManager.seek(chapterStartPositionTicks, player) - ).finally(() => { + ).catch(() => { + // prevent 'ActionSheet closed without resolving' error + }).finally(() => { resetIdle(); });