mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge 19af2bf834
into 7d84185d0e
This commit is contained in:
commit
6aa843af18
5 changed files with 96 additions and 8 deletions
|
@ -88,6 +88,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
/* to prevent truncation of the displayed text when the scrollbar is visible */
|
||||||
|
.actionSheetMenuItem {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionSheetScroller-tv {
|
.actionSheetScroller-tv {
|
||||||
|
|
|
@ -70,6 +70,10 @@
|
||||||
<span class="xlargePaperIconButton material-icons favorite" aria-hidden="true"></span>
|
<span class="xlargePaperIconButton material-icons favorite" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button is="paper-icon-button-light" type="button" class="btnChapters hide autoSize paper-icon-button-light" title="${Chapters}">
|
||||||
|
<span class="xlargePaperIconButton material-icons format_list_numbered" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button is="paper-icon-button-light" class="btnSubtitles hide autoSize" title="${Subtitles}">
|
<button is="paper-icon-button-light" class="btnSubtitles hide autoSize" title="${Subtitles}">
|
||||||
<span class="xlargePaperIconButton material-icons closed_caption" aria-hidden="true"></span>
|
<span class="xlargePaperIconButton material-icons closed_caption" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -135,6 +135,25 @@ export default function (view) {
|
||||||
btnUserRating.setItem(null);
|
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
|
// Update trickplay data
|
||||||
trickplayResolution = null;
|
trickplayResolution = null;
|
||||||
|
|
||||||
|
@ -194,6 +213,7 @@ export default function (view) {
|
||||||
nowPlayingPositionSlider.disabled = true;
|
nowPlayingPositionSlider.disabled = true;
|
||||||
btnFastForward.disabled = true;
|
btnFastForward.disabled = true;
|
||||||
btnRewind.disabled = true;
|
btnRewind.disabled = true;
|
||||||
|
view.querySelector('.btnChapters').classList.add('hide');
|
||||||
view.querySelector('.btnSubtitles').classList.add('hide');
|
view.querySelector('.btnSubtitles').classList.add('hide');
|
||||||
view.querySelector('.btnAudio').classList.add('hide');
|
view.querySelector('.btnAudio').classList.add('hide');
|
||||||
view.querySelector('.osdTitle').innerHTML = '';
|
view.querySelector('.osdTitle').innerHTML = '';
|
||||||
|
@ -208,6 +228,12 @@ export default function (view) {
|
||||||
btnFastForward.disabled = false;
|
btnFastForward.disabled = false;
|
||||||
btnRewind.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) {
|
if (playbackManager.subtitleTracks(player).length) {
|
||||||
view.querySelector('.btnSubtitles').classList.remove('hide');
|
view.querySelector('.btnSubtitles').classList.remove('hide');
|
||||||
toggleSubtitleSync();
|
toggleSubtitleSync();
|
||||||
|
@ -974,6 +1000,8 @@ export default function (view) {
|
||||||
stats: true,
|
stats: true,
|
||||||
suboffset: showSubOffset,
|
suboffset: showSubOffset,
|
||||||
onOption: onSettingsOption
|
onOption: onSettingsOption
|
||||||
|
}).catch(() => {
|
||||||
|
// prevent 'ActionSheet closed without resolving' error
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
resetIdle();
|
resetIdle();
|
||||||
});
|
});
|
||||||
|
@ -1047,6 +1075,8 @@ export default function (view) {
|
||||||
if (index !== currentIndex) {
|
if (index !== currentIndex) {
|
||||||
playbackManager.setAudioStreamIndex(index, player);
|
playbackManager.setAudioStreamIndex(index, player);
|
||||||
}
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// prevent 'ActionSheet closed without resolving' error
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
resetIdle();
|
resetIdle();
|
||||||
});
|
});
|
||||||
|
@ -1094,10 +1124,11 @@ export default function (view) {
|
||||||
playbackManager.setSecondarySubtitleStreamIndex(index, player);
|
playbackManager.setSecondarySubtitleStreamIndex(index, player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}).catch(() => {
|
||||||
.finally(() => {
|
// prevent 'ActionSheet closed without resolving' error
|
||||||
resetIdle();
|
}).finally(() => {
|
||||||
});
|
resetIdle();
|
||||||
|
});
|
||||||
|
|
||||||
setTimeout(resetIdle, 0);
|
setTimeout(resetIdle, 0);
|
||||||
}
|
}
|
||||||
|
@ -1174,6 +1205,45 @@ export default function (view) {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSubtitleSync();
|
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(() => {
|
}).finally(() => {
|
||||||
resetIdle();
|
resetIdle();
|
||||||
});
|
});
|
||||||
|
@ -1641,6 +1711,7 @@ export default function (view) {
|
||||||
let programEndDateMs = 0;
|
let programEndDateMs = 0;
|
||||||
let playbackStartTimeTicks = 0;
|
let playbackStartTimeTicks = 0;
|
||||||
let subtitleSyncOverlay;
|
let subtitleSyncOverlay;
|
||||||
|
let chapterSelectionOptions = [];
|
||||||
let trickplayResolution = null;
|
let trickplayResolution = null;
|
||||||
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
|
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
|
||||||
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
|
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
|
||||||
|
@ -1960,6 +2031,7 @@ export default function (view) {
|
||||||
});
|
});
|
||||||
view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection);
|
view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection);
|
||||||
view.querySelector('.btnSubtitles').addEventListener('click', showSubtitleTrackSelection);
|
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
|
// HACK: Remove `emby-button` from the rating button to make it look like the other buttons
|
||||||
view.querySelector('.btnUserRating').classList.remove('emby-button');
|
view.querySelector('.btnUserRating').classList.remove('emby-button');
|
||||||
|
|
|
@ -71,7 +71,12 @@ export function getDisplayDuration(ticks) {
|
||||||
return result.join(' ');
|
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 ticksPerHour = 36000000000;
|
||||||
const ticksPerMinute = 600000000;
|
const ticksPerMinute = 600000000;
|
||||||
const ticksPerSecond = 10000000;
|
const ticksPerSecond = 10000000;
|
||||||
|
@ -81,7 +86,7 @@ export function getDisplayRunningTime(ticks) {
|
||||||
let hours = ticks / ticksPerHour;
|
let hours = ticks / ticksPerHour;
|
||||||
hours = Math.floor(hours);
|
hours = Math.floor(hours);
|
||||||
|
|
||||||
if (hours) {
|
if (hours || showZeroValues) {
|
||||||
parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale()));
|
parts.push(hours.toLocaleString(globalize.getCurrentDateTimeLocale()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +97,7 @@ export function getDisplayRunningTime(ticks) {
|
||||||
|
|
||||||
ticks -= (minutes * ticksPerMinute);
|
ticks -= (minutes * ticksPerMinute);
|
||||||
|
|
||||||
if (minutes < 10 && hours) {
|
if (minutes < 10 && (hours || showZeroValues)) {
|
||||||
minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
minutes = (0).toLocaleString(globalize.getCurrentDateTimeLocale()) + minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||||
} else {
|
} else {
|
||||||
minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
minutes = minutes.toLocaleString(globalize.getCurrentDateTimeLocale());
|
||||||
|
|
|
@ -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.",
|
"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",
|
"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.",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue