Skip to content

Commit 8a094ca

Browse files
committed
Merge PR jellyfin#5011 into taco-jellyfin-web
2 parents cc0dab0 + b39889b commit 8a094ca

5 files changed

Lines changed: 96 additions & 8 deletions

File tree

src/components/actionSheet/actionSheet.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@
8888
display: flex;
8989
flex-direction: column;
9090
width: 100%;
91+
92+
/* to prevent truncation of the displayed text when the scrollbar is visible */
93+
.actionSheetMenuItem {
94+
margin-right: 10px;
95+
}
9196
}
9297

9398
.actionSheetScroller-tv {

src/controllers/playback/video/index.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ <h3 class="osdTitle"></h3>
7373
<span class="xlargePaperIconButton material-icons favorite" aria-hidden="true"></span>
7474
</button>
7575

76+
<button is="paper-icon-button-light" type="button" class="btnChapters hide autoSize paper-icon-button-light" title="${Chapters}">
77+
<span class="xlargePaperIconButton material-icons format_list_numbered" aria-hidden="true"></span>
78+
</button>
79+
7680
<button is="paper-icon-button-light" class="btnSubtitles hide autoSize" title="${Subtitles}">
7781
<span class="xlargePaperIconButton material-icons closed_caption" aria-hidden="true"></span>
7882
</button>

src/controllers/playback/video/index.js

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,25 @@ export default function (view) {
148148
btnUserRating.setItem(null);
149149
}
150150

151+
if (currentItem.Chapters) {
152+
const chapters = currentItem.Chapters;
153+
// make sure all displayed chapter numbers and timestamps have the same length for a cleaner look
154+
const chapterNumberPadLength = `${chapters.length}`.length;
155+
const maxChapterStartTime = Math.max(...chapters.map(chpt => chpt.StartPositionTicks));
156+
const durationStringMaxLength = datetime.getDisplayRunningTime(maxChapterStartTime, true).length;
157+
158+
chapterSelectionOptions = chapters.map((chapter, index) => {
159+
const chapterNumber = `${index + 1}`.padStart(chapterNumberPadLength, '0');
160+
const chapterName = chapter.Name || `${globalize.translate('Chapter')} ${chapterNumber}`;
161+
162+
return {
163+
name: `${chapterNumber}: ${chapterName}`,
164+
asideText: `[${datetime.getDisplayRunningTime(chapter.StartPositionTicks, true).padStart(durationStringMaxLength, '0')}]`,
165+
id: chapter.StartPositionTicks
166+
};
167+
});
168+
}
169+
151170
// Update trickplay data
152171
trickplayResolution = null;
153172

@@ -207,6 +226,7 @@ export default function (view) {
207226
nowPlayingPositionSlider.disabled = true;
208227
btnFastForward.disabled = true;
209228
btnRewind.disabled = true;
229+
view.querySelector('.btnChapters').classList.add('hide');
210230
view.querySelector('.btnSubtitles').classList.add('hide');
211231
view.querySelector('.btnAudio').classList.add('hide');
212232
view.querySelector('.osdTitle').innerHTML = '';
@@ -221,6 +241,12 @@ export default function (view) {
221241
btnFastForward.disabled = false;
222242
btnRewind.disabled = false;
223243

244+
if (currentItem.Chapters?.length) {
245+
view.querySelector('.btnChapters').classList.remove('hide');
246+
} else {
247+
view.querySelector('.btnChapters').classList.add('hide');
248+
}
249+
224250
if (playbackManager.subtitleTracks(player).length) {
225251
view.querySelector('.btnSubtitles').classList.remove('hide');
226252
toggleSubtitleSync();
@@ -952,6 +978,8 @@ export default function (view) {
952978
stats: true,
953979
suboffset: showSubOffset,
954980
onOption: onSettingsOption
981+
}).catch(() => {
982+
// prevent 'ActionSheet closed without resolving' error
955983
}).finally(() => {
956984
resetIdle();
957985
});
@@ -1025,6 +1053,8 @@ export default function (view) {
10251053
if (index !== currentIndex) {
10261054
playbackManager.setAudioStreamIndex(index, player);
10271055
}
1056+
}).catch(() => {
1057+
// prevent 'ActionSheet closed without resolving' error
10281058
}).finally(() => {
10291059
resetIdle();
10301060
});
@@ -1072,10 +1102,11 @@ export default function (view) {
10721102
playbackManager.setSecondarySubtitleStreamIndex(index, player);
10731103
}
10741104
}
1075-
})
1076-
.finally(() => {
1077-
resetIdle();
1078-
});
1105+
}).catch(() => {
1106+
// prevent 'ActionSheet closed without resolving' error
1107+
}).finally(() => {
1108+
resetIdle();
1109+
});
10791110

