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 { diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html index 57b11092d7..5de5eaf9b8 100644 --- a/src/controllers/playback/video/index.html +++ b/src/controllers/playback/video/index.html @@ -70,6 +70,10 @@ + + diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js index 104a2d142a..5f179a5fcb 100644 --- a/src/controllers/playback/video/index.js +++ b/src/controllers/playback/video/index.js @@ -135,6 +135,25 @@ export default function (view) { 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 + }; + }); + } + // Update trickplay data trickplayResolution = null; @@ -194,6 +213,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 = ''; @@ -208,6 +228,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(); @@ -974,6 +1000,8 @@ export default function (view) { stats: true, suboffset: showSubOffset, onOption: onSettingsOption + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1047,6 +1075,8 @@ export default function (view) { if (index !== currentIndex) { playbackManager.setAudioStreamIndex(index, player); } + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1094,10 +1124,11 @@ export default function (view) { playbackManager.setSecondarySubtitleStreamIndex(index, player); } } - }) - .finally(() => { - resetIdle(); - }); + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error + }).finally(() => { + resetIdle(); + }); setTimeout(resetIdle, 0); } @@ -1174,6 +1205,45 @@ export default function (view) { } toggleSubtitleSync(); + }).catch(() => { + // prevent 'ActionSheet closed without resolving' error + }).finally(() => { + resetIdle(); + }); + + setTimeout(resetIdle, 0); + }); + } + + 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 }) => { + actionsheet.show({ + title: globalize.translate('Chapters'), + items: menuItems, + positionTo: positionTo, + scrollY: true + }).then( + chapterStartPositionTicks => playbackManager.seek(chapterStartPositionTicks, player) + ).catch(() => { + // prevent 'ActionSheet closed without resolving' error }).finally(() => { resetIdle(); }); @@ -1641,6 +1711,7 @@ export default function (view) { let programEndDateMs = 0; let playbackStartTimeTicks = 0; let subtitleSyncOverlay; + let chapterSelectionOptions = []; let trickplayResolution = null; const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider'); const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer'); @@ -1960,6 +2031,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 bcc48b1d74..1db6a1046a 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 2d6678c325..3ce6ba232f 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1819,5 +1819,7 @@ "ExtractTrickplayImagesHelp": "Trickplay images are similar to chapter images, except they span the entire length of the content and are used to show a preview when scrubbing through videos.", "LabelExtractTrickplayDuringLibraryScan": "Extract trickplay images during the library scan", "LabelExtractTrickplayDuringLibraryScanHelp": "Generate trickplay images when videos are imported during the library scan. Otherwise, they will be extracted during the trickplay images scheduled task. If generation is set to non-blocking this will not affect the time a library scan takes to complete.", - "LogLoadFailure": "Failed to load the log file. It may still be actively written to." + "LogLoadFailure": "Failed to load the log file. It may still be actively written to.", + "Chapter": "Chapter", + "Chapters": "Chapters" }