From f6d4e3f4a60c808d701d649cbc165fea631d8099 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 16 Jun 2025 15:22:10 +0200 Subject: [PATCH] feat: add player subtitles delay shortcuts --- src/common/animations.less | 13 ++++ src/routes/Player/Indicator/Indicator.less | 23 +++++++ src/routes/Player/Indicator/Indicator.tsx | 73 ++++++++++++++++++++++ src/routes/Player/Player.js | 47 +++++++++++++- src/routes/Player/styles.less | 7 +++ src/routes/Settings/Settings.js | 10 +++ 6 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 src/routes/Player/Indicator/Indicator.less create mode 100644 src/routes/Player/Indicator/Indicator.tsx diff --git a/src/common/animations.less b/src/common/animations.less index 91dbe386d..3b9815f14 100644 --- a/src/common/animations.less +++ b/src/common/animations.less @@ -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; diff --git a/src/routes/Player/Indicator/Indicator.less b/src/routes/Player/Indicator/Indicator.less new file mode 100644 index 000000000..699c53d23 --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.less @@ -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); + } +} \ No newline at end of file diff --git a/src/routes/Player/Indicator/Indicator.tsx b/src/routes/Player/Indicator/Indicator.tsx new file mode 100644 index 000000000..cc0ef4ccf --- /dev/null +++ b/src/routes/Player/Indicator/Indicator.tsx @@ -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 = { + 'extraSubtitlesDelay': { + label: 'SUBTITLES_DELAY', + format: (value) => `${(value / 1000).toFixed(2)}s`, + }, +}; + +type VideoState = Record; + +type Props = { + className: string, + videoState: VideoState, +}; + +const Indicator = ({ className, videoState }: Props) => { + const timeout = useRef(null); + const prevVideoState = useRef(videoState); + + const [shown, show, hide] = useBinaryState(false); + const [current, setCurrent] = useState(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 ( + +
+
+
{label} {value}
+
+
+
+ ); +}; + +export default Indicator; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 8ba813786..65c02534f 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -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} /> + { nextVideoPopupOpen ? { F +
+
+
{ t('SETTINGS_SHORTCUT_SUBTITLES_DELAY') }
+
+
+ G +
and
+ H +
+
{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }