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,