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 diff --git a/package-lock.json b/package-lock.json index 7f2e0b955..79e66d642 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.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.21", + "version": "5.0.0-beta.23", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", @@ -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..5ed9368ab 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.23", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", @@ -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/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(); + } } }, []); 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 edbb26550..a1472d38e 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -20,7 +20,10 @@ const Discover = ({ urlParams, queryParams }) => { const [inputsModalOpen, openInputsModal, closeInputsModal] = useBinaryState(false); const [addonModalOpen, openAddonModal, closeAddonModal] = useBinaryState(false); const [selectedMetaItemIndex, setSelectedMetaItemIndex] = React.useState(0); + const metasContainerRef = React.useRef(); + const metaPreviewRef = React.useRef(); + React.useEffect(() => { if (discover.catalog?.content.type === 'Loading') { metasContainerRef.current.scrollTop = 0; @@ -75,7 +78,8 @@ const Discover = ({ urlParams, queryParams }) => { } }, []); const metaItemOnClick = React.useCallback((event) => { - if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString()) { + const visible = window.getComputedStyle(metaPreviewRef.current).display !== 'none'; + if (event.currentTarget.dataset.index !== selectedMetaItemIndex.toString() && visible) { event.preventDefault(); event.currentTarget.focus(); } @@ -175,6 +179,7 @@ const Discover = ({ urlParams, 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 900944eb8..6b940e81a 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; - }; -}; +const STREMIO_URL = 'https://www.strem.io'; +const MAX_TRIES = 25; -type CustomJWTPayload = JwtPayload & { - email?: string; -}; +const getCredentials = async (state: string): Promise => { + try { + const response = await fetch(`${STREMIO_URL}/login-apple-get-acc/${state}`); + const { user } = await response.json(); -const CLIENT_ID = 'com.stremio.services'; + return Promise.resolve({ + token: user.token, + sub: user.sub, + email: user.email, + // 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); + 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/routes/Player/Player.js b/src/routes/Player/Player.js index 9c9ae5e92..8ba813786 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,21 @@ 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.replace(deepLinks.player); + } else if (deepLinks.metaDetailsStreams) { + isNavigating.current = true; + window.location.replace(deepLinks.metaDetailsStreams); + } + }, []); + const onEnded = React.useCallback(() => { + if (isNavigating.current) { + return; + } + ended(); if (player.nextVideo !== null) { onNextVideoRequested(); @@ -218,14 +234,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) { @@ -389,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/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) } %
diff --git a/src/routes/Search/styles.less b/src/routes/Search/styles.less index c5b226db9..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: 4rem 2rem; + margin-top: 1rem; + margin-bottom: 2rem; } .search-hints-wrapper { @@ -272,7 +274,7 @@ .search-container { .search-content { .search-row { - margin: 2rem 1rem; + margin-bottom: 1.5rem; } .search-row-poster, .search-row-square { @@ -285,8 +287,10 @@ .search-hints-wrapper { margin-top: 4rem; + .search-hints-container { padding: 4rem 2rem; + .search-hint-container { padding: 0 1.5rem; } diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index c9dc57c08..f1f5272a5 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -66,10 +66,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] + })) + ], selectedOption: { label: languageNames[profile.settings.subtitlesLanguage], value: profile.settings.subtitlesLanguage diff --git a/src/types/global.d.ts b/src/types/global.d.ts index e55146e9a..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 {}; 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,