diff --git a/package.json b/package.json index 5bed50260..8f29d78df 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#c23317eec194b5a3318e98c2ea6acae5cfa32e2a", + "stremio-translations": "github:Stremio/stremio-translations#fcad3f8077db865bd08b0f93d785f4090f19db40", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6aacf8a4..2256c21eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ importers: specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6 version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6 stremio-translations: - specifier: github:Stremio/stremio-translations#c23317eec194b5a3318e98c2ea6acae5cfa32e2a - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a + specifier: github:Stremio/stremio-translations#fcad3f8077db865bd08b0f93d785f4090f19db40 + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/fcad3f8077db865bd08b0f93d785f4090f19db40 url: specifier: 0.11.4 version: 0.11.4 @@ -4133,8 +4133,8 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/fcad3f8077db865bd08b0f93d785f4090f19db40: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/fcad3f8077db865bd08b0f93d785f4090f19db40} version: 1.51.0 string-length@4.0.2: @@ -9378,7 +9378,7 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/c23317eec194b5a3318e98c2ea6acae5cfa32e2a: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/fcad3f8077db865bd08b0f93d785f4090f19db40: {} string-length@4.0.2: dependencies: diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx index c9198a857..348471484 100644 --- a/src/common/Shortcuts/Shortcuts.tsx +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -19,10 +19,20 @@ type Props = { onShortcut: (name: ShortcutName) => void, }; +const REPEAT_THROTTLE_MS = 130; + const ShortcutsProvider = ({ children, onShortcut }: Props) => { const listeners = useRef>>(new Map()); + const lastRepeatTime = useRef>(new Map()); + + const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key, repeat }: KeyboardEvent) => { + if (repeat) { + const now = Date.now(); + const last = lastRepeatTime.current.get(code) ?? 0; + if (now - last < REPEAT_THROTTLE_MS) return; + lastRepeatTime.current.set(code, now); + } - const onKeyDown = useCallback(({ ctrlKey, shiftKey, code, key }: KeyboardEvent) => { SHORTCUTS.forEach(({ name, combos }) => combos.forEach((keys) => { const modifers = (keys.includes('Ctrl') ? ctrlKey : true) && (keys.includes('Shift') ? shiftKey : true); diff --git a/src/components/MetaPreview/Ratings/Ratings.tsx b/src/components/MetaPreview/Ratings/Ratings.tsx index 329ee4945..8ba867800 100644 --- a/src/components/MetaPreview/Ratings/Ratings.tsx +++ b/src/components/MetaPreview/Ratings/Ratings.tsx @@ -1,6 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import useRating from './useRating'; import { ActionsGroup } from 'stremio/components'; @@ -11,17 +12,20 @@ type Props = { }; const Ratings = ({ ratingInfo, className }: Props) => { + const { t } = useTranslation(); const { onLiked, onLoved, liked, loved } = useRating(ratingInfo); const disabled = useMemo(() => ratingInfo?.type !== 'Ready', [ratingInfo]); const items = useMemo(() => [ { icon: liked ? 'thumbs-up' : 'thumbs-up-outline', + label: liked ? t('RATING_UNLIKE') : t('RATING_LIKE'), disabled, onClick: onLiked, }, { icon: loved ? 'heart' : 'heart-outline', + label: loved ? t('RATING_UNLOVE') : t('RATING_LOVE'), disabled, onClick: onLoved, }, diff --git a/src/routes/MetaDetails/styles.less b/src/routes/MetaDetails/styles.less index d77cc3902..f666f0dd7 100644 --- a/src/routes/MetaDetails/styles.less +++ b/src/routes/MetaDetails/styles.less @@ -32,7 +32,7 @@ width: 100%; height: 100%; object-fit: cover; - object-position: center; + object-position: right; opacity: 0.3; } } @@ -137,6 +137,12 @@ @media only screen and (max-width: @minimum) { .metadetails-container { + .background-image-layer { + .background-image { + object-position: center; + } + } + .metadetails-content { display: block; overflow-y: auto; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 42ff18e7b..3a02f7c3d 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -91,7 +91,7 @@ const Player = ({ urlParams, queryParams }) => { const nextVideoPopupDismissed = React.useRef(false); const defaultSubtitlesSelected = React.useRef(false); - const subtitlesEnabled = React.useRef(true); + const lastSubtitleTrack = React.useRef(null); const defaultAudioTrackSelected = React.useRef(false); const playingOnExternalDevice = React.useRef(false); const [error, setError] = React.useState(null); @@ -247,14 +247,22 @@ const Player = ({ urlParams, queryParams }) => { }, [video.state.videoScale]); const onSubtitlesTrackSelected = React.useCallback((track) => { + defaultSubtitlesSelected.current = true; video.setSubtitlesTrack(track?.id ?? null); + if (track) { + lastSubtitleTrack.current = { id: track.id, embedded: true }; + } streamStateChanged({ subtitleTrack: track ? { id: track.id, embedded: true, lang: track.lang } : null, }); }, [streamStateChanged]); const onExtraSubtitlesTrackSelected = React.useCallback((track) => { + defaultSubtitlesSelected.current = true; video.setExtraSubtitlesTrack(track?.id ?? null); + if (track) { + lastSubtitleTrack.current = { id: track.id, embedded: false }; + } streamStateChanged({ subtitleTrack: track ? { id: track.id, embedded: false, lang: track.lang } : null, }); @@ -538,6 +546,7 @@ const Player = ({ urlParams, queryParams }) => { React.useEffect(() => { defaultSubtitlesSelected.current = false; defaultAudioTrackSelected.current = false; + lastSubtitleTrack.current = null; nextVideoPopupDismissed.current = false; playingOnExternalDevice.current = false; // we need a timeout here to make sure that previous page unloads and the new one loads @@ -660,7 +669,7 @@ const Player = ({ urlParams, queryParams }) => { onShortcut('volumeDown', () => { if (video.state.volume !== null) { - onVolumeChangeRequested(Math.min(video.state.volume - 5, 200)); + onVolumeChangeRequested(Math.max(video.state.volume - 5, 0)); } }, [video.state.volume], !menusOpen); @@ -669,21 +678,27 @@ const Player = ({ urlParams, queryParams }) => { }, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay], !menusOpen); onShortcut('subtitlesSize', (combo) => { - combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1); + combo === 1 ? onUpdateSubtitlesSize(1) : onUpdateSubtitlesSize(-1); }, [onUpdateSubtitlesSize, onUpdateSubtitlesSize], !menusOpen); onShortcut('toggleSubtitles', () => { - const savedTrack = player.streamState?.subtitleTrack; + const isEnabled = video.state.selectedSubtitlesTrackId !== null || video.state.selectedExtraSubtitlesTrackId !== null; - if (subtitlesEnabled.current) { + if (isEnabled) { + if (video.state.selectedSubtitlesTrackId) { + lastSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true }; + } else if (video.state.selectedExtraSubtitlesTrackId) { + lastSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false }; + } video.setSubtitlesTrack(null); video.setExtraSubtitlesTrack(null); - } else if (savedTrack?.id) { - savedTrack.embedded ? video.setSubtitlesTrack(savedTrack.id) : video.setExtraSubtitlesTrack(savedTrack.id); + } else { + const savedTrack = player.streamState?.subtitleTrack ?? lastSubtitleTrack.current; + if (savedTrack?.id) { + savedTrack.embedded ? video.setSubtitlesTrack(savedTrack.id) : video.setExtraSubtitlesTrack(savedTrack.id); + } } - - subtitlesEnabled.current = !subtitlesEnabled.current; - }, [player.streamState], !menusOpen); + }, [player.streamState, video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId], !menusOpen); onShortcut('subtitlesMenu', () => { closeMenus(); diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less index 49cd71575..318765dcb 100644 --- a/src/routes/Player/SideDrawer/SideDrawer.less +++ b/src/routes/Player/SideDrawer/SideDrawer.less @@ -24,6 +24,17 @@ transition: transform 0.3s ease-in-out; z-index: 1; + // Safari has a compositing bug where transform animations on a parent with + // scrollable children causes the video player element to shift left during the animation. + // Disable the slide animation on Safari until WebKit resolves this. + @supports (hanging-punctuation: first) and (-webkit-appearance: none) { + &:global(.slide-left-enter), + &:global(.slide-left-active), + &:global(.slide-left-exit) { + transition: none; + } + } + .close-button { display: none; position: absolute;