This commit is contained in:
Tim Hänlein 2026-05-09 13:21:17 +02:00 committed by GitHub
commit 103e9eb7fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 110 additions and 3 deletions

View file

@ -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';
@ -128,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;
@ -217,6 +224,46 @@ 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 skipSegmentVisible = activeSkippableSegment !== null && !menusOpen && skipSegmentButtonLabel !== null;
const onPlaybackSpeedChanged = React.useCallback((rate, skipUpdate) => {
video.setPlaybackSpeed(rate);
@ -441,7 +488,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 +510,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(() => {
@ -536,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(() => {
@ -885,6 +948,20 @@ const Player = ({ urlParams, queryParams }) => {
onMouseOver={onBarMouseMove}
onTouchEnd={onContainerMouseLeave}
/>
{
skipSegmentVisible ?
<div className={classnames(styles['layer'], styles['skip-segment-layer'])}>
<Button
ref={skipSegmentButtonRef}
className={styles['skip-segment-button']}
onClick={onSkipSegmentRequested}
>
{skipSegmentButtonLabel}
</Button>
</div>
:
null
}
<Indicator
className={classnames(styles['layer'], styles['indicator-layer'])}
videoState={video.state}

View file

@ -123,6 +123,32 @@ html:not(.active-slider-within) {
bottom: 10rem;
}
&.skip-segment-layer {
top: initial;
left: initial;
right: 4rem;
bottom: 11rem;
pointer-events: none;
display: flex;
justify-content: flex-end;
align-items: flex-end;
.skip-segment-button {
pointer-events: auto;
border-radius: 4rem;
padding: 1rem 1.6rem;
font-weight: 700;
color: var(--primary-foreground-color);
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;
}
}
}
&.menu-layer {
top: initial;
left: initial;
@ -153,6 +179,10 @@ html:not(.active-slider-within) {
&.side-drawer-button-layer {
right: -2rem;
}
&.skip-segment-layer {
right: 2rem;
}
}
}
}