Merge branch 'development' into feat/replace-multiselect-settings

This commit is contained in:
Botzy 2025-05-22 17:00:30 +03:00
commit 42a55a254d
14 changed files with 131 additions and 203 deletions

View file

@ -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"
]
}
}

14
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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();
}
}
}, []);

View file

@ -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
<div className={styles['logo-placeholder']}>{name}</div>
), [name]);
return (
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })}>
<div className={classnames(className, styles['meta-preview-container'], { [styles['compact']]: compact })} ref={ref}>
{
typeof background === 'string' && background.length > 0 ?
<div className={styles['background-image-layer']}>
@ -261,7 +261,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
</div>
</div>
);
};
});
MetaPreview.Placeholder = MetaPreviewPlaceholder;

View file

@ -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 }) => {
<MetaPreview
className={styles['meta-preview-container']}
compact={true}
ref={metaPreviewRef}
name={selectedMetaItem.name}
logo={selectedMetaItem.logo}
background={selectedMetaItem.poster}

View file

@ -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(() => {

View file

@ -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<AppleLoginResponse> => {
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<AppleLoginResponse>, () => void] => {
const platform = usePlatform();
const started = useRef(false);
const timeout = useRef<NodeJS.Timeout | null>(null);
const start = useCallback((): Promise<AppleLoginResponse> => {
return new Promise((resolve, reject) => {
if (typeof window.AppleID === 'undefined') {
reject(new Error('Apple Sign-In not loaded'));
return;
}
const start = useCallback(() => new Promise<AppleLoginResponse>((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;

View file

@ -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);

View file

@ -33,7 +33,7 @@ const StatisticsMenu = ({ className, peers, speed, completed, infoHash }) => {
Completed
</div>
<div className={styles['value']}>
{ completed } %
{ Math.min(completed, 100) } %
</div>
</div>
</div>

View file

@ -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;
}

View file

@ -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

25
src/types/global.d.ts vendored
View file

@ -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 {};

View file

@ -35,7 +35,7 @@ type Settings = {
subtitlesBackgroundColor: string,
subtitlesBold: boolean,
subtitlesFont: string,
subtitlesLanguage: string,
subtitlesLanguage: string | null,
subtitlesOffset: number,
subtitlesOutlineColor: string,
subtitlesSize: number,