From e8a6e72b13297bdd77ea1ce3cd877d77fedfbeb1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 17:06:26 +0200 Subject: [PATCH 001/140] feat(NumberInput): added NumberInput common component --- src/components/NumberInput/NumberInput.tsx | 84 ++++++++++++++++++++++ src/components/NumberInput/index.ts | 5 ++ src/components/NumberInput/styles.less | 75 +++++++++++++++++++ src/components/index.ts | 2 + 4 files changed, 166 insertions(+) create mode 100644 src/components/NumberInput/NumberInput.tsx create mode 100644 src/components/NumberInput/index.ts create mode 100644 src/components/NumberInput/styles.less diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx new file mode 100644 index 000000000..fe6b84806 --- /dev/null +++ b/src/components/NumberInput/NumberInput.tsx @@ -0,0 +1,84 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React, { forwardRef, useCallback, useState } from 'react'; +import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; +import classnames from 'classnames'; +import styles from './styles.less'; +import Button from '../Button'; +import Icon from '@stremio/stremio-icons/react'; + +type Props = InputHTMLAttributes & { + containerClassName?: string; + className?: string; + disabled?: boolean; + showButtons?: boolean; + defaultValue?: number; + label?: string; + min?: number; + max?: number; + onKeyDown?: (event: KeyboardEvent) => void; + onSubmit?: (event: KeyboardEvent) => void; +}; + +const NumberInput = forwardRef(({ defaultValue, ...props }, ref) => { + const [value, setValue] = useState(defaultValue || 1); + const onKeyDown = useCallback((event: KeyboardEvent) => { + props.onKeyDown && props.onKeyDown(event); + + if (event.key === 'Enter' ) { + props.onSubmit && props.onSubmit(event); + } + }, [props.onKeyDown, props.onSubmit]); + + const handleIncrease = () => { + const { max } = props; + if (max) { + return setValue((prevVal) => + prevVal + 1 > max ? max : prevVal + 1 + ); + } + setValue((prevVal) => prevVal + 1); + }; + + const handleDecrease = () => { + const { min } = props; + if (min) { + return setValue((prevVal) => + prevVal - 1 < min ? min : prevVal - 1 + ); + } + setValue((prevVal) => prevVal - 1); + }; + + return ( +
+ {props.showButtons ? : null} +
+ {props.label &&
{props.label}
} + setValue(event.value)} + onKeyDown={onKeyDown} + /> +
+ {props.showButtons ? : null} +
+ ); +}); + +NumberInput.displayName = 'NumberInput'; + +export default NumberInput; diff --git a/src/components/NumberInput/index.ts b/src/components/NumberInput/index.ts new file mode 100644 index 000000000..4a25f86df --- /dev/null +++ b/src/components/NumberInput/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import NumberInput from './NumberInput'; + +export default NumberInput; diff --git a/src/components/NumberInput/styles.less b/src/components/NumberInput/styles.less new file mode 100644 index 000000000..0fe8e8c81 --- /dev/null +++ b/src/components/NumberInput/styles.less @@ -0,0 +1,75 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +.number-input { + user-select: text; + display: flex; + max-width: 12rem; + margin-bottom: 1rem; + color: var(--primary-foreground-color); + background: var(--overlay-color); + border-radius: 100rem; + + .btn { + width: 2.875rem; + height: 2.875rem; + background: var(--overlay-color); + border: none; + border-radius: 50%; + color: white; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background 0.3s; + z-index: 1; + + svg { + width: 1.625rem; + } + } + + .number-display { + height: 2.875rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 0 1rem; + + &::-moz-focus-inner { + border: none; + } + + &.with-btns { + padding: 0 1.9375rem; + margin-left: -1.4375rem; + margin-right: -1.4375rem; + max-width: 9.125rem; + } + } + + /* Label */ + .number-display .label { + font-size: 0.625rem; + font-weight: 400; + opacity: 0.7; + } + + /* Value */ + .number-display .value { + font-size: 1.3125rem; + display: flex; + width: 100%; + color: white; + text-align: center; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + -moz-appearance: textfield; + margin: 0; + } + } + + } diff --git a/src/components/index.ts b/src/components/index.ts index f65d66f81..ca3561bcd 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -17,6 +17,7 @@ import ModalDialog from './ModalDialog'; import Multiselect from './Multiselect'; import MultiselectMenu from './MultiselectMenu'; import { HorizontalNavBar, VerticalNavBar } from './NavBar'; +import NumberInput from './NumberInput'; import Popup from './Popup'; import RadioButton from './RadioButton'; import SearchBar from './SearchBar'; @@ -48,6 +49,7 @@ export { MultiselectMenu, HorizontalNavBar, VerticalNavBar, + NumberInput, Popup, RadioButton, SearchBar, From 15c6a231a6d0fc1ceff4423aba30a98028c32371 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 17:27:53 +0200 Subject: [PATCH 002/140] feat(EpisodePicker): added season and episode picker when no streams loaded --- src/routes/MetaDetails/MetaDetails.js | 15 +++++++- .../EpisodePicker/EpisodePicker.less | 19 ++++++++++ .../EpisodePicker/EpisodePicker.tsx | 35 +++++++++++++++++++ .../StreamsList/EpisodePicker/index.ts | 7 ++++ .../MetaDetails/StreamsList/StreamsList.js | 13 +++++-- 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less create mode 100644 src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx create mode 100644 src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index 24c904cfd..a8338df53 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -76,6 +76,18 @@ const MetaDetails = ({ urlParams, queryParams }) => { const seasonOnSelect = React.useCallback((event) => { setSeason(event.value); }, [setSeason]); + const handleEpisodeSearch = React.useCallback((season, episode) => { + const searchVideoId = streamPath.id.replace(/(:\d+:\d+)$/, `:${season}:${episode}`); + const videoFound = metaDetails.metaItem.content.content.videos.find((video) => video.id === searchVideoId); + if (videoFound) { + if (typeof videoFound.deepLinks.player === 'string') { + window.location = videoFound.deepLinks.player; + } else if (typeof videoFound.deepLinks.metaDetailsStreams === 'string') { + window.location.replace(videoFound.deepLinks.metaDetailsStreams); + } + } + }, [streamPath, metaDetails.metaItem]); + const renderBackgroundImageFallback = React.useCallback(() => null, []); const renderBackground = React.useMemo(() => !!( metaPath && @@ -129,7 +141,7 @@ const MetaDetails = ({ urlParams, queryParams }) => { metaDetails.metaItem === null ?
{' -
No addons ware requested for this meta!
+
No addons were requested for this meta!
: metaDetails.metaItem.content.type === 'Err' ? @@ -169,6 +181,7 @@ const MetaDetails = ({ urlParams, queryParams }) => { className={styles['streams-list']} streams={metaDetails.streams} video={video} + onEpisodeSearch={handleEpisodeSearch} /> : metaPath !== null ? diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less new file mode 100644 index 000000000..879cff167 --- /dev/null +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less @@ -0,0 +1,19 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +.button-container { + display: flex; + align-items: center; + justify-content: center; + padding: 1rem 2.625rem; + margin: 2rem 0; + background-color: var(--primary-accent-color); + transition: 0.3s all ease-in-out; + border-radius: 2.625rem; + color: var(--primary-foreground-color); + border: 2px solid transparent; + + &:hover, &:focus { + outline: var(--focus-outline-size) solid var(--primary-foreground-color); + background-color: transparent; + } +} \ No newline at end of file diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx new file mode 100644 index 000000000..b6c6e9fe0 --- /dev/null +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -0,0 +1,35 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React, { useRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, NumberInput } from 'stremio/components'; +import styles from './EpisodePicker.less'; + +type Props = { + className?: string, + seriesId: string; + onSubmit: (season: number, episode: number) => void; +}; +export const EpisodePicker = ({ className, seriesId, onSubmit }: Props) => { + const { t } = useTranslation(); + const [initialSeason, initialEpisode] = React.useMemo(() => { + const [, season, episode] = seriesId ? seriesId.split(':') : []; + return [parseInt(season || '1'), parseInt(episode || '1')]; + }, [seriesId]); + const seasonRef = useRef(null); + const episodeRef = useRef(null); + + const handleSubmit = React.useCallback(() => { + const season = seasonRef.current?.value; + const episode = episodeRef.current?.value; + if (typeof onSubmit === 'function') onSubmit(season, episode); + }, [onSubmit, seasonRef, episodeRef]); + + return
+ + + +
; +}; + +export default EpisodePicker; diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts b/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts new file mode 100644 index 000000000..fca5cad2e --- /dev/null +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts @@ -0,0 +1,7 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import SeasonEpisodePicker from './EpisodePicker'; + +export default { + SeasonEpisodePicker +}; diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index bcb5cb015..2353a4268 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -10,10 +10,11 @@ const { useServices } = require('stremio/services'); const Stream = require('./Stream'); const styles = require('./styles'); const { usePlatform, useProfile } = require('stremio/common'); +const { SeasonEpisodePicker } = require('./EpisodePicker'); const ALL_ADDONS_KEY = 'ALL'; -const StreamsList = ({ className, video, ...props }) => { +const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); @@ -93,6 +94,11 @@ const StreamsList = ({ className, video, ...props }) => { onSelect: onAddonSelected }; }, [streamsByAddon, selectedAddon]); + + const handleEpisodePicker = React.useCallback((season, episode) => { + onEpisodeSearch(season, episode); + }, [onEpisodeSearch]); + return (
@@ -122,12 +128,14 @@ const StreamsList = ({ className, video, ...props }) => { { props.streams.length === 0 ?
+ {'
No addons were requested for streams!
: props.streams.every((streams) => streams.content.type === 'Err') ?
+ {'
{t('NO_STREAM')}
{ @@ -193,7 +201,8 @@ const StreamsList = ({ className, video, ...props }) => { StreamsList.propTypes = { className: PropTypes.string, streams: PropTypes.arrayOf(PropTypes.object).isRequired, - video: PropTypes.object + video: PropTypes.object, + onEpisodeSearch: PropTypes.func }; module.exports = StreamsList; From 461c9d3d53a92e9a223c72f748d2cde75ec5e703 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 17:49:27 +0200 Subject: [PATCH 003/140] fix(EpisodePicker): fix export --- src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts b/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts index fca5cad2e..e71d7147d 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts @@ -2,6 +2,6 @@ import SeasonEpisodePicker from './EpisodePicker'; -export default { +export { SeasonEpisodePicker }; From 39f168a34c0cf57d1bb76afe465ffb626cc83b89 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 18:22:24 +0200 Subject: [PATCH 004/140] fix(EpisodePicker): handle season 0 as value --- src/components/NumberInput/NumberInput.tsx | 2 +- .../MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index fe6b84806..391017fc1 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -21,7 +21,7 @@ type Props = InputHTMLAttributes & { }; const NumberInput = forwardRef(({ defaultValue, ...props }, ref) => { - const [value, setValue] = useState(defaultValue || 1); + const [value, setValue] = useState(defaultValue || 0); const onKeyDown = useCallback((event: KeyboardEvent) => { props.onKeyDown && props.onKeyDown(event); diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index b6c6e9fe0..67ceba055 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -14,7 +14,9 @@ export const EpisodePicker = ({ className, seriesId, onSubmit }: Props) => { const { t } = useTranslation(); const [initialSeason, initialEpisode] = React.useMemo(() => { const [, season, episode] = seriesId ? seriesId.split(':') : []; - return [parseInt(season || '1'), parseInt(episode || '1')]; + const initialSeason = isNaN(parseInt(season)) ? 1 : parseInt(season); + const initialEpisode = isNaN(parseInt(episode)) ? 1 : parseInt(episode); + return [initialSeason, initialEpisode]; }, [seriesId]); const seasonRef = useRef(null); const episodeRef = useRef(null); @@ -26,7 +28,7 @@ export const EpisodePicker = ({ className, seriesId, onSubmit }: Props) => { }, [onSubmit, seasonRef, episodeRef]); return
- +
; From 538e462b12de94fc7d1e74a91e5b13c8e612fcb8 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 18:23:28 +0200 Subject: [PATCH 005/140] fix(StreamsList): hide Install addons button if episode is upcoming --- src/routes/MetaDetails/StreamsList/StreamsList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 2353a4268..cb2b437d0 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -26,8 +26,8 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { setSelectedAddon(event.value); }, [platform]); const showInstallAddonsButton = React.useMemo(() => { - return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true; - }, [profile]); + return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true && !video?.upcoming; + }, [profile, video]); const backButtonOnClick = React.useCallback(() => { if (video.deepLinks && typeof video.deepLinks.metaDetailsVideos === 'string') { window.location.replace(video.deepLinks.metaDetailsVideos + ( From 36a2896525cac77fee0d85090ccbb2fa522a8a06 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 19:13:23 +0200 Subject: [PATCH 006/140] fix(NumberInput): use color variable for font color --- src/components/NumberInput/styles.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/NumberInput/styles.less b/src/components/NumberInput/styles.less index 0fe8e8c81..484f3661a 100644 --- a/src/components/NumberInput/styles.less +++ b/src/components/NumberInput/styles.less @@ -15,7 +15,7 @@ background: var(--overlay-color); border: none; border-radius: 50%; - color: white; + color: var(--primary-foreground-color); display: flex; align-items: center; justify-content: center; @@ -61,7 +61,7 @@ font-size: 1.3125rem; display: flex; width: 100%; - color: white; + color: var(--primary-foreground-color); text-align: center; &::-webkit-outer-spin-button, From 7fa4f462c5e8183b9e6c3e6f84488958bda3e632 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 7 Feb 2025 19:23:33 +0200 Subject: [PATCH 007/140] feat(StreamsList): added upcoming label when no results --- src/routes/MetaDetails/StreamsList/StreamsList.js | 1 + src/routes/MetaDetails/StreamsList/styles.less | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index cb2b437d0..0ccb05e28 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -136,6 +136,7 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { props.streams.every((streams) => streams.content.type === 'Err') ?
+ {video?.upcoming &&
{t('UPCOMING')}...
} {'
{t('NO_STREAM')}
{ diff --git a/src/routes/MetaDetails/StreamsList/styles.less b/src/routes/MetaDetails/StreamsList/styles.less index 0f9ab2a0a..8c274c761 100644 --- a/src/routes/MetaDetails/StreamsList/styles.less +++ b/src/routes/MetaDetails/StreamsList/styles.less @@ -38,6 +38,7 @@ font-size: 1.4rem; text-align: center; color: var(--primary-foreground-color); + margin-bottom: 2rem; } } @@ -171,6 +172,7 @@ max-height: 3.6em; text-align: center; color: var(--primary-foreground-color); + margin-bottom: 0; } } } From 5e983558967f0b981bc2b8477355f49e0c86aab2 Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 12:37:40 +0200 Subject: [PATCH 008/140] fix(NumberInput): fix check for min and max values --- src/components/NumberInput/NumberInput.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 391017fc1..874019162 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -32,7 +32,7 @@ const NumberInput = forwardRef(({ defaultValue, ...prop const handleIncrease = () => { const { max } = props; - if (max) { + if (typeof max !== 'undefined') { return setValue((prevVal) => prevVal + 1 > max ? max : prevVal + 1 ); @@ -42,7 +42,7 @@ const NumberInput = forwardRef(({ defaultValue, ...prop const handleDecrease = () => { const { min } = props; - if (min) { + if (typeof min !== 'undefined') { return setValue((prevVal) => prevVal - 1 < min ? min : prevVal - 1 ); @@ -55,7 +55,7 @@ const NumberInput = forwardRef(({ defaultValue, ...prop {props.showButtons ? : null}
@@ -72,7 +72,7 @@ const NumberInput = forwardRef(({ defaultValue, ...prop />
{props.showButtons ? : null}
From 6b30b90893384d31310cdc0e5d80eb13a374cccd Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 13:31:44 +0200 Subject: [PATCH 009/140] fix(MetaDetails): handle search for any episode and season --- src/routes/MetaDetails/MetaDetails.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index a8338df53..e2c89bb74 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -77,16 +77,11 @@ const MetaDetails = ({ urlParams, queryParams }) => { setSeason(event.value); }, [setSeason]); const handleEpisodeSearch = React.useCallback((season, episode) => { - const searchVideoId = streamPath.id.replace(/(:\d+:\d+)$/, `:${season}:${episode}`); - const videoFound = metaDetails.metaItem.content.content.videos.find((video) => video.id === searchVideoId); - if (videoFound) { - if (typeof videoFound.deepLinks.player === 'string') { - window.location = videoFound.deepLinks.player; - } else if (typeof videoFound.deepLinks.metaDetailsStreams === 'string') { - window.location.replace(videoFound.deepLinks.metaDetailsStreams); - } - } - }, [streamPath, metaDetails.metaItem]); + const searchVideoHash = encodeURIComponent(`${urlParams.id}:${season}:${episode}`); + const url = window.location.hash; + const searchVideoPath = url.replace(encodeURIComponent(urlParams.videoId), searchVideoHash); + window.location = searchVideoPath; + }, [urlParams, window.location]); const renderBackgroundImageFallback = React.useCallback(() => null, []); const renderBackground = React.useMemo(() => !!( From 34808d6014826e8318e00c14bc8e6bc247bb0c15 Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 14:18:13 +0200 Subject: [PATCH 010/140] fix(EpisodePicker): set season and episode from url --- src/components/NumberInput/NumberInput.tsx | 8 +++---- .../EpisodePicker/EpisodePicker.tsx | 22 ++++++++++--------- .../MetaDetails/StreamsList/StreamsList.js | 4 ++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 874019162..37daf6401 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React, { forwardRef, useCallback, useState } from 'react'; +import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react'; import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import styles from './styles.less'; @@ -55,7 +55,7 @@ const NumberInput = forwardRef(({ defaultValue, ...prop {props.showButtons ? : null}
@@ -67,12 +67,12 @@ const NumberInput = forwardRef(({ defaultValue, ...prop value={value} {...props} className={classnames(props.className, styles['value'], { 'disabled': props.disabled })} - onChange={(event) => setValue(event.value)} + onChange={(event: ChangeEvent) => setValue(parseInt(event.target.value))} onKeyDown={onKeyDown} />
{props.showButtons ? : null}
diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index 67ceba055..51e13ee7a 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -10,21 +10,23 @@ type Props = { seriesId: string; onSubmit: (season: number, episode: number) => void; }; -export const EpisodePicker = ({ className, seriesId, onSubmit }: Props) => { +export const EpisodePicker = ({ className, onSubmit }: Props) => { const { t } = useTranslation(); + const splitPath = window.location.hash.split('/'); + const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); const [initialSeason, initialEpisode] = React.useMemo(() => { - const [, season, episode] = seriesId ? seriesId.split(':') : []; - const initialSeason = isNaN(parseInt(season)) ? 1 : parseInt(season); - const initialEpisode = isNaN(parseInt(episode)) ? 1 : parseInt(episode); + const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; + const initialSeason = isNaN(parseInt(pathSeason)) ? 1 : parseInt(pathSeason); + const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode); return [initialSeason, initialEpisode]; - }, [seriesId]); - const seasonRef = useRef(null); - const episodeRef = useRef(null); + }, [videoId]); + const seasonRef = useRef(null); + const episodeRef = useRef(null); const handleSubmit = React.useCallback(() => { - const season = seasonRef.current?.value; - const episode = episodeRef.current?.value; - if (typeof onSubmit === 'function') onSubmit(season, episode); + const season = seasonRef.current?.value || 1; + const episode = episodeRef.current?.value || 1; + if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(parseInt(episode))) onSubmit(season, episode); }, [onSubmit, seasonRef, episodeRef]); return
diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 0ccb05e28..3964a6e53 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -128,14 +128,14 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { { props.streams.length === 0 ?
- + {'
No addons were requested for streams!
: props.streams.every((streams) => streams.content.type === 'Err') ?
- + {video?.upcoming &&
{t('UPCOMING')}...
} {'
{t('NO_STREAM')}
From d407e6c7b7ae8bfb9f3f6fc54324f4af3f706c25 Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 15:12:43 +0200 Subject: [PATCH 011/140] fix(NumberInput): min & max validation when entering value from keyboard --- src/components/NumberInput/NumberInput.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 37daf6401..303e04cbf 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -67,7 +67,12 @@ const NumberInput = forwardRef(({ defaultValue, ...prop value={value} {...props} className={classnames(props.className, styles['value'], { 'disabled': props.disabled })} - onChange={(event: ChangeEvent) => setValue(parseInt(event.target.value))} + onChange={(event: ChangeEvent) => { + const newValue = parseInt(event.target.value); + if (props.min !== undefined && newValue < props.min) return props.min; + if (props.max !== undefined && newValue > props.max) return props.max; + setValue(newValue); + }} onKeyDown={onKeyDown} />
From fbdfa110b50729d4e37046e379c3a4421f0f2c5e Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 17:38:32 +0200 Subject: [PATCH 012/140] fix(StreamsList): add scroll when episode picker is shown on landscape orientation on mobile --- src/routes/MetaDetails/StreamsList/StreamsList.js | 4 ++-- src/routes/MetaDetails/StreamsList/styles.less | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 3964a6e53..ef7cea157 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -128,14 +128,14 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { { props.streams.length === 0 ?
- + {'
No addons were requested for streams!
: props.streams.every((streams) => streams.content.type === 'Err') ?
- + {video?.upcoming &&
{t('UPCOMING')}...
} {'
{t('NO_STREAM')}
diff --git a/src/routes/MetaDetails/StreamsList/styles.less b/src/routes/MetaDetails/StreamsList/styles.less index 8c274c761..0bffa8fcc 100644 --- a/src/routes/MetaDetails/StreamsList/styles.less +++ b/src/routes/MetaDetails/StreamsList/styles.less @@ -22,6 +22,10 @@ padding: 1rem; overflow-y: auto; + .search { + flex: none; + } + .image { flex: none; width: 10rem; From f7494d6e9714b20f4f2d2d3915549c64573a177a Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Feb 2025 17:52:57 +0200 Subject: [PATCH 013/140] fix(EpisodePicker): typings --- .../StreamsList/EpisodePicker/EpisodePicker.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index 51e13ee7a..aa70d021e 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -24,9 +24,11 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { const episodeRef = useRef(null); const handleSubmit = React.useCallback(() => { - const season = seasonRef.current?.value || 1; - const episode = episodeRef.current?.value || 1; - if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(parseInt(episode))) onSubmit(season, episode); + const season = parseInt(seasonRef.current?.value || '1'); + const episode = parseInt(episodeRef.current?.value || '1'); + if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) { + onSubmit(season, episode); + } }, [onSubmit, seasonRef, episodeRef]); return
From 6ca94a21248baa5e572d01c7933e5c9302b79a5d Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Feb 2025 16:41:36 +0200 Subject: [PATCH 014/140] fix(EpisodePicker): unify styles with install addons button --- .../EpisodePicker/EpisodePicker.less | 26 +++++++++++++------ .../EpisodePicker/EpisodePicker.tsx | 25 +++++++++++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less index 879cff167..f5e1f509c 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less @@ -1,19 +1,29 @@ // Copyright (C) 2017-2025 Smart code 203358507 .button-container { + flex: none; + align-self: stretch; display: flex; align-items: center; justify-content: center; - padding: 1rem 2.625rem; - margin: 2rem 0; + border: var(--focus-outline-size) solid var(--primary-accent-color); background-color: var(--primary-accent-color); - transition: 0.3s all ease-in-out; - border-radius: 2.625rem; - color: var(--primary-foreground-color); - border: 2px solid transparent; + height: 4rem; + padding: 0 2rem; + margin: 1rem auto; + border-radius: 2rem; - &:hover, &:focus { - outline: var(--focus-outline-size) solid var(--primary-foreground-color); + &:hover { background-color: transparent; } + + .label { + flex: 0 1 auto; + font-size: 1rem; + font-weight: 700; + max-height: 3.6em; + text-align: center; + color: var(--primary-foreground-color); + margin-bottom: 0; + } } \ No newline at end of file diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index aa70d021e..44da39bfa 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -14,15 +14,22 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { const { t } = useTranslation(); const splitPath = window.location.hash.split('/'); const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); - const [initialSeason, initialEpisode] = React.useMemo(() => { - const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; + const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; + const [season, setSeason] = React.useState(() => { const initialSeason = isNaN(parseInt(pathSeason)) ? 1 : parseInt(pathSeason); + return initialSeason; + }); + const [episode, setEpisode] = React.useState(() => { const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode); - return [initialSeason, initialEpisode]; - }, [videoId]); + return initialEpisode; + }); const seasonRef = useRef(null); const episodeRef = useRef(null); + const handleSeasonChange = (value?: number) => setSeason(value !== undefined ? value : 1); + + const handleEpisodeChange = (value?: number) => setEpisode(value !== undefined ? value : 1); + const handleSubmit = React.useCallback(() => { const season = parseInt(seasonRef.current?.value || '1'); const episode = parseInt(episodeRef.current?.value || '1'); @@ -31,10 +38,14 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { } }, [onSubmit, seasonRef, episodeRef]); + const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]); + return
- - - + + +
; }; From 3f60df9073b5ea8493a0b4dead5ae4f471074b1a Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Feb 2025 19:05:46 +0200 Subject: [PATCH 015/140] refactor(EpisodePicker): improve styles and typings --- src/components/NumberInput/NumberInput.tsx | 90 +++++++++----- src/components/NumberInput/styles.less | 112 ++++++++---------- .../EpisodePicker/EpisodePicker.less | 2 +- .../EpisodePicker/EpisodePicker.tsx | 19 ++- 4 files changed, 119 insertions(+), 104 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 303e04cbf..2add02be5 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react'; +import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from 'react'; import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import styles from './styles.less'; @@ -18,10 +18,11 @@ type Props = InputHTMLAttributes & { max?: number; onKeyDown?: (event: KeyboardEvent) => void; onSubmit?: (event: KeyboardEvent) => void; + onUpdate?: (value: number) => void; }; -const NumberInput = forwardRef(({ defaultValue, ...props }, ref) => { - const [value, setValue] = useState(defaultValue || 0); +const NumberInput = forwardRef(({ defaultValue, showButtons, onUpdate, ...props }, ref) => { + const [value, setValue] = useState(defaultValue || 0); const onKeyDown = useCallback((event: KeyboardEvent) => { props.onKeyDown && props.onKeyDown(event); @@ -32,33 +33,63 @@ const NumberInput = forwardRef(({ defaultValue, ...prop const handleIncrease = () => { const { max } = props; - if (typeof max !== 'undefined') { - return setValue((prevVal) => - prevVal + 1 > max ? max : prevVal + 1 - ); + if (max !== undefined) { + return setValue((prevVal) => { + const value = prevVal || 0; + return value + 1 > max ? max : value + 1; + }); } - setValue((prevVal) => prevVal + 1); + setValue((prevVal) => { + const value = prevVal || 0; + return value + 1; + }); }; const handleDecrease = () => { const { min } = props; - if (typeof min !== 'undefined') { - return setValue((prevVal) => - prevVal - 1 < min ? min : prevVal - 1 - ); + if (min !== undefined) { + return setValue((prevVal) => { + const value = prevVal || 0; + return value - 1 < min ? min : value - 1; + }); } - setValue((prevVal) => prevVal - 1); + setValue((prevVal) => { + const value = prevVal || 0; + return value - 1; + }); }; + const handleChange = (event: ChangeEvent) => { + const min = props.min || 0; + let newValue = event.target.valueAsNumber; + if (newValue && newValue < min) { + newValue = min; + } + if (props.max !== undefined && newValue && newValue > props.max) { + newValue = props.max; + } + setValue(newValue); + }; + + useEffect(() => { + if (typeof onUpdate === 'function') { + onUpdate(value); + } + }, [value]); + return (
- {props.showButtons ? : null} -
+ { + showButtons ? + + : null + } +
{props.label &&
{props.label}
} (({ defaultValue, ...prop value={value} {...props} className={classnames(props.className, styles['value'], { 'disabled': props.disabled })} - onChange={(event: ChangeEvent) => { - const newValue = parseInt(event.target.value); - if (props.min !== undefined && newValue < props.min) return props.min; - if (props.max !== undefined && newValue > props.max) return props.max; - setValue(newValue); - }} + onChange={handleChange} onKeyDown={onKeyDown} />
- {props.showButtons ? : null} + { + showButtons ? + + : null + }
); }); diff --git a/src/components/NumberInput/styles.less b/src/components/NumberInput/styles.less index 484f3661a..a88bc6d20 100644 --- a/src/components/NumberInput/styles.less +++ b/src/components/NumberInput/styles.less @@ -3,73 +3,63 @@ .number-input { user-select: text; display: flex; - max-width: 12rem; + max-width: 14rem; + height: 3.5rem; margin-bottom: 1rem; color: var(--primary-foreground-color); background: var(--overlay-color); - border-radius: 100rem; + border-radius: 3.5rem; - .btn { - width: 2.875rem; - height: 2.875rem; - background: var(--overlay-color); - border: none; - border-radius: 50%; - color: var(--primary-foreground-color); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: background 0.3s; - z-index: 1; - - svg { - width: 1.625rem; - } - } - - .number-display { - height: 2.875rem; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: 0 1rem; - - &::-moz-focus-inner { + .button { + flex: none; + width: 3.5rem; + height: 3.5rem; + padding: 1rem; + background: var(--overlay-color); border: none; + border-radius: 100%; + cursor: pointer; + z-index: 1; + + .icon { + width: 100%; + height: 100%; + } } - &.with-btns { - padding: 0 1.9375rem; - margin-left: -1.4375rem; - margin-right: -1.4375rem; - max-width: 9.125rem; - } - } - - /* Label */ - .number-display .label { - font-size: 0.625rem; - font-weight: 400; - opacity: 0.7; - } - - /* Value */ - .number-display .value { - font-size: 1.3125rem; - display: flex; - width: 100%; - color: var(--primary-foreground-color); - text-align: center; + .number-display { + display: flex; + flex: 1; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 0 1rem; - &::-webkit-outer-spin-button, - &::-webkit-inner-spin-button { - -webkit-appearance: none; - -moz-appearance: textfield; - margin: 0; + &::-moz-focus-inner { + border: none; + } + + .label { + font-size: 0.8rem; + font-weight: 400; + opacity: 0.7; + } + + .value { + font-size: 1.2rem; + display: flex; + justify-content: center; + width: 100%; + color: var(--primary-foreground-color); + text-align: center; + appearance: none; + + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } } - } - - } +} \ No newline at end of file diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less index f5e1f509c..260eb8ebe 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less @@ -21,7 +21,7 @@ flex: 0 1 auto; font-size: 1rem; font-weight: 700; - max-height: 3.6em; + max-height: 3.5rem; text-align: center; color: var(--primary-foreground-color); margin-bottom: 0; diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index 44da39bfa..ddb38e353 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React, { useRef } from 'react'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { Button, NumberInput } from 'stremio/components'; import styles from './EpisodePicker.less'; @@ -16,33 +16,28 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; const [season, setSeason] = React.useState(() => { - const initialSeason = isNaN(parseInt(pathSeason)) ? 1 : parseInt(pathSeason); + const initialSeason = isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason); return initialSeason; }); const [episode, setEpisode] = React.useState(() => { const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode); return initialEpisode; }); - const seasonRef = useRef(null); - const episodeRef = useRef(null); + const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 1); - const handleSeasonChange = (value?: number) => setSeason(value !== undefined ? value : 1); - - const handleEpisodeChange = (value?: number) => setEpisode(value !== undefined ? value : 1); + const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1); const handleSubmit = React.useCallback(() => { - const season = parseInt(seasonRef.current?.value || '1'); - const episode = parseInt(episodeRef.current?.value || '1'); if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) { onSubmit(season, episode); } - }, [onSubmit, seasonRef, episodeRef]); + }, [onSubmit, season, episode]); const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]); return
- - + + From 3c2ab92bd698b6c0b5c35093422793c3d976e225 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Feb 2025 19:27:49 +0200 Subject: [PATCH 016/140] fix(EpisodePicker): make 0 the initial value for season when no value provided, remove placeholder --- .../MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index ddb38e353..031e3883d 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -23,7 +23,7 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { const initialEpisode = isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode); return initialEpisode; }); - const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 1); + const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 0); const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1); @@ -36,7 +36,7 @@ export const EpisodePicker = ({ className, onSubmit }: Props) => { const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]); return
- + -
; +return ( +
+ + + +
+ ); }; export default EpisodePicker; From aa3dedf8be56db2c53a5bc6092ce4f4e6c215037 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 12 Feb 2025 15:38:14 +0200 Subject: [PATCH 025/140] fix: linting --- src/components/NumberInput/NumberInput.tsx | 4 ++-- .../MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx | 2 +- src/routes/MetaDetails/StreamsList/StreamsList.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 71879a1fe..b883d3e2e 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -91,9 +91,9 @@ const NumberInput = forwardRef(({ defaultValue, showBut }
{ - props.label ? + props.label ?
{props.label}
- : null + : null } { const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]); -return ( + return (
diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 7bb8a33f7..576e13669 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -137,9 +137,9 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => {
{ - video?.upcoming ? + video?.upcoming ?
{t('UPCOMING')}...
- : null + : null } {'
{t('NO_STREAM')}
From 10a36d2c4dd565456927e857123d41f71e40ef02 Mon Sep 17 00:00:00 2001 From: Botsy Date: Wed, 12 Feb 2025 15:46:52 +0200 Subject: [PATCH 026/140] Update src/components/NumberInput/NumberInput.tsx Co-authored-by: Timothy Z. --- src/components/NumberInput/NumberInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index b883d3e2e..38433d874 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -89,7 +89,7 @@ const NumberInput = forwardRef(({ defaultValue, showBut : null } -
+
{ props.label ?
{props.label}
From f678375633e0c1fe0f88c10c77817a77b2eea484 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 12 Feb 2025 16:00:41 +0200 Subject: [PATCH 027/140] refactor(NumberInput): apply suggested improvements --- src/components/NumberInput/NumberInput.tsx | 52 ++++++++-------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 38433d874..457db28b8 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -31,52 +31,38 @@ const NumberInput = forwardRef(({ defaultValue, showBut } }, [props.onKeyDown, props.onSubmit]); + const updateValueAndNotify = (valueAsNumber: number) => { + setValue(valueAsNumber); + onUpdate?.(valueAsNumber); + }; + const handleIncrease = () => { - const { max } = props; - if (max !== undefined) { - return setValue((prevVal) => { - const value = prevVal || 0; - return value + 1 > max ? max : value + 1; - }); + if (props.max !== undefined) { + updateValueAndNotify(Math.min(props.max, (value || 0) + 1)); + return; } - setValue((prevVal) => { - const value = prevVal || 0; - return value + 1; - }); + updateValueAndNotify((value || 0) + 1); }; const handleDecrease = () => { - const { min } = props; - if (min !== undefined) { - return setValue((prevVal) => { - const value = prevVal || 0; - return value - 1 < min ? min : value - 1; - }); + if (props.min !== undefined) { + updateValueAndNotify(Math.max(props.min, (value || 0) - 1)); + return; } - setValue((prevVal) => { - const value = prevVal || 0; - return value - 1; - }); + updateValueAndNotify((value || 0) - 1); }; - const handleChange = (event: ChangeEvent) => { + const handleChange = ({ target: { valueAsNumber }}: ChangeEvent) => { const min = props.min || 0; - let newValue = event.target.valueAsNumber; - if (newValue && newValue < min) { - newValue = min; + if (valueAsNumber && valueAsNumber < min) { + valueAsNumber = min; } - if (props.max !== undefined && newValue && newValue > props.max) { - newValue = props.max; + if (props.max !== undefined && valueAsNumber && valueAsNumber > props.max) { + valueAsNumber = props.max; } - setValue(newValue); + updateValueAndNotify(valueAsNumber); }; - useEffect(() => { - if (typeof onUpdate === 'function') { - onUpdate(value); - } - }, [value]); - return (
{ From 4cd9db53d1ad8beae5d71dff878010f7fc342812 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 12 Feb 2025 16:01:28 +0200 Subject: [PATCH 028/140] fix(NumberInput): remove unused import --- src/components/NumberInput/NumberInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 457db28b8..0d3e55c53 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 import Icon from '@stremio/stremio-icons/react'; -import React, { ChangeEvent, forwardRef, useCallback, useEffect, useState } from 'react'; +import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react'; import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import styles from './NumberInput.less'; From 07cc2a9b2d56767739537fb55b2447436b24e683 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 12 Feb 2025 16:59:37 +0200 Subject: [PATCH 029/140] refactor(EpisodePicker): apply suggested improvements --- .../EpisodePicker/EpisodePicker.tsx | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index d47d44abd..4a82ec591 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, NumberInput } from 'stremio/components'; import styles from './EpisodePicker.less'; @@ -13,22 +13,32 @@ type Props = { const EpisodePicker = ({ className, onSubmit }: Props) => { const { t } = useTranslation(); - const splitPath = window.location.hash.split('/'); - const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); - const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; - const [season, setSeason] = React.useState(isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason)); - const [episode, setEpisode] = React.useState(isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode)); - const handleSeasonChange = (value: number) => setSeason(!isNaN(value) ? value : 0); - const handleEpisodeChange = (value: number) => setEpisode(!isNaN(value) ? value : 1); + const { initialSeason, initialEpisode } = useMemo(() => { + const splitPath = window.location.hash.split('/'); + const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); + const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; + return { + initialSeason: isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason), + initialEpisode: isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode) + }; + }, []); - const handleSubmit = React.useCallback(() => { + const [season, setSeason] = useState(initialSeason); + const [episode, setEpisode] = useState(initialEpisode); + + const handleSeasonChange = useCallback((value: number) => setSeason(!isNaN(value) ? value : 0), []); + const handleEpisodeChange = useCallback((value: number) => setEpisode(!isNaN(value) ? value : 1), []); + + const handleSubmit = useCallback(() => { if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) { onSubmit(season, episode); } }, [onSubmit, season, episode]); - const disabled = React.useMemo(() => season === parseInt(pathSeason) && episode === parseInt(pathEpisode), [pathSeason, pathEpisode, season, episode]); + const disabled = useMemo(() => { + return season === initialSeason && episode === initialEpisode; + }, [season, episode, initialSeason, initialEpisode]); return (
From 675328ca081553068adab8db07b3c93f9fa2902b Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 12 Feb 2025 17:27:37 +0200 Subject: [PATCH 030/140] fix(StreamsList): show EpisodePicker only if type is series --- src/routes/MetaDetails/MetaDetails.js | 1 + src/routes/MetaDetails/StreamsList/StreamsList.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/routes/MetaDetails/MetaDetails.js b/src/routes/MetaDetails/MetaDetails.js index e2c89bb74..8a50b59c1 100644 --- a/src/routes/MetaDetails/MetaDetails.js +++ b/src/routes/MetaDetails/MetaDetails.js @@ -176,6 +176,7 @@ const MetaDetails = ({ urlParams, queryParams }) => { className={styles['streams-list']} streams={metaDetails.streams} video={video} + type={streamPath.type} onEpisodeSearch={handleEpisodeSearch} /> : diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 576e13669..2d44d35fc 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -14,7 +14,7 @@ const { default: SeasonEpisodePicker } = require('./EpisodePicker'); const ALL_ADDONS_KEY = 'ALL'; -const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { +const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => { const { t } = useTranslation(); const { core } = useServices(); const platform = usePlatform(); @@ -128,14 +128,22 @@ const StreamsList = ({ className, video, onEpisodeSearch, ...props }) => { { props.streams.length === 0 ?
- + { + type === 'series' ? + + : null + } {'
No addons were requested for streams!
: props.streams.every((streams) => streams.content.type === 'Err') ?
- + { + type === 'series' ? + + : null + } { video?.upcoming ?
{t('UPCOMING')}...
@@ -207,6 +215,7 @@ StreamsList.propTypes = { className: PropTypes.string, streams: PropTypes.arrayOf(PropTypes.object).isRequired, video: PropTypes.object, + type: PropTypes.string, onEpisodeSearch: PropTypes.func }; From 6999ef6a8d45b58fe05692e6c11892380f7e309c Mon Sep 17 00:00:00 2001 From: Botsy Date: Thu, 13 Feb 2025 12:37:36 +0200 Subject: [PATCH 031/140] Update src/components/NumberInput/NumberInput.tsx Co-authored-by: Timothy Z. --- src/components/NumberInput/NumberInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 0d3e55c53..29070c70c 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -26,7 +26,7 @@ const NumberInput = forwardRef(({ defaultValue, showBut const onKeyDown = useCallback((event: KeyboardEvent) => { props.onKeyDown && props.onKeyDown(event); - if (event.key === 'Enter' ) { + if (event.key === 'Enter') { props.onSubmit && props.onSubmit(event); } }, [props.onKeyDown, props.onSubmit]); From dad4804bad35ddf9b383f954de3604aea3163673 Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 17 Feb 2025 12:57:20 +0200 Subject: [PATCH 032/140] fix(Discover): Hide duplicated catalog type filter on mobile --- src/routes/Discover/styles.less | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 62e3adaee..50eee3c04 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -375,4 +375,16 @@ } } } + + .selectable-inputs-modal { + .selectable-inputs-modal-container { + .selectable-inputs-modal-content { + .select-input { + &:first-child { + display: none; + } + } + } + } + } } \ No newline at end of file From b1365e31d4d9327065ffaf1c2aeeeca52a55c1a7 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 18 Feb 2025 15:33:11 +0200 Subject: [PATCH 033/140] fix(Multiselect): destruct options from props to not pass them to component --- src/components/Multiselect/Multiselect.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/Multiselect/Multiselect.js b/src/components/Multiselect/Multiselect.js index c791c60d1..0e353eef4 100644 --- a/src/components/Multiselect/Multiselect.js +++ b/src/components/Multiselect/Multiselect.js @@ -10,16 +10,16 @@ const ModalDialog = require('stremio/components/ModalDialog'); const useBinaryState = require('stremio/common/useBinaryState'); const styles = require('./styles'); -const Multiselect = ({ className, mode, direction, title, disabled, dataset, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => { +const Multiselect = ({ className, mode, direction, title, disabled, dataset, options, renderLabelContent, renderLabelText, onOpen, onClose, onSelect, ...props }) => { const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); - const options = React.useMemo(() => { - return Array.isArray(props.options) ? - props.options.filter((option) => { + const filteredOptions = React.useMemo(() => { + return Array.isArray(options) ? + options.filter((option) => { return option && (typeof option.value === 'string' || option.value === null); }) : []; - }, [props.options]); + }, [options]); const selected = React.useMemo(() => { return Array.isArray(props.selected) ? props.selected.filter((value) => { @@ -94,7 +94,7 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, ren : selected.length > 0 ? selected.map((value) => { - const option = options.find((option) => option.value === value); + const option = filteredOptions.find((option) => option.value === value); return option && typeof option.label === 'string' ? option.label : @@ -109,12 +109,12 @@ const Multiselect = ({ className, mode, direction, title, disabled, dataset, ren } {children} - ), [menuOpen, title, disabled, options, selected, labelOnClick, renderLabelContent, renderLabelText]); + ), [menuOpen, title, disabled, filteredOptions, selected, labelOnClick, renderLabelContent, renderLabelText]); const renderMenu = React.useCallback(() => (
{ - options.length > 0 ? - options.map(({ label, title, value }) => ( + filteredOptions.length > 0 ? + filteredOptions.map(({ label, title, value }) => (
- ), [options, selected, menuOnKeyDown, menuOnClick, optionOnClick]); + ), [filteredOptions, selected, menuOnKeyDown, menuOnClick, optionOnClick]); const renderPopupLabel = React.useMemo(() => (labelProps) => { return renderLabel({ ...labelProps, From 224b6e6f76bdaa376c5b802df8cdc4a57ca7f59b Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 18 Feb 2025 15:35:14 +0200 Subject: [PATCH 034/140] fix(Discover): improve styles to handle filters edge cases --- src/routes/Discover/Discover.js | 8 +++-- src/routes/Discover/styles.less | 55 +++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index abcdfb205..db614a363 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -98,9 +98,11 @@ const Discover = ({ urlParams, queryParams }) => { onSelect={onSelect} /> ))} - +
+ +
{ discover.catalog !== null && !discover.catalog.installed ? diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 50eee3c04..bf396c5ce 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -59,7 +59,9 @@ display: none; &~.filter-container { - display: flex; + .filter-button { + display: flex; + } } } @@ -69,20 +71,26 @@ } .filter-container { - flex: none; - display: none; - align-items: center; - justify-content: center; - width: 3rem; - height: 3rem; - border-radius: var(--border-radius); - background-color: var(--overlay-color); - - .filter-icon { + display: flex; + flex: 1 0 5rem; + justify-content: flex-end; + .filter-button { flex: none; - width: 1.4rem; - height: 1.4rem; - color: var(--primary-foreground-color); + display: none; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + margin-left: 1.5rem; + border-radius: var(--border-radius); + background-color: var(--overlay-color); + + .filter-icon { + flex: none; + width: 1.4rem; + height: 1.4rem; + color: var(--primary-foreground-color); + } } } } @@ -219,9 +227,14 @@ .select-input { height: 3.5rem; + display: none; - &:not(:last-child) { - margin-bottom: 1rem; + &:nth-child(n+4) { + display: flex; + + &:not(:last-child) { + margin-bottom: 1rem; + } } .multiselect-menu-container { @@ -362,7 +375,7 @@ &:nth-child(n+2) { display: none; - &~.filter-container { + &~.filter-button { display: flex; } } @@ -380,8 +393,12 @@ .selectable-inputs-modal-container { .selectable-inputs-modal-content { .select-input { - &:first-child { - display: none; + display: none; + &:nth-child(n+2) { + display: flex; + &:not(:last-child) { + margin-bottom: 1rem; + } } } } From b8c328507edbc89582733a5a830e4eed6d7cb171 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 18 Feb 2025 18:05:18 +0200 Subject: [PATCH 035/140] fix(Library): default to type All filter after selected type is no longer available --- src/routes/Library/Library.js | 5 +++++ src/routes/Library/useSelectableInputs.js | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/routes/Library/Library.js b/src/routes/Library/Library.js index 2871a9b3f..3329624fd 100644 --- a/src/routes/Library/Library.js +++ b/src/routes/Library/Library.js @@ -62,6 +62,11 @@ const Library = ({ model, urlParams, queryParams }) => { scrollContainerRef.current.scrollTop = 0; } }, [profile.auth, library.selected]); + React.useEffect(() => { + if (!library.selected?.type && typeSelect.selected) { + window.location = typeSelect.selected[0]; + } + }, [typeSelect.selected, library.selected]); return (
diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js index 6f24dc2c9..d173d663a 100644 --- a/src/routes/Library/useSelectableInputs.js +++ b/src/routes/Library/useSelectableInputs.js @@ -4,6 +4,8 @@ const React = require('react'); const { useTranslate } = require('stremio/common'); const mapSelectableInputs = (library, t) => { + const selectedType = library.selectable.types + .filter(({ selected }) => selected).map(({ deepLinks }) => deepLinks.library); const typeSelect = { title: t.string('SELECT_TYPE'), options: library.selectable.types @@ -11,9 +13,9 @@ const mapSelectableInputs = (library, t) => { value: deepLinks.library, label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_') })), - selected: library.selectable.types - .filter(({ selected }) => selected) - .map(({ deepLinks }) => deepLinks.library), + selected: selectedType.length + ? selectedType + : [library.selectable.types[0]].map(({ deepLinks }) => deepLinks.library), onSelect: (event) => { window.location = event.value; } From 0506d53f4e341db832918e89c1755ccd514dc1d8 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 19 Feb 2025 17:38:17 +0200 Subject: [PATCH 036/140] fix(Discover): fix mobile styles for filter button --- src/routes/Discover/styles.less | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index bf396c5ce..266ef1481 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -374,9 +374,10 @@ .select-input { &:nth-child(n+2) { display: none; - - &~.filter-button { - display: flex; + &~.filter-container { + .filter-button { + display: flex; + } } } } From a50e3c7186f661cc048f6704a6bb70ffb3399df1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 21 Feb 2025 15:09:05 +0200 Subject: [PATCH 037/140] fix(Discover): apply new lines in styles where missed --- src/routes/Discover/styles.less | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/routes/Discover/styles.less b/src/routes/Discover/styles.less index 266ef1481..527238757 100644 --- a/src/routes/Discover/styles.less +++ b/src/routes/Discover/styles.less @@ -74,6 +74,7 @@ display: flex; flex: 1 0 5rem; justify-content: flex-end; + .filter-button { flex: none; display: none; @@ -374,6 +375,7 @@ .select-input { &:nth-child(n+2) { display: none; + &~.filter-container { .filter-button { display: flex; @@ -395,8 +397,10 @@ .selectable-inputs-modal-content { .select-input { display: none; + &:nth-child(n+2) { display: flex; + &:not(:last-child) { margin-bottom: 1rem; } From 794f4e48ac4346e6147915e91aa2bd94cd4e45e1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 28 Feb 2025 17:45:13 +0200 Subject: [PATCH 038/140] feat(MultiselectMenu): handle title function --- src/components/MultiselectMenu/MultiselectMenu.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx index 35be107c9..55b818d80 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.tsx +++ b/src/components/MultiselectMenu/MultiselectMenu.tsx @@ -11,7 +11,7 @@ import useOutsideClick from 'stremio/common/useOutsideClick'; type Props = { className?: string, - title?: string; + title?: string | (() => string); options: MultiselectMenuOption[]; selectedOption?: MultiselectMenuOption; onSelect: (value: number) => void; @@ -35,7 +35,11 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } aria-haspopup='listbox' aria-expanded={menuOpen} > - {title} + { + typeof title === 'function' + ? title() + : title ?? selectedOption?.label + } { From 7ea974f1da1368c3c6ad9536b91bdbb52bffbf08 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 28 Feb 2025 17:49:20 +0200 Subject: [PATCH 039/140] refactor(Settings): use MultiselectMenu instead Multiselect --- src/routes/Settings/Settings.js | 26 ++--- .../Settings/useProfileSettingsInputs.js | 98 ++++++++++++------- .../useStreamingServerSettingsInputs.js | 40 +++++--- 3 files changed, 101 insertions(+), 63 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 6ad15163a..867be206e 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -8,7 +8,7 @@ const { default: Icon } = require('@stremio/stremio-icons/react'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); const { useProfile, usePlatform, useStreamingServer, withCoreSuspender, useToast } = require('stremio/common'); -const { Button, ColorInput, MainNavBars, Multiselect, Toggle } = require('stremio/components'); +const { Button, ColorInput, MainNavBars, MultiselectMenu, Toggle } = require('stremio/components'); const useProfileSettingsInputs = require('./useProfileSettingsInputs'); const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); const useDataExport = require('./useDataExport'); @@ -316,7 +316,7 @@ const Settings = () => {
{ t('SETTINGS_UI_LANGUAGE') }
- {
{ t('SETTINGS_SUBTITLES_LANGUAGE') }
- @@ -356,7 +356,7 @@ const Settings = () => {
{ t('SETTINGS_SUBTITLES_SIZE') }
- @@ -398,7 +398,7 @@ const Settings = () => {
{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }
- @@ -423,7 +423,7 @@ const Settings = () => {
{ t('SETTINGS_SEEK_KEY') }
- @@ -432,7 +432,7 @@ const Settings = () => {
{ t('SETTINGS_SEEK_KEY_SHIFT') }
- @@ -467,7 +467,7 @@ const Settings = () => {
{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }
- {
{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }
- @@ -527,7 +527,7 @@ const Settings = () => {
{ t('SETTINGS_HTTPS_ENDPOINT') }
- @@ -541,7 +541,7 @@ const Settings = () => {
{ t('SETTINGS_SERVER_CACHE_SIZE') }
- @@ -555,7 +555,7 @@ const Settings = () => {
{ t('SETTINGS_SERVER_TORRENT_PROFILE') }
- @@ -569,7 +569,7 @@ const Settings = () => {
{ t('SETTINGS_TRANSCODE_PROFILE') }
- diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index d36b169f9..9746a798f 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -15,17 +15,18 @@ const useProfileSettingsInputs = (profile) => { value: codes[0], label: name, })), - selected: [ - interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage - ], - onSelect: (event) => { + selectedOption: { + label: interfaceLanguages.find(({ codes }) => codes[0] === profile.settings.interfaceLanguage)?.name, + value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage + }, + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - interfaceLanguage: event.value + interfaceLanguage: value } } }); @@ -36,15 +37,18 @@ const useProfileSettingsInputs = (profile) => { value: code, label: languageNames[code] })), - selected: [profile.settings.subtitlesLanguage], - onSelect: (event) => { + selectedOption: { + label: languageNames[profile.settings.subtitlesLanguage], + value: profile.settings.subtitlesLanguage + }, + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - subtitlesLanguage: event.value + subtitlesLanguage: value } } }); @@ -55,18 +59,21 @@ const useProfileSettingsInputs = (profile) => { value: `${size}`, label: `${size}%` })), - selected: [`${profile.settings.subtitlesSize}`], - renderLabelText: () => { + selectedOption: { + label: `${profile.settings.subtitlesSize}%`, + value: `${profile.settings.subtitlesSize}` + }, + title: () => { return `${profile.settings.subtitlesSize}%`; }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - subtitlesSize: parseInt(event.value, 10) + subtitlesSize: parseInt(value, 10) } } }); @@ -74,14 +81,14 @@ const useProfileSettingsInputs = (profile) => { }), [profile.settings]); const subtitlesTextColorInput = React.useMemo(() => ({ value: profile.settings.subtitlesTextColor, - onChange: (event) => { + onChange: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - subtitlesTextColor: event.value + subtitlesTextColor: value } } }); @@ -89,14 +96,14 @@ const useProfileSettingsInputs = (profile) => { }), [profile.settings]); const subtitlesBackgroundColorInput = React.useMemo(() => ({ value: profile.settings.subtitlesBackgroundColor, - onChange: (event) => { + onChange: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - subtitlesBackgroundColor: event.value + subtitlesBackgroundColor: value } } }); @@ -104,14 +111,14 @@ const useProfileSettingsInputs = (profile) => { }), [profile.settings]); const subtitlesOutlineColorInput = React.useMemo(() => ({ value: profile.settings.subtitlesOutlineColor, - onChange: (event) => { + onChange: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - subtitlesOutlineColor: event.value + subtitlesOutlineColor: value } } }); @@ -122,15 +129,18 @@ const useProfileSettingsInputs = (profile) => { value: code, label: languageNames[code] })), - selected: [profile.settings.audioLanguage], - onSelect: (event) => { + selectedOption: { + label: languageNames[profile.settings.audioLanguage], + value: profile.settings.audioLanguage + }, + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - audioLanguage: event.value + audioLanguage: value } } }); @@ -172,18 +182,21 @@ const useProfileSettingsInputs = (profile) => { value: `${size}`, label: `${size / 1000} ${t('SECONDS')}` })), - selected: [`${profile.settings.seekTimeDuration}`], - renderLabelText: () => { + selectedOption: { + label: `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`, + value: `${profile.settings.seekTimeDuration}` + }, + title: () => { return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - seekTimeDuration: parseInt(event.value, 10) + seekTimeDuration: parseInt(value, 10) } } }); @@ -194,18 +207,21 @@ const useProfileSettingsInputs = (profile) => { value: `${size}`, label: `${size / 1000} ${t('SECONDS')}` })), - selected: [`${profile.settings.seekShortTimeDuration}`], - renderLabelText: () => { + selectedOption: { + label: `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`, + value: `${profile.settings.seekShortTimeDuration}`, + }, + title: () => { return `${profile.settings.seekShortTimeDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - seekShortTimeDuration: parseInt(event.value, 10) + seekShortTimeDuration: parseInt(value, 10) } } }); @@ -218,19 +234,22 @@ const useProfileSettingsInputs = (profile) => { value, label: t(label), })), - selected: [profile.settings.playerType], - renderLabelText: () => { + selectedOption: { + label: CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType)?.label, + value: profile.settings.playerType + }, + title: () => { const selectedOption = CONSTANTS.EXTERNAL_PLAYERS.find(({ value }) => value === profile.settings.playerType); return selectedOption ? t(selectedOption.label, { defaultValue: selectedOption.label }) : profile.settings.playerType; }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - playerType: event.value + playerType: value } } }); @@ -241,21 +260,26 @@ const useProfileSettingsInputs = (profile) => { value: `${duration}`, label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}` })), - selected: [`${profile.settings.nextVideoNotificationDuration}`], - renderLabelText: () => { + selectedOption: { + label: profile.settings.nextVideoNotificationDuration === 0 + ? 'Disabled' + : `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`, + value: `${profile.settings.nextVideoNotificationDuration}` + }, + title: () => { return profile.settings.nextVideoNotificationDuration === 0 ? 'Disabled' : `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`; }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'UpdateSettings', args: { ...profile.settings, - nextVideoNotificationDuration: parseInt(event.value, 10) + nextVideoNotificationDuration: parseInt(value, 10) } } }); diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/useStreamingServerSettingsInputs.js index 1d4eee066..0313830fc 100644 --- a/src/routes/Settings/useStreamingServerSettingsInputs.js +++ b/src/routes/Settings/useStreamingServerSettingsInputs.js @@ -77,15 +77,18 @@ const useStreamingServerSettingsInputs = (streamingServer) => { value: address, })) ], - selected: [streamingServer.settings.content.remoteHttps], - onSelect: (event) => { + selectedOption: { + label: streamingServer.settings.content.remoteHttps || t('SETTINGS_DISABLED'), + value: streamingServer.settings.content.remoteHttps + }, + onSelect: (value) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { ...streamingServer.settings.content, - remoteHttps: event.value, + remoteHttps: value, } } }); @@ -103,18 +106,21 @@ const useStreamingServerSettingsInputs = (streamingServer) => { label: cacheSizeToString(size), value: JSON.stringify(size) })), - selected: [JSON.stringify(streamingServer.settings.content.cacheSize)], - renderLabelText: () => { + selectedOption: { + label: cacheSizeToString(streamingServer.settings.content.cacheSize), + value: JSON.stringify(streamingServer.settings.content.cacheSize) + }, + title: () => { return cacheSizeToString(streamingServer.settings.content.cacheSize); }, - onSelect: (event) => { + onSelect: (value) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { ...streamingServer.settings.content, - cacheSize: JSON.parse(event.value), + cacheSize: JSON.parse(value), } } }); @@ -152,15 +158,20 @@ const useStreamingServerSettingsInputs = (streamingServer) => { : [] ), - selected: [JSON.stringify(selectedTorrentProfile)], - onSelect: (event) => { + selectedOption: { + label: isCustomTorrentProfileSelected + ? 'custom' + : Object.keys(TORRENT_PROFILES).find((profileName) => JSON.stringify(TORRENT_PROFILES[profileName]) === JSON.stringify(selectedTorrentProfile)), + value: JSON.stringify(selectedTorrentProfile) + }, + onSelect: (value) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { ...streamingServer.settings.content, - ...JSON.parse(event.value), + ...JSON.parse(value), } } }); @@ -183,15 +194,18 @@ const useStreamingServerSettingsInputs = (streamingServer) => { value: name, })) ], - selected: [streamingServer.settings.content.transcodeProfile], - onSelect: (event) => { + selectedOption: { + label: streamingServer.settings.content.transcodeProfile || t('SETTINGS_DISABLED'), + value: streamingServer.settings.content.transcodeProfile + }, + onSelect: (value) => { core.transport.dispatch({ action: 'StreamingServer', args: { action: 'UpdateSettings', args: { ...streamingServer.settings.content, - transcodeProfile: event.value, + transcodeProfile: value, } } }); From 5f8aaf395d530c306b836177ec4dc7f9b8e1507b Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Mar 2025 14:41:16 +0200 Subject: [PATCH 040/140] fix(Settings): display name of default UI language option --- src/routes/Settings/useProfileSettingsInputs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index 9746a798f..3511e957d 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -16,7 +16,7 @@ const useProfileSettingsInputs = (profile) => { label: name, })), selectedOption: { - label: interfaceLanguages.find(({ codes }) => codes[0] === profile.settings.interfaceLanguage)?.name, + label: interfaceLanguages.find(({ codes }) => codes[0] === profile.settings.interfaceLanguage || codes[1] === profile.settings.interfaceLanguage)?.name, value: interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage }, onSelect: (value) => { From 79d9e886beee1d7873a623cbf1e559c387ef8225 Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Mar 2025 14:57:39 +0200 Subject: [PATCH 041/140] fix(Settings): align MultiselectMenu styles to multiselect ones --- src/components/MultiselectMenu/MultiselectMenu.less | 1 + src/routes/Settings/styles.less | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less index 3c7b81b59..b09a0d916 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.less +++ b/src/components/MultiselectMenu/MultiselectMenu.less @@ -17,6 +17,7 @@ color: var(--primary-foreground-color); padding: 0.75rem 1.5rem; display: flex; + flex: 1; justify-content: space-between; align-items: center; gap: 0 0.5rem; diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less index de37d17c2..6b61b8f69 100644 --- a/src/routes/Settings/styles.less +++ b/src/routes/Settings/styles.less @@ -261,7 +261,10 @@ } .option-input-container { - padding: 1rem 1.5rem; + + &.multiselect-container { + background: var(--overlay-color); + } &.button-container { justify-content: center; From 7f244c4fdd69ae633aa9a384dbe7d56d3934ee0c Mon Sep 17 00:00:00 2001 From: Botzy Date: Mon, 10 Mar 2025 15:05:10 +0200 Subject: [PATCH 042/140] fix(Settings): revert input container padding change for all fields and apply only to multiselect menu --- src/routes/Settings/styles.less | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/Settings/styles.less b/src/routes/Settings/styles.less index 6b61b8f69..1dc9301ce 100644 --- a/src/routes/Settings/styles.less +++ b/src/routes/Settings/styles.less @@ -261,8 +261,10 @@ } .option-input-container { + padding: 1rem 1.5rem; &.multiselect-container { + padding: 0; background: var(--overlay-color); } From 12c75d2d11728216324b38223fe4a4d749a7c142 Mon Sep 17 00:00:00 2001 From: Ivelin Megdanov Date: Mon, 10 Mar 2025 17:23:29 +0200 Subject: [PATCH 043/140] Fix infinite scroll issue on large screens --- src/routes/Discover/Discover.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index abcdfb205..f81696246 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -26,6 +26,15 @@ const Discover = ({ urlParams, queryParams }) => { metasContainerRef.current.scrollTop = 0; } }, [discover.catalog]); + React.useEffect(() => { + if (hasNextPage && metasContainerRef.current) { + const containerHeight = metasContainerRef.current.scrollHeight; + const viewportHeight = metasContainerRef.current.clientHeight; + if (containerHeight <= viewportHeight) { + loadNextPage(); + } + } + }, [hasNextPage, loadNextPage]); const selectedMetaItem = React.useMemo(() => { return discover.catalog !== null && discover.catalog.content.type === 'Ready' && From 98784779b53c92e78897bbe99da28016173a1d56 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Mar 2025 19:31:46 +0200 Subject: [PATCH 044/140] refactor(StreamsList): replace Multiselect with MultiselectMenu --- src/routes/MetaDetails/StreamsList/StreamsList.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index bcb5cb015..b30297c0f 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Button, Image, Multiselect } = require('stremio/components'); +const { Button, Image, MultiselectMenu } = require('stremio/components'); const { useServices } = require('stremio/services'); const Stream = require('./Stream'); const styles = require('./styles'); @@ -20,9 +20,9 @@ const StreamsList = ({ className, video, ...props }) => { const profile = useProfile(); const streamsContainerRef = React.useRef(null); const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY); - const onAddonSelected = React.useCallback((event) => { + const onAddonSelected = React.useCallback((value) => { streamsContainerRef.current.scrollTo({ top: 0, left: 0, behavior: platform.name === 'ios' ? 'smooth' : 'instant' }); - setSelectedAddon(event.value); + setSelectedAddon(value); }, [platform]); const showInstallAddonsButton = React.useMemo(() => { return !profile || profile.auth === null || profile.auth?.user?.isNewUser === true; @@ -76,7 +76,6 @@ const StreamsList = ({ className, video, ...props }) => { }, [streamsByAddon, selectedAddon]); const selectableOptions = React.useMemo(() => { return { - title: 'Select Addon', options: [ { value: ALL_ADDONS_KEY, @@ -89,7 +88,10 @@ const StreamsList = ({ className, video, ...props }) => { title: streamsByAddon[transportUrl].addon.manifest.name, })) ], - selected: [selectedAddon], + selectedOption: { + label: selectedAddon === ALL_ADDONS_KEY ? t('ALL_ADDONS') : streamsByAddon[selectedAddon]?.addon.manifest.name, + value: selectedAddon + }, onSelect: onAddonSelected }; }, [streamsByAddon, selectedAddon]); @@ -111,7 +113,7 @@ const StreamsList = ({ className, video, ...props }) => { } { Object.keys(streamsByAddon).length > 1 ? - From 5365c1739ee09b3e75c1ded936d715d937bb9770 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Mar 2025 19:43:04 +0200 Subject: [PATCH 045/140] fix(MultiselectMenu): keep background color when state is open --- src/components/MultiselectMenu/MultiselectMenu.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less index b09a0d916..88ae83d1b 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.less +++ b/src/components/MultiselectMenu/MultiselectMenu.less @@ -34,7 +34,7 @@ } } - &:hover { + &:hover, .open { background-color: var(--overlay-color); } } \ No newline at end of file From a21e5698c8e8fe3e5c28cd3b02b92f82457cea8e Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Mar 2025 20:45:18 +0200 Subject: [PATCH 046/140] refactor(Library): replace Multiselect with MultiselectMenu --- .../MultiselectMenu/MultiselectMenu.less | 2 +- src/components/MultiselectMenu/MultiselectMenu.tsx | 2 +- src/routes/Library/Library.js | 4 ++-- src/routes/Library/styles.less | 1 + src/routes/Library/useSelectableInputs.js | 14 +++++++------- src/routes/MetaDetails/StreamsList/styles.less | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less index 88ae83d1b..c26c2480e 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.less +++ b/src/components/MultiselectMenu/MultiselectMenu.less @@ -34,7 +34,7 @@ } } - &:hover, .open { + &:hover, &.active { background-color: var(--overlay-color); } } \ No newline at end of file diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx index 55b818d80..8f41278e7 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.tsx +++ b/src/components/MultiselectMenu/MultiselectMenu.tsx @@ -27,7 +27,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } }; return ( -
+
+ { state.form === SIGNUP_FORM ?
diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts new file mode 100644 index 000000000..695888bee --- /dev/null +++ b/src/routes/Intro/useAppleLogin.ts @@ -0,0 +1,80 @@ +import { useCallback, useRef } from 'react'; + +type AppleLoginResponse = { + email: string; + password: string; +}; + +type AppleSignInResponse = { + authorization: { + code: string; + id_token: string; + state: string; + }; + user: string; + email?: string; +}; + +const CLIENT_ID = 'com.stremio.one'; + +const useAppleLogin = (): [() => Promise, () => void] => { + const started = useRef(false); + + const start = useCallback((): Promise => { + return new Promise((resolve, reject) => { + if (typeof window.AppleID === 'undefined') { + reject(new Error('Apple Sign-In not loaded')); + return; + } + + if (started.current) { + reject(new Error('Apple login already in progress')); + return; + } + + started.current = true; + + window.AppleID.auth.init({ + clientId: CLIENT_ID, + scope: 'name email', + redirectURI: window.location.origin, + state: 'signin', + usePopup: true + }); + + window.AppleID.auth.signIn() + .then((response: AppleSignInResponse) => { + if (response.authorization) { + const userEmail = response.email || response.user; + + if (!userEmail) { + reject(new Error('No email received from Apple')); + return; + } + + resolve({ + email: userEmail as string, + password: response.authorization.id_token + }); + } else { + reject(new Error('No authorization received from Apple')); + } + }) + .catch((error: Error) => { + console.error('Error during Apple Sign-In:', error); + reject(error); + }) + .finally(() => { + started.current = false; + }); + }); + }, []); + + const stop = useCallback(() => { + started.current = false; + }, []); + + return [start, stop]; +}; + +export default useAppleLogin; diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 3849b8914..4b46cb22d 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -26,6 +26,28 @@ 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; + }; + user: string; + email?: string; + }>; + }; + }; + } } export {}; From 7389e7b4d02a44b26a3402183e0591c33fe0b175 Mon Sep 17 00:00:00 2001 From: Ivelin Megdanov Date: Tue, 25 Mar 2025 12:25:05 +0200 Subject: [PATCH 051/140] Added threshold for next page calculation --- src/routes/Discover/Discover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index f81696246..2c61e4388 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -30,7 +30,7 @@ const Discover = ({ urlParams, queryParams }) => { if (hasNextPage && metasContainerRef.current) { const containerHeight = metasContainerRef.current.scrollHeight; const viewportHeight = metasContainerRef.current.clientHeight; - if (containerHeight <= viewportHeight) { + if (containerHeight <= viewportHeight + SCROLL_TO_BOTTOM_TRESHOLD) { loadNextPage(); } } From 70f5b515d0c9dd5cb3382704b584dd87dfe926ef Mon Sep 17 00:00:00 2001 From: Ivelin Megdanov Date: Tue, 25 Mar 2025 12:27:58 +0200 Subject: [PATCH 052/140] Fix typo in scroll threshold constant name --- src/routes/Discover/Discover.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 2c61e4388..bab40a1c2 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -11,7 +11,7 @@ const useDiscover = require('./useDiscover'); const useSelectableInputs = require('./useSelectableInputs'); const styles = require('./styles'); -const SCROLL_TO_BOTTOM_TRESHOLD = 400; +const SCROLL_TO_BOTTOM_THRESHOLD = 400; const Discover = ({ urlParams, queryParams }) => { const { core } = useServices(); @@ -30,7 +30,7 @@ const Discover = ({ urlParams, queryParams }) => { if (hasNextPage && metasContainerRef.current) { const containerHeight = metasContainerRef.current.scrollHeight; const viewportHeight = metasContainerRef.current.clientHeight; - if (containerHeight <= viewportHeight + SCROLL_TO_BOTTOM_TRESHOLD) { + if (containerHeight <= viewportHeight + SCROLL_TO_BOTTOM_THRESHOLD) { loadNextPage(); } } @@ -85,7 +85,7 @@ const Discover = ({ urlParams, queryParams }) => { loadNextPage(); } }, [hasNextPage, loadNextPage]); - const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_TRESHOLD); + const onScroll = useOnScrollToBottom(onScrollToBottom, SCROLL_TO_BOTTOM_THRESHOLD); React.useEffect(() => { closeInputsModal(); closeAddonModal(); From 56762353f284016d959e776b87feba5bd2c5d1fc Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 25 Mar 2025 18:03:36 +0200 Subject: [PATCH 053/140] refactor(NumberInput): simplify --- src/components/NumberInput/NumberInput.tsx | 82 ++++++++++++---------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 29070c70c..90cb565ac 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 import Icon from '@stremio/stremio-icons/react'; -import React, { ChangeEvent, forwardRef, useCallback, useState } from 'react'; +import React, { ChangeEvent, forwardRef, memo, useCallback, useMemo, useState } from 'react'; import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import styles from './NumberInput.less'; @@ -12,55 +12,61 @@ type Props = InputHTMLAttributes & { className?: string; disabled?: boolean; showButtons?: boolean; - defaultValue?: number; label?: string; min?: number; max?: number; + value?: number; + defaultValue?: number; onKeyDown?: (event: KeyboardEvent) => void; onSubmit?: (event: KeyboardEvent) => void; onUpdate?: (value: number) => void; + onChange?: (event: ChangeEvent) => void; }; -const NumberInput = forwardRef(({ defaultValue, showButtons, onUpdate, ...props }, ref) => { - const [value, setValue] = useState(defaultValue || 0); - const onKeyDown = useCallback((event: KeyboardEvent) => { - props.onKeyDown && props.onKeyDown(event); +const NumberInput = forwardRef(({ defaultValue = 1, showButtons, onUpdate, onKeyDown, onSubmit, min, max, onChange, ...props }, ref) => { + const [value, setValue] = useState(defaultValue); + const displayValue = useMemo(() => props.value ?? value, [props.value, value]); + + const handleKeyDown = useCallback((event: KeyboardEvent) => { + onKeyDown && onKeyDown(event); if (event.key === 'Enter') { - props.onSubmit && props.onSubmit(event); + onSubmit && onSubmit(event); } - }, [props.onKeyDown, props.onSubmit]); + }, [onKeyDown, onSubmit]); - const updateValueAndNotify = (valueAsNumber: number) => { - setValue(valueAsNumber); - onUpdate?.(valueAsNumber); + const handleValueChange = (value: number) => { + if (props.value === undefined) { + setValue(value); + } + onUpdate?.(value); + onChange?.({ target: { value: value.toString() }} as ChangeEvent); }; - const handleIncrease = () => { - if (props.max !== undefined) { - updateValueAndNotify(Math.min(props.max, (value || 0) + 1)); - return; - } - updateValueAndNotify((value || 0) + 1); + const handleIncrement = () => { + handleValueChange(clampValueToRange((displayValue || 0) + 1)); }; - const handleDecrease = () => { - if (props.min !== undefined) { - updateValueAndNotify(Math.max(props.min, (value || 0) - 1)); - return; - } - updateValueAndNotify((value || 0) - 1); + const handleDecrement = () => { + handleValueChange(clampValueToRange((displayValue || 0) - 1)); }; - const handleChange = ({ target: { valueAsNumber }}: ChangeEvent) => { - const min = props.min || 0; - if (valueAsNumber && valueAsNumber < min) { - valueAsNumber = min; + const clampValueToRange = (value: number): number => { + const minValue = min ?? 0; + + if (value < minValue) { + return minValue; } - if (props.max !== undefined && valueAsNumber && valueAsNumber > props.max) { - valueAsNumber = props.max; + + if (max !== undefined && value > max) { + return max; } - updateValueAndNotify(valueAsNumber); + + return value; + }; + + const handleInputChange = ({ target: { valueAsNumber }}: ChangeEvent) => { + handleValueChange(clampValueToRange(valueAsNumber || 0)); }; return ( @@ -69,8 +75,8 @@ const NumberInput = forwardRef(({ defaultValue, showBut showButtons ? : null @@ -85,17 +91,17 @@ const NumberInput = forwardRef(({ defaultValue, showBut ref={ref} type={'number'} tabIndex={0} - value={value} + value={displayValue} {...props} - className={classnames(props.className, styles['value'], { 'disabled': props.disabled })} - onChange={handleChange} - onKeyDown={onKeyDown} + className={classnames(props.className, styles['value'], { [styles.disabled]: props.disabled })} + onChange={handleInputChange} + onKeyDown={handleKeyDown} />
{ showButtons ? : null @@ -106,4 +112,4 @@ const NumberInput = forwardRef(({ defaultValue, showBut NumberInput.displayName = 'NumberInput'; -export default NumberInput; +export default memo(NumberInput); From 60ba559500b0ec0a85fc5466ec1431519b46d73f Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 25 Mar 2025 18:11:14 +0200 Subject: [PATCH 054/140] refactor(EpisodePicker): simplify --- src/components/NumberInput/NumberInput.tsx | 14 ++++++-------- .../EpisodePicker/EpisodePicker.tsx | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 90cb565ac..9298cf8ea 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -12,20 +12,19 @@ type Props = InputHTMLAttributes & { className?: string; disabled?: boolean; showButtons?: boolean; + defaultValue?: number; label?: string; min?: number; max?: number; value?: number; - defaultValue?: number; onKeyDown?: (event: KeyboardEvent) => void; onSubmit?: (event: KeyboardEvent) => void; - onUpdate?: (value: number) => void; onChange?: (event: ChangeEvent) => void; }; -const NumberInput = forwardRef(({ defaultValue = 1, showButtons, onUpdate, onKeyDown, onSubmit, min, max, onChange, ...props }, ref) => { +const NumberInput = forwardRef(({ defaultValue = 0, showButtons, onKeyDown, onSubmit, min, max, onChange, ...props }, ref) => { const [value, setValue] = useState(defaultValue); - const displayValue = useMemo(() => props.value ?? value, [props.value, value]); + const displayValue = props.value ?? value; const handleKeyDown = useCallback((event: KeyboardEvent) => { onKeyDown && onKeyDown(event); @@ -35,12 +34,11 @@ const NumberInput = forwardRef(({ defaultValue = 1, sho } }, [onKeyDown, onSubmit]); - const handleValueChange = (value: number) => { + const handleValueChange = (newValue: number) => { if (props.value === undefined) { - setValue(value); + setValue(newValue); } - onUpdate?.(value); - onChange?.({ target: { value: value.toString() }} as ChangeEvent); + onChange?.({ target: { value: newValue.toString() }} as ChangeEvent); }; const handleIncrement = () => { diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index 4a82ec591..24d12229f 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -1,6 +1,6 @@ // Copyright (C) 2017-2025 Smart code 203358507 -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState, ChangeEvent } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, NumberInput } from 'stremio/components'; import styles from './EpisodePicker.less'; @@ -19,16 +19,21 @@ const EpisodePicker = ({ className, onSubmit }: Props) => { const videoId = decodeURIComponent(splitPath[splitPath.length - 1]); const [, pathSeason, pathEpisode] = videoId ? videoId.split(':') : []; return { - initialSeason: isNaN(parseInt(pathSeason)) ? 0 : parseInt(pathSeason), - initialEpisode: isNaN(parseInt(pathEpisode)) ? 1 : parseInt(pathEpisode) + initialSeason: parseInt(pathSeason) || 0, + initialEpisode: parseInt(pathEpisode) || 1 }; }, []); const [season, setSeason] = useState(initialSeason); const [episode, setEpisode] = useState(initialEpisode); - const handleSeasonChange = useCallback((value: number) => setSeason(!isNaN(value) ? value : 0), []); - const handleEpisodeChange = useCallback((value: number) => setEpisode(!isNaN(value) ? value : 1), []); + const handleSeasonChange = useCallback((event: ChangeEvent) => { + setSeason(parseInt(event.target.value)); + }, []); + + const handleEpisodeChange = useCallback((event: ChangeEvent) => { + setEpisode(parseInt(event.target.value)); + }, []); const handleSubmit = useCallback(() => { if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) { @@ -42,8 +47,8 @@ const EpisodePicker = ({ className, onSubmit }: Props) => { return (
- - + + From cdaad5c83415906ae129208b125e1a6c151157e3 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 25 Mar 2025 18:12:29 +0200 Subject: [PATCH 055/140] fix(NumberInput): lint --- src/components/NumberInput/NumberInput.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 9298cf8ea..7094aca51 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2017-2025 Smart code 203358507 import Icon from '@stremio/stremio-icons/react'; -import React, { ChangeEvent, forwardRef, memo, useCallback, useMemo, useState } from 'react'; +import React, { ChangeEvent, forwardRef, memo, useCallback, useState } from 'react'; import { type KeyboardEvent, type InputHTMLAttributes } from 'react'; import classnames from 'classnames'; import styles from './NumberInput.less'; @@ -25,7 +25,7 @@ type Props = InputHTMLAttributes & { const NumberInput = forwardRef(({ defaultValue = 0, showButtons, onKeyDown, onSubmit, min, max, onChange, ...props }, ref) => { const [value, setValue] = useState(defaultValue); const displayValue = props.value ?? value; - + const handleKeyDown = useCallback((event: KeyboardEvent) => { onKeyDown && onKeyDown(event); @@ -51,15 +51,15 @@ const NumberInput = forwardRef(({ defaultValue = 0, sho const clampValueToRange = (value: number): number => { const minValue = min ?? 0; - + if (value < minValue) { return minValue; } - + if (max !== undefined && value > max) { return max; } - + return value; }; From b2f5fb74c8fddcf3c73b0aa0d5c4b88233521f7c Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 31 Mar 2025 13:51:31 +0300 Subject: [PATCH 056/140] refactor(EpisodePicker): simplify --- .../StreamsList/EpisodePicker/EpisodePicker.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index 24d12229f..e89674cbe 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -35,16 +35,15 @@ const EpisodePicker = ({ className, onSubmit }: Props) => { setEpisode(parseInt(event.target.value)); }, []); - const handleSubmit = useCallback(() => { - if (typeof onSubmit === 'function' && !isNaN(season) && !isNaN(episode)) { - onSubmit(season, episode); - } - }, [onSubmit, season, episode]); - const disabled = useMemo(() => { return season === initialSeason && episode === initialEpisode; }, [season, episode, initialSeason, initialEpisode]); + + const handleSubmit = () => { + onSubmit(season, episode); + }; + return (
From 7915424fa4abd2b0fdfeefed5086a6de0e539b0a Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 31 Mar 2025 13:53:38 +0300 Subject: [PATCH 057/140] fix(EpisodePicker): lint --- .../MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index e89674cbe..f144c2755 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -39,7 +39,6 @@ const EpisodePicker = ({ className, onSubmit }: Props) => { return season === initialSeason && episode === initialEpisode; }, [season, episode, initialSeason, initialEpisode]); - const handleSubmit = () => { onSubmit(season, episode); }; From 4eca979d97a23736b7d41cae7a315ff53096191a Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 1 Apr 2025 11:57:37 +0300 Subject: [PATCH 058/140] refactor(EpisodePicker): simplify --- .../EpisodePicker/EpisodePicker.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx index f144c2755..256c827a9 100644 --- a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx +++ b/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx @@ -35,19 +35,33 @@ const EpisodePicker = ({ className, onSubmit }: Props) => { setEpisode(parseInt(event.target.value)); }, []); - const disabled = useMemo(() => { - return season === initialSeason && episode === initialEpisode; - }, [season, episode, initialSeason, initialEpisode]); - const handleSubmit = () => { onSubmit(season, episode); }; + const disabled = season === initialSeason && episode === initialEpisode; + return (
- - -
From e156c27b64949e78858516cc936c42ef82a18f36 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 1 Apr 2025 11:57:45 +0300 Subject: [PATCH 059/140] refactor(NumberInput): simplify --- src/components/NumberInput/NumberInput.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/NumberInput/NumberInput.tsx b/src/components/NumberInput/NumberInput.tsx index 7094aca51..a286decf4 100644 --- a/src/components/NumberInput/NumberInput.tsx +++ b/src/components/NumberInput/NumberInput.tsx @@ -27,10 +27,10 @@ const NumberInput = forwardRef(({ defaultValue = 0, sho const displayValue = props.value ?? value; const handleKeyDown = useCallback((event: KeyboardEvent) => { - onKeyDown && onKeyDown(event); + onKeyDown?.(event); if (event.key === 'Enter') { - onSubmit && onSubmit(event); + onSubmit?.(event); } }, [onKeyDown, onSubmit]); @@ -63,9 +63,9 @@ const NumberInput = forwardRef(({ defaultValue = 0, sho return value; }; - const handleInputChange = ({ target: { valueAsNumber }}: ChangeEvent) => { + const handleInputChange = useCallback(({ target: { valueAsNumber }}: ChangeEvent) => { handleValueChange(clampValueToRange(valueAsNumber || 0)); - }; + }, []); return (
From bbb26717c5e3afbc3ba1fd332991d99a75fa85cf Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 1 Apr 2025 16:36:00 +0200 Subject: [PATCH 060/140] feat(shell): implement pause on minimize setting --- src/App/App.js | 13 ++----- src/common/useFullscreen.ts | 4 +-- src/common/useShell.ts | 33 +++++++++++++++-- src/routes/Player/Player.js | 35 +++++++++++-------- src/routes/Settings/Settings.js | 13 +++++++ .../Settings/useProfileSettingsInputs.js | 16 +++++++++ 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 803515b09..38be271ac 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -21,7 +21,6 @@ const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)) const App = () => { const { i18n } = useTranslation(); const shell = useShell(); - const [windowHidden, setWindowHidden] = React.useState(false); const onPathNotMatch = React.useCallback(() => { return NotFound; }, []); @@ -102,10 +101,6 @@ const App = () => { // Handle shell events React.useEffect(() => { - const onWindowVisibilityChanged = (state) => { - setWindowHidden(state.visible === false && state.visibility === 0); - }; - const onOpenMedia = (data) => { if (data.startsWith('stremio:///')) return; if (data.startsWith('stremio://')) { @@ -116,11 +111,9 @@ const App = () => { } }; - shell.on('win-visibility-changed', onWindowVisibilityChanged); shell.on('open-media', onOpenMedia); return () => { - shell.off('win-visibility-changed', onWindowVisibilityChanged); shell.off('open-media', onOpenMedia); }; }, []); @@ -133,7 +126,7 @@ const App = () => { i18n.changeLanguage(args.settings.interfaceLanguage); } - if (args?.settings?.quitOnClose && windowHidden) { + if (args?.settings?.quitOnClose && shell.windowClosed) { shell.send('quit'); } @@ -146,7 +139,7 @@ const App = () => { i18n.changeLanguage(state.profile.settings.interfaceLanguage); } - if (state?.profile?.settings?.quitOnClose && windowHidden) { + if (state?.profile?.settings?.quitOnClose && shell.windowClosed) { shell.send('quit'); } }; @@ -191,7 +184,7 @@ const App = () => { services.core.transport.off('CoreEvent', onCoreEvent); } }; - }, [initialized, windowHidden]); + }, [initialized, shell.windowClosed]); return ( diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index d81003266..b63fb9dd2 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -1,7 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 import { useCallback, useEffect, useState } from 'react'; -import useShell, { type WindowVisibilityState } from './useShell'; +import useShell, { type WindowVisibility } from './useShell'; import useSettings from './useSettings'; const useFullscreen = () => { @@ -31,7 +31,7 @@ const useFullscreen = () => { }, [fullscreen]); useEffect(() => { - const onWindowVisibilityChanged = (state: WindowVisibilityState) => { + const onWindowVisibilityChanged = (state: WindowVisibility) => { setFullscreen(state.isFullscreen === true); }; diff --git a/src/common/useShell.ts b/src/common/useShell.ts index 7ef7ce0a4..0471e38ab 100644 --- a/src/common/useShell.ts +++ b/src/common/useShell.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import EventEmitter from 'eventemitter3'; const SHELL_EVENT_OBJECT = 'transport'; @@ -17,13 +17,22 @@ type ShellEvent = { args: string[]; }; -export type WindowVisibilityState = { +export type WindowVisibility = { + visible: boolean; + visibility: number; isFullscreen: boolean; }; +export type WindowState = { + state: number; +}; + const createId = () => Math.floor(Math.random() * 9999) + 1; const useShell = () => { + const [windowClosed, setWindowClosed] = useState(false); + const [windowHidden, setWindowHidden] = useState(false); + const on = (name: string, listener: (arg: any) => void) => { events.on(name, listener); }; @@ -46,6 +55,24 @@ const useShell = () => { } }; + useEffect(() => { + const onWindowVisibilityChanged = (data: WindowVisibility) => { + setWindowClosed(data.visible === false && data.visibility === 0); + }; + + const onWindowStateChanged = (data: WindowState) => { + setWindowHidden(data.state === 9); + }; + + on('win-visibility-changed', onWindowVisibilityChanged); + on('win-state-changed', onWindowStateChanged); + + return () => { + off('win-visibility-changed', onWindowVisibilityChanged); + off('win-state-changed', onWindowStateChanged); + }; + }, []); + useEffect(() => { if (!transport) return; @@ -70,6 +97,8 @@ const useShell = () => { send, on, off, + windowClosed, + windowHidden, }; }; diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index e631e3000..9c9ae5e92 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,7 +8,7 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS } = require('stremio/common'); +const { onFileDrop, useSettings, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell } = require('stremio/common'); const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components'); const BufferingLoader = require('./BufferingLoader'); const VolumeChangeIndicator = require('./VolumeChangeIndicator'); @@ -30,7 +30,8 @@ const Video = require('./Video'); const Player = ({ urlParams, queryParams }) => { const { t } = useTranslation(); - const { chromecast, shell, core } = useServices(); + const services = useServices(); + const shell = useShell(); const forceTranscoding = React.useMemo(() => { return queryParams.has('forceTranscoding'); }, [queryParams]); @@ -46,7 +47,7 @@ const Player = ({ urlParams, queryParams }) => { const [seeking, setSeeking] = React.useState(false); const [casting, setCasting] = React.useState(() => { - return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED; + return services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED; }); const playbackDevices = React.useMemo(() => streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [], [streamingServer]); @@ -320,8 +321,8 @@ const Player = ({ urlParams, queryParams }) => { null, seriesInfo: player.seriesInfo, }, { - chromecastTransport: chromecast.active ? chromecast.transport : null, - shellTransport: shell.active ? shell.transport : null, + chromecastTransport: services.chromecast.active ? services.chromecast.transport : null, + shellTransport: services.shell.active ? services.shell.transport : null, }); } }, [streamingServer.baseUrl, player.selected, forceTranscoding, casting]); @@ -442,12 +443,12 @@ const Player = ({ urlParams, queryParams }) => { const toastFilter = (item) => item?.dataset?.type === 'CoreEvent'; toast.addFilter(toastFilter); const onCastStateChange = () => { - setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED); + setCasting(services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED); }; const onChromecastServiceStateChange = () => { onCastStateChange(); - if (chromecast.active) { - chromecast.transport.on( + if (services.chromecast.active) { + services.chromecast.transport.on( cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChange ); @@ -458,15 +459,15 @@ const Player = ({ urlParams, queryParams }) => { onPauseRequested(); } }; - chromecast.on('stateChanged', onChromecastServiceStateChange); - core.transport.on('CoreEvent', onCoreEvent); + services.chromecast.on('stateChanged', onChromecastServiceStateChange); + services.core.transport.on('CoreEvent', onCoreEvent); onChromecastServiceStateChange(); return () => { toast.removeFilter(toastFilter); - chromecast.off('stateChanged', onChromecastServiceStateChange); - core.transport.off('CoreEvent', onCoreEvent); - if (chromecast.active) { - chromecast.transport.off( + services.chromecast.off('stateChanged', onChromecastServiceStateChange); + services.core.transport.off('CoreEvent', onCoreEvent); + if (services.chromecast.active) { + services.chromecast.transport.off( cast.framework.CastContextEventType.CAST_STATE_CHANGED, onCastStateChange ); @@ -474,6 +475,12 @@ const Player = ({ urlParams, queryParams }) => { }; }, []); + React.useEffect(() => { + if (settings.pauseOnMinimize && (shell.windowClosed || shell.windowHidden)) { + onPauseRequested(); + } + }, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]); + React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 1d695c177..d6fe9b7a4 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -48,6 +48,7 @@ const Settings = () => { bingeWatchingToggle, playInBackgroundToggle, hardwareDecodingToggle, + pauseOnMinimizeToggle, } = useProfileSettingsInputs(profile); const { streamingServerRemoteUrlInput, @@ -529,6 +530,18 @@ const Settings = () => { />
} + { + shell.active && +
+
+
{ t('SETTINGS_PAUSE_MINIMIZED') }
+
+ +
+ }
{ t('SETTINGS_NAV_STREAMING') }
diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index afad298ed..2a31fc254 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -339,6 +339,21 @@ const useProfileSettingsInputs = (profile) => { }); } }), [profile.settings]); + const pauseOnMinimizeToggle = React.useMemo(() => ({ + checked: profile.settings.pauseOnMinimize, + onClick: () => { + core.transport.dispatch({ + action: 'Ctx', + args: { + action: 'UpdateSettings', + args: { + ...profile.settings, + pauseOnMinimize: !profile.settings.pauseOnMinimize, + } + } + }); + } + }), [profile.settings]); return { interfaceLanguageSelect, hideSpoilersToggle, @@ -358,6 +373,7 @@ const useProfileSettingsInputs = (profile) => { bingeWatchingToggle, playInBackgroundToggle, hardwareDecodingToggle, + pauseOnMinimizeToggle, }; }; From 639d5f8d1c0bcb7678b7413675efcc22a20b0085 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 2 Apr 2025 14:25:41 +0300 Subject: [PATCH 061/140] refactor(EpisodePicker): moved EpisodePicker to higher level in MetaDetails folder --- .../{StreamsList => }/EpisodePicker/EpisodePicker.less | 0 .../{StreamsList => }/EpisodePicker/EpisodePicker.tsx | 0 src/routes/MetaDetails/{StreamsList => }/EpisodePicker/index.ts | 0 src/routes/MetaDetails/StreamsList/StreamsList.js | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename src/routes/MetaDetails/{StreamsList => }/EpisodePicker/EpisodePicker.less (100%) rename src/routes/MetaDetails/{StreamsList => }/EpisodePicker/EpisodePicker.tsx (100%) rename src/routes/MetaDetails/{StreamsList => }/EpisodePicker/index.ts (100%) diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.less similarity index 100% rename from src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.less rename to src/routes/MetaDetails/EpisodePicker/EpisodePicker.less diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx b/src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx similarity index 100% rename from src/routes/MetaDetails/StreamsList/EpisodePicker/EpisodePicker.tsx rename to src/routes/MetaDetails/EpisodePicker/EpisodePicker.tsx diff --git a/src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts b/src/routes/MetaDetails/EpisodePicker/index.ts similarity index 100% rename from src/routes/MetaDetails/StreamsList/EpisodePicker/index.ts rename to src/routes/MetaDetails/EpisodePicker/index.ts diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 2d44d35fc..82ba57a51 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -10,7 +10,7 @@ const { useServices } = require('stremio/services'); const Stream = require('./Stream'); const styles = require('./styles'); const { usePlatform, useProfile } = require('stremio/common'); -const { default: SeasonEpisodePicker } = require('./EpisodePicker'); +const { default: SeasonEpisodePicker } = require('../EpisodePicker'); const ALL_ADDONS_KEY = 'ALL'; From 7c932a93e57a0eb3bbf99b9f8c0f4c08fc18d757 Mon Sep 17 00:00:00 2001 From: Botzy Date: Wed, 2 Apr 2025 14:35:04 +0300 Subject: [PATCH 062/140] feat(MetaDetails): added season picker when no metadetails loaded in videos list --- src/routes/MetaDetails/VideosList/VideosList.js | 11 +++++++++++ src/routes/MetaDetails/VideosList/styles.less | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 58614c9d9..a47b2f517 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -7,6 +7,7 @@ const { t } = require('i18next'); const { useServices } = require('stremio/services'); const { Image, SearchBar, Toggle, Video } = require('stremio/components'); const SeasonsBar = require('./SeasonsBar'); +const { default: EpisodePicker } = require('../EpisodePicker'); const styles = require('./styles'); const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, toggleNotifications }) => { @@ -92,6 +93,15 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, }); }; + const onSeasonSearch = (value) => { + if (value) { + seasonOnSelect({ + type: 'select', + value, + }); + } + }; + return (
{ @@ -110,6 +120,7 @@ const VideosList = ({ className, metaItem, libraryItem, season, seasonOnSelect, : metaItem.content.type === 'Err' || videosForSeason.length === 0 ?
+ {'
No videos found for this meta!
diff --git a/src/routes/MetaDetails/VideosList/styles.less b/src/routes/MetaDetails/VideosList/styles.less index 9f9a7edee..22d51116c 100644 --- a/src/routes/MetaDetails/VideosList/styles.less +++ b/src/routes/MetaDetails/VideosList/styles.less @@ -13,10 +13,13 @@ display: flex; flex-direction: column; align-items: center; - justify-content: center; padding: 2rem; overflow-y: auto; + .episode-picker { + margin-bottom: 2rem; + } + .image { flex: none; width: 10rem; From f451af161c8f14909eff62081bbb97f3ab291574 Mon Sep 17 00:00:00 2001 From: Timothy Z Date: Wed, 2 Apr 2025 15:26:31 +0300 Subject: [PATCH 063/140] chore: v5.0.0-beta.21 --- 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 ca8e10d52..3592aca2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "license": "gpl-2.0", "dependencies": { "@babel/runtime": "7.26.0", diff --git a/package.json b/package.json index cfc412be4..8eebb2685 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "stremio", "displayName": "Stremio", - "version": "5.0.0-beta.20", + "version": "5.0.0-beta.21", "author": "Smart Code OOD", "private": true, "license": "gpl-2.0", From 9aa0490989bfe63ae5a2b26d5064353f9649b3ca Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 4 Apr 2025 18:58:36 +0200 Subject: [PATCH 064/140] fix(App): non latin chars font --- src/App/styles.less | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/App/styles.less b/src/App/styles.less index 50819f883..5365e8dc3 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -151,14 +151,13 @@ svg { html { width: @html-width; height: @html-height; - font-family: 'PlusJakartaSans', 'sans-serif'; + font-family: 'PlusJakartaSans', 'Arial', 'Helvetica', 'sans-serif'; overflow: auto; overscroll-behavior: none; user-select: none; touch-action: manipulation; -webkit-tap-highlight-color: transparent; - @media (display-mode: standalone) { width: @html-standalone-width; height: @html-standalone-height; From 2ef3f52c1cba8fa31c32a17d7a12f1b3ea64d58b Mon Sep 17 00:00:00 2001 From: Alexandru Branza Date: Tue, 8 Apr 2025 16:55:36 +0300 Subject: [PATCH 065/140] Fix Font Weight for Safari --- src/App/styles.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/App/styles.less b/src/App/styles.less index 5365e8dc3..373b46900 100644 --- a/src/App/styles.less +++ b/src/App/styles.less @@ -167,6 +167,7 @@ html { width: 100%; height: 100%; background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%); + -webkit-font-smoothing: antialiased; :global(#app) { position: relative; From 592fb17fa14b63793da74f4befcbdd357c0f1885 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 12:42:41 +0300 Subject: [PATCH 066/140] refactor(Apple login): support new endpoint --- src/routes/Intro/Intro.js | 9 +++++---- src/routes/Intro/useAppleLogin.ts | 28 ++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 0613e1490..d20e3ea60 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -112,16 +112,17 @@ const Intro = ({ queryParams }) => { const loginWithApple = React.useCallback(() => { openLoaderModal(); startAppleLogin() - .then(({ email, password }) => { + .then(({ email, token, sub, name }) => { core.transport.dispatch({ action: 'Ctx', args: { action: 'Authenticate', args: { - type: 'Login', + type: 'AuthWithApple', + token, + sub, email, - password, - apple: true + name } } }); diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 695888bee..a3ec8fa38 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -1,8 +1,10 @@ import { useCallback, useRef } from 'react'; type AppleLoginResponse = { + token: string; + sub: string; email: string; - password: string; + name: string; }; type AppleSignInResponse = { @@ -13,6 +15,10 @@ type AppleSignInResponse = { }; user: string; email?: string; + fullName?: { + firstName?: string; + lastName?: string; + }; }; const CLIENT_ID = 'com.stremio.one'; @@ -45,16 +51,26 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.signIn() .then((response: AppleSignInResponse) => { if (response.authorization) { - const userEmail = response.email || response.user; + const email = response.email || ''; + const sub = response.user; + + let name = ''; + if (response.fullName) { + const firstName = response.fullName.firstName || ''; + const lastName = response.fullName.lastName || ''; + name = [firstName, lastName].filter(Boolean).join(' '); + } - if (!userEmail) { - reject(new Error('No email received from Apple')); + if (!sub) { + reject(new Error('No sub token received from Apple')); return; } resolve({ - email: userEmail as string, - password: response.authorization.id_token + token: response.authorization.id_token, + sub: sub, + email: email, + name: name }); } else { reject(new Error('No authorization received from Apple')); From a0fa5e2a924f7463cc08ab3dd7c97d52a74a501e Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 12:45:06 +0300 Subject: [PATCH 067/140] fix(chore): lint --- src/routes/Intro/useAppleLogin.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index a3ec8fa38..d600f4c13 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -45,15 +45,16 @@ const useAppleLogin = (): [() => Promise, () => void] => { scope: 'name email', redirectURI: window.location.origin, state: 'signin', - usePopup: true + usePopup: true, }); - window.AppleID.auth.signIn() + window.AppleID.auth + .signIn() .then((response: AppleSignInResponse) => { if (response.authorization) { const email = response.email || ''; const sub = response.user; - + let name = ''; if (response.fullName) { const firstName = response.fullName.firstName || ''; @@ -70,7 +71,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { token: response.authorization.id_token, sub: sub, email: email, - name: name + name: name, }); } else { reject(new Error('No authorization received from Apple')); From d457db6f1e5d3a93e181a688e01706d42fd97c6c Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 12:52:56 +0300 Subject: [PATCH 068/140] refactor(useapplelogin): update redirect 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 d600f4c13..e6508df23 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -43,7 +43,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.init({ clientId: CLIENT_ID, scope: 'name email', - redirectURI: window.location.origin, + redirectURI: 'https://www.stremio.com/login', state: 'signin', usePopup: true, }); From 200d3a76d8628f073a77a33e6a29139d90beb20b Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 14:20:23 +0300 Subject: [PATCH 069/140] fix(useapplelogin): use correct client id --- 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 e6508df23..5ecb88c98 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -21,7 +21,7 @@ type AppleSignInResponse = { }; }; -const CLIENT_ID = 'com.stremio.one'; +const CLIENT_ID = 'com.stremio.services'; const useAppleLogin = (): [() => Promise, () => void] => { const started = useRef(false); From 96f0baadc20d7fe4f5e0da8d97b7c8729e8fef33 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 16:46:19 +0300 Subject: [PATCH 070/140] fix(Player): on options menu open crash --- src/routes/Player/ControlBar/ControlBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index e336aaa1b..ba721036c 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -176,7 +176,7 @@ const ControlBar = ({ : null } -
From 6b9a3a91c79fc122e1c1510feb87976582741db3 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 10 Apr 2025 19:23:23 +0200 Subject: [PATCH 071/140] chore: update stremio-video --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3592aca2b..b052d8342 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.49.2", "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.53", + "@stremio/stremio-video": "0.0.60", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", @@ -3409,9 +3409,10 @@ ] }, "node_modules/@stremio/stremio-video": { - "version": "0.0.53", - "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.53.tgz", - "integrity": "sha512-hSlk8GqMdk4N8VbcdvduYqWVZsQLgHyU7GfFmd1k+t0pSpDKAhI3C6dohG5Sr09CKCjHa8D1rls+CwMNPXLSGw==", + "version": "0.0.60", + "resolved": "https://registry.npmjs.org/@stremio/stremio-video/-/stremio-video-0.0.60.tgz", + "integrity": "sha512-RbmSi+Lk+3pb6f2ZkGVCnoMoJoujvVvSLDHiLGkXnzQwjYf2B2022NKlAQmHRuHN1sjD+VEsKD8foQH4hXGG1A==", + "license": "MIT", "dependencies": { "buffer": "6.0.3", "color": "4.2.3", diff --git a/package.json b/package.json index 8eebb2685..f7fc127a1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@stremio/stremio-colors": "5.2.0", "@stremio/stremio-core-web": "0.49.2", "@stremio/stremio-icons": "5.4.1", - "@stremio/stremio-video": "0.0.53", + "@stremio/stremio-video": "0.0.60", "a-color-picker": "1.2.1", "bowser": "2.11.0", "buffer": "6.0.3", From a1acb7423a9089d2f44ad03ce92bbddfc3b39ccc Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 20:40:05 +0300 Subject: [PATCH 072/140] fix(useapplelogin): auth response --- src/routes/Intro/useAppleLogin.ts | 9 +++++---- src/types/global.d.ts | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 5ecb88c98..b73cb4d09 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -9,11 +9,11 @@ type AppleLoginResponse = { type AppleSignInResponse = { authorization: { - code: string; id_token: string; - state: string; }; - user: string; + authorizedData: { + userId: string; + }; email?: string; fullName?: { firstName?: string; @@ -52,8 +52,9 @@ const useAppleLogin = (): [() => Promise, () => void] => { .signIn() .then((response: AppleSignInResponse) => { if (response.authorization) { + console.log('Apple Sign-In response:', response); // eslint-disable-line no-console const email = response.email || ''; - const sub = response.user; + const sub = response.authorizedData.userId; let name = ''; if (response.fullName) { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 4b46cb22d..f1c3eeecc 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -38,9 +38,10 @@ declare global { }) => void; signIn: () => Promise<{ authorization: { - code: string; id_token: string; - state: string; + }; + authorizedData: { + userId: string; }; user: string; email?: string; From 50edda25576b0a6e8524cc37b45e35dbfb41d941 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 20:45:51 +0300 Subject: [PATCH 073/140] fix(useapplelogin): use redirect uri for testing --- 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 b73cb4d09..29e385f45 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -43,7 +43,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.init({ clientId: CLIENT_ID, scope: 'name email', - redirectURI: 'https://www.stremio.com/login', + redirectURI: 'https://stremio.github.io/stremio-web/feat/example-apple-login/', state: 'signin', usePopup: true, }); From 846445001c637c704e50b40924b817de6b9066bb Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 21:10:50 +0300 Subject: [PATCH 074/140] fix(useapplelogin): get sub id from token --- package-lock.json | 10 ++++++++ package.json | 1 + src/routes/Intro/useAppleLogin.ts | 39 ++++++++++++++----------------- src/types/global.d.ts | 5 ++-- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3592aca2b..07806b2a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "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", @@ -10233,6 +10234,15 @@ "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 8eebb2685..e5386fbea 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "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/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 29e385f45..c8c752fea 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -1,4 +1,5 @@ import { useCallback, useRef } from 'react'; +import jwtDecode from 'jwt-decode'; type AppleLoginResponse = { token: string; @@ -9,10 +10,9 @@ type AppleLoginResponse = { type AppleSignInResponse = { authorization: { + code?: string; id_token: string; - }; - authorizedData: { - userId: string; + state?: string; }; email?: string; fullName?: { @@ -20,7 +20,6 @@ type AppleSignInResponse = { lastName?: string; }; }; - const CLIENT_ID = 'com.stremio.services'; const useAppleLogin = (): [() => Promise, () => void] => { @@ -48,13 +47,15 @@ const useAppleLogin = (): [() => Promise, () => void] => { usePopup: true, }); - window.AppleID.auth - .signIn() - .then((response: AppleSignInResponse) => { - if (response.authorization) { - console.log('Apple Sign-In response:', response); // eslint-disable-line no-console + window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { + if (response.authorization) { + console.log('Apple Sign-In response:', response); // eslint-disable-line no-console + + try { + const idToken = response.authorization.id_token; const email = response.email || ''; - const sub = response.authorizedData.userId; + const payload = jwtDecode.jwtDecode(response.authorization.id_token); + const sub = payload.sub; let name = ''; if (response.fullName) { @@ -69,22 +70,18 @@ const useAppleLogin = (): [() => Promise, () => void] => { } resolve({ - token: response.authorization.id_token, + token: idToken, sub: sub, email: email, name: name, }); - } else { - reject(new Error('No authorization received from Apple')); + } catch (error) { + reject(new Error(`Failed to parse id_token: ${error}`)); } - }) - .catch((error: Error) => { - console.error('Error during Apple Sign-In:', error); - reject(error); - }) - .finally(() => { - started.current = false; - }); + } else { + reject(new Error('No authorization received from Apple')); + } + }); }); }, []); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index f1c3eeecc..82b4c55cf 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -38,10 +38,9 @@ declare global { }) => void; signIn: () => Promise<{ authorization: { + code?: string; id_token: string; - }; - authorizedData: { - userId: string; + state?: string; }; user: string; email?: string; From b86887e111a9f6177417b725106833fadae2ca77 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 21:11:41 +0300 Subject: [PATCH 075/140] chore: add testing logs --- src/routes/Intro/useAppleLogin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index c8c752fea..6f934a1c6 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -55,6 +55,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { const idToken = response.authorization.id_token; const email = response.email || ''; const payload = jwtDecode.jwtDecode(response.authorization.id_token); + console.log('Decoded id_token:', payload); // eslint-disable-line no-console const sub = payload.sub; let name = ''; From 2713c8b46d5a174995c0ed369933b0db3aaa7013 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 21:31:34 +0300 Subject: [PATCH 076/140] fix(useapplelogin): jwt errors --- src/routes/Intro/useAppleLogin.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 6f934a1c6..ec86700fc 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -1,5 +1,5 @@ import { useCallback, useRef } from 'react'; -import jwtDecode from 'jwt-decode'; +import { jwtDecode, JwtPayload } from 'jwt-decode'; type AppleLoginResponse = { token: string; @@ -20,6 +20,12 @@ type AppleSignInResponse = { lastName?: string; }; }; + +type CustomJWTPayload = JwtPayload & { + sub: string; + email?: string; +}; + const CLIENT_ID = 'com.stremio.services'; const useAppleLogin = (): [() => Promise, () => void] => { @@ -49,14 +55,14 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { if (response.authorization) { - console.log('Apple Sign-In response:', response); // eslint-disable-line no-console + console.log('Apple Sign-In response:', response.authorization); // eslint-disable-line no-console try { const idToken = response.authorization.id_token; - const email = response.email || ''; - const payload = jwtDecode.jwtDecode(response.authorization.id_token); + const payload: CustomJWTPayload = jwtDecode(idToken); console.log('Decoded id_token:', payload); // eslint-disable-line no-console const sub = payload.sub; + const email = payload.email ?? response.email ?? ''; let name = ''; if (response.fullName) { From 2ce16193131be2ead5b7c7b78b80d7a83bba8a49 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 21:52:57 +0300 Subject: [PATCH 077/140] chore: remove logs & use correct core action --- src/routes/Intro/Intro.js | 2 +- src/routes/Intro/useAppleLogin.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index d20e3ea60..136d4ccea 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -118,7 +118,7 @@ const Intro = ({ queryParams }) => { args: { action: 'Authenticate', args: { - type: 'AuthWithApple', + type: 'Apple', token, sub, email, diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index ec86700fc..7d5465838 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -22,7 +22,6 @@ type AppleSignInResponse = { }; type CustomJWTPayload = JwtPayload & { - sub: string; email?: string; }; @@ -55,12 +54,9 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { if (response.authorization) { - console.log('Apple Sign-In response:', response.authorization); // eslint-disable-line no-console - try { const idToken = response.authorization.id_token; const payload: CustomJWTPayload = jwtDecode(idToken); - console.log('Decoded id_token:', payload); // eslint-disable-line no-console const sub = payload.sub; const email = payload.email ?? response.email ?? ''; From 5acc32411dc0869d22757e83c64584073c7cb74b Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 21:54:11 +0300 Subject: [PATCH 078/140] fix(app): apple login types --- src/types/global.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 82b4c55cf..e55146e9a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -42,8 +42,11 @@ declare global { id_token: string; state?: string; }; - user: string; email?: string; + fullName?: { + firstName?: string; + lastName?: string; + }; }>; }; }; From 43c76b6a4b42af5c4aff4b001d79bf89ddd109b3 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 10 Apr 2025 22:48:56 +0300 Subject: [PATCH 079/140] refactor(Intro): adjust styles --- src/routes/Intro/Intro.js | 2 +- src/routes/Intro/styles.less | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 136d4ccea..01ca5eab7 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -381,7 +381,7 @@ const Intro = ({ queryParams }) => {
Continue with Facebook
{ diff --git a/src/routes/Intro/styles.less b/src/routes/Intro/styles.less index 935b0fad1..335ab9a5c 100644 --- a/src/routes/Intro/styles.less +++ b/src/routes/Intro/styles.less @@ -175,15 +175,27 @@ position: relative; width: 22rem; margin-left: 2rem; + display: flex; + flex-direction: column; .facebook-button { background: var(--color-facebook); + margin-bottom: 1rem; &:hover, &:focus { outline: var(--focus-outline-size) solid var(--color-facebook); background-color: transparent; } } + + .apple-button { + background: var(--color-x); + + &:hover, &:focus { + outline: var(--focus-outline-size) solid var(--color-x); + background-color: transparent; + } + } } } } From 09c1b0c45eb9494e434cd65a7897f659c7d434a7 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 13:12:08 +0300 Subject: [PATCH 080/140] chore(core-web): v0.49.3 --- package-lock.json | 44 ++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07806b2a5..195ac30d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.49.2", + "@stremio/stremio-core-web": "0.49.3", "@stremio/stremio-icons": "5.4.1", "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", @@ -79,6 +79,21 @@ "workbox-webpack-plugin": "^7.3.0" } }, + "../stremio-core/stremio-core-web": { + "name": "@stremio/stremio-core-web", + "version": "0.49.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "7.24.1" + }, + "devDependencies": { + "@babel/cli": "7.24.1", + "@babel/core": "7.24.3", + "@babel/plugin-transform-runtime": "7.24.3", + "@babel/preset-env": "7.24.3", + "babel-plugin-bundled-import-meta": "0.3.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3372,31 +3387,8 @@ "integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==" }, "node_modules/@stremio/stremio-core-web": { - "version": "0.49.2", - "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.49.2.tgz", - "integrity": "sha512-IYU+pdHkq4iEfqZ9G+DFZheIE53nY8XyhI1OJLvZp68/4ntRwssXwfj9InHK2Wau20fH+oV2KD1ZWb0CsTLqPA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "7.24.1" - } - }, - "node_modules/@stremio/stremio-core-web/node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@stremio/stremio-core-web/node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" + "resolved": "../stremio-core/stremio-core-web", + "link": true }, "node_modules/@stremio/stremio-icons": { "version": "5.4.1", diff --git a/package.json b/package.json index e5386fbea..b04b5aaae 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "@babel/runtime": "7.26.0", "@sentry/browser": "8.42.0", "@stremio/stremio-colors": "5.2.0", - "@stremio/stremio-core-web": "0.49.2", + "@stremio/stremio-core-web": "0.49.3", "@stremio/stremio-icons": "5.4.1", "@stremio/stremio-video": "0.0.53", "a-color-picker": "1.2.1", From befcef6dd2803f6772bda5609993e5c1a7c46f2e Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 13:18:12 +0300 Subject: [PATCH 081/140] fix: update core-web --- package-lock.json | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 195ac30d5..89bc98d3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,21 +79,6 @@ "workbox-webpack-plugin": "^7.3.0" } }, - "../stremio-core/stremio-core-web": { - "name": "@stremio/stremio-core-web", - "version": "0.49.3", - "license": "MIT", - "dependencies": { - "@babel/runtime": "7.24.1" - }, - "devDependencies": { - "@babel/cli": "7.24.1", - "@babel/core": "7.24.3", - "@babel/plugin-transform-runtime": "7.24.3", - "@babel/preset-env": "7.24.3", - "babel-plugin-bundled-import-meta": "0.3.2" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3387,8 +3372,31 @@ "integrity": "sha512-dYlPgu9W/H7c9s1zmW5tiDnRenaUa4Hg1QCyOg1lhOcgSfM/bVTi5nnqX+IfvGTTUNA0zgzh8hI3o3miwnZxTg==" }, "node_modules/@stremio/stremio-core-web": { - "resolved": "../stremio-core/stremio-core-web", - "link": true + "version": "0.49.3", + "resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.49.3.tgz", + "integrity": "sha512-Ql/08LbwU99IUL6fOLy+v1Iv75boHXpunEPScKgXJALdq/OV5tZLG/IycN0O+5+50Nc/NHrI6HslnMNLTWA8JQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "7.24.1" + } + }, + "node_modules/@stremio/stremio-core-web/node_modules/@babel/runtime": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", + "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@stremio/stremio-core-web/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/@stremio/stremio-icons": { "version": "5.4.1", From 0c3b7e8d4a217f983bc8adc5b50364b96959f6b0 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 13:36:40 +0300 Subject: [PATCH 082/140] chore: add debug logs --- src/routes/Intro/useAppleLogin.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 7d5465838..5fc40e52c 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -72,6 +72,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { return; } + console.log('Apple login response:', payload, idToken, sub, email, name); // eslint-disable-line no-console resolve({ token: idToken, sub: sub, From 468dc604ae2b4fcea74da1b2b9ea2630728871d7 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 14:12:05 +0300 Subject: [PATCH 083/140] Revert "chore: add debug logs" This reverts commit 0c3b7e8d4a217f983bc8adc5b50364b96959f6b0. --- 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 5fc40e52c..7d5465838 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -72,7 +72,6 @@ const useAppleLogin = (): [() => Promise, () => void] => { return; } - console.log('Apple login response:', payload, idToken, sub, email, name); // eslint-disable-line no-console resolve({ token: idToken, sub: sub, From e454cecc451629d5f3d6720c2dfc5e92490faa11 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 14:38:19 +0300 Subject: [PATCH 084/140] fix(useapplelogin): remove state signin --- src/routes/Intro/useAppleLogin.ts | 1 - src/types/global.d.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 7d5465838..58c83f127 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -48,7 +48,6 @@ const useAppleLogin = (): [() => Promise, () => void] => { clientId: CLIENT_ID, scope: 'name email', redirectURI: 'https://stremio.github.io/stremio-web/feat/example-apple-login/', - state: 'signin', usePopup: true, }); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index e55146e9a..5697dc574 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -33,7 +33,6 @@ declare global { clientId: string; scope: string; redirectURI: string; - state: string; usePopup: boolean; }) => void; signIn: () => Promise<{ From 52dc7722ad45a86cbafa4c3f8032ffd92191214f Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 14:53:44 +0300 Subject: [PATCH 085/140] Revert "fix(useapplelogin): remove state signin" This reverts commit e454cecc451629d5f3d6720c2dfc5e92490faa11. --- src/routes/Intro/useAppleLogin.ts | 1 + src/types/global.d.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 58c83f127..7d5465838 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -48,6 +48,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { clientId: CLIENT_ID, scope: 'name email', redirectURI: 'https://stremio.github.io/stremio-web/feat/example-apple-login/', + state: 'signin', usePopup: true, }); diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 5697dc574..e55146e9a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -33,6 +33,7 @@ declare global { clientId: string; scope: string; redirectURI: string; + state: string; usePopup: boolean; }) => void; signIn: () => Promise<{ From 922de40134d1dbedcd6fb73cae29495271b17ec3 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 16:12:27 +0300 Subject: [PATCH 086/140] fix(useapplelogin): add timeout for close --- src/routes/Intro/useAppleLogin.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 7d5465838..e6a39cbb9 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -52,7 +52,17 @@ const useAppleLogin = (): [() => Promise, () => void] => { usePopup: true, }); + const timeoutId = setTimeout(() => { + if (started.current) { + started.current = false; + reject(new Error('Apple login popup was closed')); + } + }, 1000); + window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { + clearTimeout(timeoutId); + started.current = false; + if (response.authorization) { try { const idToken = response.authorization.id_token; @@ -84,6 +94,10 @@ const useAppleLogin = (): [() => Promise, () => void] => { } else { reject(new Error('No authorization received from Apple')); } + }).catch((error) => { + clearTimeout(timeoutId); + started.current = false; + reject(error); }); }); }, []); From 0744fdfb8db8b7ac1c618e3c3de70e20bb09f319 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 16:16:02 +0300 Subject: [PATCH 087/140] refactor(Intro): new button design --- src/routes/Intro/styles.less | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/routes/Intro/styles.less b/src/routes/Intro/styles.less index 335ab9a5c..31a09d54c 100644 --- a/src/routes/Intro/styles.less +++ b/src/routes/Intro/styles.less @@ -189,11 +189,27 @@ } .apple-button { - background: var(--color-x); + background: var(--primary-foreground-color); + + .icon { + color: var(--primary-background-color); + } + + .label { + color: var(--primary-background-color); + } &:hover, &:focus { - outline: var(--focus-outline-size) solid var(--color-x); + outline: var(--focus-outline-size) solid var(--primary-foreground-color); background-color: transparent; + + .icon { + color: var(--primary-foreground-color); + } + + .label { + color: var(--primary-foreground-color); + } } } } From 66abce42c5368e9c6caa63f9dca22d82fb76054c Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 17:20:54 +0300 Subject: [PATCH 088/140] revert(useapplelogin): timeoutid --- src/routes/Intro/useAppleLogin.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index e6a39cbb9..583dd4411 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -52,17 +52,8 @@ const useAppleLogin = (): [() => Promise, () => void] => { usePopup: true, }); - const timeoutId = setTimeout(() => { - if (started.current) { - started.current = false; - reject(new Error('Apple login popup was closed')); - } - }, 1000); window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { - clearTimeout(timeoutId); - started.current = false; - if (response.authorization) { try { const idToken = response.authorization.id_token; @@ -95,8 +86,6 @@ const useAppleLogin = (): [() => Promise, () => void] => { reject(new Error('No authorization received from Apple')); } }).catch((error) => { - clearTimeout(timeoutId); - started.current = false; reject(error); }); }); From 3aac148258593903a3e3b37f77d9bd4c5a7fde52 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 17:23:09 +0300 Subject: [PATCH 089/140] Update useAppleLogin.ts --- 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 583dd4411..4128ee259 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -52,7 +52,6 @@ const useAppleLogin = (): [() => Promise, () => void] => { usePopup: true, }); - window.AppleID.auth.signIn().then((response: AppleSignInResponse) => { if (response.authorization) { try { From 303dd9858b80a9196108e83bafcd3aed9d1b76ca Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 19:29:12 +0300 Subject: [PATCH 090/140] refactor(useapplelogin): handle error cases --- src/routes/Intro/Intro.js | 6 +++++- src/routes/Intro/useAppleLogin.ts | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js index 01ca5eab7..716fe0f0d 100644 --- a/src/routes/Intro/Intro.js +++ b/src/routes/Intro/Intro.js @@ -129,7 +129,11 @@ const Intro = ({ queryParams }) => { }) .catch((error) => { closeLoaderModal(); - dispatch({ type: 'error', error: error.message }); + if (error.error === 'popup_closed_by_user') { + dispatch({ type: 'error', error: 'Apple login popup was closed.' }); + } else { + dispatch({ type: 'error', error: error.error }); + } }); }, []); const cancelLoginWithApple = React.useCallback(() => { diff --git a/src/routes/Intro/useAppleLogin.ts b/src/routes/Intro/useAppleLogin.ts index 4128ee259..580ec2d86 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { jwtDecode, JwtPayload } from 'jwt-decode'; type AppleLoginResponse = { @@ -94,6 +94,10 @@ const useAppleLogin = (): [() => Promise, () => void] => { started.current = false; }, []); + useEffect(() => { + return () => stop(); + }, []); + return [start, stop]; }; From 57d16957f2cfd483f9fb6f4651153d2d0ab57230 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Mon, 14 Apr 2025 23:26:04 +0300 Subject: [PATCH 091/140] chore(useapplelogin): update redirect to prod --- 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 580ec2d86..900944eb8 100644 --- a/src/routes/Intro/useAppleLogin.ts +++ b/src/routes/Intro/useAppleLogin.ts @@ -47,7 +47,7 @@ const useAppleLogin = (): [() => Promise, () => void] => { window.AppleID.auth.init({ clientId: CLIENT_ID, scope: 'name email', - redirectURI: 'https://stremio.github.io/stremio-web/feat/example-apple-login/', + redirectURI: 'https://web.stremio.com/', state: 'signin', usePopup: true, }); From 6faf70d33edd9c2b73776d113331168ea8a203bd Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 15 Apr 2025 16:30:13 +0300 Subject: [PATCH 092/140] 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 093/140] 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 094/140] 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 095/140] 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 096/140] 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 097/140] 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 098/140] 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 099/140] 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 107564b9d4740953db799f09fe5f5b7d49bd84e8 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Wed, 30 Apr 2025 12:18:08 +0300 Subject: [PATCH 100/140] feat(gh): auto assignments on PRs --- .github/workflows/auto_assign.yml | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/auto_assign.yml diff --git a/.github/workflows/auto_assign.yml b/.github/workflows/auto_assign.yml new file mode 100644 index 000000000..87e9c8f37 --- /dev/null +++ b/.github/workflows/auto_assign.yml @@ -0,0 +1,65 @@ +name: PR and Issue Workflow +on: + pull_request: + types: [opened, reopened] + issues: + types: [opened] +jobs: + auto-assign-and-label: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + # Auto assign PR to author + - name: Auto Assign PR to Author + if: github.event_name == 'pull_request' + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + if (pr) { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + assignees: [pr.user.login] + }); + console.log(`Assigned PR #${pr.number} to author @${pr.user.login}`); + } + + # Dynamic labeling based on PR/Issue title + - name: Label PRs and Issues + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prTitle = context.payload.pull_request ? context.payload.pull_request.title : context.payload.issue.title; + const issueNumber = context.payload.pull_request ? context.payload.pull_request.number : context.payload.issue.number; + const isIssue = context.payload.issue !== undefined; + const labelMappings = [ + { pattern: /^feat(ure)?/i, label: 'feature' }, + { pattern: /^fix/i, label: 'bug' }, + { pattern: /^refactor/i, label: 'refactor' }, + { pattern: /^chore/i, label: 'chore' }, + { pattern: /^docs?/i, label: 'documentation' }, + { pattern: /^perf(ormance)?/i, label: 'performance' }, + { pattern: /^test/i, label: 'testing' } + ]; + let labelsToAdd = []; + for (const mapping of labelMappings) { + if (mapping.pattern.test(prTitle)) { + labelsToAdd.push(mapping.label); + } + } + if (labelsToAdd.length > 0) { + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + labels: labelsToAdd + }); + } \ No newline at end of file From 5b56c58e5b0edd1133cbab2ce68439eca30cb533 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 1 May 2025 21:56:37 +0300 Subject: [PATCH 101/140] 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 102/140] 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 103/140] 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 104/140] 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 105/140] 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 106/140] 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 107/140] 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 108/140] 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 109/140] 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 110/140] 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 111/140] 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 112/140] 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 113/140] 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 114/140] 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 115/140] 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 116/140] 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 117/140] 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 118/140] 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 119/140] 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 120/140] 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 121/140] 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 122/140] 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 123/140] 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 124/140] 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 125/140] 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 126/140] 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 127/140] 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 128/140] 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 129/140] 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; + } } } } From e8ac50135b19daea39c219b61220b16febacd4aa Mon Sep 17 00:00:00 2001 From: dexter21767-dev Date: Mon, 26 May 2025 20:00:06 +0100 Subject: [PATCH 130/140] fix trakt logout button --- src/routes/Settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index d6fe9b7a4..a6ec7e6ba 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -314,7 +314,7 @@ const Settings = () => {
From 6dfa3fdae0499f86243b2a8284efb6ee82ee9b0f Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 10:19:39 +0200 Subject: [PATCH 131/140] fix: toggle fullscreen --- src/common/useFullscreen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 5f0975fb8..451063eeb 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -60,7 +60,7 @@ const useFullscreen = () => { document.removeEventListener('keydown', onKeyDown); document.removeEventListener('fullscreenchange', onFullscreenChange); }; - }, [settings.escExitFullscreen]); + }, [settings.escExitFullscreen, toggleFullscreen]); return [fullscreen, requestFullscreen, exitFullscreen, toggleFullscreen]; }; From 2b44367a263c3789c67e54c342ceda00d59e951b Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 10:28:02 +0200 Subject: [PATCH 132/140] feat: toggle fullscreen with F key with shell --- src/common/useFullscreen.ts | 4 ++++ src/services/KeyboardShortcuts/KeyboardShortcuts.js | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/common/useFullscreen.ts b/src/common/useFullscreen.ts index 451063eeb..9bd5d0fc5 100644 --- a/src/common/useFullscreen.ts +++ b/src/common/useFullscreen.ts @@ -46,6 +46,10 @@ const useFullscreen = () => { exitFullscreen(); } + if (event.code === 'KeyF') { + toggleFullscreen(); + } + if (event.code === 'F11' && shell.active) { toggleFullscreen(); } diff --git a/src/services/KeyboardShortcuts/KeyboardShortcuts.js b/src/services/KeyboardShortcuts/KeyboardShortcuts.js index 22ef0e41f..4bc4683fc 100644 --- a/src/services/KeyboardShortcuts/KeyboardShortcuts.js +++ b/src/services/KeyboardShortcuts/KeyboardShortcuts.js @@ -56,16 +56,6 @@ function KeyboardShortcuts() { window.history.back(); } - break; - } - case 'KeyF': { - event.preventDefault(); - if (document.fullscreenElement === document.documentElement) { - document.exitFullscreen(); - } else { - document.documentElement.requestFullscreen(); - } - break; } } From 41546d65d246e2dcf69aa51bc7161d57f41ddc49 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 20:16:26 +0200 Subject: [PATCH 133/140] feat: full deeplink support for shell --- src/App/App.js | 16 +++++++++++----- src/common/CONSTANTS.js | 3 +++ src/common/routesRegexp.js | 2 +- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 38be271ac..57bdaee6a 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -102,12 +102,18 @@ const App = () => { // Handle shell events React.useEffect(() => { const onOpenMedia = (data) => { - if (data.startsWith('stremio:///')) return; - if (data.startsWith('stremio://')) { - const transportUrl = data.replace('stremio://', 'https://'); - if (URL.canParse(transportUrl)) { - window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`; + try { + const { protocol, hostname, pathname, searchParams } = new URL(data); + if (protocol === CONSTANTS.PROTOCOL) { + if (hostname.length) { + const transportUrl = `https://${hostname}${pathname}`; + window.location.href = `#/addons?addon=${encodeURIComponent(transportUrl)}`; + } else { + window.location.href = `#${pathname}?${searchParams.toString()}`; + } } + } catch (e) { + console.error("Failed to open media:", e); } }; diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 8e4e3efdc..92d009895 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -106,6 +106,8 @@ const EXTERNAL_PLAYERS = [ const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'x.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle']; +const PROTOCOL = 'stremio:'; + module.exports = { CHROMECAST_RECEIVER_APP_ID, DEFAULT_STREAMING_SERVER_URL, @@ -127,4 +129,5 @@ module.exports = { SUPPORTED_LOCAL_SUBTITLES, EXTERNAL_PLAYERS, WHITELISTED_HOSTS, + PROTOCOL, }; diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index 3903da44b..43f5810b1 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -6,7 +6,7 @@ const routesRegexp = { urlParamsNames: [] }, board: { - regexp: /^\/?$/, + regexp: /^\/(?:board)?$/, urlParamsNames: [] }, discover: { From 597b366ce2d3b8329a400eaacaf4cf2ea04bcbee Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 20:26:02 +0200 Subject: [PATCH 134/140] fix(common): allow board regex to match empty --- src/common/routesRegexp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/routesRegexp.js b/src/common/routesRegexp.js index 43f5810b1..b9989b4b5 100644 --- a/src/common/routesRegexp.js +++ b/src/common/routesRegexp.js @@ -6,7 +6,7 @@ const routesRegexp = { urlParamsNames: [] }, board: { - regexp: /^\/(?:board)?$/, + regexp: /^\/?(?:board)?$/, urlParamsNames: [] }, discover: { From 5d9a005686b838390ba6532bd3207d63d58773af Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 20:29:42 +0200 Subject: [PATCH 135/140] style(App): use singlequote for string --- src/App/App.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App/App.js b/src/App/App.js index 57bdaee6a..3e816be9f 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -113,7 +113,7 @@ const App = () => { } } } catch (e) { - console.error("Failed to open media:", e); + console.error('Failed to open media:', e); } }; From 8968055493994e4f6a62a4e08c5b45762d6c99a2 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 27 May 2025 21:33:13 +0300 Subject: [PATCH 136/140] fix(Settings): trakt text checks repetition --- src/routes/Settings/Settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index a6ec7e6ba..74753090a 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -314,7 +314,7 @@ const Settings = () => {
From 824763a277e08a833067da6c514d721ceb4bb291 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 31 May 2025 17:12:42 +0200 Subject: [PATCH 137/140] refactor(common): remove use of ipc for opening external url --- src/common/Platform/Platform.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 2212303e4..0da1881ef 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -1,6 +1,5 @@ import React, { createContext, useContext } from 'react'; import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; -import useShell from 'stremio/common/useShell'; import { name, isMobile } from './device'; interface PlatformContext { @@ -16,19 +15,13 @@ type Props = { }; const PlatformProvider = ({ children }: Props) => { - const shell = useShell(); - const openExternal = (url: string) => { try { const { hostname } = new URL(url); const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host)); const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url; - if (shell.active) { - shell.send('open-external', finalUrl); - } else { - window.open(finalUrl, '_blank'); - } + window.open(finalUrl, '_blank'); } catch (e) { console.error('Failed to parse external url:', e); } From fd4c9e73c82d4d66bab2fb95490f5968f38607dd Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 3 Jun 2025 12:34:14 +0300 Subject: [PATCH 138/140] refactor(MultiselectMenu): use value only --- .../MultiselectMenu/Dropdown/Dropdown.tsx | 16 +++---- .../Dropdown/Option/Option.tsx | 9 ++-- .../MultiselectMenu/MultiselectMenu.less | 8 +++- .../MultiselectMenu/MultiselectMenu.tsx | 16 ++++--- src/routes/Addons/useSelectableInputs.js | 14 +------ src/routes/Discover/Discover.js | 8 ++-- src/routes/Discover/useSelectableInputs.js | 25 ++++------- src/routes/Library/Library.js | 6 +-- src/routes/Library/useSelectableInputs.js | 5 +-- .../MetaDetails/StreamsList/StreamsList.js | 5 +-- .../VideosList/SeasonsBar/SeasonsBar.js | 4 +- .../Settings/useProfileSettingsInputs.js | 42 ++++--------------- .../useStreamingServerSettingsInputs.js | 22 ++-------- 13 files changed, 63 insertions(+), 117 deletions(-) diff --git a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx index 438ced13c..5f1ee4fa0 100644 --- a/src/components/MultiselectMenu/Dropdown/Dropdown.tsx +++ b/src/components/MultiselectMenu/Dropdown/Dropdown.tsx @@ -10,23 +10,25 @@ import styles from './Dropdown.less'; type Props = { options: MultiselectMenuOption[]; - selectedOption?: MultiselectMenuOption | null; + value?: string | number | null; menuOpen: boolean | (() => void); level: number; setLevel: (level: number) => void; - onSelect: (value: number) => void; + onSelect: (value: string | number | null) => void; }; -const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen }: Props) => { +const Dropdown = ({ level, setLevel, options, onSelect, value, menuOpen }: Props) => { const { t } = useTranslation(); const optionsRef = useRef(new Map()); const containerRef = useRef(null); - const handleSetOptionRef = useCallback((value: number) => (node: HTMLButtonElement | null) => { + const selectedOption = options.find(opt => opt.value === value) || null; + + const handleSetOptionRef = useCallback((optionValue: string | number) => (node: HTMLButtonElement | null) => { if (node) { - optionsRef.current.set(value, node); + optionsRef.current.set(optionValue, node); } else { - optionsRef.current.delete(value); + optionsRef.current.delete(optionValue); } }, []); @@ -67,7 +69,7 @@ const Dropdown = ({ level, setLevel, options, onSelect, selectedOption, menuOpen ref={handleSetOptionRef(option.value)} option={option} onSelect={onSelect} - selectedOption={selectedOption} + selectedValue={value} /> )) } diff --git a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx index 91aa173f7..444e1876e 100644 --- a/src/components/MultiselectMenu/Dropdown/Option/Option.tsx +++ b/src/components/MultiselectMenu/Dropdown/Option/Option.tsx @@ -8,13 +8,12 @@ import Icon from '@stremio/stremio-icons/react'; type Props = { option: MultiselectMenuOption; - selectedOption?: MultiselectMenuOption | null; - onSelect: (value: number) => void; + selectedValue?: string | number | null; + onSelect: (value: string | number | null) => void; }; -const Option = forwardRef(({ option, selectedOption, onSelect }, ref) => { - // consider using option.id === selectedOption?.id instead - const selected = useMemo(() => option?.value === selectedOption?.value, [option, selectedOption]); +const Option = forwardRef(({ option, selectedValue, onSelect }, ref) => { + const selected = useMemo(() => option?.value === selectedValue, [option, selectedValue]); const handleClick = useCallback(() => { onSelect(option.value); diff --git a/src/components/MultiselectMenu/MultiselectMenu.less b/src/components/MultiselectMenu/MultiselectMenu.less index c26c2480e..4aee1a4a8 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.less +++ b/src/components/MultiselectMenu/MultiselectMenu.less @@ -14,7 +14,6 @@ } .multiselect-button { - color: var(--primary-foreground-color); padding: 0.75rem 1.5rem; display: flex; flex: 1; @@ -23,6 +22,13 @@ gap: 0 0.5rem; border-radius: @border-radius; + .label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: var(--primary-foreground-color); + } + .icon { width: 1rem; color: var(--primary-foreground-color); diff --git a/src/components/MultiselectMenu/MultiselectMenu.tsx b/src/components/MultiselectMenu/MultiselectMenu.tsx index 8f41278e7..9a84cb8eb 100644 --- a/src/components/MultiselectMenu/MultiselectMenu.tsx +++ b/src/components/MultiselectMenu/MultiselectMenu.tsx @@ -13,17 +13,19 @@ type Props = { className?: string, title?: string | (() => string); options: MultiselectMenuOption[]; - selectedOption?: MultiselectMenuOption; - onSelect: (value: number) => void; + value?: string | number | null; + onSelect: (value: string | number | null) => void; }; -const MultiselectMenu = ({ className, title, options, selectedOption, onSelect }: Props) => { +const MultiselectMenu = ({ className, title, options, value, onSelect }: Props) => { const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false); const multiselectMenuRef = useOutsideClick(() => closeMenu()); const [level, setLevel] = React.useState(0); - const onOptionSelect = (value: number) => { - level ? setLevel(level + 1) : onSelect(value), closeMenu(); + const selectedOption = options.find(opt => opt.value === value); + + const onOptionSelect = (selectedValue: string | number | null) => { + level ? setLevel(level + 1) : onSelect(selectedValue), closeMenu(); }; return ( @@ -35,11 +37,13 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } aria-haspopup='listbox' aria-expanded={menuOpen} > +
{ typeof title === 'function' ? title() : title ?? selectedOption?.label } +
{ @@ -50,7 +54,7 @@ const MultiselectMenu = ({ className, title, options, selectedOption, onSelect } options={options} onSelect={onOptionSelect} menuOpen={menuOpen} - selectedOption={selectedOption} + value={value} /> : null } diff --git a/src/routes/Addons/useSelectableInputs.js b/src/routes/Addons/useSelectableInputs.js index c201b3976..a8af37fbe 100644 --- a/src/routes/Addons/useSelectableInputs.js +++ b/src/routes/Addons/useSelectableInputs.js @@ -13,12 +13,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => { label: t.stringWithPrefix(name, 'ADDON_'), title: t.stringWithPrefix(name, 'ADDON_'), })), - selectedOption: selectedCatalog - ? { - label: t.stringWithPrefix(selectedCatalog.name, 'ADDON_'), - value: selectedCatalog.deepLinks.addons, - } - : undefined, + value: selectedCatalog ? selectedCatalog.deepLinks.addons : undefined, title: remoteAddons.selected !== null ? () => { const selectableCatalog = remoteAddons.selectable.catalogs @@ -44,12 +39,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons, t) => { value: deepLinks.addons, label: t.stringWithPrefix(type, 'TYPE_') })), - selectedOption: selectedType - ? { - label: selectedType.type !== null ? t.stringWithPrefix(selectedType.type, 'TYPE_') : t.string('TYPE_ALL'), - value: selectedType.deepLinks.addons - } - : undefined, + value: selectedType ? selectedType.deepLinks.addons : undefined, title: () => { return installedAddons.selected !== null ? installedAddons.selected.request.type === null ? diff --git a/src/routes/Discover/Discover.js b/src/routes/Discover/Discover.js index 695822ecb..1c2b6f122 100644 --- a/src/routes/Discover/Discover.js +++ b/src/routes/Discover/Discover.js @@ -100,13 +100,13 @@ const Discover = ({ urlParams, queryParams }) => {
- {selectInputs.map(({ title, options, selectedOption, onSelect }, index) => ( + {selectInputs.map(({ title, options, value, onSelect }, index) => ( ))} @@ -202,13 +202,13 @@ const Discover = ({ urlParams, queryParams }) => { { inputsModalOpen ? - {selectInputs.map(({ title, options, selectedOption, onSelect }, index) => ( + {selectInputs.map(({ title, options, value, onSelect }, index) => ( ))} diff --git a/src/routes/Discover/useSelectableInputs.js b/src/routes/Discover/useSelectableInputs.js index 459e095cf..2c476c804 100644 --- a/src/routes/Discover/useSelectableInputs.js +++ b/src/routes/Discover/useSelectableInputs.js @@ -11,11 +11,8 @@ const mapSelectableInputs = (discover, t) => { value: deepLinks.discover, label: t.stringWithPrefix(type, 'TYPE_') })), - selectedOption: selectedType - ? { - label: t.stringWithPrefix(selectedType.type, 'TYPE_'), - value: selectedType.deepLinks.discover, - } + value: selectedType + ? selectedType.deepLinks.discover : undefined, title: discover.selected !== null ? () => t.stringWithPrefix(discover.selected.request.path.type, 'TYPE_') @@ -32,11 +29,8 @@ const mapSelectableInputs = (discover, t) => { label: t.catalogTitle({ addon, id, name }), title: `${name} (${addon.manifest.name})` })), - selectedOption: discover.selected?.request.path.id - ? { - label: t.catalogTitle({ addon: selectedCatalog.addon, id: selectedCatalog.id, name: selectedCatalog.name }), - value: selectedCatalog.deepLinks.discover - } + value: discover.selected?.request.path.id + ? selectedCatalog.deepLinks.discover : undefined, title: discover.selected !== null ? () => { @@ -61,13 +55,10 @@ const mapSelectableInputs = (discover, t) => { value }) })), - selectedOption: { - label: typeof selectedExtra.value === 'string' ? t.stringWithPrefix(selectedExtra.value) : t.string('NONE'), - value: JSON.stringify({ - href: selectedExtra.deepLinks.discover, - value: selectedExtra.value, - }) - }, + value: JSON.stringify({ + href: selectedExtra.deepLinks.discover, + value: selectedExtra.value, + }), title: options.some(({ selected, value }) => selected && value === null) ? () => t.stringWithPrefix(name, 'SELECT_') : t.stringWithPrefix(selectedExtra.value), diff --git a/src/routes/Library/Library.js b/src/routes/Library/Library.js index 6b12afe08..2fd81cde2 100644 --- a/src/routes/Library/Library.js +++ b/src/routes/Library/Library.js @@ -64,10 +64,10 @@ const Library = ({ model, urlParams, queryParams }) => { } }, [profile.auth, library.selected]); React.useEffect(() => { - if (!library.selected?.type && typeSelect.selectedOption) { - window.location = typeSelect.selectedOption.value; + if (!library.selected?.type && typeSelect.value) { + window.location = typeSelect.value; } - }, [typeSelect.selectedOption, library.selected]); + }, [typeSelect.value, library.selected]); return ( { diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js index 426359a10..84816b646 100644 --- a/src/routes/Library/useSelectableInputs.js +++ b/src/routes/Library/useSelectableInputs.js @@ -10,10 +10,7 @@ const mapSelectableInputs = (library, t) => { value: deepLinks.library, label: type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(type, 'TYPE_') })), - selectedOption: { - label: selectedType?.type === null ? t.string('TYPE_ALL') : t.stringWithPrefix(selectedType?.type, 'TYPE_'), - value: selectedType?.deepLinks.library - }, + value: selectedType?.deepLinks.library, onSelect: (value) => { window.location = value; } diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 3e1b1339c..627b41857 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -89,10 +89,7 @@ const StreamsList = ({ className, video, type, onEpisodeSearch, ...props }) => { title: streamsByAddon[transportUrl].addon.manifest.name, })) ], - selectedOption: { - label: selectedAddon === ALL_ADDONS_KEY ? t('ALL_ADDONS') : streamsByAddon[selectedAddon]?.addon.manifest.name, - value: selectedAddon - }, + value: selectedAddon, onSelect: onAddonSelected }; }, [streamsByAddon, selectedAddon]); diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 29637a24c..51b51f0e7 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -17,7 +17,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { })); }, [seasons]); const selectedSeason = React.useMemo(() => { - return { label: String(season), value: String(season) }; + return String(season); }, [season]); const prevNextButtonOnClick = React.useCallback((event) => { if (typeof onSelect === 'function') { @@ -64,7 +64,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { className={styles['seasons-popup-label-container']} options={options} title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} - selectedOption={selectedSeason} + value={selectedSeason} onSelect={seasonOnSelect} /> From eab1b8def37a2ded0b8856b0b7344379116a19c2 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Tue, 3 Jun 2025 12:48:44 +0300 Subject: [PATCH 140/140] refactor(StreamingServerInputs): non null value --- src/routes/Settings/useStreamingServerSettingsInputs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/Settings/useStreamingServerSettingsInputs.js b/src/routes/Settings/useStreamingServerSettingsInputs.js index 8bbea176b..e4bd7e79c 100644 --- a/src/routes/Settings/useStreamingServerSettingsInputs.js +++ b/src/routes/Settings/useStreamingServerSettingsInputs.js @@ -183,7 +183,7 @@ const useStreamingServerSettingsInputs = (streamingServer) => { value: name, })) ], - value: streamingServer.settings.content.transcodeProfile ?? null, + value: streamingServer.settings.content.transcodeProfile, onSelect: (value) => { core.transport.dispatch({ action: 'StreamingServer',