From 443e86a1adf57a3f13e84187eb935cbfdbb51c4f Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 25 Sep 2024 15:34:32 +0200 Subject: [PATCH 01/16] feat: impl openExternal for shell compatibility --- src/App/App.js | 28 ++++++------- .../AddonDetailsModal/AddonDetailsModal.js | 5 ++- src/common/Platform/Platform.tsx | 39 +++++++++++++++++++ src/common/Platform/index.ts | 5 +++ src/common/Platform/useShell.ts | 22 +++++++++++ src/common/index.js | 3 ++ src/routes/Addons/Addons.js | 5 ++- .../PasswordResetModal/PasswordResetModal.js | 5 ++- src/routes/Player/OptionsMenu/OptionsMenu.js | 5 ++- src/routes/Settings/Settings.js | 9 +++-- src/types/global.d.ts | 13 +++++++ 11 files changed, 114 insertions(+), 25 deletions(-) create mode 100644 src/common/Platform/Platform.tsx create mode 100644 src/common/Platform/index.ts create mode 100644 src/common/Platform/useShell.ts create mode 100644 src/types/global.d.ts diff --git a/src/App/App.js b/src/App/App.js index 21ba1132b..40b456cc0 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,7 +6,7 @@ const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); -const { ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); +const { PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); @@ -162,18 +162,20 @@ const App = () => { services.core.error instanceof Error ? : - - - - - - - - + + + + + + + + + + :
} diff --git a/src/common/AddonDetailsModal/AddonDetailsModal.js b/src/common/AddonDetailsModal/AddonDetailsModal.js index ffe2671d0..b4ffdab71 100644 --- a/src/common/AddonDetailsModal/AddonDetailsModal.js +++ b/src/common/AddonDetailsModal/AddonDetailsModal.js @@ -3,7 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const ModalDialog = require('stremio/common/ModalDialog'); -const { withCoreSuspender } = require('stremio/common/CoreSuspender'); +const { usePlatform, withCoreSuspender } = require('stremio/common/CoreSuspender'); const { useServices } = require('stremio/services'); const AddonDetailsWithRemoteAndLocalAddon = withRemoteAndLocalAddon(require('./AddonDetails')); const useAddonDetails = require('./useAddonDetails'); @@ -43,6 +43,7 @@ function withRemoteAndLocalAddon(AddonDetails) { const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { const { core } = useServices(); + const platform = usePlatform(); const addonDetails = useAddonDetails(transportUrl); const modalButtons = React.useMemo(() => { const cancelButton = { @@ -68,7 +69,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => { label: 'Configure', props: { onClick: (event) => { - window.open(transportUrl.replace('manifest.json', 'configure')); + platform.openExternal(transportUrl.replace('manifest.json', 'configure')); if (typeof onCloseRequest === 'function') { onCloseRequest({ type: 'configure', diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx new file mode 100644 index 000000000..c93064d78 --- /dev/null +++ b/src/common/Platform/Platform.tsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext } from 'react'; +import useShell from './useShell'; + +interface PlatformContext { + openExternal: (url: string) => void, +}; + +const PlatformContext = createContext(null); + +type Props = { + children: JSX.Element, +}; + +const PlatformProvider = ({ children }: Props) => { + const shell = useShell(); + + const openExternal = (url: string) => { + if (shell.active) { + shell.send('open-external', url); + } else { + window.open(url, '_blank'); + } + }; + + return ( + + {children} + + ); +}; + +const usePlatform = () => { + return useContext(PlatformContext); +}; + +export { + PlatformProvider, + usePlatform, +}; \ No newline at end of file diff --git a/src/common/Platform/index.ts b/src/common/Platform/index.ts new file mode 100644 index 000000000..8d2b68f12 --- /dev/null +++ b/src/common/Platform/index.ts @@ -0,0 +1,5 @@ +import { PlatformProvider, usePlatform } from './Platform'; +export { + PlatformProvider, + usePlatform, +}; \ No newline at end of file diff --git a/src/common/Platform/useShell.ts b/src/common/Platform/useShell.ts new file mode 100644 index 000000000..aaf5a028e --- /dev/null +++ b/src/common/Platform/useShell.ts @@ -0,0 +1,22 @@ +const createId = () => Math.floor(Math.random() * 9999) + 1; + +const useShell = () => { + const transport = globalThis?.qt?.webChannelTransport; + + const send = (method: string, ...args: (string | number)[]) => { + transport?.send(JSON.stringify({ + id: createId(), + type: 6, + object: 'transport', + method: 'onEvent', + args: [method, ...args], + })); + }; + + return { + active: !!transport, + send, + }; +}; + +export default useShell; \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index c582a13ca..1188b8c61 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -18,6 +18,7 @@ const Multiselect = require('./Multiselect'); const { default: MultiselectMenu } = require('./MultiselectMenu'); const { HorizontalNavBar, VerticalNavBar } = require('./NavBar'); const PaginationInput = require('./PaginationInput'); +const { PlatformProvider, usePlatform } = require('./Platform'); const PlayIconCircleCentered = require('./PlayIconCircleCentered'); const Popup = require('./Popup'); const SearchBar = require('./SearchBar'); @@ -68,6 +69,8 @@ module.exports = { HorizontalNavBar, VerticalNavBar, PaginationInput, + PlatformProvider, + usePlatform, PlayIconCircleCentered, Popup, SearchBar, diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index c4c99173a..0de34ad4a 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.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 { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState, withCoreSuspender } = require('stremio/common'); +const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, usePlatform, useBinaryState, withCoreSuspender } = require('stremio/common'); const Addon = require('./Addon'); const useInstalledAddons = require('./useInstalledAddons'); const useRemoteAddons = require('./useRemoteAddons'); @@ -15,6 +15,7 @@ const styles = require('./styles'); const Addons = ({ urlParams, queryParams }) => { const { t } = useTranslation(); + const platform = usePlatform(); const installedAddons = useInstalledAddons(urlParams); const remoteAddons = useRemoteAddons(urlParams); const [addonDetailsTransportUrl, setAddonDetailsTransportUrl] = useAddonDetailsTransportUrl(urlParams, queryParams); @@ -59,7 +60,7 @@ const Addons = ({ urlParams, queryParams }) => { setAddonDetailsTransportUrl(event.dataset.addon.transportUrl); }, [setAddonDetailsTransportUrl]); const onAddonConfigure = React.useCallback((event) => { - window.open(event.dataset.addon.transportUrl.replace('manifest.json', 'configure')); + platform.openExternal(event.dataset.addon.transportUrl.replace('manifest.json', 'configure')); }, []); const closeAddonDetails = React.useCallback(() => { setAddonDetailsTransportUrl(null); diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js index 0d7bcaae4..1061f6044 100644 --- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js +++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js @@ -3,17 +3,18 @@ const React = require('react'); const PropTypes = require('prop-types'); const { useRouteFocused } = require('stremio-router'); -const { ModalDialog } = require('stremio/common'); +const { ModalDialog, usePlatform } = require('stremio/common'); const CredentialsTextInput = require('../CredentialsTextInput'); const styles = require('./styles'); const PasswordResetModal = ({ email, onCloseRequest }) => { const routeFocused = useRouteFocused(); + const platform = usePlatform(); const [error, setError] = React.useState(''); const emailRef = React.useRef(null); const goToPasswordReset = React.useCallback(() => { emailRef.current.value.length > 0 && emailRef.current.validity.valid ? - window.open('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank') + platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank') : setError('Invalid email'); }, []); diff --git a/src/routes/Player/OptionsMenu/OptionsMenu.js b/src/routes/Player/OptionsMenu/OptionsMenu.js index 6c50e1551..11738cb3e 100644 --- a/src/routes/Player/OptionsMenu/OptionsMenu.js +++ b/src/routes/Player/OptionsMenu/OptionsMenu.js @@ -4,7 +4,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { useTranslation } = require('react-i18next'); -const { useToast } = require('stremio/common'); +const { usePlatform, useToast } = require('stremio/common'); const { useServices } = require('stremio/services'); const Option = require('./Option'); const styles = require('./styles'); @@ -12,6 +12,7 @@ const styles = require('./styles'); const OptionsMenu = ({ className, stream, playbackDevices }) => { const { t } = useTranslation(); const { core } = useServices(); + const platform = usePlatform(); const toast = useToast(); const [streamingUrl, downloadUrl] = React.useMemo(() => { return stream !== null ? @@ -48,7 +49,7 @@ const OptionsMenu = ({ className, stream, playbackDevices }) => { }, [streamingUrl, downloadUrl]); const onDownloadVideoButtonClick = React.useCallback(() => { if (streamingUrl || downloadUrl) { - window.open(streamingUrl || downloadUrl); + platform.openExternal(streamingUrl || downloadUrl); } }, [streamingUrl, downloadUrl]); const onExternalDeviceRequested = React.useCallback((deviceId) => { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 071f14d0e..5c5d51774 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -7,7 +7,7 @@ const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, useStreamingServer, useBinaryState, withCoreSuspender, useToast } = require('stremio/common'); +const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, ModalDialog, useProfile, usePlatform, useStreamingServer, useBinaryState, withCoreSuspender, useToast } = require('stremio/common'); const useProfileSettingsInputs = require('./useProfileSettingsInputs'); const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); const useDataExport = require('./useDataExport'); @@ -25,6 +25,7 @@ const Settings = () => { const profile = useProfile(); const [dataExport, loadDataExport] = useDataExport(); const streamingServer = useStreamingServer(); + const platform = usePlatform(); const toast = useToast(); const { interfaceLanguageSelect, @@ -90,7 +91,7 @@ const Settings = () => { }, []); const toggleTraktOnClick = React.useCallback(() => { if (!isTraktAuthenticated && profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string') { - window.open(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`); + platform.openExternal(`https://www.strem.io/trakt/auth/${profile.auth.user._id}`); setTraktAuthStarted(true); } else { core.transport.dispatch({ @@ -103,7 +104,7 @@ const Settings = () => { }, [isTraktAuthenticated, profile.auth]); const subscribeCalendarOnClick = React.useCallback(() => { const url = `webcal://www.strem.io/calendar/${profile.auth.user._id}.ics`; - window.open(url); + platform.openExternal(url); toast.show({ type: 'success', title: 'Calendar has been added to your default caldendar app', @@ -181,7 +182,7 @@ const Settings = () => { }, [isTraktAuthenticated, traktAuthStarted]); React.useEffect(() => { if (dataExport.exportUrl !== null && typeof dataExport.exportUrl === 'string') { - window.open(dataExport.exportUrl); + platform.openExternal(dataExport.exportUrl); } }, [dataExport.exportUrl]); React.useLayoutEffect(() => { diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 000000000..97da1d705 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,13 @@ +interface QtTransport { + send: (message: string) => void, +} + +interface Qt { + webChannelTransport: QtTransport, +} + +declare global { + var qt: Qt | undefined; +} + +export { }; \ No newline at end of file From c5b20800c0870feb68b82feb300c2091d845ee8d Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 25 Sep 2024 15:43:04 +0200 Subject: [PATCH 02/16] fix(App): lint --- src/App/App.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/App/App.js b/src/App/App.js index 40b456cc0..730e75f28 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -165,14 +165,14 @@ const App = () => { - - - - + + + + From 4173fca28cb6f3d2c95aa635a77e1e4fd88c4c4e Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 12:28:48 +0300 Subject: [PATCH 03/16] feature: check if url is whitelisted --- src/common/CONSTANTS.js | 3 +++ src/common/Platform/Platform.tsx | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index 742c138d6..c64c84bcd 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -93,6 +93,8 @@ const EXTERNAL_PLAYERS = [ }, ]; +const WHITELISTED_HOSTS = ['www.stremio.com', 'blog.stremio.com', 'web.stremio.com', 'web.strem.io', 'stremio.zendesk.com', 'www.google.com', 'www.youtube.com', 'www.twitch.tv', 'twitter.com', 'www.netflix.com', 'www.adex.network', 'www.amazon.com', 'docs.google.com', 'blog.stremio.com', 'forms.gle']; + module.exports = { CHROMECAST_RECEIVER_APP_ID, SUBTITLES_SIZES, @@ -110,4 +112,5 @@ module.exports = { TYPE_PRIORITIES, ICON_FOR_TYPE, EXTERNAL_PLAYERS, + WHITELISTED_HOSTS, }; diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index c93064d78..0804d512b 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -1,5 +1,6 @@ import React, { createContext, useContext } from 'react'; import useShell from './useShell'; +import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; interface PlatformContext { openExternal: (url: string) => void, @@ -15,10 +16,22 @@ const PlatformProvider = ({ children }: Props) => { const shell = useShell(); const openExternal = (url: string) => { + let finalUrl = url; + try { + const parsedUrl = new URL(url); + const hostname = parsedUrl.hostname; + const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname === host || hostname.endsWith('.' + host)); + if (!isWhitelisted) { + finalUrl = 'https://www.stremio.com/warning#' + encodeURIComponent(url); + } + } catch (e) { + finalUrl = 'https://www.stremio.com/warning#' + encodeURIComponent(url); + } + if (shell.active) { - shell.send('open-external', url); + shell.send('open-external', finalUrl); } else { - window.open(url, '_blank'); + window.open(finalUrl, '_blank'); } }; @@ -36,4 +49,4 @@ const usePlatform = () => { export { PlatformProvider, usePlatform, -}; \ No newline at end of file +}; From 50b93014babbe80b3b53f943be2a911936cb8154 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 13:06:57 +0300 Subject: [PATCH 04/16] refactor: wrap everything in try catch --- src/common/Platform/Platform.tsx | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 0804d512b..13e8592ad 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -16,23 +16,19 @@ const PlatformProvider = ({ children }: Props) => { const shell = useShell(); const openExternal = (url: string) => { - let finalUrl = url; try { - const parsedUrl = new URL(url); - const hostname = parsedUrl.hostname; - const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname === host || hostname.endsWith('.' + host)); - if (!isWhitelisted) { - finalUrl = 'https://www.stremio.com/warning#' + encodeURIComponent(url); + 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'); } } catch (e) { - finalUrl = 'https://www.stremio.com/warning#' + encodeURIComponent(url); - } - - if (shell.active) { - shell.send('open-external', finalUrl); - } else { - window.open(finalUrl, '_blank'); - } + console.error('Failed to parse external url:', e); + } }; return ( From c1b9a057309215e97f4949331ea2f0b931b3721d Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 13:09:08 +0300 Subject: [PATCH 05/16] refactor: whitelisted hosts list --- src/common/CONSTANTS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/CONSTANTS.js b/src/common/CONSTANTS.js index c64c84bcd..c08cf471d 100644 --- a/src/common/CONSTANTS.js +++ b/src/common/CONSTANTS.js @@ -93,7 +93,7 @@ const EXTERNAL_PLAYERS = [ }, ]; -const WHITELISTED_HOSTS = ['www.stremio.com', 'blog.stremio.com', 'web.stremio.com', 'web.strem.io', 'stremio.zendesk.com', 'www.google.com', 'www.youtube.com', 'www.twitch.tv', 'twitter.com', 'www.netflix.com', 'www.adex.network', 'www.amazon.com', 'docs.google.com', 'blog.stremio.com', 'forms.gle']; +const WHITELISTED_HOSTS = ['stremio.com', 'strem.io', 'stremio.zendesk.com', 'google.com', 'youtube.com', 'twitch.tv', 'twitter.com', 'netflix.com', 'adex.network', 'amazon.com', 'forms.gle']; module.exports = { CHROMECAST_RECEIVER_APP_ID, From bd1305927919c9260195901c5327afccd4c5e610 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 14:51:12 +0300 Subject: [PATCH 06/16] refactor: turn button into a link --- src/routes/Settings/Settings.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 5c5d51774..ab69d26f6 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -11,6 +11,7 @@ const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, Modal const useProfileSettingsInputs = require('./useProfileSettingsInputs'); const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); const useDataExport = require('./useDataExport'); +const { name: platformName } = require('stremio/common/platform'); const styles = require('./styles'); const GENERAL_SECTION = 'general'; @@ -102,16 +103,20 @@ const Settings = () => { }); } }, [isTraktAuthenticated, profile.auth]); + const protocol = platformName === 'ios' ? 'webcal' : 'http'; + const calendarUrl = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; + const calendarFileName = `${profile.auth.user._id}.ics`; const subscribeCalendarOnClick = React.useCallback(() => { - const url = `webcal://www.strem.io/calendar/${profile.auth.user._id}.ics`; - platform.openExternal(url); + platform.openExternal(calendarUrl); toast.show({ type: 'success', - title: 'Calendar has been added to your default caldendar app', + title: platformName === 'android' ? + 'Calendar has been downloaded. Please open it in your calendar app.' : + 'Calendar has been added to your default calendar app.', timeout: 25000 }); - //Stremio 4 emits not documented event subscribeCalendar - }, []); + // Stremio 4 emits not documented event subscribeCalendar + }, [platformName, profile.auth.user._id, platform, toast]); const exportDataOnClick = React.useCallback(() => { loadDataExport(); }, []); @@ -269,7 +274,7 @@ const Settings = () => { { profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?
-
From 0ee4b6d396417d43c9caafd3f1884998d97229c6 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 14:55:43 +0300 Subject: [PATCH 07/16] remove: download file name --- src/routes/Settings/Settings.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index ab69d26f6..7b1afe620 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -103,9 +103,8 @@ const Settings = () => { }); } }, [isTraktAuthenticated, profile.auth]); - const protocol = platformName === 'ios' ? 'webcal' : 'http'; + const protocol = platformName === 'ios' ? 'webcal' : 'https'; const calendarUrl = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; - const calendarFileName = `${profile.auth.user._id}.ics`; const subscribeCalendarOnClick = React.useCallback(() => { platform.openExternal(calendarUrl); toast.show({ @@ -274,7 +273,7 @@ const Settings = () => { { profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?
-
From 9b160a0c6b60a85011705dfbf3a0e41ea4393412 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 14:57:40 +0300 Subject: [PATCH 08/16] remove: href and target on button --- src/routes/Settings/Settings.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 7b1afe620..3b2846305 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -103,10 +103,10 @@ const Settings = () => { }); } }, [isTraktAuthenticated, profile.auth]); - const protocol = platformName === 'ios' ? 'webcal' : 'https'; - const calendarUrl = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; const subscribeCalendarOnClick = React.useCallback(() => { - platform.openExternal(calendarUrl); + const protocol = platformName === 'ios' ? 'webcal' : 'https'; + const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; + platform.openExternal(url); toast.show({ type: 'success', title: platformName === 'android' ? @@ -273,7 +273,7 @@ const Settings = () => { { profile.auth !== null && profile.auth.user !== null && typeof profile.auth.user._id === 'string' ?
-
From 0ebdf83c7f3988502e6282cd3733bbe5071233ff Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 14:59:32 +0300 Subject: [PATCH 09/16] refactor: dependencies on useCallback --- 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 3b2846305..fb97d7386 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -115,7 +115,7 @@ const Settings = () => { timeout: 25000 }); // Stremio 4 emits not documented event subscribeCalendar - }, [platformName, profile.auth.user._id, platform, toast]); + }, [profile.auth.user._id]); const exportDataOnClick = React.useCallback(() => { loadDataExport(); }, []); From dab0169038cf537430958d91d3512d641321cae1 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 15:02:50 +0300 Subject: [PATCH 10/16] refactor: add translations --- src/routes/Settings/Settings.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index fb97d7386..92ad44763 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -109,9 +109,7 @@ const Settings = () => { platform.openExternal(url); toast.show({ type: 'success', - title: platformName === 'android' ? - 'Calendar has been downloaded. Please open it in your calendar app.' : - 'Calendar has been added to your default calendar app.', + title: platformName === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS') : t('SETTINGS_SUBSCRIBE_CALENDAR'), timeout: 25000 }); // Stremio 4 emits not documented event subscribeCalendar From f84bc813f9c71b0759cedea082d05f0ca399ffbb Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 15:08:47 +0300 Subject: [PATCH 11/16] refactor: translations strings --- 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 92ad44763..3e0c4cf8e 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -109,7 +109,7 @@ const Settings = () => { platform.openExternal(url); toast.show({ type: 'success', - title: platformName === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS') : t('SETTINGS_SUBSCRIBE_CALENDAR'), + title: platformName === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'), timeout: 25000 }); // Stremio 4 emits not documented event subscribeCalendar From 8e2da823eaf041435ad7d9782570a599aba13d8e Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 16:00:55 +0300 Subject: [PATCH 12/16] chore: update translations pkg --- package-lock.json | 9 ++++----- package.json | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5775eb1a9..31a0fdda8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,7 +36,7 @@ "react-i18next": "^12.1.1", "react-is": "18.2.0", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd", + "stremio-translations": "github:Stremio/stremio-translations#378218c9617f3e763ba5f6755e4d39c1c158747d", "url": "0.11.0", "use-long-press": "^3.1.5" }, @@ -12470,10 +12470,9 @@ } }, "node_modules/stremio-translations": { - "version": "1.44.7", - "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd", - "integrity": "sha512-OtRAM3j9ie89llgI379p4utCbgnNMswE+LtL/lyLRVLfm5B+jpBLp4ozpU25iQg0O4tvN+OHBjXZ870CCHtZMA==", - "license": "MIT" + "version": "1.44.9", + "resolved": "git+ssh://git@github.com/Stremio/stremio-translations.git#378218c9617f3e763ba5f6755e4d39c1c158747d", + "integrity": "sha512-3GboN8JS2LgrdIVK/gW+n6r1kLrGG+D/tWkRv8PJo2mZLzh49HTzS2u7XXUSkNmA4AGUyEf8QRjyBhlOg8JNTQ==" }, "node_modules/string_decoder": { "version": "1.1.1", diff --git a/package.json b/package.json index 6610395a6..1339ba710 100755 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "react-i18next": "^12.1.1", "react-is": "18.2.0", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", - "stremio-translations": "github:Stremio/stremio-translations#b13b3e2653bd0dcf644d2a20ffa32074fe6532dd", + "stremio-translations": "github:Stremio/stremio-translations#378218c9617f3e763ba5f6755e4d39c1c158747d", "url": "0.11.0", "use-long-press": "^3.1.5" }, From d474ec60ca86e791c1f6f1d0d37bef7a8942b131 Mon Sep 17 00:00:00 2001 From: "Timothy Z." Date: Thu, 26 Sep 2024 17:06:27 +0300 Subject: [PATCH 13/16] refactor: logic unite --- src/common/Platform/Platform.tsx | 66 ++++++++++++++++++++++++++------ src/common/platform.js | 36 ----------------- src/routes/Settings/Settings.js | 5 +-- 3 files changed, 57 insertions(+), 50 deletions(-) delete mode 100644 src/common/platform.js diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 13e8592ad..2dee98bff 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -1,26 +1,70 @@ import React, { createContext, useContext } from 'react'; -import useShell from './useShell'; import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; +import useShell from './useShell'; +import Bowser from 'bowser'; interface PlatformContext { - openExternal: (url: string) => void, -}; + name: string; + isMobile: () => boolean; + openExternal: (url: string) => void; +} const PlatformContext = createContext(null); type Props = { - children: JSX.Element, + children: JSX.Element; }; const PlatformProvider = ({ children }: Props) => { const shell = useShell(); + // this detects ipad properly in safari + // while bowser does not + const iOS = () => { + return ( + [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod', + ].includes(navigator.platform) || + (navigator.userAgent.includes('Mac') && 'ontouchend' in document) + ); + }; + + // Edge case: iPad is included in this function + // Keep in mind maxTouchPoints for Vision Pro might change in the future + const isVisionProUser = () => { + const isMacintosh = navigator.userAgent.includes('Macintosh'); + const hasFiveTouchPoints = navigator.maxTouchPoints === 5; + return isMacintosh && hasFiveTouchPoints; + }; + + const browser = Bowser.getParser(window.navigator?.userAgent || ''); + const osName = browser.getOSName().toLowerCase(); + + const name = isVisionProUser() + ? 'visionos' + : iOS() + ? 'ios' + : osName || 'unknown'; + + const isMobile = () => { + return name === 'ios' || name === 'android'; + }; + 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; - + 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 { @@ -28,11 +72,11 @@ const PlatformProvider = ({ children }: Props) => { } } catch (e) { console.error('Failed to parse external url:', e); - } + } }; return ( - + {children} ); @@ -42,7 +86,7 @@ const usePlatform = () => { return useContext(PlatformContext); }; -export { +export { PlatformProvider, - usePlatform, + usePlatform }; diff --git a/src/common/platform.js b/src/common/platform.js deleted file mode 100644 index 7e423489a..000000000 --- a/src/common/platform.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2017-2024 Smart code 203358507 - -// this detects ipad properly in safari -// while bowser does not -const iOS = () => { - return [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod' - ].includes(navigator.platform) - || (navigator.userAgent.includes('Mac') && 'ontouchend' in document); -}; - -const Bowser = require('bowser'); - -const browser = Bowser.parse(window.navigator?.userAgent || ''); - -// Edge case: iPad is included in this function -// Keep in mind maxTouchPoints for Vision Pro might change in the future -const isVisionProUser = () => { - const isMacintosh = navigator.userAgent.includes('Macintosh'); - const hasFiveTouchPoints = navigator.maxTouchPoints === 5; - return isMacintosh && hasFiveTouchPoints; -}; - -const name = isVisionProUser() ? 'visionos' : (iOS() ? 'ios' : (browser?.os?.name || 'unknown').toLowerCase()); - -module.exports = { - name, - isMobile: () => { - return name === 'ios' || name === 'android'; - } -}; diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 3e0c4cf8e..0d9199225 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -11,7 +11,6 @@ const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, TextInput, Modal const useProfileSettingsInputs = require('./useProfileSettingsInputs'); const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs'); const useDataExport = require('./useDataExport'); -const { name: platformName } = require('stremio/common/platform'); const styles = require('./styles'); const GENERAL_SECTION = 'general'; @@ -104,12 +103,12 @@ const Settings = () => { } }, [isTraktAuthenticated, profile.auth]); const subscribeCalendarOnClick = React.useCallback(() => { - const protocol = platformName === 'ios' ? 'webcal' : 'https'; + const protocol = platform.name === 'ios' ? 'webcal' : 'https'; const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; platform.openExternal(url); toast.show({ type: 'success', - title: platformName === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'), + title: platform.name === 'ios' ? t('SETTINGS_SUBSCRIBE_CALENDAR_IOS_TOAST') : t('SETTINGS_SUBSCRIBE_CALENDAR_TOAST'), timeout: 25000 }); // Stremio 4 emits not documented event subscribeCalendar From ff662d087282bf6665885e4daaa4535a4b52b369 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 27 Sep 2024 09:05:02 +0200 Subject: [PATCH 14/16] refactor: platform device logic --- src/common/Platform/Platform.tsx | 49 +++----------------------------- src/common/Platform/device.ts | 31 ++++++++++++++++++++ src/common/index.js | 2 -- 3 files changed, 35 insertions(+), 47 deletions(-) create mode 100644 src/common/Platform/device.ts diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx index 2dee98bff..69ff3e8c3 100644 --- a/src/common/Platform/Platform.tsx +++ b/src/common/Platform/Platform.tsx @@ -1,11 +1,11 @@ import React, { createContext, useContext } from 'react'; import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS'; import useShell from './useShell'; -import Bowser from 'bowser'; +import { name, isMobile } from './device'; interface PlatformContext { name: string; - isMobile: () => boolean; + isMobile: boolean; openExternal: (url: string) => void; } @@ -18,52 +18,11 @@ type Props = { const PlatformProvider = ({ children }: Props) => { const shell = useShell(); - // this detects ipad properly in safari - // while bowser does not - const iOS = () => { - return ( - [ - 'iPad Simulator', - 'iPhone Simulator', - 'iPod Simulator', - 'iPad', - 'iPhone', - 'iPod', - ].includes(navigator.platform) || - (navigator.userAgent.includes('Mac') && 'ontouchend' in document) - ); - }; - - // Edge case: iPad is included in this function - // Keep in mind maxTouchPoints for Vision Pro might change in the future - const isVisionProUser = () => { - const isMacintosh = navigator.userAgent.includes('Macintosh'); - const hasFiveTouchPoints = navigator.maxTouchPoints === 5; - return isMacintosh && hasFiveTouchPoints; - }; - - const browser = Bowser.getParser(window.navigator?.userAgent || ''); - const osName = browser.getOSName().toLowerCase(); - - const name = isVisionProUser() - ? 'visionos' - : iOS() - ? 'ios' - : osName || 'unknown'; - - const isMobile = () => { - return name === 'ios' || name === 'android'; - }; - 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; + 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); diff --git a/src/common/Platform/device.ts b/src/common/Platform/device.ts new file mode 100644 index 000000000..68ca72b32 --- /dev/null +++ b/src/common/Platform/device.ts @@ -0,0 +1,31 @@ +import Bowser from 'bowser'; + +const APPLE_MOBILE_DEVICES = [ + 'iPad Simulator', + 'iPhone Simulator', + 'iPod Simulator', + 'iPad', + 'iPhone', + 'iPod', +]; + +const { userAgent, platform, maxTouchPoints } = globalThis.navigator; + +// this detects ipad properly in safari +// while bowser does not +const isIOS = APPLE_MOBILE_DEVICES.includes(platform) || (userAgent.includes('Mac') && 'ontouchend' in document); + +// Edge case: iPad is included in this function +// Keep in mind maxTouchPoints for Vision Pro might change in the future +const isVisionOS = userAgent.includes('Macintosh') || maxTouchPoints === 5; + +const bowser = Bowser.getParser(userAgent); +const os = bowser.getOSName().toLowerCase(); + +const name = isVisionOS ? 'visionos' : isIOS ? 'ios' : os || 'unknown'; +const isMobile = ['ios', 'android'].includes(name); + +export { + name, + isMobile, +}; \ No newline at end of file diff --git a/src/common/index.js b/src/common/index.js index 1188b8c61..5adef3e60 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -46,7 +46,6 @@ const useProfile = require('./useProfile'); const useStreamingServer = require('./useStreamingServer'); const useTorrent = require('./useTorrent'); const useTranslate = require('./useTranslate'); -const platform = require('./platform'); const EventModal = require('./EventModal'); module.exports = { @@ -101,6 +100,5 @@ module.exports = { useStreamingServer, useTorrent, useTranslate, - platform, EventModal, }; From 3c517f6a32a2b29e8b789e23771378916096cc45 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 27 Sep 2024 10:59:48 +0200 Subject: [PATCH 15/16] fix: replace platform by usePlatform --- src/routes/MetaDetails/StreamsList/Stream/Stream.js | 3 ++- src/routes/Settings/useProfileSettingsInputs.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/routes/MetaDetails/StreamsList/Stream/Stream.js b/src/routes/MetaDetails/StreamsList/Stream/Stream.js index 40ccb2f07..e4829ece0 100644 --- a/src/routes/MetaDetails/StreamsList/Stream/Stream.js +++ b/src/routes/MetaDetails/StreamsList/Stream/Stream.js @@ -5,7 +5,7 @@ const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); const { t } = require('i18next'); -const { Button, Image, useProfile, platform, useToast, Popup, useBinaryState } = require('stremio/common'); +const { Button, Image, useProfile, usePlatform, useToast, Popup, useBinaryState } = require('stremio/common'); const { useServices } = require('stremio/services'); const { useRouteFocused } = require('stremio-router'); const StreamPlaceholder = require('./StreamPlaceholder'); @@ -14,6 +14,7 @@ const styles = require('./styles'); const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => { const profile = useProfile(); const toast = useToast(); + const platform = usePlatform(); const { core } = useServices(); const routeFocused = useRouteFocused(); diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index 939e5e7cf..a90d0d483 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -3,11 +3,12 @@ const React = require('react'); const { useTranslation } = require('react-i18next'); const { useServices } = require('stremio/services'); -const { CONSTANTS, interfaceLanguages, languageNames, platform } = require('stremio/common'); +const { CONSTANTS, usePlatform, interfaceLanguages, languageNames } = require('stremio/common'); const useProfileSettingsInputs = (profile) => { const { t } = useTranslation(); const { core } = useServices(); + const platform = usePlatform(); // TODO combine those useMemo in one const interfaceLanguageSelect = React.useMemo(() => ({ options: interfaceLanguages.map(({ name, codes }) => ({ From 0a2ad7b6aa4331f20ae48b8c82f43b60405366d9 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 27 Sep 2024 11:20:57 +0200 Subject: [PATCH 16/16] fix(Settings): check for profile.auth in subscribeCalendarOnClick --- src/routes/Settings/Settings.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 0d9199225..ed4f45e16 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -103,6 +103,8 @@ const Settings = () => { } }, [isTraktAuthenticated, profile.auth]); const subscribeCalendarOnClick = React.useCallback(() => { + if (!profile.auth) return; + const protocol = platform.name === 'ios' ? 'webcal' : 'https'; const url = `${protocol}://www.strem.io/calendar/${profile.auth.user._id}.ics`; platform.openExternal(url); @@ -112,7 +114,7 @@ const Settings = () => { timeout: 25000 }); // Stremio 4 emits not documented event subscribeCalendar - }, [profile.auth.user._id]); + }, [profile.auth]); const exportDataOnClick = React.useCallback(() => { loadDataExport(); }, []);