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();
});