From 6faf70d33edd9c2b73776d113331168ea8a203bd Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 15 Apr 2025 16:30:13 +0300 Subject: [PATCH 01/37] fix(Player): update video event handlers on handlers changes --- src/routes/Player/Player.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9c9ae5e92..5e3552dbc 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -620,7 +620,14 @@ const Player = ({ urlParams, queryParams }) => { video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); video.events.off('implementationChanged', onImplementationChanged); }; - }, []); + }, [ + onError, + onEnded, + onSubtitlesTrackLoaded, + onExtraSubtitlesTrackLoaded, + onExtraSubtitlesTrackAdded, + onImplementationChanged + ]); React.useLayoutEffect(() => { return () => { From ad4df3bac53b617a1ced5874fd65d80d7f7d21fb Mon Sep 17 00:00:00 2001 From: AlvinHV Date: Thu, 17 Apr 2025 01:15:10 +0400 Subject: [PATCH 02/37] fix: use wildcard for app site association --- .well-known/apple-app-site-association | 89 +++++++------------------- 1 file changed, 24 insertions(+), 65 deletions(-) diff --git a/.well-known/apple-app-site-association b/.well-known/apple-app-site-association index 54b0dd1bf..296ae88ad 100644 --- a/.well-known/apple-app-site-association +++ b/.well-known/apple-app-site-association @@ -1,67 +1,26 @@ { - "applinks": { - "apps": [], - "details": [ - { - "appID": "9EWRZ4QP3J.com.stremio.one", - "paths": [ - "/", - "/#/player/*", - "/#/discover/*", - "/#/detail/*", - "/#/library/*", - "/#/addons/*", - "/#/settings", - "/#/search/*" - ], - "components": [ - { - "/": "/", - "#": "/player/*", - "comment": "Matches deep link for player" - }, - { - "/": "/", - "#": "/discover/*", - "comment": "Matches deep link for discover" - }, - { - "/": "/", - "#": "/detail/*", - "comment": "Matches deep link for detail" - }, - { - "/": "/", - "#": "/library/*", - "comment": "Matches deep link for library" - }, - { - "/": "/", - "#": "/addons/*", - "comment": "Matches deep link for addons" - }, - { - "/": "/", - "#": "/settings", - "comment": "Matches deep link for settings" - }, - { - "/": "/", - "#": "/search/*", - "comment": "Matches deep link for search" - } - ] - } + "applinks": { + "apps": [], + "details": [ + { + "appIDs": [ + "9EWRZ4QP3J.com.stremio.one" + ], + "appID": "9EWRZ4QP3J.com.stremio.one", + "paths": [ + "*" ] - }, - "activitycontinuation": { - "apps": [ - "9EWRZ4QP3J.com.stremio.one" - ] - }, - "webcredentials": { - "apps": [ - "9EWRZ4QP3J.com.stremio.one" - ] - } -} + } + ] + }, + "activitycontinuation": { + "apps": [ + "9EWRZ4QP3J.com.stremio.one" + ] + }, + "webcredentials": { + "apps": [ + "9EWRZ4QP3J.com.stremio.one" + ] + } +} \ No newline at end of file From fb7c5642b0272c7de48303b1c02e27db785ca506 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 24 Apr 2025 13:04:12 +0300 Subject: [PATCH 03/37] test: do not use popup --- src/routes/Intro/useAppleLogin.ts | 2 +- src/types/global.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 900944eb8..176387a30 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -49,7 +49,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { scope: 'name email', redirectURI: 'https://web.stremio.com/', state: 'signin', - usePopup: true, + // usePopup: true, }); window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index e55146e9a..78152ec45 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -34,7 +34,7 @@ declare global { scope: string; redirectURI: string; state: string; - usePopup: boolean; + // usePopup: boolean; }) => void; signIn: () => Promise<{ authorization: { From 63624a9554a17035004ff230aeccdacda01a13ea Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 24 Apr 2025 15:59:48 +0300 Subject: [PATCH 04/37] refactor(useapplelogin): use w/out popup instead --- package-lock.json | 10 --- package.json | 1 - src/routes/Intro/Intro.js | 8 +- src/routes/Intro/useAppleLogin.ts | 123 ++++++++++++------------------ src/types/global.d.ts | 25 ------ 5 files changed, 52 insertions(+), 115 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f2e0b955..3deeaa24f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "filter-invalid-dom-props": "3.0.1", "hat": "^0.0.3", "i18next": "^24.0.5", - "jwt-decode": "^4.0.0", "langs": "github:Stremio/nodejs-langs", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", @@ -10235,15 +10234,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/package.json b/package.json index c12a0b7e8..9322aae83 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "filter-invalid-dom-props": "3.0.1", "hat": "^0.0.3", "i18next": "^24.0.5", - "jwt-decode": "^4.0.0", "langs": "github:Stremio/nodejs-langs", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 716fe0f0d..6af8f0167 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -112,7 +112,7 @@ const Intro = ({ queryParams }) => { const loginWithApple = React.useCallback(() => { openLoaderModal(); startAppleLogin() - .then(({ email, token, sub, name }) => { + .then(({ token, sub, email, name }) => { core.transport.dispatch({ action: 'Ctx', args: { @@ -129,11 +129,7 @@ const Intro = ({ queryParams }) => { }) .catch((error) => { closeLoaderModal(); - if (error.error === 'popup_closed_by_user') { - dispatch({ type: 'error', error: 'Apple login popup was closed.' }); - } else { - dispatch({ type: 'error', error: error.error }); - } + dispatch({ type: 'error', error: error.message }); }); }, []); const cancelLoginWithApple = React.useCallback(() => { diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 176387a30..f3b5fc5af 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -1,5 +1,8 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + import { useCallback, useEffect, useRef } from 'react'; -import { jwtDecode, JwtPayload } from 'jwt-decode'; +import { usePlatform } from 'stremio/common'; +import hat from 'hat'; type AppleLoginResponse = { token: string; @@ -8,97 +11,71 @@ type AppleLoginResponse = { name: string; }; -type AppleSignInResponse = { - authorization: { - code?: string; - id_token: string; - state?: string; - }; - email?: string; - fullName?: { - firstName?: string; - lastName?: string; - }; -}; -type CustomJWTPayload = JwtPayload & { - email?: string; -}; +const STREMIO_URL = 'http://localhost:3001'; +const MAX_TRIES = 25; -const CLIENT_ID = 'com.stremio.services'; +const getCredentials = async (state: string): Promise => { + try { + const response = await fetch(`${STREMIO_URL}/login-apple-get-acc/${state}`); + const { user } = await response.json(); + + return Promise.resolve({ + token: user.token, + sub: user.sub, + email: user.email, + name: user.name + }); + } catch (e) { + console.error('Failed to get credentials from Apple auth', e); + return Promise.reject(e); + } +}; const useAppleLogin = (): [() => Promise, () => void] => { + const platform = usePlatform(); const started = useRef(false); + const timeout = useRef(null); - const start = useCallback((): Promise => { - return new Promise((resolve, reject) => { - if (typeof window.AppleID === 'undefined') { - reject(new Error('Apple Sign-In not loaded')); - return; - } + const start = useCallback(() => new Promise((resolve, reject) => { + started.current = true; + const state = hat(128); + let tries = 0; + platform.openExternal(`${STREMIO_URL}/login-apple/${state}`); + + const waitForCredentials = () => { if (started.current) { - reject(new Error('Apple login already in progress')); - return; + timeout.current && clearTimeout(timeout.current); + timeout.current = setTimeout(() => { + if (tries >= MAX_TRIES) + return reject(new Error('Failed to authenticate with Apple')); + + tries++; + + getCredentials(state) + .then(resolve) + .catch(waitForCredentials); + }, 2000); } + }; - started.current = true; - - window.AppleID.auth.init({ - clientId: CLIENT_ID, - scope: 'name email', - redirectURI: 'https://web.stremio.com/', - state: 'signin', - // usePopup: true, - }); - - window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { - if (response.authorization) { - try { - const idToken = response.authorization.id_token; - const payload: CustomJWTPayload = jwtDecode(idToken); - const sub = payload.sub; - const email = payload.email ?? response.email ?? ''; - - let name = ''; - if (response.fullName) { - const firstName = response.fullName.firstName || ''; - const lastName = response.fullName.lastName || ''; - name = [firstName, lastName].filter(Boolean).join(' '); - } - - if (!sub) { - reject(new Error('No sub token received from Apple')); - return; - } - - resolve({ - token: idToken, - sub: sub, - email: email, - name: name, - }); - } catch (error) { - reject(new Error(`Failed to parse id_token: ${error}`)); - } - } else { - reject(new Error('No authorization received from Apple')); - } - }).catch((error) => { - reject(error); - }); - }); - }, []); + waitForCredentials(); + }), []); const stop = useCallback(() => { started.current = false; + timeout.current && clearTimeout(timeout.current); }, []); useEffect(() => { return () => stop(); }, []); - return [start, stop]; + return [ + start, + stop, + ]; }; export default useAppleLogin; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 78152ec45..3849b8914 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -26,31 +26,6 @@ interface Chrome { declare global { var qt: Qt | undefined; var chrome: Chrome | undefined; - interface Window { - AppleID: { - auth: { - init: (config: { - clientId: string; - scope: string; - redirectURI: string; - state: string; - // usePopup: boolean; - }) => void; - signIn: () => Promise<{ - authorization: { - code?: string; - id_token: string; - state?: string; - }; - email?: string; - fullName?: { - firstName?: string; - lastName?: string; - }; - }>; - }; - }; - } } export {}; From d09c760a1c8026d154c41d4b2ff9f4d87e19c0cb Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 24 Apr 2025 16:01:44 +0300 Subject: [PATCH 05/37] fix(useapplelogin ): lint --- src/routes/Intro/useAppleLogin.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index f3b5fc5af..b2f914f37 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -11,7 +11,6 @@ type AppleLoginResponse = { name: string; }; - const STREMIO_URL = 'http://localhost:3001'; const MAX_TRIES = 25; From 1568ba1bb26e3af8b185633cbfaf492b86078df9 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 24 Apr 2025 16:49:01 +0300 Subject: [PATCH 06/37] refactor(useapplelogin): use correct url --- src/routes/Intro/useAppleLogin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index b2f914f37..c5ec99625 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -11,7 +11,7 @@ type AppleLoginResponse = { name: string; }; -const STREMIO_URL = 'http://localhost:3001'; +const STREMIO_URL = 'https://www.strem.io'; const MAX_TRIES = 25; const getCredentials = async (state: string): Promise => { From 22ba03dea274a9726d15c9c55379869b1cc5feaf Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 25 Apr 2025 14:10:01 +0300 Subject: [PATCH 07/37] refactor(useapplelogin): fallback in case no name --- src/routes/Intro/useAppleLogin.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index c5ec99625..6b940e81a 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -23,7 +23,8 @@ const getCredentials = async (state: string): Promise => { token: user.token, sub: user.sub, email: user.email, - name: user.name + // We might not receive a name from Apple, so we use an empty string as a fallback + name: user.name ?? '', }); } catch (e) { console.error('Failed to get credentials from Apple auth', e); From 242a4a811097f4a5753fa921809358fbb62ef748 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 28 Apr 2025 16:55:22 +0300 Subject: [PATCH 08/37] chore(bump): v5.0.0-beta.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3deeaa24f..c938759ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.21", + "version": "5.0.0-beta.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.21", + "version": "5.0.0-beta.22", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", diff --git a/package.json b/package.json index 9322aae83..0a431dfec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.21", + "version": "5.0.0-beta.22", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", From 5b56c58e5b0edd1133cbab2ce68439eca30cb533 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 21:56:37 +0300 Subject: [PATCH 09/37] fix(player): redirect to MetaDetails --- src/routes/Player/Player.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 5e3552dbc..638a7162b 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -219,7 +219,6 @@ const Player = ({ urlParams, queryParams }) => { const deepLinks = player.nextVideo.deepLinks; if (deepLinks.metaDetailsStreams && deepLinks.player) { - window.location.replace(deepLinks.metaDetailsStreams); window.location.href = deepLinks.player; } else { window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams); From 6bfe079030f366e7943cf5ea08dd8c2a3d04eed0 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 22:23:14 +0300 Subject: [PATCH 10/37] test(player): use replace instead of href --- src/routes/Player/Player.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 638a7162b..f04c960f4 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -219,7 +219,7 @@ const Player = ({ urlParams, queryParams }) => { const deepLinks = player.nextVideo.deepLinks; if (deepLinks.metaDetailsStreams && deepLinks.player) { - window.location.href = deepLinks.player; + window.location.replace(deepLinks.player); } else { window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams); } @@ -620,11 +620,7 @@ const Player = ({ urlParams, queryParams }) => { video.events.off('implementationChanged', onImplementationChanged); }; }, [ - onError, onEnded, - onSubtitlesTrackLoaded, - onExtraSubtitlesTrackLoaded, - onExtraSubtitlesTrackAdded, onImplementationChanged ]); From 0efd1453bb6697d1964ceaf172c7f5275b96abe2 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 22:41:04 +0300 Subject: [PATCH 11/37] test(player): fix binge watching (2) --- src/routes/Player/Player.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index f04c960f4..d26248452 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -216,12 +216,12 @@ const Player = ({ urlParams, queryParams }) => { const onNextVideoRequested = React.useCallback(() => { if (player.nextVideo !== null) { nextVideo(); - const deepLinks = player.nextVideo.deepLinks; - if (deepLinks.metaDetailsStreams && deepLinks.player) { + + if (deepLinks.player) { window.location.replace(deepLinks.player); - } else { - window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams); + } else if (deepLinks.metaDetailsStreams) { + window.location.replace(deepLinks.metaDetailsStreams); } } }, [player.nextVideo]); From 980d0038ece0bf09635a84199fabc2995feb9528 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 22:52:39 +0300 Subject: [PATCH 12/37] test(player): fix binge watching (3) --- src/routes/Player/Player.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index d26248452..f7d918696 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -215,14 +215,19 @@ const Player = ({ urlParams, queryParams }) => { const onNextVideoRequested = React.useCallback(() => { if (player.nextVideo !== null) { - nextVideo(); const deepLinks = player.nextVideo.deepLinks; - - if (deepLinks.player) { - window.location.replace(deepLinks.player); - } else if (deepLinks.metaDetailsStreams) { - window.location.replace(deepLinks.metaDetailsStreams); - } + const navigateToPlayer = deepLinks.player ? deepLinks.player : null; + const navigateToDetails = deepLinks.metaDetailsStreams ? deepLinks.metaDetailsStreams : null; + + nextVideo(); + + requestAnimationFrame(() => { + if (navigateToPlayer) { + window.location.replace(navigateToPlayer); + } else if (navigateToDetails) { + window.location.replace(navigateToDetails); + } + }); } }, [player.nextVideo]); From 7924200daba892195131f2b7ad0ce43d0a7db3c1 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 23:01:02 +0300 Subject: [PATCH 13/37] chore(player): lint --- src/routes/Player/Player.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index f7d918696..251cabe1e 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -218,9 +218,9 @@ const Player = ({ urlParams, queryParams }) => { const deepLinks = player.nextVideo.deepLinks; const navigateToPlayer = deepLinks.player ? deepLinks.player : null; const navigateToDetails = deepLinks.metaDetailsStreams ? deepLinks.metaDetailsStreams : null; - + nextVideo(); - + requestAnimationFrame(() => { if (navigateToPlayer) { window.location.replace(navigateToPlayer); From 18ac3583b4e80d1cb200f54aefc7b472517f7174 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 23:07:00 +0300 Subject: [PATCH 14/37] chore(Player): lint (2) --- src/routes/Player/Player.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 251cabe1e..496c39878 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -218,9 +218,9 @@ const Player = ({ urlParams, queryParams }) => { const deepLinks = player.nextVideo.deepLinks; const navigateToPlayer = deepLinks.player ? deepLinks.player : null; const navigateToDetails = deepLinks.metaDetailsStreams ? deepLinks.metaDetailsStreams : null; - + nextVideo(); - + requestAnimationFrame(() => { if (navigateToPlayer) { window.location.replace(navigateToPlayer); From 672a0067cea64f6065f36c0871f010aa8822e9c6 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 23:37:44 +0300 Subject: [PATCH 15/37] test(Player): fix binge watching --- src/routes/Player/Player.js | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 496c39878..e47a2179c 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -102,6 +102,7 @@ const Player = ({ urlParams, queryParams }) => { }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); const onEnded = React.useCallback(() => { + console.log('Player in on ended callback', player.nextVideo); // eslint-disable-line no-console ended(); if (player.nextVideo !== null) { onNextVideoRequested(); @@ -215,19 +216,26 @@ const Player = ({ urlParams, queryParams }) => { const onNextVideoRequested = React.useCallback(() => { if (player.nextVideo !== null) { - const deepLinks = player.nextVideo.deepLinks; - const navigateToPlayer = deepLinks.player ? deepLinks.player : null; - const navigateToDetails = deepLinks.metaDetailsStreams ? deepLinks.metaDetailsStreams : null; + const navigationData = { + playerLink: player.nextVideo.deepLinks.player, + metaDetailsLink: player.nextVideo.deepLinks.metaDetailsStreams + }; - nextVideo(); - - requestAnimationFrame(() => { - if (navigateToPlayer) { - window.location.replace(navigateToPlayer); - } else if (navigateToDetails) { - window.location.replace(navigateToDetails); - } - }); + if (navigationData.playerLink) { + requestAnimationFrame(() => { + window.location.replace(navigationData.playerLink); + }); + setTimeout(() => { + nextVideo(); + }, 500); + } else if (navigationData.metaDetailsLink) { + requestAnimationFrame(() => { + window.location.replace(navigationData.metaDetailsLink); + }); + setTimeout(() => { + nextVideo(); + }, 500); + } } }, [player.nextVideo]); @@ -629,6 +637,10 @@ const Player = ({ urlParams, queryParams }) => { onImplementationChanged ]); + React.useEffect(() => { + console.log('Player next video in use effect', player.nextVideo); // eslint-disable-line no-console + }, [player.nextVideo]); + React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); From 9aed64d998ee1163640fec5bd8de6954d0958cc1 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 00:00:15 +0300 Subject: [PATCH 16/37] test(Player): fix binge watching (4) --- src/routes/Player/Player.js | 78 ++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index e47a2179c..2ad9efd17 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -83,6 +83,7 @@ const Player = ({ urlParams, queryParams }) => { return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen; }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]); + const nextVideoHandledRef = React.useRef(false); const nextVideoPopupDismissed = React.useRef(false); const defaultSubtitlesSelected = React.useRef(false); const defaultAudioTrackSelected = React.useRef(false); @@ -215,26 +216,22 @@ const Player = ({ urlParams, queryParams }) => { }, []); const onNextVideoRequested = React.useCallback(() => { - if (player.nextVideo !== null) { + if (player.nextVideo !== null && !nextVideoHandledRef.current) { + nextVideoHandledRef.current = true; + const navigationData = { playerLink: player.nextVideo.deepLinks.player, metaDetailsLink: player.nextVideo.deepLinks.metaDetailsStreams }; - if (navigationData.playerLink) { - requestAnimationFrame(() => { - window.location.replace(navigationData.playerLink); - }); - setTimeout(() => { - nextVideo(); - }, 500); - } else if (navigationData.metaDetailsLink) { - requestAnimationFrame(() => { - window.location.replace(navigationData.metaDetailsLink); - }); - setTimeout(() => { - nextVideo(); - }, 500); + const targetLink = navigationData.playerLink || navigationData.metaDetailsLink; + + if (targetLink) { + nextVideo(); + + window.location.replace(targetLink); + } else { + nextVideo(); } } }, [player.nextVideo]); @@ -430,6 +427,7 @@ const Player = ({ urlParams, queryParams }) => { defaultSubtitlesSelected.current = false; defaultAudioTrackSelected.current = false; nextVideoPopupDismissed.current = false; + nextVideoHandledRef.current = false; }, [video.state.stream]); React.useEffect(() => { @@ -493,6 +491,31 @@ const Player = ({ urlParams, queryParams }) => { } }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); + React.useEffect(() => { + nextVideoHandledRef.current = false; + }, [player.selected]); + + React.useEffect(() => { + video.events.on('error', onError); + video.events.on('ended', onEnded); + video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded); + video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); + video.events.on('implementationChanged', onImplementationChanged); + + return () => { + video.events.off('error', onError); + video.events.off('ended', onEnded); + video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded); + video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); + video.events.off('implementationChanged', onImplementationChanged); + }; + }, [ + onEnded, + onImplementationChanged + ]); + React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { @@ -616,31 +639,6 @@ const Player = ({ urlParams, queryParams }) => { }; }, [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]); - React.useEffect(() => { - video.events.on('error', onError); - video.events.on('ended', onEnded); - video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded); - video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); - video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); - video.events.on('implementationChanged', onImplementationChanged); - - return () => { - video.events.off('error', onError); - video.events.off('ended', onEnded); - video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded); - video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); - video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); - video.events.off('implementationChanged', onImplementationChanged); - }; - }, [ - onEnded, - onImplementationChanged - ]); - - React.useEffect(() => { - console.log('Player next video in use effect', player.nextVideo); // eslint-disable-line no-console - }, [player.nextVideo]); - React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); From 9b405c53d8fffe24616acd3bcfc2c29cd8c98e6b Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 00:22:39 +0300 Subject: [PATCH 17/37] test(player): fix binge watching (5) --- src/routes/Player/Player.js | 60 ++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 2ad9efd17..6fb5b02c8 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -103,7 +103,6 @@ const Player = ({ urlParams, queryParams }) => { }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); const onEnded = React.useCallback(() => { - console.log('Player in on ended callback', player.nextVideo); // eslint-disable-line no-console ended(); if (player.nextVideo !== null) { onNextVideoRequested(); @@ -216,22 +215,36 @@ const Player = ({ urlParams, queryParams }) => { }, []); const onNextVideoRequested = React.useCallback(() => { - if (player.nextVideo !== null && !nextVideoHandledRef.current) { - nextVideoHandledRef.current = true; + if (player.nextVideo !== null) { + // Call nextVideo only for analytics + nextVideo(); - const navigationData = { - playerLink: player.nextVideo.deepLinks.player, - metaDetailsLink: player.nextVideo.deepLinks.metaDetailsStreams - }; + // Capture navigation data + const navigationLink = player.nextVideo.deepLinks.player || + player.nextVideo.deepLinks.metaDetailsStreams; - const targetLink = navigationData.playerLink || navigationData.metaDetailsLink; + if (navigationLink) { + // Force immediate navigation with no chance of React re-renders affecting it + // This bypasses the React lifecycle entirely + const navigateImmediately = () => { + const form = document.createElement('form'); + form.style.display = 'none'; + form.method = 'GET'; + form.action = navigationLink; - if (targetLink) { - nextVideo(); + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'navigationFromPlayer'; + input.value = 'true'; + form.appendChild(input); - window.location.replace(targetLink); - } else { - nextVideo(); + // Force immediate navigation + document.body.appendChild(form); + form.submit(); + }; + + // Execute immediately + navigateImmediately(); } } }, [player.nextVideo]); @@ -511,10 +524,7 @@ const Player = ({ urlParams, queryParams }) => { video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); video.events.off('implementationChanged', onImplementationChanged); }; - }, [ - onEnded, - onImplementationChanged - ]); + }, [onEnded]); React.useLayoutEffect(() => { const onKeyDown = (event) => { @@ -647,6 +657,22 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); + React.useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + // eslint-disable-next-line + const cameFromPlayer = urlParams.get('navigationFromPlayer'); + + if (cameFromPlayer === 'true') { + // eslint-disable-next-line + urlParams.delete('navigationFromPlayer'); + const newUrl = window.location.pathname + + (urlParams.toString() ? '?' + urlParams.toString() : '') + + window.location.hash; + + window.history.replaceState({}, '', newUrl); + } + }, []); + return (
Date: Fri, 2 May 2025 00:53:42 +0300 Subject: [PATCH 18/37] test(Player): fix binge watching (6) --- src/routes/Player/Player.js | 53 +++++++------------------------------ 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 6fb5b02c8..285f54a28 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -85,6 +85,8 @@ const Player = ({ urlParams, queryParams }) => { const nextVideoHandledRef = React.useRef(false); const nextVideoPopupDismissed = React.useRef(false); + const nextVideoInitialData = React.useRef(player.nextVideo); + nextVideoInitialData.current = player.nextVideo; const defaultSubtitlesSelected = React.useRef(false); const defaultAudioTrackSelected = React.useRef(false); const [error, setError] = React.useState(null); @@ -103,6 +105,7 @@ const Player = ({ urlParams, queryParams }) => { }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); const onEnded = React.useCallback(() => { + player.nextVideo = nextVideoInitialData.current; ended(); if (player.nextVideo !== null) { onNextVideoRequested(); @@ -216,35 +219,13 @@ const Player = ({ urlParams, queryParams }) => { const onNextVideoRequested = React.useCallback(() => { if (player.nextVideo !== null) { - // Call nextVideo only for analytics nextVideo(); - - // Capture navigation data - const navigationLink = player.nextVideo.deepLinks.player || - player.nextVideo.deepLinks.metaDetailsStreams; - - if (navigationLink) { - // Force immediate navigation with no chance of React re-renders affecting it - // This bypasses the React lifecycle entirely - const navigateImmediately = () => { - const form = document.createElement('form'); - form.style.display = 'none'; - form.method = 'GET'; - form.action = navigationLink; - - const input = document.createElement('input'); - input.type = 'hidden'; - input.name = 'navigationFromPlayer'; - input.value = 'true'; - form.appendChild(input); - - // Force immediate navigation - document.body.appendChild(form); - form.submit(); - }; - - // Execute immediately - navigateImmediately(); + const deepLinks = player.nextVideo.deepLinks; + if (deepLinks.metaDetailsStreams && deepLinks.player) { + window.location.replace(deepLinks.metaDetailsStreams); + window.location.href = deepLinks.player; + } else { + window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams); } } }, [player.nextVideo]); @@ -657,22 +638,6 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); - React.useEffect(() => { - const urlParams = new URLSearchParams(window.location.search); - // eslint-disable-next-line - const cameFromPlayer = urlParams.get('navigationFromPlayer'); - - if (cameFromPlayer === 'true') { - // eslint-disable-next-line - urlParams.delete('navigationFromPlayer'); - const newUrl = window.location.pathname + - (urlParams.toString() ? '?' + urlParams.toString() : '') + - window.location.hash; - - window.history.replaceState({}, '', newUrl); - } - }, []); - return (
Date: Fri, 2 May 2025 01:14:48 +0300 Subject: [PATCH 19/37] Update Player.js --- src/routes/Player/Player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 285f54a28..307ae2302 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -505,7 +505,7 @@ const Player = ({ urlParams, queryParams }) => { video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); video.events.off('implementationChanged', onImplementationChanged); }; - }, [onEnded]); + }, []); React.useLayoutEffect(() => { const onKeyDown = (event) => { From d6372c4f86fb7d22ac8fcd343b97e1e583ca31b0 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 01:30:58 +0300 Subject: [PATCH 20/37] test: another test --- src/routes/Player/Player.js | 43 ++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 307ae2302..9ec495907 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -83,7 +83,6 @@ const Player = ({ urlParams, queryParams }) => { return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen; }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]); - const nextVideoHandledRef = React.useRef(false); const nextVideoPopupDismissed = React.useRef(false); const nextVideoInitialData = React.useRef(player.nextVideo); nextVideoInitialData.current = player.nextVideo; @@ -220,6 +219,7 @@ const Player = ({ urlParams, queryParams }) => { const onNextVideoRequested = React.useCallback(() => { if (player.nextVideo !== null) { nextVideo(); + const deepLinks = player.nextVideo.deepLinks; if (deepLinks.metaDetailsStreams && deepLinks.player) { window.location.replace(deepLinks.metaDetailsStreams); @@ -421,7 +421,6 @@ const Player = ({ urlParams, queryParams }) => { defaultSubtitlesSelected.current = false; defaultAudioTrackSelected.current = false; nextVideoPopupDismissed.current = false; - nextVideoHandledRef.current = false; }, [video.state.stream]); React.useEffect(() => { @@ -485,28 +484,6 @@ const Player = ({ urlParams, queryParams }) => { } }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); - React.useEffect(() => { - nextVideoHandledRef.current = false; - }, [player.selected]); - - React.useEffect(() => { - video.events.on('error', onError); - video.events.on('ended', onEnded); - video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded); - video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); - video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); - video.events.on('implementationChanged', onImplementationChanged); - - return () => { - video.events.off('error', onError); - video.events.off('ended', onEnded); - video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded); - video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); - video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); - video.events.off('implementationChanged', onImplementationChanged); - }; - }, []); - React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { @@ -630,6 +607,24 @@ const Player = ({ urlParams, queryParams }) => { }; }, [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]); + React.useEffect(() => { + video.events.on('error', onError); + video.events.on('ended', onEnded); + video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded); + video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); + video.events.on('implementationChanged', onImplementationChanged); + + return () => { + video.events.off('error', onError); + video.events.off('ended', onEnded); + video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded); + video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded); + video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded); + video.events.off('implementationChanged', onImplementationChanged); + }; + }, []); + React.useLayoutEffect(() => { return () => { setImmersedDebounced.cancel(); From ce0c5da3fd39ba7199c24d92f8beddc5d3983327 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 01:46:13 +0300 Subject: [PATCH 21/37] test: again --- src/routes/Player/Player.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9ec495907..9c9ae5e92 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -84,8 +84,6 @@ const Player = ({ urlParams, queryParams }) => { }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]); const nextVideoPopupDismissed = React.useRef(false); - const nextVideoInitialData = React.useRef(player.nextVideo); - nextVideoInitialData.current = player.nextVideo; const defaultSubtitlesSelected = React.useRef(false); const defaultAudioTrackSelected = React.useRef(false); const [error, setError] = React.useState(null); @@ -104,7 +102,6 @@ const Player = ({ urlParams, queryParams }) => { }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); const onEnded = React.useCallback(() => { - player.nextVideo = nextVideoInitialData.current; ended(); if (player.nextVideo !== null) { onNextVideoRequested(); From f3a7ef5978455d552e1c0968a9d282ea0d82bada Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 19:42:49 +0300 Subject: [PATCH 22/37] fix(Player): binge watching in shell --- src/routes/Player/Player.js | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9c9ae5e92..c9173af40 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -88,6 +88,8 @@ const Player = ({ urlParams, queryParams }) => { const defaultAudioTrackSelected = React.useRef(false); const [error, setError] = React.useState(null); + const isNavigating = React.useRef(false); + const onImplementationChanged = React.useCallback(() => { video.setProp('subtitlesSize', settings.subtitlesSize); video.setProp('subtitlesOffset', settings.subtitlesOffset); @@ -101,7 +103,24 @@ const Player = ({ urlParams, queryParams }) => { video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor); }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]); + const handleNextVideoNavigation = React.useCallback((deepLinks) => { + if (deepLinks.player) { + isNavigating.current = true; + window.location.href = deepLinks.player; + return true; + } else if (deepLinks.metaDetailsStreams) { + isNavigating.current = true; + window.location.href = deepLinks.metaDetailsStreams; + return true; + } + return false; + }, []); + const onEnded = React.useCallback(() => { + if (isNavigating.current) { + return; + } + ended(); if (player.nextVideo !== null) { onNextVideoRequested(); @@ -218,14 +237,9 @@ const Player = ({ urlParams, queryParams }) => { nextVideo(); const deepLinks = player.nextVideo.deepLinks; - if (deepLinks.metaDetailsStreams && deepLinks.player) { - window.location.replace(deepLinks.metaDetailsStreams); - window.location.href = deepLinks.player; - } else { - window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams); - } + handleNextVideoNavigation(deepLinks); } - }, [player.nextVideo]); + }, [player.nextVideo, handleNextVideoNavigation]); const onVideoClick = React.useCallback(() => { if (video.state.paused !== null) { From 3985c88346fef98d57d3e0e88a0c01493f0dcfdb Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 19:44:55 +0300 Subject: [PATCH 23/37] chore(Player): lint --- src/routes/Player/Player.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index c9173af40..24a1b249e 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -123,11 +123,13 @@ const Player = ({ urlParams, queryParams }) => { ended(); if (player.nextVideo !== null) { - onNextVideoRequested(); + nextVideo(); + const deepLinks = player.nextVideo.deepLinks; + handleNextVideoNavigation(deepLinks); } else { window.history.back(); } - }, [player.nextVideo, onNextVideoRequested]); + }, [player.nextVideo, nextVideo, handleNextVideoNavigation]); const onError = React.useCallback((error) => { console.error('Player', error); From ebb15463b48b1ee7e5866d38c83d264de5d851d0 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 19:47:04 +0300 Subject: [PATCH 24/37] chore(Player): lint (2) --- src/routes/Player/Player.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 24a1b249e..8e2b2f615 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -120,7 +120,7 @@ const Player = ({ urlParams, queryParams }) => { if (isNavigating.current) { return; } - + ended(); if (player.nextVideo !== null) { nextVideo(); From 17312f64fd0b596c26e4b0c17d7f042ee64c6e49 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 19:48:04 +0300 Subject: [PATCH 25/37] revert: chore lint 1 --- src/routes/Player/Player.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 8e2b2f615..cb6371986 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -123,13 +123,11 @@ const Player = ({ urlParams, queryParams }) => { ended(); if (player.nextVideo !== null) { - nextVideo(); - const deepLinks = player.nextVideo.deepLinks; - handleNextVideoNavigation(deepLinks); + onNextVideoRequested(); } else { window.history.back(); } - }, [player.nextVideo, nextVideo, handleNextVideoNavigation]); + }, [player.nextVideo, onNextVideoRequested]); const onError = React.useCallback((error) => { console.error('Player', error); From b51791baa015077ded7109ba9c2f6588a8ae8307 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Fri, 2 May 2025 19:55:09 +0300 Subject: [PATCH 26/37] fix(Player): replace history entry - fix for the navigation --- src/routes/Player/Player.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index cb6371986..49395e2ba 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -106,11 +106,11 @@ const Player = ({ urlParams, queryParams }) => { const handleNextVideoNavigation = React.useCallback((deepLinks) => { if (deepLinks.player) { isNavigating.current = true; - window.location.href = deepLinks.player; + window.location.replace(deepLinks.player); return true; } else if (deepLinks.metaDetailsStreams) { isNavigating.current = true; - window.location.href = deepLinks.metaDetailsStreams; + window.location.replace(deepLinks.metaDetailsStreams); return true; } return false; From 4d53952368f8cdd2eddc51b4ef33af941e1141cc Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 5 May 2025 17:51:12 +0200 Subject: [PATCH 27/37] remove(Player): unnecessary returns --- src/routes/Player/Player.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 49395e2ba..740ed46d4 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -107,13 +107,10 @@ const Player = ({ urlParams, queryParams }) => { if (deepLinks.player) { isNavigating.current = true; window.location.replace(deepLinks.player); - return true; } else if (deepLinks.metaDetailsStreams) { isNavigating.current = true; window.location.replace(deepLinks.metaDetailsStreams); - return true; } - return false; }, []); const onEnded = React.useCallback(() => { From f8ab1a7dbce2561822ec867278281268712b20e4 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 12 May 2025 13:45:53 +0300 Subject: [PATCH 28/37] fix: exitFullscreen Signed-off-by: Lachezar Lechev --- src/common/useFullscreen.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index b63fb9dd2..5f0975fb8 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -22,7 +22,9 @@ const useFullscreen = () => { if (shell.active) { shell.send('win-set-visibility', { fullscreen: false }); } else { - document.exitFullscreen(); + if (document.fullscreenElement === document.documentElement) { + document.exitFullscreen(); + } } }, []); From 2dec01923ae8cf75ab8bcc8446bfa19c3594e37e Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 12 May 2025 13:47:09 +0300 Subject: [PATCH 29/37] fix: StatisticsMenu - max of 100% for completed Signed-off-by: Lachezar Lechev --- src/routes/Player/StatisticsMenu/StatisticsMenu.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/StatisticsMenu/StatisticsMenu.js b/src/routes/Player/StatisticsMenu/StatisticsMenu.js index 6bab8ecf5..69ee2bf8d 100644 --- a/src/routes/Player/StatisticsMenu/StatisticsMenu.js +++ b/src/routes/Player/StatisticsMenu/StatisticsMenu.js @@ -33,7 +33,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => { Completed
- { completed } % + { Math.min(completed, 100) } %
From cc105f327c24c957367131b7a9323bf837a647ff Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 13 May 2025 11:52:31 +0300 Subject: [PATCH 30/37] chore: bump v5.0.0-beta.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c938759ed..79e66d642 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.22", + "version": "5.0.0-beta.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.22", + "version": "5.0.0-beta.23", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", diff --git a/package.json b/package.json index 0a431dfec..5ed9368ab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.22", + "version": "5.0.0-beta.23", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", From 878af40c1d1dfdca09d2a63eafed0324440566a4 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 13 May 2025 14:33:35 +0300 Subject: [PATCH 31/37] fix(Search): align styles with board catalogs --- src/routes/Search/styles.less | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/routes/Search/styles.less b/src/routes/Search/styles.less index c5b226db9..066edf9e2 100644 --- a/src/routes/Search/styles.less +++ b/src/routes/Search/styles.less @@ -22,7 +22,7 @@ overflow-y: auto; .search-row { - margin: 4rem 2rem; + margin: 2rem 1rem; } .search-hints-wrapper { @@ -271,10 +271,6 @@ @media only screen and (max-width: @minimum) { .search-container { .search-content { - .search-row { - margin: 2rem 1rem; - } - .search-row-poster, .search-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+4) { From 62f8bb367ff698989a0443f3d9c195482f189947 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 13 May 2025 15:18:30 +0300 Subject: [PATCH 32/37] refactor(Search): align completely to Board --- src/routes/Search/styles.less | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/Search/styles.less b/src/routes/Search/styles.less index 066edf9e2..278d65693 100644 --- a/src/routes/Search/styles.less +++ b/src/routes/Search/styles.less @@ -19,10 +19,12 @@ .search-content { height: 100%; width: 100%; + padding: 0 1rem; overflow-y: auto; .search-row { - margin: 2rem 1rem; + margin-top: 1rem; + margin-bottom: 2rem; } .search-hints-wrapper { @@ -271,6 +273,10 @@ @media only screen and (max-width: @minimum) { .search-container { .search-content { + .search-row { + margin-bottom: 1.5rem; + } + .search-row-poster, .search-row-square { .meta-item, .meta-item-placeholder { &:nth-child(n+4) { @@ -281,8 +287,10 @@ .search-hints-wrapper { margin-top: 4rem; + .search-hints-container { padding: 4rem 2rem; + .search-hint-container { padding: 0 1.5rem; } From 1d8401e4dfba82c0f477cbcb306ea00130773450 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 19 May 2025 10:56:28 +0300 Subject: [PATCH 33/37] feat(Settings): allow disabling subs globally --- src/routes/Player/Player.js | 7 +++++++ src/routes/Settings/useProfileSettingsInputs.js | 11 +++++++---- src/types/models/Ctx.d.ts | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 740ed46d4..8ba813786 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -400,6 +400,13 @@ const Player = ({ urlParams, queryParams }) => { 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); + defaultSubtitlesSelected.current = true; + return; + } + const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index 2a31fc254..4cb021f37 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -65,10 +65,13 @@ const useProfileSettingsInputs = (profile) => { }), [profile.settings]); const subtitlesLanguageSelect = React.useMemo(() => ({ - options: Object.keys(languageNames).map((code) => ({ - value: code, - label: languageNames[code] - })), + options: [ + { value: null, label: t('NONE') }, + ...Object.keys(languageNames).map((code) => ({ + value: code, + label: languageNames[code] + })) + ], selected: [profile.settings.subtitlesLanguage], onSelect: (event) => { core.transport.dispatch({ diff --git a/src/types/models/Ctx.d.ts b/src/types/models/Ctx.d.ts index 47f18749f..e649b305b 100644 --- a/src/types/models/Ctx.d.ts +++ b/src/types/models/Ctx.d.ts @@ -35,7 +35,7 @@ type Settings = { subtitlesBackgroundColor: string, subtitlesBold: boolean, subtitlesFont: string, - subtitlesLanguage: string, + subtitlesLanguage: string | null, subtitlesOffset: number, subtitlesOutlineColor: string, subtitlesSize: number, From 01c5100aaffcf55b44e7abac5aaa3a99102e0241 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 20 May 2025 19:38:37 +0300 Subject: [PATCH 34/37] fix(Discover): enable direct navigation to meta item on mobile instead focus first --- src/routes/Discover/Discover.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index edbb26550..7fcd8e2dd 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { useServices } = require('stremio/services'); -const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common'); +const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender, usePlatform } = require('stremio/common'); const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, Multiselect, ModalDialog } = require('stremio/components'); const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); @@ -15,6 +15,7 @@ const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { const { core } = useServices(); + const platform = usePlatform(); const [discover, loadNextPage] = useDiscover(urlParams, queryParams); const [selectInputs, hasNextPage] = useSelectableInputs(discover); const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); @@ -75,7 +76,7 @@ const Discover = ({ urlParams, queryParams }) => { } }, []); const metaItemOnClick = React.useCallback((event) => { - if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString()) { + if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && !platform.isMobile) { event.preventDefault(); event.currentTarget.focus(); } From 2e72f5af9d6327753fee9aeaa48afcb29bb61dc3 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 21 May 2025 14:10:42 +0300 Subject: [PATCH 35/37] refactor(Discover): check for window innerWitdth instead isMobile to enable direct navigation --- src/routes/Discover/Discover.js | 51 +++++++++++++++++++++------------ src/routes/Discover/styles.less | 8 ------ 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 7fcd8e2dd..59279a4c3 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { useServices } = require('stremio/services'); -const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender, usePlatform } = require('stremio/common'); +const { CONSTANTS, useBinaryState, useOnScrollToBottom, withCoreSuspender } = require('stremio/common'); const { AddonDetailsModal, Button, DelayedRenderer, Image, MainNavBars, MetaItem, MetaPreview, Multiselect, ModalDialog } = require('stremio/components'); const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); @@ -15,13 +15,24 @@ const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { const { core } = useServices(); - const platform = usePlatform(); const [discover, loadNextPage] = useDiscover(urlParams, queryParams); const [selectInputs, hasNextPage] = useSelectableInputs(discover); const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false); const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0); + const [showMetaPreview, setShowMetaPreview] = React.useState(window.innerWidth > 1000); + const metasContainerRef = React.useRef(); + React.useEffect(() => { + const handleResize = () => { + setShowMetaPreview(window.innerWidth > 1000); + }; + window.addEventListener('resize', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + React.useEffect(() => { if (discover.catalog?.content.type === 'Loading') { metasContainerRef.current.scrollTop = 0; @@ -76,7 +87,7 @@ const Discover = ({ urlParams, queryParams }) => { } }, []); const metaItemOnClick = React.useCallback((event) => { - if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && !platform.isMobile) { + if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && showMetaPreview) { event.preventDefault(); event.currentTarget.focus(); } @@ -173,22 +184,24 @@ const Discover = ({ urlParams, queryParams }) => { { selectedMetaItem !== null ? - + showMetaPreview ? + + : null : discover.catalog !== null && discover.catalog.content.type === 'Loading' ?
diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 527238757..c4a38ee9c 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -340,10 +340,6 @@ margin-right: 0; } } - - .meta-preview-container { - display: none; - } } } } @@ -357,10 +353,6 @@ margin-right: 0; } } - - .meta-preview-container { - display: none; - } } } } From fa07709d31d5c5b6a9b01bd710c044931c6350ab Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 22 May 2025 14:59:31 +0300 Subject: [PATCH 36/37] refactor(Discover): use a reference instead --- src/components/MetaPreview/MetaPreview.js | 6 +-- src/routes/Discover/Discover.js | 49 +++++++++-------------- src/routes/Discover/styles.less | 8 ++++ 3 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/components/MetaPreview/MetaPreview.js b/src/components/MetaPreview/MetaPreview.js index c4eb47c0a..c0e9fb165 100644 --- a/src/components/MetaPreview/MetaPreview.js +++ b/src/components/MetaPreview/MetaPreview.js @@ -24,7 +24,7 @@ const ALLOWED_LINK_REDIRECTS = [ routesRegexp.metadetails.regexp ]; -const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }) => { +const MetaPreview = React.forwardRef(({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }, ref) => { const { t } = useTranslation(); const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false); const linksGroups = React.useMemo(() => { @@ -98,7 +98,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
{name}
), [name]); return ( -
+
{ typeof background === 'string' && background.length > 0 ?
@@ -261,7 +261,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
); -}; +}); MetaPreview.Placeholder = MetaPreviewPlaceholder; diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 59279a4c3..a1472d38e 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -20,18 +20,9 @@ const Discover = ({ urlParams, queryParams }) => { const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false); const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0); - const [showMetaPreview, setShowMetaPreview] = React.useState(window.innerWidth > 1000); const metasContainerRef = React.useRef(); - React.useEffect(() => { - const handleResize = () => { - setShowMetaPreview(window.innerWidth > 1000); - }; - window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - }; - }, []); + const metaPreviewRef = React.useRef(); React.useEffect(() => { if (discover.catalog?.content.type === 'Loading') { @@ -87,7 +78,8 @@ const Discover = ({ urlParams, queryParams }) => { } }, []); const metaItemOnClick = React.useCallback((event) => { - if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && showMetaPreview) { + const visible = window.getComputedStyle(metaPreviewRef.current).display !== 'none'; + if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && visible) { event.preventDefault(); event.currentTarget.focus(); } @@ -184,24 +176,23 @@ const Discover = ({ urlParams, queryParams }) => {
{ selectedMetaItem !== null ? - showMetaPreview ? - - : null + : discover.catalog !== null && discover.catalog.content.type === 'Loading' ?
diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index c4a38ee9c..7f445d7ee 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -341,6 +341,10 @@ } } } + + .meta-preview-container { + display: none; + } } } @@ -354,6 +358,10 @@ } } } + + .meta-preview-container { + display: none; + } } } From 7b7c700533444d07d1d74566469e7f789595167c Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 22 May 2025 15:01:21 +0300 Subject: [PATCH 37/37] chore(styles): align styles code --- src/routes/Discover/styles.less | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 7f445d7ee..527238757 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -340,10 +340,10 @@ margin-right: 0; } } - } - .meta-preview-container { - display: none; + .meta-preview-container { + display: none; + } } } } @@ -357,10 +357,10 @@ margin-right: 0; } } - } - .meta-preview-container { - display: none; + .meta-preview-container { + display: none; + } } } }