-
-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;
-+}