55ee501cf5
Co-authored-by: rlauuzo <46294892+rlauuzo@users.noreply.github.com> Co-authored-by: rlauu <46294892+rlauu@users.noreply.github.com>
227 lines
8.8 KiB
Diff
227 lines
8.8 KiB
Diff
diff --git a/src/controllers/playback/video/index.html b/src/controllers/playback/video/index.html
|
|
index a460ee8f6a3..d7b344d4b1b 100644
|
|
--- a/src/controllers/playback/video/index.html
|
|
+++ b/src/controllers/playback/video/index.html
|
|
@@ -6,6 +6,12 @@
|
|
</div>
|
|
</div>
|
|
<div class="upNextContainer hide"></div>
|
|
+ <div class="skipIntro hide">
|
|
+ <button is="emby-button" type="button" class="btnSkipIntro injected">
|
|
+ <span id="btnSkipSegmentText"></span>
|
|
+ <span class="material-icons skip_next"></span>
|
|
+ </button>
|
|
+ </div>
|
|
<div class="videoOsdBottom videoOsdBottom-maincontrols">
|
|
<div class="osdControls">
|
|
<div class="osdTextContainer osdMainTextContainer">
|
|
diff --git a/src/controllers/playback/video/index.js b/src/controllers/playback/video/index.js
|
|
index 2adad5708c3..5b81eebc7f1 100644
|
|
--- a/src/controllers/playback/video/index.js
|
|
+++ b/src/controllers/playback/video/index.js
|
|
@@ -365,7 +365,7 @@ export default function (view) {
|
|
toggleSubtitleSync('hide');
|
|
|
|
// Firefox does not blur by itself
|
|
- if (document.activeElement) {
|
|
+ if (document.activeElement && !skipButton.contains(document.activeElement)) {
|
|
document.activeElement.blur();
|
|
}
|
|
}
|
|
@@ -517,9 +517,95 @@ export default function (view) {
|
|
updatePlaylist();
|
|
enableStopOnBack(true);
|
|
updatePlaybackRate(player);
|
|
+ getIntroTimestamps(state.NowPlayingItem);
|
|
}
|
|
}
|
|
|
|
+ function secureFetch(url) {
|
|
+ const apiClient = ServerConnections.currentApiClient();
|
|
+ const address = apiClient.serverAddress();
|
|
+ const reqInit = {
|
|
+ headers: {
|
|
+ "Authorization": `MediaBrowser Token=${apiClient.accessToken()}`
|
|
+ }
|
|
+ };
|
|
+ return fetch(`${address}${url}`, reqInit).then(r => {
|
|
+ return r.ok ? r.json() : null;
|
|
+ });
|
|
+ }
|
|
+
|
|
+ function getIntroTimestamps(item) {
|
|
+ secureFetch(`/Episode/${item.Id}/IntroSkipperSegments`).then(segments => {
|
|
+ skipSegments = segments;
|
|
+ hasCreditsSegment = Object.keys(segments).some(key => key === "Credits");
|
|
+ }).catch(err => {
|
|
+ skipSegments = {};
|
|
+ hasCreditsSegment = false; });
|
|
+ secureFetch(`/Intros/UserInterfaceConfiguration`).then(config => {
|
|
+ skipButton.dataset.Introduction = config.SkipButtonIntroText;
|
|
+ skipButton.dataset.Credits = config.SkipButtonEndCreditsText;
|
|
+ }).catch(err => {
|
|
+ skipButton.dataset.Introduction = 'Skip Intro';
|
|
+ skipButton.dataset.Credits = 'Next'; });
|
|
+ }
|
|
+
|
|
+ function getCurrentSegment(position) {
|
|
+ for (const [key, segment] of Object.entries(skipSegments)) {
|
|
+ if ((position > segment.ShowSkipPromptAt && position < segment.HideSkipPromptAt - 1) ||
|
|
+ (currentVisibleMenu === 'osd' && position > segment.IntroStart && position < segment.IntroEnd - 1)) {
|
|
+ segment.SegmentType = key;
|
|
+ return segment;
|
|
+ }
|
|
+ }
|
|
+ return { SegmentType: "None" };
|
|
+ }
|
|
+
|
|
+ function videoPositionChanged(currentTime) {
|
|
+ const embyButton = skipButton.querySelector(".emby-button");
|
|
+ const segmentType = getCurrentSegment(currentTime / TICKS_PER_SECOND).SegmentType;
|
|
+ if (segmentType === "None") {
|
|
+ if (!skipButton.classList.contains('show')) return;
|
|
+ skipButton.classList.remove('show');
|
|
+ embyButton.addEventListener("transitionend", () => {
|
|
+ skipButton.classList.add("hide");
|
|
+ if (!currentVisibleMenu) {
|
|
+ embyButton.blur();
|
|
+ } else {
|
|
+ _focus(osdBottomElement.querySelector('.btnPause'));
|
|
+ }
|
|
+ }, { once: true });
|
|
+ return;
|
|
+ }
|
|
+ skipButton.querySelector("#btnSkipSegmentText").textContent = skipButton.dataset[segmentType];
|
|
+ if (!skipButton.classList.contains("hide")) {
|
|
+ if (!currentVisibleMenu && !embyButton.contains(document.activeElement)) _focus(embyButton);
|
|
+ return;
|
|
+ }
|
|
+ requestAnimationFrame(() => {
|
|
+ skipButton.classList.remove("hide");
|
|
+ requestAnimationFrame(() => {
|
|
+ skipButton.classList.add('show');
|
|
+ _focus(embyButton);
|
|
+ });
|
|
+ });
|
|
+ }
|
|
+
|
|
+ function doSkip() {
|
|
+ const segment = getCurrentSegment(playbackManager.currentTime(currentPlayer) / 1000);
|
|
+ if (segment.SegmentType === "None") {
|
|
+ console.warn("[intro skipper] doSkip() called without an active segment");
|
|
+ return;
|
|
+ }
|
|
+ playbackManager.seek(segment.IntroEnd * TICKS_PER_SECOND, currentPlayer);
|
|
+ }
|
|
+
|
|
+ function eventHandler(e) {
|
|
+ if (e.key !== "Enter") return;
|
|
+ e.stopPropagation();
|
|
+ e.preventDefault();
|
|
+ doSkip();
|
|
+ }
|
|
+
|
|
function onPlayPauseStateChanged() {
|
|
if (isEnabled) {
|
|
updatePlayPauseState(this.paused());
|
|
@@ -637,12 +723,13 @@ export default function (view) {
|
|
const item = currentItem;
|
|
refreshProgramInfoIfNeeded(player, item);
|
|
showComingUpNextIfNeeded(player, item, currentTime, currentRuntimeTicks);
|
|
+ videoPositionChanged(currentTime);
|
|
}
|
|
}
|
|
}
|
|
|
|
function showComingUpNextIfNeeded(player, currentItem, currentTimeTicks, runtimeTicks) {
|
|
- if (runtimeTicks && currentTimeTicks && !comingUpNextDisplayed && !currentVisibleMenu && currentItem.Type === 'Episode' && userSettings.enableNextVideoInfoOverlay()) {
|
|
+ if (!hasCreditsSegment && runtimeTicks && currentTimeTicks && !comingUpNextDisplayed && !currentVisibleMenu && currentItem.Type === 'Episode' && userSettings.enableNextVideoInfoOverlay()) {
|
|
let showAtSecondsLeft = 30;
|
|
if (runtimeTicks >= 50 * TICKS_PER_MINUTE) {
|
|
showAtSecondsLeft = 40;
|
|
@@ -1543,7 +1630,10 @@ export default function (view) {
|
|
let programEndDateMs = 0;
|
|
let playbackStartTimeTicks = 0;
|
|
let subtitleSyncOverlay;
|
|
+ let skipSegments = {};
|
|
+ let hasCreditsSegment;
|
|
let trickplayResolution = null;
|
|
+ const skipButton = document.querySelector(".skipIntro");
|
|
const nowPlayingVolumeSlider = view.querySelector('.osdVolumeSlider');
|
|
const nowPlayingVolumeSliderContainer = view.querySelector('.osdVolumeSliderContainer');
|
|
const nowPlayingPositionSlider = view.querySelector('.osdPositionSlider');
|
|
@@ -1699,6 +1789,10 @@ export default function (view) {
|
|
let lastPointerDown = 0;
|
|
/* eslint-disable-next-line compat/compat */
|
|
dom.addEventListener(view, window.PointerEvent ? 'pointerdown' : 'click', function (e) {
|
|
+ if (dom.parentWithClass(e.target, ['btnSkipIntro'])) {
|
|
+ return;
|
|
+ }
|
|
+
|
|
if (dom.parentWithClass(e.target, ['videoOsdBottom', 'upNextContainer'])) {
|
|
showOsd();
|
|
return;
|
|
@@ -1854,6 +1948,8 @@ export default function (view) {
|
|
});
|
|
view.querySelector('.btnAudio').addEventListener('click', showAudioTrackSelection);
|
|
view.querySelector('.btnSubtitles').addEventListener('click', showSubtitleTrackSelection);
|
|
+ skipButton.addEventListener('click', doSkip);
|
|
+ skipButton.addEventListener("keydown", eventHandler);
|
|
|
|
// HACK: Remove `emby-button` from the rating button to make it look like the other buttons
|
|
view.querySelector('.btnUserRating').classList.remove('emby-button');
|
|
@@ -1964,4 +2060,3 @@ export default function (view) {
|
|
});
|
|
}
|
|
}
|
|
-
|
|
diff --git a/src/styles/videoosd.scss b/src/styles/videoosd.scss
|
|
index 2c8c00e2601..336b2bacad3 100644
|
|
--- a/src/styles/videoosd.scss
|
|
+++ b/src/styles/videoosd.scss
|
|
@@ -346,3 +346,44 @@
|
|
transform: rotate(-360deg);
|
|
}
|
|
}
|
|
+
|
|
+:root {
|
|
+ --rounding: 4px;
|
|
+ --accent: 0, 164, 220;
|
|
+}
|
|
+.skipIntro {
|
|
+ position: absolute;
|
|
+ bottom: 7.5em;
|
|
+ right: 5em;
|
|
+ background-color: transparent;
|
|
+}
|
|
+.skipIntro .emby-button {
|
|
+ color: #ffffff;
|
|
+ font-size: 110%;
|
|
+ background: rgba(0, 0, 0, 0.7);
|
|
+ border-radius: var(--rounding);
|
|
+ box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
|
|
+ transition: opacity 0.3s cubic-bezier(0.4,0,0.2,1),
|
|
+ transform 0.3s cubic-bezier(0.4,0,0.2,1),
|
|
+ background-color 0.2s ease-out,
|
|
+ box-shadow 0.2s ease-out;
|
|
+ opacity: 0;
|
|
+ transform: translateY(50%);
|
|
+}
|
|
+.skipIntro.show .emby-button {
|
|
+ opacity: 1;
|
|
+ transform: translateY(0);
|
|
+}
|
|
+.skipIntro .emby-button:hover {
|
|
+ background: rgb(var(--accent));
|
|
+ box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
|
+ filter: brightness(1.2);
|
|
+}
|
|
+.skipIntro .emby-button:focus {
|
|
+ background: rgb(var(--accent));
|
|
+ box-shadow: 0 0 8px rgba(var(--accent), 0.6);
|
|
+}
|
|
+.btnSkipSegmentText {
|
|
+ letter-spacing: 0.5px;
|
|
+ padding: 0 5px 0 5px;
|
|
+}
|