diff --git a/package-lock.json b/package-lock.json index 758c190a7..39a17e5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", "use-long-press": "^3.2.0" }, @@ -13434,8 +13434,8 @@ }, "node_modules/stremio-translations": { "version": "1.44.12", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", - "integrity": "sha512-b38OjGwlsvFm/aNn/ia18mPxPjZvnI/GaToppn1XaQqCuZuSHxQlYDddwOYTztskWo4VO/IZmCi3UFewqpsqCQ==", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#8212fa77c4febd22ddb611590e9fb574dc845416", + "integrity": "sha512-5DladLUsghLlVRsZh2bBnb7UMqU8NEYMHc+YbzBvb1llgMk9elXFSHtAjInepZlC5zWx2pJYOQ8lQzzqogQdFw==", "license": "MIT" }, "node_modules/string_decoder": { diff --git a/package.json b/package.json index 91fdb0491..9599f3587 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "react-i18next": "^15.1.3", "react-is": "18.3.1", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#8efdffbcf6eeadf01ab658e54adcc6a236b7b10f", + "stremio-translations": "github:Stremio/stremio-translations#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", "use-long-press": "^3.2.0" }, 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 ? ((_, ref) => { F +