From 900b746018da03ae1b08c64bfefe4308ba9571db Mon Sep 17 00:00:00 2001 From: THaenlein Date: Sat, 9 May 2026 12:25:28 +0200 Subject: [PATCH 1/2] Add button for skippable segments --- src/routes/Player/Player.js | 68 +++++++++++++++++++++++++++++++++-- src/routes/Player/styles.less | 26 ++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 5a76be3d9..9ca5505a7 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -11,7 +11,7 @@ const { useCore } = require('stremio/core'); const { useServices, useGamepad } = require('stremio/services'); const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation'); const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, useShell, usePlatform, onShortcut } = require('stremio/common'); -const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components'); +const { HorizontalNavBar, Transition, ContextMenu, Button } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); const Error = require('./Error'); @@ -35,6 +35,11 @@ const { default: useMediaSession } = require('./useMediaSession'); const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); const findTrackById = (tracks, id) => tracks.find((track) => track.id === id); +const formatSegmentName = (segment) => String(segment || '') + .split(/[_-]/) + .filter(Boolean) + .map((word) => word[0] ? `${word[0].toUpperCase()}${word.slice(1)}` : word) + .join(' '); const GAMEPAD_HANDLER_ID = 'player'; @@ -217,6 +222,45 @@ const Player = ({ urlParams, queryParams }) => { video.setTime(time); seek(time, video.state.duration, video.state.manifest?.name); }, [video.state.duration, video.state.manifest]); + const activeSkippableSegment = React.useMemo(() => { + if (typeof video.state.time !== 'number') { + return null; + } + + const segments = player.introOutro?.segments; + if (!Array.isArray(segments)) { + return null; + } + + return segments.find((segment) => + typeof segment?.from === 'number' && + typeof segment?.to === 'number' && + typeof segment?.segment === 'string' && + video.state.time >= segment.from && + video.state.time <= segment.to + ) || null; + }, [player.introOutro, video.state.time]); + const activeSkippableSegmentLabel = React.useMemo(() => { + return activeSkippableSegment ? formatSegmentName(activeSkippableSegment.segment) : null; + }, [activeSkippableSegment]); + const skipSegmentButtonLabel = React.useMemo(() => { + if (!activeSkippableSegmentLabel) { + return null; + } + + const translated = t('STREMIO_TV_PLAYER_BUTTON_SKIP_CHAPTER'); + if (typeof translated === 'string' && translated !== 'STREMIO_TV_PLAYER_BUTTON_SKIP_CHAPTER') { + return translated.replace('${1}', activeSkippableSegmentLabel); + } + + return `Skip ${activeSkippableSegmentLabel}`; + }, [activeSkippableSegmentLabel]); + const onSkipSegmentRequested = React.useCallback(() => { + if (activeSkippableSegment !== null) { + setSeeking(true); + onSeekRequested(activeSkippableSegment.to); + } + }, [activeSkippableSegment, onSeekRequested]); const onPlaybackSpeedChanged = React.useCallback((rate, skipUpdate) => { video.setPlaybackSpeed(rate); @@ -441,7 +485,15 @@ const Player = ({ urlParams, queryParams }) => { React.useEffect(() => { if (player.nextVideo !== null && !nextVideoPopupDismissed.current) { - if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) { + const enteredOutro = typeof player.introOutro?.outro === 'number' && + typeof video.state.time === 'number' && + video.state.time >= player.introOutro.outro; + const withinDefaultWindow = video.state.time !== null && + video.state.duration !== null && + video.state.time < video.state.duration && + (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration; + + if (enteredOutro || withinDefaultWindow) { openNextVideoPopup(); } else { closeNextVideoPopup(); @@ -455,7 +507,7 @@ const Player = ({ urlParams, queryParams }) => { } else { window.playerNextVideo = null; } - }, [player.nextVideo, video.state.time, video.state.duration]); + }, [player.nextVideo, player.introOutro, video.state.time, video.state.duration]); // Auto audio track selection React.useEffect(() => { @@ -895,6 +947,16 @@ const Player = ({ urlParams, queryParams }) => { onMouseOver={onBarMouseMove} onTouchEnd={onContainerMouseLeave} /> + { + activeSkippableSegment !== null && !menusOpen && skipSegmentButtonLabel !== null ? +
+ +
+ : + null + } Date: Sat, 9 May 2026 12:47:14 +0200 Subject: [PATCH 2/2] Skip button styling and focus behavior --- src/routes/Player/Player.js | 19 +++++++++++++++++-- src/routes/Player/styles.less | 10 +++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9ca5505a7..d850f9ef4 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -133,6 +133,8 @@ const Player = ({ urlParams, queryParams }) => { const pressTimer = React.useRef(null); const longPress = React.useRef(false); const controlBarRef = React.useRef(null); + const skipSegmentButtonRef = React.useRef(null); + const skipSegmentVisibleRef = React.useRef(false); const HOLD_DELAY = 400; @@ -261,6 +263,7 @@ const Player = ({ urlParams, queryParams }) => { onSeekRequested(activeSkippableSegment.to); } }, [activeSkippableSegment, onSeekRequested]); + const skipSegmentVisible = activeSkippableSegment !== null && !menusOpen && skipSegmentButtonLabel !== null; const onPlaybackSpeedChanged = React.useCallback((rate, skipUpdate) => { video.setPlaybackSpeed(rate); @@ -588,6 +591,14 @@ const Player = ({ urlParams, queryParams }) => { } }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); + React.useEffect(() => { + if (skipSegmentVisible && !skipSegmentVisibleRef.current) { + skipSegmentButtonRef.current?.focus(); + } + + skipSegmentVisibleRef.current = skipSegmentVisible; + }, [skipSegmentVisible]); + useMediaSession(video.state, player, onPlayRequested, onPauseRequested, onNextVideoRequested); React.useEffect(() => { @@ -948,9 +959,13 @@ const Player = ({ urlParams, queryParams }) => { onTouchEnd={onContainerMouseLeave} /> { - activeSkippableSegment !== null && !menusOpen && skipSegmentButtonLabel !== null ? + skipSegmentVisible ?
-
diff --git a/src/routes/Player/styles.less b/src/routes/Player/styles.less index b578bd665..16623d8a5 100644 --- a/src/routes/Player/styles.less +++ b/src/routes/Player/styles.less @@ -135,9 +135,13 @@ html:not(.active-slider-within) { padding: 1rem 1.6rem; font-weight: 700; color: var(--primary-foreground-color); - background-color: var(--modal-background-color); - box-shadow: 0 1.35rem 2.7rem @color-background-dark5-40, - 0 1.1rem 0.85rem @color-background-dark5-20; + background-color: var(--primary-accent-color); + transition: background-color 120ms ease-in; + + &:hover, &:focus { + outline: var(--focus-outline-size) solid var(--primary-accent-color); + background-color: transparent; + } } }