From 794f4e48ac4346e6147915e91aa2bd94cd4e45e1 Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 28 Feb 2025 17:45:13 +0200 Subject: [PATCH 01/23] 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 02/23] 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 03/23] 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 04/23] 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 05/23] 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 98784779b53c92e78897bbe99da28016173a1d56 Mon Sep 17 00:00:00 2001 From: Botzy Date: Tue, 11 Mar 2025 19:31:46 +0200 Subject: [PATCH 06/23] 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 07/23] 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 08/23] 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 ( -
+
From 6dfa3fdae0499f86243b2a8284efb6ee82ee9b0f Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 27 May 2025 10:19:39 +0200 Subject: [PATCH 14/23] 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 15/23] 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 16/23] 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 17/23] 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 18/23] 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 19/23] 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 20/23] 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 21/23] 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 23/23] 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',