10801111
setTimeout(resetIdle, 0);
10811112
}
@@ -1152,6 +1183,45 @@ export default function (view) {
11521183
}
11531184

11541185
toggleSubtitleSync();
1186+
}).catch(() => {
1187+
// prevent 'ActionSheet closed without resolving' error
1188+
}).finally(() => {
1189+
resetIdle();
1190+
});
1191+
1192+
setTimeout(resetIdle, 0);
1193+
});
1194+
}
1195+
1196+
function showChapterSelection() {
1197+
// 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, ...).
1198+
// For MKV files that use one or more of these features it's not guaranteed that chapters are correctly ordered or displayed.
1199+
// If support of one of these features is added in the future, it's maybe necessary to adjust the chapter handling.
1200+
1201+
const player = currentPlayer;
1202+
const currentTicks = playbackManager.getCurrentTicks(player);
1203+
1204+
const menuItems = chapterSelectionOptions.map((chapter, index) => {
1205+
return {
1206+
...chapter,
1207+
selected: currentTicks >= chapter.id // the id is equal to StartPositionTicks
1208+
&& (chapterSelectionOptions[index + 1] == null
1209+
|| currentTicks < chapterSelectionOptions[index + 1].id)
1210+
};
1211+
});
1212+
1213+
const positionTo = this;
1214+
1215+
import('../../../components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
1216+
actionsheet.show({
1217+
title: globalize.translate('Chapters'),
1218+
items: menuItems,
1219+
positionTo: positionTo,
1220+
scrollY: true
1221+
}).then(
1222+
chapterStartPositionTicks => playbackManager.seek(chapterStartPositionTicks, player)
1223+
).catch(() => {
1224+
// prevent 'ActionSheet closed without resolving' error
11551225
}).finally(() => {
11561226
resetIdle();
11571227
});
@@ -1569,6 +1639,7 @@ export default function (view) {
15691639
let programEndDateMs = 0;
15701640
let playbackStartTimeTicks = 0;
15711641
let subtitleSyncOverlay;
1642+
let chapterSelectionOptions = [];
15721643
let trickplayResolution = null;
15731644
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
15741645
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
@@ -1889,6 +1960,7 @@ export default function (view) {
18891960
});
18901961
view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection);
18911962
view.querySelector('.btnSubtitles').addEventListener('click', showSubtitleTrackSelection);
1963+
view.querySelector('.btnChapters').addEventListener('click', showChapterSelection);
18921964

18931965
// HACK: Remove `emby-button` from the rating button to make it look like the other buttons
18941966
view.querySelector('.btnUserRating').classList.remove('emby-button');

src/scripts/datetime.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,12 @@ export function getDisplayDuration(ticks) {
7171
return result.join(' ');
7272
}
7373

74-
export function getDisplayRunningTime(ticks) {
74+
/**
75+
* Return a string in h:mm:ss format for the running time.
76+
* @param {number} ticks - Running ime in ticks.
77+
* @param {boolean} showZeroValues - If true, hours and minutes are always visible in resulting string, even if 0
78+
*/
79+
export function getDisplayRunningTime(ticks, showZeroValues = false) {
7580
const ticksPerHour = 36000000000;
7681
const ticksPerMinute = 600000000;
7782
const ticksPerSecond = 10000000;
@@ -81,7 +86,7 @@ export function getDisplayRunningTime(ticks) {
8186
let hours = ticks / ticksPerHour;
8287
hours = Math.floor(hours);
8388

84-
if (hours) {
89+
if (hours || showZeroValues) {
8590
parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale()));
8691
}
8792

@@ -92,7 +97,7 @@ export function getDisplayRunningTime(ticks) {
9297

9398
ticks -= (minutes * ticksPerMinute);
9499

95-
if (minutes < 10 && hours) {
100+
if (minutes < 10 && (hours || showZeroValues)) {
96101
minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
97102
} else {
98103
minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale());

src/strings/en-us.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1757,5 +1757,7 @@
17571757
"OptionExtractTrickplayImage": "Enable trickplay image extraction",
17581758
"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.",
17591759
"LabelExtractTrickplayDuringLibraryScan": "Extract trickplay images during the library scan",
1760-
"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."
1760+
"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.",
1761+
"Chapter": "Chapter",
1762+
"Chapters": "Chapters"
17611763
}

0 commit comments

Comments
 (0)