From e8bee4997a7a7a5c8cfaf1d60d036278760aa84b Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 21 Nov 2025 09:11:35 +0100 Subject: [PATCH 01/14] feat: add ass subtitles styling setting --- src/routes/Player/Player.js | 1 + src/routes/Settings/Player/Player.tsx | 10 ++++++++++ src/routes/Settings/Player/usePlayerOptions.ts | 17 +++++++++++++++++ src/types/models/Ctx.d.ts | 1 + 4 files changed, 29 insertions(+) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index bcd973267..7ff578c37 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -346,6 +346,7 @@ const Player = ({ urlParams, queryParams }) => { forceTranscoding: forceTranscoding || casting, maxAudioChannels: settings.surroundSound ? 32 : 2, hardwareDecoding: settings.hardwareDecoding, + assSubtitlesStyling: settings.assSubtitlesStyling, videoMode: settings.videoMode, platform: platform.name, streamingServerURL: streamingServer.baseUrl ? diff --git a/src/routes/Settings/Player/Player.tsx b/src/routes/Settings/Player/Player.tsx index 29b98d650..dc2a2a3f0 100644 --- a/src/routes/Settings/Player/Player.tsx +++ b/src/routes/Settings/Player/Player.tsx @@ -19,6 +19,7 @@ const Player = forwardRef(({ profile }: Props, ref) => { subtitlesTextColorInput, subtitlesBackgroundColorInput, subtitlesOutlineColorInput, + assSubtitlesStylingToggle, audioLanguageSelect, surroundSoundToggle, seekTimeDurationSelect, @@ -65,6 +66,15 @@ const Player = forwardRef(({ profile }: Props, ref) => { {...subtitlesOutlineColorInput} /> + { + shell.active && + + } - { - shell.active && - - } } + { + shell.active && + + } ); From 487fde70e049dcce8809a5fdc0f9691ec50a0a84 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 20 Jan 2026 21:25:32 +0100 Subject: [PATCH 04/14] chore: update video --- package.json | 2 +- pnpm-lock.yaml | 32 ++++++-------------------------- 2 files changed, 7 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 8b6efe5cc..6a1047e2e 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.51.1", "@stremio/stremio-icons": "5.8.0", - "@stremio/stremio-video": "0.0.64", + "@stremio/stremio-video": "0.0.70", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48afa6562..f964d9bf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: 5.8.0 version: 5.8.0 '@stremio/stremio-video': - specifier: 0.0.64 - version: 0.0.64 + specifier: 0.0.70 + version: 0.0.70 a-color-picker: specifier: 1.2.1 version: 1.2.1 @@ -1126,8 +1126,8 @@ packages: '@stremio/stremio-icons@5.8.0': resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==} - '@stremio/stremio-video@0.0.64': - resolution: {integrity: sha512-29w/lwU8BB6ai8LUyCnpRc2F9kPf7cpys40NCobt70MqBP/UqvYISsrnD/ijoBwvtpKdZ6ptv5h9BbDj6rrerw==} + '@stremio/stremio-video@0.0.70': + resolution: {integrity: sha512-a0flQYAUdrZNMm7mmts2vpZOqN1nus7Hs9Mjl4mrN5rtduD0ojUyhD5J4lPcCpZ7WB0YdEUOGLXR19qHpgoKmg==} '@stylistic/eslint-plugin-jsx@4.4.1': resolution: {integrity: sha512-83SInq4u7z71vWwGG+6ViOtlOmZ6tSrDkMPhrvdBBTGMLA0gs22WSdhQ4vZP3oJ5Xg4ythvqeUiFSedvVxzhyA==} @@ -3738,9 +3738,6 @@ packages: prr@1.0.1: resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} - punycode@1.3.2: - resolution: {integrity: sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==} - punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -3759,11 +3756,6 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} - querystring@0.2.0: - resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -4420,9 +4412,6 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - url@0.11.0: - resolution: {integrity: sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==} - url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -5887,7 +5876,7 @@ snapshots: '@stremio/stremio-icons@5.8.0': {} - '@stremio/stremio-video@0.0.64': + '@stremio/stremio-video@0.0.70': dependencies: buffer: 6.0.3 color: 4.2.3 @@ -5897,7 +5886,7 @@ snapshots: hls.js: https://github.com/Stremio/hls.js/releases/download/v1.5.4-patch2/hls.js-1.5.4-patch2.tgz lodash.clonedeep: 4.5.0 magnet-uri: 6.2.0 - url: 0.11.0 + url: 0.11.4 video-name-parser: 1.4.6 vtt.js: https://codeload.github.com/jaruba/vtt.js/tar.gz/84d33d157848407d790d78423dacc41a096294f0 @@ -8941,8 +8930,6 @@ snapshots: prr@1.0.1: optional: true - punycode@1.3.2: {} - punycode@1.4.1: {} punycode@2.3.1: {} @@ -8957,8 +8944,6 @@ snapshots: dependencies: side-channel: 1.1.0 - querystring@0.2.0: {} - queue-microtask@1.2.3: {} randombytes@2.1.0: @@ -9688,11 +9673,6 @@ snapshots: dependencies: punycode: 2.3.1 - url@0.11.0: - dependencies: - punycode: 1.3.2 - querystring: 0.2.0 - url@0.11.4: dependencies: punycode: 1.4.1 From f046e65e7369c9f7087abb8b30716c885e632377 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 20 Jan 2026 21:38:44 +0100 Subject: [PATCH 05/14] chore: update translations --- package.json | 2 +- pnpm-lock.yaml | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 6a1047e2e..1173798d8 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#0e7fbd8522148f5727ac6adee3b2eb96132c10ac", + "stremio-translations": "github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef", "url": "0.11.4", "use-long-press": "^3.2.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f964d9bf9..8b5f17c81 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#0e7fbd8522148f5727ac6adee3b2eb96132c10ac - version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac + specifier: github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef + version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef url: specifier: 0.11.4 version: 0.11.4 @@ -4133,9 +4133,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} - stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac: - resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/0e7fbd8522148f5727ac6adee3b2eb96132c10ac} - version: 1.44.14 + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef: + resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef} + version: 1.45.0 string-length@4.0.2: resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} @@ -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/0e7fbd8522148f5727ac6adee3b2eb96132c10ac: {} + stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef: {} string-length@4.0.2: dependencies: From 5aaee645491a2790d27737e27646dc6337a9f7f7 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 20 Jan 2026 21:39:03 +0100 Subject: [PATCH 06/14] chore: add new interface languages --- src/common/interfaceLanguages.json | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/common/interfaceLanguages.json b/src/common/interfaceLanguages.json index ce3504691..91b87d5af 100644 --- a/src/common/interfaceLanguages.json +++ b/src/common/interfaceLanguages.json @@ -3,6 +3,10 @@ "name": "العربية", "codes": ["ar-AR", "ara"] }, + { + "name": "Беларуская", + "codes": ["be-BY", "bel"] + }, { "name": "български език", "codes": ["bg-BG", "bul"] @@ -13,7 +17,7 @@ }, { "name": "català", - "codes": ["ca-CA", "cat"] + "codes": ["ca-ES", "cat"] }, { "name": "čeština", @@ -43,6 +47,10 @@ "name": "español", "codes": ["es-ES", "spa"] }, + { + "name": "Eesti", + "codes": ["et-EE", "est"] + }, { "name": "euskara", "codes": ["eu-ES", "eus"] @@ -111,6 +119,10 @@ "name": "Norsk nynorsk", "codes": ["nn-NO", "nno"] }, + { + "name": "ਪੰਜਾਬੀ", + "codes": ["pa-IN", "pan"] + }, { "name": "język polski", "codes": ["pl-PL", "pol"] @@ -151,6 +163,10 @@ "name": "తెలుగు", "codes": ["te-IN", "tel"] }, + { + "name": "தமிழ்", + "codes": ["tl-TM", "tam"] + }, { "name": "Türkçe", "codes": ["tr-TR", "tur"] @@ -159,6 +175,10 @@ "name": "українська мова", "codes": ["uk-UA", "ukr"] }, + { + "name": "اُرْدُو", + "codes": ["ur-PK", "urd"] + }, { "name": "Tiếng Việt", "codes": ["vi-VN", "vie"] From 7cd49b516f734f148a24a36e89d58a16b27189ea Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 20 Jan 2026 22:01:33 +0100 Subject: [PATCH 07/14] refactor(Settings): move interface settings to dedicated section --- src/routes/Settings/General/General.tsx | 45 +-------------- src/routes/Settings/Interface/Interface.tsx | 57 +++++++++++++++++++ src/routes/Settings/Interface/index.ts | 2 + .../useInterfaceOptions.ts} | 4 +- src/routes/Settings/Menu/Menu.tsx | 3 + src/routes/Settings/Settings.tsx | 7 +++ src/routes/Settings/constants.ts | 1 + 7 files changed, 74 insertions(+), 45 deletions(-) create mode 100644 src/routes/Settings/Interface/Interface.tsx create mode 100644 src/routes/Settings/Interface/index.ts rename src/routes/Settings/{General/useGeneralOptions.ts => Interface/useInterfaceOptions.ts} (96%) diff --git a/src/routes/Settings/General/General.tsx b/src/routes/Settings/General/General.tsx index 8f5496dbd..49cfdfdd6 100644 --- a/src/routes/Settings/General/General.tsx +++ b/src/routes/Settings/General/General.tsx @@ -1,13 +1,12 @@ import React, { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, MultiselectMenu, Toggle } from 'stremio/components'; +import { Button } from 'stremio/components'; import { useServices } from 'stremio/services'; import { usePlatform, useToast } from 'stremio/common'; import { Section, Option, Link } from '../components'; import User from './User'; import useDataExport from './useDataExport'; import styles from './General.less'; -import useGeneralOptions from './useGeneralOptions'; type Props = { profile: Profile, @@ -15,18 +14,11 @@ type Props = { const General = forwardRef(({ profile }: Props, ref) => { const { t } = useTranslation(); - const { core, shell } = useServices(); + const { core } = useServices(); const platform = usePlatform(); const toast = useToast(); const [dataExport, loadDataExport] = useDataExport(); - const { - interfaceLanguageSelect, - quitOnCloseToggle, - escExitFullscreenToggle, - hideSpoilersToggle, - } = useGeneralOptions(profile); - const [traktAuthStarted, setTraktAuthStarted] = useState(false); const isTraktAuthenticated = useMemo(() => { @@ -143,39 +135,6 @@ const General = forwardRef(({ profile }: Props, ref) => { - -
- - { - shell.active && - - } - { - shell.active && - - } - -
; }); diff --git a/src/routes/Settings/Interface/Interface.tsx b/src/routes/Settings/Interface/Interface.tsx new file mode 100644 index 000000000..a4b429a56 --- /dev/null +++ b/src/routes/Settings/Interface/Interface.tsx @@ -0,0 +1,57 @@ +import React, { forwardRef } from 'react'; +import { useServices } from 'stremio/services'; +import { MultiselectMenu, Toggle } from 'stremio/components'; +import { Section, Option } from '../components'; +import useInterfaceOptions from './useInterfaceOptions'; + +type Props = { + profile: Profile, +}; + +const Interface = forwardRef(({ profile }: Props, ref) => { + const { shell } = useServices(); + + const { + interfaceLanguageSelect, + quitOnCloseToggle, + escExitFullscreenToggle, + hideSpoilersToggle, + } = useInterfaceOptions(profile); + + return ( +
+ + { + shell.active && + + } + { + shell.active && + + } + +
+ ); +}); + +export default Interface; diff --git a/src/routes/Settings/Interface/index.ts b/src/routes/Settings/Interface/index.ts new file mode 100644 index 000000000..480fa5ff4 --- /dev/null +++ b/src/routes/Settings/Interface/index.ts @@ -0,0 +1,2 @@ +import Interface from './Interface'; +export default Interface; diff --git a/src/routes/Settings/General/useGeneralOptions.ts b/src/routes/Settings/Interface/useInterfaceOptions.ts similarity index 96% rename from src/routes/Settings/General/useGeneralOptions.ts rename to src/routes/Settings/Interface/useInterfaceOptions.ts index a4ab84c5e..67780b7e1 100644 --- a/src/routes/Settings/General/useGeneralOptions.ts +++ b/src/routes/Settings/Interface/useInterfaceOptions.ts @@ -2,7 +2,7 @@ import { useMemo } from 'react'; import { interfaceLanguages, useLanguageSorting } from 'stremio/common'; import { useServices } from 'stremio/services'; -const useGeneralOptions = (profile: Profile) => { +const useInterfaceOptions = (profile: Profile) => { const { core } = useServices(); const interfaceLanguageOptions = useMemo(() => @@ -89,4 +89,4 @@ const useGeneralOptions = (profile: Profile) => { }; }; -export default useGeneralOptions; +export default useInterfaceOptions; diff --git a/src/routes/Settings/Menu/Menu.tsx b/src/routes/Settings/Menu/Menu.tsx index ceafee94b..33cf41dc0 100644 --- a/src/routes/Settings/Menu/Menu.tsx +++ b/src/routes/Settings/Menu/Menu.tsx @@ -26,6 +26,9 @@ const Menu = ({ selected, streamingServer, onSelect }: Props) => { + diff --git a/src/routes/Settings/Settings.tsx b/src/routes/Settings/Settings.tsx index 2db68da2f..727da9e82 100644 --- a/src/routes/Settings/Settings.tsx +++ b/src/routes/Settings/Settings.tsx @@ -9,6 +9,7 @@ import { MainNavBars } from 'stremio/components'; import { SECTIONS } from './constants'; import Menu from './Menu'; import General from './General'; +import Interface from './Interface'; import Player from './Player'; import Streaming from './Streaming'; import Shortcuts from './Shortcuts'; @@ -23,12 +24,14 @@ const Settings = () => { const sectionsContainerRef = useRef(null); const generalSectionRef = useRef(null); + const interfaceSectionRef = useRef(null); const playerSectionRef = useRef(null); const streamingServerSectionRef = useRef(null); const shortcutsSectionRef = useRef(null); const sections = useMemo(() => ([ { ref: generalSectionRef, id: SECTIONS.GENERAL }, + { ref: interfaceSectionRef, id: SECTIONS.INTERFACE }, { ref: playerSectionRef, id: SECTIONS.PLAYER }, { ref: streamingServerSectionRef, id: SECTIONS.STREAMING }, { ref: shortcutsSectionRef, id: SECTIONS.SHORTCUTS }, @@ -82,6 +85,10 @@ const Settings = () => { ref={generalSectionRef} profile={profile} /> + Date: Wed, 21 Jan 2026 09:47:17 +0100 Subject: [PATCH 08/14] feat: remember selected tracks on player --- src/routes/Player/Player.js | 62 ++++++++++++++++++++++++---------- src/routes/Player/usePlayer.js | 21 ++++++++++-- src/types/models/Player.d.ts | 15 ++++++++ 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 3ae2f066c..d6814193f 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -29,6 +29,9 @@ const styles = require('./styles'); const Video = require('./Video'); const { default: Indicator } = require('./Indicator/Indicator'); +const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); +const findTrackById = (tracks, id) => tracks.find((track) => track.id === id); + const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); const services = useServices(); @@ -37,7 +40,7 @@ const Player = ({ urlParams, queryParams }) => { return queryParams.has('forceTranscoding'); }, [queryParams]); const profile = useProfile(); - const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams); + const [player, videoParamsChanged, streamStateChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams); const [settings, updateSettings] = useSettings(); const streamingServer = useStreamingServer(); const statistics = useStatistics(player, streamingServer); @@ -224,15 +227,32 @@ const Player = ({ urlParams, queryParams }) => { const onSubtitlesTrackSelected = React.useCallback((id) => { video.setSubtitlesTrack(id); - }, []); + streamStateChanged({ + subtitleTrack: { + id, + embedded: true, + }, + }); + }, [streamStateChanged]); const onExtraSubtitlesTrackSelected = React.useCallback((id) => { video.setExtraSubtitlesTrack(id); - }, []); + streamStateChanged({ + subtitleTrack: { + id, + embedded: false, + }, + }); + }, [streamStateChanged]); const onAudioTrackSelected = React.useCallback((id) => { video.setProp('selectedAudioTrackId', id); - }, []); + streamStateChanged({ + audioTrack: { + id, + }, + }); + }, [streamStateChanged]); const onExtraSubtitlesDelayChanged = React.useCallback((delay) => { video.setProp('extraSubtitlesDelay', delay); @@ -444,41 +464,49 @@ const Player = ({ urlParams, queryParams }) => { } }, [player.nextVideo, video.state.time, video.state.duration]); + // Auto subtitles track selection React.useEffect(() => { if (!defaultSubtitlesSelected.current) { - const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); - if (settings.subtitlesLanguage === null) { - onSubtitlesTrackSelected(null); - onExtraSubtitlesTrackSelected(null); + video.setSubtitlesTrack(null); + video.setExtraSubtitlesTrack(null); defaultSubtitlesSelected.current = true; return; } - const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); - const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); + const savedTrackId = player.streamState?.subtitleTrack?.id; + const subtitlesTrack = savedTrackId ? + findTrackById(video.state.subtitlesTracks, savedTrackId) : + findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); + + const extraSubtitlesTrack = savedTrackId ? + findTrackById(video.state.extraSubtitlesTracks, savedTrackId) : + findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); if (subtitlesTrack && subtitlesTrack.id) { - onSubtitlesTrackSelected(subtitlesTrack.id); + video.setSubtitlesTrack(subtitlesTrack.id); defaultSubtitlesSelected.current = true; } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) { - onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id); + video.setExtraSubtitlesTrack(extraSubtitlesTrack.id); defaultSubtitlesSelected.current = true; } } - }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]); + }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, player.streamState]); + // Auto audio track selection React.useEffect(() => { if (!defaultAudioTrackSelected.current) { - const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang); - const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage); + const savedTrackId = player.streamState?.audioTrack?.id; + const audioTrack = savedTrackId ? + findTrackById(video.state.audioTracks, savedTrackId) : + findTrackByLang(video.state.audioTracks, settings.audioLanguage); if (audioTrack && audioTrack.id) { - onAudioTrackSelected(audioTrack.id); + video.setProp('selectedAudioTrackId', audioTrack.id); defaultAudioTrackSelected.current = true; } } - }, [video.state.audioTracks]); + }, [video.state.audioTracks, player.streamState]); React.useEffect(() => { defaultSubtitlesSelected.current = false; diff --git a/src/routes/Player/usePlayer.js b/src/routes/Player/usePlayer.js index 4ca2574ba..2584517eb 100644 --- a/src/routes/Player/usePlayer.js +++ b/src/routes/Player/usePlayer.js @@ -86,6 +86,9 @@ const usePlayer = (urlParams) => { }; } }, [urlParams]); + + const player = useModelState({ model: 'player', action, map }); + const videoParamsChanged = React.useCallback((videoParams) => { core.transport.dispatch({ action: 'Player', @@ -153,8 +156,22 @@ const usePlayer = (urlParams) => { }, 'player'); }, []); - const player = useModelState({ model: 'player', action, map }); - return [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo]; + const streamStateChanged = React.useCallback((partialStreamState) => { + return core.transport.dispatch({ + action: 'Player', + args: { + action: 'StreamStateChanged', + args: { + state: { + ...player.streamState, + ...partialStreamState, + }, + }, + }, + }, 'player'); + }, [player.streamState]); + + return [player, videoParamsChanged, streamStateChanged, timeChanged, seek, pausedChanged, ended, nextVideo]; }; module.exports = usePlayer; diff --git a/src/types/models/Player.d.ts b/src/types/models/Player.d.ts index 7f7dbc318..a076f78f4 100644 --- a/src/types/models/Player.d.ts +++ b/src/types/models/Player.d.ts @@ -30,6 +30,20 @@ type SeriesInfo = { season: number, }; +type SubtitlesTrackState = { + id: string, + embedded: boolean, +}; + +type AudioTrackState = { + id: string, +}; + +type StreamState = { + subtitleTrack?: SubtitlesTrackState, + audioTrack?: AudioTrackState, +}; + type Player = { addon: Addon | null, libraryItem: LibraryItemPlayer | null, @@ -42,6 +56,7 @@ type Player = { subtitlesPath: ResourceRequestPath, } | null, seriesInfo: SeriesInfo | null, + streamState: StreamState | null, subtitles: Subtitle[], title: string | null, }; From 0dcc07c46903ebe5dc95cb0d34fc4fe8a4ede5f7 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Jan 2026 00:12:53 +0100 Subject: [PATCH 09/14] chore: update core --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1173798d8..0c9ca54ec 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.51.1", + "@stremio/stremio-core-web": "0.52.0", "@stremio/stremio-icons": "5.8.0", "@stremio/stremio-video": "0.0.70", "a-color-picker": "1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b5f17c81..435a809e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: 5.2.0 version: 5.2.0 '@stremio/stremio-core-web': - specifier: 0.51.1 - version: 0.51.1 + specifier: 0.52.0 + version: 0.52.0 '@stremio/stremio-icons': specifier: 5.8.0 version: 5.8.0 @@ -1120,8 +1120,8 @@ packages: '@stremio/stremio-colors@5.2.0': resolution: {integrity: sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==} - '@stremio/stremio-core-web@0.51.1': - resolution: {integrity: sha512-BD8i6zkDdMPeCyH50Bb7SB8r4nYx4eJwz4kLEJEl0PFjdr0gOmwHtEIgNa89ShJLNXUjPnpv4sVSNxFRG8fb5Q==} + '@stremio/stremio-core-web@0.52.0': + resolution: {integrity: sha512-zT0P8JspGZ1oI9/11f3RIt7XG9b/1fOZE+xSnP+oAyhRmzzkqrnPUJkHdJdgoVD9XELDFAS2awNfl5/eRdh5kA==} '@stremio/stremio-icons@5.8.0': resolution: {integrity: sha512-IVUvQbIWfA4YEHCTed7v/sdQJCJ+OOCf84LTWpkE2W6GLQ+15WHcMEJrVkE1X3ekYJnGg3GjT0KLO6tKSU0P4w==} @@ -5870,7 +5870,7 @@ snapshots: '@stremio/stremio-colors@5.2.0': {} - '@stremio/stremio-core-web@0.51.1': + '@stremio/stremio-core-web@0.52.0': dependencies: '@babel/runtime': 7.24.1 From a30307789c6d8cee36c4731e7c162faddda42b19 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Jan 2026 09:57:46 +0100 Subject: [PATCH 10/14] feat: remember subtitles settings on player --- src/routes/Player/Player.js | 64 +++++++++++++++++++----------------- src/types/models/Player.d.ts | 3 ++ 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 502dfcedb..d82207893 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -41,7 +41,7 @@ const Player = ({ urlParams, queryParams }) => { }, [queryParams]); const profile = useProfile(); const [player, videoParamsChanged, streamStateChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams); - const [settings, updateSettings] = useSettings(); + const [settings] = useSettings(); const streamingServer = useStreamingServer(); const statistics = useStatistics(player, streamingServer); const video = useVideo(); @@ -256,7 +256,8 @@ const Player = ({ urlParams, queryParams }) => { const onExtraSubtitlesDelayChanged = React.useCallback((delay) => { video.setProp('extraSubtitlesDelay', delay); - }, []); + streamStateChanged({ subtitleDelay: delay }); + }, [streamStateChanged]); const onIncreaseSubtitlesDelay = React.useCallback(() => { const delay = video.state.extraSubtitlesDelay + 250; @@ -269,8 +270,10 @@ const Player = ({ urlParams, queryParams }) => { }, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]); const onSubtitlesSizeChanged = React.useCallback((size) => { - updateSettings({ subtitlesSize: size }); - }, [updateSettings]); + video.setProp('subtitlesSize', size); + video.setProp('extraSubtitlesSize', size); + streamStateChanged({ subtitleSize: size }); + }, [streamStateChanged]); const onUpdateSubtitlesSize = React.useCallback((delta) => { const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize); @@ -279,8 +282,10 @@ const Player = ({ urlParams, queryParams }) => { }, [video.state.subtitlesSize, onSubtitlesSizeChanged]); const onSubtitlesOffsetChanged = React.useCallback((offset) => { - updateSettings({ subtitlesOffset: offset }); - }, [updateSettings]); + video.setProp('subtitlesOffset', offset); + video.setProp('extraSubtitlesOffset', offset); + streamStateChanged({ subtitleOffset: offset }); + }, [streamStateChanged]); const onDismissNextVideoPopup = React.useCallback(() => { closeNextVideoPopup(); @@ -408,31 +413,6 @@ const Player = ({ urlParams, queryParams }) => { } }, [player.subtitles, video.state.stream]); - React.useEffect(() => { - video.setProp('subtitlesSize', settings.subtitlesSize); - video.setProp('extraSubtitlesSize', settings.subtitlesSize); - }, [settings.subtitlesSize]); - - React.useEffect(() => { - video.setProp('subtitlesOffset', settings.subtitlesOffset); - video.setProp('extraSubtitlesOffset', settings.subtitlesOffset); - }, [settings.subtitlesOffset]); - - React.useEffect(() => { - video.setProp('subtitlesTextColor', settings.subtitlesTextColor); - video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor); - }, [settings.subtitlesTextColor]); - - React.useEffect(() => { - video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor); - video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor); - }, [settings.subtitlesBackgroundColor]); - - React.useEffect(() => { - video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor); - video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor); - }, [settings.subtitlesOutlineColor]); - React.useEffect(() => { !seeking && timeChanged(video.state.time, video.state.duration, video.state.manifest?.name); }, [video.state.time, video.state.duration, video.state.manifest, seeking]); @@ -509,6 +489,28 @@ const Player = ({ urlParams, queryParams }) => { } }, [video.state.audioTracks, player.streamState]); + // Saved subtitles settings + React.useEffect(() => { + if (video.state.stream !== null) { + const subtitlesDelay = player.streamState?.subtitleDelay; + if (typeof subtitlesDelay === 'number') { + video.setProp('extraSubtitlesDelay', subtitlesDelay); + } + + const subtitlesSize = player.streamState?.subtitleSize; + if (typeof subtitlesSize === 'number') { + video.setProp('subtitlesSize', subtitlesSize); + video.setProp('extraSubtitlesSize', subtitlesSize); + } + + const subtitlesOffset = player.streamState?.subtitleOffset; + if (typeof subtitlesOffset === 'number') { + video.setProp('subtitlesOffset', subtitlesOffset); + video.setProp('extraSubtitlesOffset', subtitlesOffset); + } + } + }, [video.state.stream, player.streamState]); + React.useEffect(() => { defaultSubtitlesSelected.current = false; defaultAudioTrackSelected.current = false; diff --git a/src/types/models/Player.d.ts b/src/types/models/Player.d.ts index a076f78f4..321127316 100644 --- a/src/types/models/Player.d.ts +++ b/src/types/models/Player.d.ts @@ -41,6 +41,9 @@ type AudioTrackState = { type StreamState = { subtitleTrack?: SubtitlesTrackState, + subtitleDelay?: number, + subtitleSize?: number, + subtitleOffset?: number, audioTrack?: AudioTrackState, }; From e85b67268d3437a009c43f1f0b54884bcfdde3f7 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Jan 2026 10:21:42 +0100 Subject: [PATCH 11/14] refactor(Player): wrap video setProp calls in functions --- src/routes/Player/Player.js | 63 ++++++++++++++------------------- src/routes/Player/useVideo.js | 66 ++++++++++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index d82207893..196bb813b 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -96,17 +96,12 @@ const Player = ({ urlParams, queryParams }) => { const isNavigating = React.useRef(false); const onImplementationChanged = React.useCallback(() => { - video.setProp('subtitlesSize', settings.subtitlesSize); - video.setProp('subtitlesOffset', settings.subtitlesOffset); - video.setProp('subtitlesTextColor', settings.subtitlesTextColor); - video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor); - video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor); - video.setProp('extraSubtitlesSize', settings.subtitlesSize); - video.setProp('extraSubtitlesOffset', settings.subtitlesOffset); - video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor); - video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor); - video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor); - }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); + video.setSubtitlesSize(settings.subtitlesSize); + video.setSubtitlesOffset(settings.subtitlesOffset); + video.setSubtitlesTextColor(settings.subtitlesTextColor); + video.setSubtitlesBackgroundColor(settings.subtitlesBackgroundColor); + video.setSubtitlesOutlineColor(settings.subtitlesOutlineColor); + }, [settings]); const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => { if (ended) { @@ -193,36 +188,36 @@ const Player = ({ urlParams, queryParams }) => { }, []); const onPlayRequested = React.useCallback(() => { - video.setProp('paused', false); + video.setPaused(false); setSeeking(false); }, []); const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []); const onPauseRequested = React.useCallback(() => { - video.setProp('paused', true); + video.setPaused(true); }, []); const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []); const onMuteRequested = React.useCallback(() => { - video.setProp('muted', true); + video.setMuted(true); }, []); const onUnmuteRequested = React.useCallback(() => { - video.setProp('muted', false); + video.setMuted(false); }, []); const onVolumeChangeRequested = React.useCallback((volume) => { - video.setProp('volume', volume); + video.setVolume(volume); }, []); const onSeekRequested = React.useCallback((time) => { - video.setProp('time', time); + video.setTime(time); seek(time, video.state.duration, video.state.manifest?.name); }, [video.state.duration, video.state.manifest]); const onPlaybackSpeedChanged = React.useCallback((rate) => { - video.setProp('playbackSpeed', rate); + video.setPlaybackSpeed(rate); }, []); const onSubtitlesTrackSelected = React.useCallback((id) => { @@ -246,7 +241,7 @@ const Player = ({ urlParams, queryParams }) => { }, [streamStateChanged]); const onAudioTrackSelected = React.useCallback((id) => { - video.setProp('selectedAudioTrackId', id); + video.setAudioTrack(id); streamStateChanged({ audioTrack: { id, @@ -255,7 +250,7 @@ const Player = ({ urlParams, queryParams }) => { }, [streamStateChanged]); const onExtraSubtitlesDelayChanged = React.useCallback((delay) => { - video.setProp('extraSubtitlesDelay', delay); + video.setSubtitlesDelay(delay); streamStateChanged({ subtitleDelay: delay }); }, [streamStateChanged]); @@ -270,8 +265,7 @@ const Player = ({ urlParams, queryParams }) => { }, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]); const onSubtitlesSizeChanged = React.useCallback((size) => { - video.setProp('subtitlesSize', size); - video.setProp('extraSubtitlesSize', size); + video.setSubtitlesSize(size); streamStateChanged({ subtitleSize: size }); }, [streamStateChanged]); @@ -282,8 +276,7 @@ const Player = ({ urlParams, queryParams }) => { }, [video.state.subtitlesSize, onSubtitlesSizeChanged]); const onSubtitlesOffsetChanged = React.useCallback((offset) => { - video.setProp('subtitlesOffset', offset); - video.setProp('extraSubtitlesOffset', offset); + video.setSubtitlesOffset(offset); streamStateChanged({ subtitleOffset: offset }); }, [streamStateChanged]); @@ -483,7 +476,7 @@ const Player = ({ urlParams, queryParams }) => { findTrackByLang(video.state.audioTracks, settings.audioLanguage); if (audioTrack && audioTrack.id) { - video.setProp('selectedAudioTrackId', audioTrack.id); + video.setAudioTrack(audioTrack.id); defaultAudioTrackSelected.current = true; } } @@ -492,21 +485,19 @@ const Player = ({ urlParams, queryParams }) => { // Saved subtitles settings React.useEffect(() => { if (video.state.stream !== null) { - const subtitlesDelay = player.streamState?.subtitleDelay; - if (typeof subtitlesDelay === 'number') { - video.setProp('extraSubtitlesDelay', subtitlesDelay); + const delay = player.streamState?.subtitleDelay; + if (typeof delay === 'number') { + video.setSubtitlesDelay(delay); } - const subtitlesSize = player.streamState?.subtitleSize; - if (typeof subtitlesSize === 'number') { - video.setProp('subtitlesSize', subtitlesSize); - video.setProp('extraSubtitlesSize', subtitlesSize); + const size = player.streamState?.subtitleSize; + if (typeof size === 'number') { + video.setSubtitlesSize(size); } - const subtitlesOffset = player.streamState?.subtitleOffset; - if (typeof subtitlesOffset === 'number') { - video.setProp('subtitlesOffset', subtitlesOffset); - video.setProp('extraSubtitlesOffset', subtitlesOffset); + const offset = player.streamState?.subtitleOffset; + if (typeof offset === 'number') { + video.setSubtitlesOffset(offset); } } }, [video.state.stream, player.streamState]); diff --git a/src/routes/Player/useVideo.js b/src/routes/Player/useVideo.js index 9b54ba129..b3a5d2e39 100644 --- a/src/routes/Player/useVideo.js +++ b/src/routes/Player/useVideo.js @@ -94,6 +94,30 @@ const useVideo = () => { dispatch({ type: 'setProp', propName: name, propValue: value }); }; + const setPaused = (state) => { + setProp('paused', state); + }; + + const setVolume = (volume) => { + setProp('volume', volume); + }; + + const setMuted = (state) => { + setProp('muted', state); + }; + + const setTime = (time) => { + setProp('time', time); + }; + + const setPlaybackSpeed = (rate) => { + setProp('playbackSpeed', rate); + }; + + const setAudioTrack = (id) => { + setProp('selectedAudioTrackId', id); + }; + const setSubtitlesTrack = (id) => { setProp('selectedSubtitlesTrackId', id); setProp('selectedExtraSubtitlesTrackId', null); @@ -104,6 +128,35 @@ const useVideo = () => { setProp('selectedExtraSubtitlesTrackId', id); }; + const setSubtitlesDelay = (delay) => { + setProp('extraSubtitlesDelay', delay); + }; + + const setSubtitlesSize = (size) => { + setProp('subtitlesSize', size); + setProp('extraSubtitlesSize', size); + }; + + const setSubtitlesOffset = (offset) => { + setProp('subtitlesOffset', offset); + setProp('extraSubtitlesOffset', offset); + }; + + const setSubtitlesTextColor = (color) => { + setProp('subtitlesTextColor', color); + setProp('extraSubtitlesTextColor', color); + }; + + const setSubtitlesBackgroundColor = (color) => { + setProp('subtitlesBackgroundColor', color); + setProp('extraSubtitlesBackgroundColor', color); + }; + + const setSubtitlesOutlineColor = (color) => { + setProp('subtitlesOutlineColor', color); + setProp('extraSubtitlesOutlineColor', color); + }; + const onError = (error) => { events.emit('error', error); }; @@ -171,8 +224,19 @@ const useVideo = () => { unload, addExtraSubtitlesTracks, addLocalSubtitles, - setProp, + setPaused, + setVolume, + setMuted, + setTime, + setPlaybackSpeed, + setAudioTrack, setSubtitlesTrack, + setSubtitlesDelay, + setSubtitlesSize, + setSubtitlesOffset, + setSubtitlesTextColor, + setSubtitlesBackgroundColor, + setSubtitlesOutlineColor, setExtraSubtitlesTrack, }; }; From 15575ee699687f0c7d76afbd6a5aa322bd71bb3a Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Jan 2026 10:24:14 +0100 Subject: [PATCH 12/14] refactor(Player): set audio menu max-height same as subtitles menu --- src/routes/Player/AudioMenu/AudioMenu.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/Player/AudioMenu/AudioMenu.less b/src/routes/Player/AudioMenu/AudioMenu.less index 344ec13e7..ae3e8acb1 100644 --- a/src/routes/Player/AudioMenu/AudioMenu.less +++ b/src/routes/Player/AudioMenu/AudioMenu.less @@ -7,6 +7,7 @@ align-self: stretch; display: flex; flex-direction: column; + max-height: 25rem; width: 16rem; .header { From cce556e639ab97b131037db66bbe1a7df71516e4 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 22 Jan 2026 10:32:07 +0100 Subject: [PATCH 13/14] feat(Player): show disabled state when ends of range are reached for subtitles settings --- .../Player/SubtitlesMenu/Stepper/Stepper.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx b/src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx index 0d402a455..9307eb2fe 100644 --- a/src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx +++ b/src/routes/Player/SubtitlesMenu/Stepper/Stepper.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import Icon from '@stremio/stremio-icons/react'; @@ -37,6 +37,14 @@ const Stepper = ({ className, label, value, unit, step, min, max, disabled, onCh timeout.cancel(); }; + const decreaseDisabled = useMemo(() => { + return disabled || typeof value !== 'number' || (typeof min === 'number' && value <= min); + }, [disabled, min, value]); + + const increaseDisabled = useMemo(() => { + return disabled || typeof value !== 'number' || (typeof max === 'number' && value >= max); + }, [disabled, max, value]); + const updateValue = useCallback((delta: number) => { onChange(clamp(localValue.current + delta, min, max)); }, [onChange]); @@ -72,7 +80,7 @@ const Stepper = ({ className, label, value, unit, step, min, max, disabled, onCh
- { disabled ? '--' : `${value}${unit}` } + { valueLabel }