feat: add player subtitles delay shortcuts

This commit is contained in:
Tim 2025-06-16 15:22:10 +02:00
parent d75c9b1d99
commit f6d4e3f4a6
6 changed files with 172 additions and 1 deletions

View file

@ -82,6 +82,19 @@
transform: translateY(100%);
}
.fade-enter {
opacity: 0;
}
.fade-active {
opacity: 1;
transition: opacity 0.3s cubic-bezier(0.32, 0, 0.67, 0);
}
.fade-exit {
opacity: 0;
}
@keyframes fade-in-no-motion {
0% {
opacity: 0;

View file

@ -0,0 +1,23 @@
.indicator-container {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
height: 4rem;
user-select: none;
.indicator {
flex: none;
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 0 2rem;
border-radius: 4rem;
text-align: center;
font-weight: bold;
color: var(--primary-foreground-color);
background-color: var(--modal-background-color);
}
}

View file

@ -0,0 +1,73 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import classNames from 'classnames';
import { t } from 'i18next';
import { Transition } from 'stremio/components';
import { useBinaryState } from 'stremio/common';
import styles from './Indicator.less';
type Property = {
label: string,
format: (value: number) => string,
};
const PROPERTIES: Record<string, Property> = {
'extraSubtitlesDelay': {
label: 'SUBTITLES_DELAY',
format: (value) => `${(value / 1000).toFixed(2)}s`,
},
};
type VideoState = Record<string, number>;
type Props = {
className: string,
videoState: VideoState,
};
const Indicator = ({ className, videoState }: Props) => {
const timeout = useRef<NodeJS.Timeout | null>(null);
const prevVideoState = useRef<VideoState>(videoState);
const [shown, show, hide] = useBinaryState(false);
const [current, setCurrent] = useState<string | null>(null);
const label = useMemo(() => {
const property = current && PROPERTIES[current];
return property && t(property.label);
}, [current]);
const value = useMemo(() => {
const property = current && PROPERTIES[current];
const value = current && videoState[current];
return property && value && property.format(value);
}, [current, videoState]);
useEffect(() => {
for (const property of Object.keys(PROPERTIES)) {
const prev = prevVideoState.current[property];
const next = videoState[property];
if (next && next !== prev) {
setCurrent(property);
show();
timeout.current && clearTimeout(timeout.current);
timeout.current = setTimeout(hide, 1000);
}
}
prevVideoState.current = videoState;
}, [videoState]);
return (
<Transition when={shown} name={'fade'}>
<div className={classNames(className, styles['indicator-container'])}>
<div className={styles['indicator']}>
<div>{label} {value}</div>
</div>
</div>
</Transition>
);
};
export default Indicator;

View file

@ -27,6 +27,7 @@ const useStatistics = require('./useStatistics');
const useVideo = require('./useVideo');
const styles = require('./styles');
const Video = require('./Video');
const { default: Indicator } = require('./Indicator/Indicator');
const Player = ({ urlParams, queryParams }) => {
const { t } = useTranslation();
@ -216,6 +217,16 @@ const Player = ({ urlParams, queryParams }) => {
video.setProp('extraSubtitlesDelay', delay);
}, []);
const onIncreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay + 250;
onExtraSubtitlesDelayChanged(delay);
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
const onDecreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay - 250;
onExtraSubtitlesDelayChanged(delay);
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
const onSubtitlesSizeChanged = React.useCallback((size) => {
updateSettings({ subtitlesSize: size });
}, [updateSettings]);
@ -587,6 +598,14 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
case 'KeyG': {
onDecreaseSubtitlesDelay();
break;
}
case 'KeyH': {
onIncreaseSubtitlesDelay();
break;
}
case 'Escape': {
closeMenus();
!settings.escExitFullscreen && window.history.back();
@ -620,7 +639,29 @@ const Player = ({ urlParams, queryParams }) => {
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('wheel', onWheel);
};
}, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, settings.escExitFullscreen, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleStatisticsMenu, toggleSideDrawer]);
}, [
player.metaItem,
player.selected,
streamingServer.statistics,
settings.seekTimeDuration,
settings.seekShortTimeDuration,
settings.escExitFullscreen,
routeFocused,
menusOpen,
nextVideoPopupOpen,
video.state.paused,
video.state.time,
video.state.volume,
video.state.audioTracks,
video.state.subtitlesTracks,
video.state.extraSubtitlesTracks,
video.state.playbackSpeed,
toggleSubtitlesMenu,
toggleStatisticsMenu,
toggleSideDrawer,
onDecreaseSubtitlesDelay,
onIncreaseSubtitlesDelay,
]);
React.useEffect(() => {
video.events.on('error', onError);
@ -760,6 +801,10 @@ const Player = ({ urlParams, queryParams }) => {
onMouseOver={onBarMouseMove}
onTouchEnd={onContainerMouseLeave}
/>
<Indicator
className={classnames(styles['layer'], styles['indicator-layer'])}
videoState={video.state}
/>
{
nextVideoPopupOpen ?
<NextVideoPopup

View file

@ -107,6 +107,13 @@ html:not(.active-slider-within) {
}
}
&.indicator-layer {
top: initial;
left: 0;
right: 0;
bottom: 10rem;
}
&.menu-layer {
top: initial;
left: initial;

View file

@ -709,6 +709,16 @@ const Settings = () => {
<kbd>F</kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SUBTITLES_DELAY') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>G</kbd>
<div className={styles['label']}>and</div>
<kbd>H</kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }</div>