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"
}