diff --git a/src/routes/Player/SubtitlesMenu/SubtitleVariant/SubtitleVariant.less b/src/routes/Player/SubtitlesMenu/SubtitleVariant/SubtitleVariant.less index 600b79658..b2901d7f1 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitleVariant/SubtitleVariant.less +++ b/src/routes/Player/SubtitlesMenu/SubtitleVariant/SubtitleVariant.less @@ -48,6 +48,16 @@ margin-left: 1rem; background-color: var(--secondary-accent-color); } + + .secondary-icon { + flex: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + margin-left: 1rem; + border: 2px solid var(--secondary-accent-color); + background-color: transparent; + } } .context-menu-option { diff --git a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js index a71969bd5..57baf0fe1 100644 --- a/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js +++ b/src/routes/Player/SubtitlesMenu/SubtitlesMenu.js @@ -75,6 +75,18 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => { : null; }, [subtitlesTracks, extraSubtitlesTracks, props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId]); + const selectedSecondarySubtitlesLanguage = React.useMemo(() => { + return typeof props.selectedSecondarySubtitlesTrackId === 'string' ? + allSubtitles + .reduce((selected, { id, lang }) => { + if (id === props.selectedSecondarySubtitlesTrackId) { + return lang; + } + return selected; + }, null) + : + null; + }, [allSubtitles, props.selectedSecondarySubtitlesTrackId]); const subtitlesTracksForLanguage = React.useMemo(() => { const tracks = allSubtitles.filter(({ lang }) => lang === selectedSubtitlesLanguage); return sortByValues(tracks, ORIGIN_PRIORITIES); @@ -153,6 +165,41 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => { } } }, [props.selectedSubtitlesTrackId, props.selectedExtraSubtitlesTrackId, props.subtitlesOffset, props.extraSubtitlesOffset, props.onSubtitlesOffsetChanged, props.onExtraSubtitlesOffsetChanged]); + + // Secondary subtitle support (desktop-only via ShellVideo/mpv) + const hasPrimary = typeof props.selectedSubtitlesTrackId === 'string' || + typeof props.selectedExtraSubtitlesTrackId === 'string'; + const showSecondarySection = hasPrimary && props.isShellActive && + typeof props.onSecondarySubtitlesTrackSelected === 'function'; + + const secondaryLanguages = React.useMemo(() => { + return subtitlesLanguages.filter((lang) => lang !== selectedSubtitlesLanguage); + }, [subtitlesLanguages, selectedSubtitlesLanguage]); + + const secondaryLanguageOnClick = React.useCallback((event) => { + const lang = event.currentTarget.dataset.lang; + // OFF button has no data-lang + if (!lang) { + if (typeof props.onSecondarySubtitlesTrackSelected === 'function') { + props.onSecondarySubtitlesTrackSelected(null); + } + return; + } + + if (lang === selectedSecondarySubtitlesLanguage) { + // Deselect secondary if clicking the already-selected one + if (typeof props.onSecondarySubtitlesTrackSelected === 'function') { + props.onSecondarySubtitlesTrackSelected(null); + } + return; + } + + const tracks = allSubtitles.filter(({ lang: tLang }) => tLang === lang); + const track = sortByValues(tracks, ORIGIN_PRIORITIES).shift(); + if (track && typeof props.onSecondarySubtitlesTrackSelected === 'function') { + props.onSecondarySubtitlesTrackSelected(track); + } + }, [allSubtitles, selectedSecondarySubtitlesLanguage, props.onSecondarySubtitlesTrackSelected]); return (
@@ -183,6 +230,40 @@ const SubtitlesMenu = React.memo(React.forwardRef((props, ref) => { ))}
+ { + showSecondarySection && secondaryLanguages.length > 0 ? + +
{ t('PLAYER_SUBTITLES_SECONDARY', { defaultValue: 'Secondary Subtitle' }) }
+
+ + {secondaryLanguages.map((lang, index) => ( + + ))} +
+
+ : + null + }
{ t('PLAYER_SUBTITLES_VARIANTS') }
@@ -250,6 +331,7 @@ SubtitlesMenu.displayName = 'MainNavBars'; SubtitlesMenu.propTypes = { className: PropTypes.string, + isShellActive: PropTypes.bool, subtitlesLanguage: PropTypes.string, interfaceLanguage: PropTypes.string, subtitlesTracks: PropTypes.arrayOf(PropTypes.shape({ @@ -258,6 +340,7 @@ SubtitlesMenu.propTypes = { origin: PropTypes.string.isRequired })), selectedSubtitlesTrackId: PropTypes.string, + selectedSecondarySubtitlesTrackId: PropTypes.string, subtitlesOffset: PropTypes.number, subtitlesSize: PropTypes.number, extraSubtitlesTracks: PropTypes.arrayOf(PropTypes.shape({ @@ -276,6 +359,7 @@ SubtitlesMenu.propTypes = { extraSubtitlesSize: PropTypes.number, onSubtitlesTrackSelected: PropTypes.func, onExtraSubtitlesTrackSelected: PropTypes.func, + onSecondarySubtitlesTrackSelected: PropTypes.func, onSubtitlesOffsetChanged: PropTypes.func, onSubtitlesSizeChanged: PropTypes.func, onExtraSubtitlesOffsetChanged: PropTypes.func, diff --git a/src/routes/Player/SubtitlesMenu/styles.less b/src/routes/Player/SubtitlesMenu/styles.less index b0aa3b051..1f173299c 100644 --- a/src/routes/Player/SubtitlesMenu/styles.less +++ b/src/routes/Player/SubtitlesMenu/styles.less @@ -36,10 +36,15 @@ margin-bottom: 0.5rem; border-radius: var(--border-radius); - &:global(.selected), &:hover { + &:global(.selected), &:global(.secondary-selected), &:hover { background-color: var(--overlay-color); } + &:global(.secondary-selected) { + background-color: var(--overlay-color); + opacity: 0.8; + } + .language-label { flex: 1; font-size: 1.1rem; @@ -56,12 +61,36 @@ margin-left: 1rem; background-color: var(--secondary-accent-color); } + + .secondary-icon { + flex: none; + width: 0.5rem; + height: 0.5rem; + border-radius: 100%; + margin-left: 1rem; + border: 2px solid var(--secondary-accent-color); + background-color: transparent; + } } } } .languages-container { width: 16rem; + + .secondary-header { + flex: none; + align-self: stretch; + padding: 0.75rem 2rem 0.25rem; + font-size: 0.9rem; + font-weight: 700; + color: var(--color-placeholder-text); + text-transform: uppercase; + letter-spacing: 0.05em; + border-top: 1px solid var(--overlay-color); + margin-top: 0.5rem; + padding-top: 1rem; + } } .variants-container { diff --git a/src/routes/Player/useSubtitles.d.ts b/src/routes/Player/useSubtitles.d.ts index 93084e7b2..c8e7fa5a1 100644 --- a/src/routes/Player/useSubtitles.d.ts +++ b/src/routes/Player/useSubtitles.d.ts @@ -22,6 +22,7 @@ type VideoSubtitleState = { stream: unknown | null, subtitlesTracks: SubtitleTrack[], selectedSubtitlesTrackId: string | null, + selectedSecondarySubtitlesTrackId: string | null, subtitlesOffset: number | null, subtitlesSize: number | null, extraSubtitlesTracks: SubtitleTrack[], @@ -43,6 +44,7 @@ type VideoController = { addLocalSubtitles: (filename: string, buffer: ArrayBuffer) => void, setSubtitlesTrack: (id: string | null) => void, setExtraSubtitlesTrack: (id: string | null) => void, + setSecondarySubtitlesTrack: (id: string | null) => void, setSubtitlesDelay: (delay: number) => void, setSubtitlesSize: (size: number) => void, setSubtitlesOffset: (offset: number) => void, @@ -63,10 +65,12 @@ type UseSubtitlesArgs = { }; type SubtitlesMenuProps = { + isShellActive: boolean, subtitlesLanguage: string | null, interfaceLanguage: string, subtitlesTracks: SubtitleTrack[], selectedSubtitlesTrackId: string | null, + selectedSecondarySubtitlesTrackId: string | null, subtitlesOffset: number | null, subtitlesSize: number | null, extraSubtitlesTracks: SubtitleTrack[], @@ -76,6 +80,7 @@ type SubtitlesMenuProps = { extraSubtitlesSize: number | null, onSubtitlesTrackSelected: (track: SubtitleTrack | null) => void, onExtraSubtitlesTrackSelected: (track: SubtitleTrack | null) => void, + onSecondarySubtitlesTrackSelected: (track: SubtitleTrack | null) => void, onSubtitlesOffsetChanged: (offset: number) => void, onSubtitlesSizeChanged: (size: number) => void, onExtraSubtitlesOffsetChanged: (offset: number) => void, diff --git a/src/routes/Player/useSubtitles.ts b/src/routes/Player/useSubtitles.ts index 26de76d74..73a5bf4e4 100644 --- a/src/routes/Player/useSubtitles.ts +++ b/src/routes/Player/useSubtitles.ts @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { CONSTANTS, languages, onFileDrop, onShortcut, useToast } from 'stremio/common'; +import { CONSTANTS, languages, onFileDrop, onShortcut, usePlatform, useToast } from 'stremio/common'; const withFallbackLabels = (tracks?: SubtitleTrack[] | null): SubtitleTrack[] => { if (!Array.isArray(tracks)) { @@ -47,6 +47,7 @@ const useSubtitles = ({ }: UseSubtitlesArgs): UseSubtitlesResult => { const { t } = useTranslation(); const toast = useToast(); + const platform = usePlatform(); const videoRef = useRef(video); const settingsRef = useRef(settings); const defaultTrackSelected = useRef(false); @@ -95,6 +96,7 @@ const useSubtitles = ({ defaultTrackSelected.current = true; video.setSubtitlesTrack(null); video.setExtraSubtitlesTrack(null); + video.setSecondarySubtitlesTrack(null); streamStateChanged({ subtitleTrack: null }); }, [streamStateChanged, video]); @@ -120,6 +122,16 @@ const useSubtitles = ({ rememberTrack(track, false); }, [disableSubtitles, rememberTrack, video]); + const selectSecondaryTrack = useCallback((track: SubtitleTrack | null) => { + if (!track) { + video.setSecondarySubtitlesTrack(null); + return; + } + + defaultTrackSelected.current = true; + video.setSecondarySubtitlesTrack(track.id); + }, [video]); + const changeDelay = useCallback((delay: number) => { video.setSubtitlesDelay(delay); streamStateChanged({ subtitleDelay: delay }); @@ -301,7 +313,8 @@ const useSubtitles = ({ onShortcut('toggleSubtitles', () => { const subtitlesEnabled = video.state.selectedSubtitlesTrackId !== null || - video.state.selectedExtraSubtitlesTrackId !== null; + video.state.selectedExtraSubtitlesTrackId !== null || + video.state.selectedSecondarySubtitlesTrackId !== null; if (subtitlesEnabled) { if (video.state.selectedSubtitlesTrackId) { @@ -318,6 +331,7 @@ const useSubtitles = ({ video.setSubtitlesTrack(null); video.setExtraSubtitlesTrack(null); + video.setSecondarySubtitlesTrack(null); return; } @@ -332,6 +346,7 @@ const useSubtitles = ({ player.streamState, video.state.selectedExtraSubtitlesTrackId, video.state.selectedSubtitlesTrackId, + video.state.selectedSecondarySubtitlesTrackId, ], !menusOpen); onShortcut('subtitlesMenu', () => { @@ -342,10 +357,12 @@ const useSubtitles = ({ }, [closeMenus, hasTracks, toggleSubtitlesMenu]); const menuProps = useMemo(() => ({ + isShellActive: platform.shell.active, subtitlesLanguage: settings.subtitlesLanguage, interfaceLanguage: settings.interfaceLanguage, subtitlesTracks: video.state.subtitlesTracks, selectedSubtitlesTrackId: video.state.selectedSubtitlesTrackId, + selectedSecondarySubtitlesTrackId: video.state.selectedSecondarySubtitlesTrackId, subtitlesOffset: video.state.subtitlesOffset, subtitlesSize: video.state.subtitlesSize, extraSubtitlesTracks: video.state.extraSubtitlesTracks, @@ -355,6 +372,7 @@ const useSubtitles = ({ extraSubtitlesSize: video.state.extraSubtitlesSize, onSubtitlesTrackSelected: selectEmbeddedTrack, onExtraSubtitlesTrackSelected: selectExtraTrack, + onSecondarySubtitlesTrackSelected: selectSecondaryTrack, onSubtitlesOffsetChanged: changeOffset, onSubtitlesSizeChanged: changeSize, onExtraSubtitlesOffsetChanged: changeOffset, @@ -364,8 +382,10 @@ const useSubtitles = ({ changeDelay, changeOffset, changeSize, + platform.shell.active, selectEmbeddedTrack, selectExtraTrack, + selectSecondaryTrack, settings.interfaceLanguage, settings.subtitlesLanguage, video.state.extraSubtitlesDelay, @@ -374,6 +394,7 @@ const useSubtitles = ({ video.state.extraSubtitlesTracks, video.state.selectedExtraSubtitlesTrackId, video.state.selectedSubtitlesTrackId, + video.state.selectedSecondarySubtitlesTrackId, video.state.subtitlesOffset, video.state.subtitlesSize, video.state.subtitlesTracks, diff --git a/src/routes/Player/useVideo.js b/src/routes/Player/useVideo.js index 570888ff1..bc26c79b9 100644 --- a/src/routes/Player/useVideo.js +++ b/src/routes/Player/useVideo.js @@ -34,6 +34,7 @@ const useVideo = () => { subtitlesOutlineColor: null, extraSubtitlesTracks: [], selectedExtraSubtitlesTrackId: null, + selectedSecondarySubtitlesTrackId: null, extraSubtitlesSize: null, extraSubtitlesDelay: null, extraSubtitlesOffset: null, @@ -130,6 +131,10 @@ const useVideo = () => { setProp('selectedExtraSubtitlesTrackId', id); }; + const setSecondarySubtitlesTrack = (id) => { + setProp('selectedSecondarySubtitlesTrackId', id); + }; + const setSubtitlesDelay = (delay) => { setProp('extraSubtitlesDelay', delay); }; @@ -248,6 +253,7 @@ const useVideo = () => { setSubtitlesBackgroundColor, setSubtitlesOutlineColor, setExtraSubtitlesTrack, + setSecondarySubtitlesTrack, setVideoScale, setFullscreen, };