From 7a3f331865ff7d27e5fa1de35b27ce7d36295aac Mon Sep 17 00:00:00 2001 From: Botzy Date: Fri, 22 May 2026 21:13:46 +0300 Subject: [PATCH] Add navigation with React Router V6 --- package.json | 2 + pnpm-lock.yaml | 37 ++++++ src/App/App.js | 51 +------- src/App/DeepLinkHandler.js | 5 +- src/App/OpenMediaHandler.tsx | 35 +++++ src/App/withProtectedRoutes.js | 29 ----- src/common/Shortcuts/Shortcuts.tsx | 4 +- src/common/index.js | 2 + src/common/routerPaths.tsx | 72 +++++++++++ src/common/toPath.ts | 5 + src/common/useModelState.js | 2 +- src/common/useNavigateWithOrigin.ts | 39 ++++++ src/common/usePlayUrl.ts | 4 +- src/common/useRouteFocused.ts | 29 +++++ src/components/LibItem/LibItem.js | 11 +- src/components/MainNavBars/MainNavBars.tsx | 12 +- src/components/MetaItem/MetaItem.js | 3 + src/components/ModalDialog/ModalDialog.js | 5 +- .../HorizontalNavBar/HorizontalNavBar.js | 13 +- .../HorizontalNavBar/NavMenu/NavMenu.js | 2 +- .../NavMenu/NavMenuContent.js | 10 +- .../HorizontalNavBar/SearchBar/SearchBar.js | 14 +- .../NavTabButton/NavTabButton.js | 7 +- src/components/Popup/Popup.js | 2 +- src/components/SharePrompt/SharePrompt.js | 2 +- src/components/Slider/Slider.js | 2 +- src/components/Video/Video.js | 12 +- src/router/Route/Route.js | 18 ++- .../RouteFocusedContext.js | 9 -- src/router/RouteFocusedContext/index.js | 9 -- .../RouteFocusedContext/useRouteFocused.js | 10 -- src/router/Router/Router.js | 121 ++++++------------ src/router/Router/Routes.tsx | 90 +++++++++++++ src/router/index.js | 2 - src/routes/Addons/Addons.js | 24 ++-- .../Addons/useAddonDetailsTransportUrl.js | 6 +- src/routes/Addons/useSelectableInputs.js | 14 +- src/routes/Calendar/Calendar.tsx | 12 +- src/routes/Calendar/List/Item/Item.tsx | 3 + src/routes/Calendar/Selector/Selector.tsx | 7 +- src/routes/Calendar/Table/Cell/Cell.tsx | 3 + src/routes/Calendar/useCalendar.ts | 3 +- src/routes/Discover/Discover.js | 21 ++- src/routes/Discover/useSelectableInputs.js | 13 +- src/routes/Intro/Intro.js | 19 ++- .../PasswordResetModal/PasswordResetModal.js | 2 +- src/routes/Library/Library.js | 46 +++---- src/routes/Library/useSelectableInputs.js | 12 +- .../EpisodePicker/EpisodePicker.tsx | 4 +- src/routes/MetaDetails/MetaDetails.js | 36 +++--- .../MetaDetails/StreamsList/Stream/Stream.js | 2 +- .../MetaDetails/StreamsList/StreamsList.js | 16 ++- src/routes/MetaDetails/useSeason.js | 17 ++- .../Player/ControlBar/SeekBar/SeekBar.js | 2 +- .../ControlBar/VolumeSlider/VolumeSlider.js | 2 +- src/routes/Player/Player.js | 48 +++---- src/routes/Search/Search.js | 16 +-- src/routes/Settings/Settings.tsx | 5 +- .../useHorizontalNavGamepadNavigation.tsx | 6 +- 59 files changed, 615 insertions(+), 394 deletions(-) create mode 100644 src/App/OpenMediaHandler.tsx delete mode 100644 src/App/withProtectedRoutes.js create mode 100644 src/common/routerPaths.tsx create mode 100644 src/common/toPath.ts create mode 100644 src/common/useNavigateWithOrigin.ts create mode 100644 src/common/useRouteFocused.ts delete mode 100644 src/router/RouteFocusedContext/RouteFocusedContext.js delete mode 100644 src/router/RouteFocusedContext/index.js delete mode 100644 src/router/RouteFocusedContext/useRouteFocused.js create mode 100644 src/router/Router/Routes.tsx diff --git a/package.json b/package.json index fbcf30d86..604dd8921 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "react-focus-lock": "2.13.7", "react-i18next": "^15.7.4", "react-is": "18.3.1", + "react-router": "6.30.0", + "react-router-dom": "6.30.0", "spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", "stremio-translations": "github:Stremio/stremio-translations#f8ef365fcc90b7904a11ad2e3ebb95c0c9b16163", "url": "0.11.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3703b3506..4918caf18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: react-is: specifier: 18.3.1 version: 18.3.1 + react-router: + specifier: 6.30.0 + version: 6.30.0(react@18.3.1) + react-router-dom: + specifier: 6.30.0 + version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) spatial-navigation-polyfill: specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6 version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6 @@ -1180,6 +1186,10 @@ packages: resolution: {integrity: sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==} engines: {node: '>=20.0.0'} + '@remix-run/router@1.23.0': + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + engines: {node: '>=14.0.0'} + '@rollup/plugin-babel@6.1.0': resolution: {integrity: sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==} engines: {node: '>=14.0.0'} @@ -4096,6 +4106,19 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-router-dom@6.30.0: + resolution: {integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.0: + resolution: {integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6283,6 +6306,8 @@ snapshots: tslib: 2.8.1 tsyringe: 4.10.0 + '@remix-run/router@1.23.0': {} + '@rollup/plugin-babel@6.1.0(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@4.60.3)': dependencies: '@babel/core': 7.29.0 @@ -9591,6 +9616,18 @@ snapshots: react-is@18.3.1: {} + react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.0(react@18.3.1) + + react-router@6.30.0(react@18.3.1): + dependencies: + '@remix-run/router': 1.23.0 + react: 18.3.1 + react@18.3.1: dependencies: loose-envify: 1.4.0 diff --git a/src/App/App.js b/src/App/App.js index 4906a5921..9ca72681b 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -6,19 +6,15 @@ const { useTranslation } = require('react-i18next'); const { useCore } = require('stremio/core'); const { Router } = require('stremio-router'); const { Chromecast, ServicesProvider, GamepadProvider } = require('stremio/services'); -const { NotFound } = require('stremio/routes'); const { FullscreenProvider, ToastProvider, TooltipProvider, ShortcutsProvider, CONSTANTS, useBinaryState, useProfile, withCoreSuspender, onFileDrop, usePlatform } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); -const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); const { default: UpdaterBanner } = require('./UpdaterBanner'); const { default: ShortcutsModal } = require('./ShortcutsModal'); const { default: GamepadModal } = require('./GamepadModal'); -const withProtectedRoutes = require('./withProtectedRoutes'); -const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); -const RouterWithProtectedRoutes = withProtectedRoutes(Router); +const RouterWithProtectedRoutes = withCoreSuspender(Router); const App = () => { const core = useCore(); @@ -26,9 +22,6 @@ const App = () => { const { i18n } = useTranslation(); const { shell } = usePlatform(); const [gamepadSupportEnabled, setGamepadSupportEnabled] = React.useState(false); - const onPathNotMatch = React.useCallback(() => { - return NotFound; - }, []); const services = React.useMemo(() => { return { chromecast: new Chromecast(), @@ -37,7 +30,7 @@ const App = () => { const [shortcutModalOpen,, closeShortcutsModal, toggleShortcutModal] = useBinaryState(false); const [gamepadModalOpen,, closeGamepadModal, toggleGamepadModal] = useBinaryState(false); - const onShortcut = React.useCallback((name, combo, key) => { + const onShortcut = React.useCallback((name) => { switch (name) { case 'shortcuts': toggleShortcutModal(); @@ -45,18 +38,6 @@ const App = () => { case 'gamepadGuide': toggleGamepadModal(); break; - case 'navigateSearch': - window.location = '#/search'; - break; - case 'navigateTabs': { - const routes = ['', 'discover', 'library', 'calendar', 'addons', 'settings']; - const index = key - 1; - if (index in routes) window.location = `#/${routes[index]}`; - break; - } - case 'navigateHistory': - combo === 0 ? window.history.back() : window.history.forward(); - break; } }, [toggleShortcutModal, toggleGamepadModal]); @@ -106,31 +87,6 @@ const App = () => { }; }, []); - // Handle shell events - React.useEffect(() => { - const onOpenMedia = (data) => { - 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); - } - }; - - shell.on('open-media', onOpenMedia); - - return () => { - shell.off('open-media', onOpenMedia); - }; - }, []); - React.useEffect(() => { if (typeof profile.settings?.interfaceLanguage === 'string') { i18n.changeLanguage(profile.settings.interfaceLanguage); @@ -196,13 +152,10 @@ const App = () => { gamepadModalOpen && } - diff --git a/src/App/DeepLinkHandler.js b/src/App/DeepLinkHandler.js index 2df2a2e43..910ba7594 100644 --- a/src/App/DeepLinkHandler.js +++ b/src/App/DeepLinkHandler.js @@ -1,9 +1,12 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); const { withCoreSuspender, useStreamingServer } = require('stremio/common'); +const { default: toPath } = require('stremio/common/toPath'); const DeepLinkHandler = () => { + const navigate = useNavigate(); const streamingServer = useStreamingServer(); React.useEffect(() => { if (streamingServer.torrent !== null) { @@ -11,7 +14,7 @@ const DeepLinkHandler = () => { if (type === 'Ready') { const [, deepLinks] = content; if (typeof deepLinks.metaDetailsVideos === 'string') { - window.location = deepLinks.metaDetailsVideos; + navigate(toPath(deepLinks.metaDetailsVideos)); } } } diff --git a/src/App/OpenMediaHandler.tsx b/src/App/OpenMediaHandler.tsx new file mode 100644 index 000000000..d63721b08 --- /dev/null +++ b/src/App/OpenMediaHandler.tsx @@ -0,0 +1,35 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +import { useEffect } from 'react'; +import { useNavigate } from 'react-router'; +import { usePlatform, CONSTANTS } from 'stremio/common'; + +const OpenMediaHandler = () => { + const navigate = useNavigate(); + const { shell } = usePlatform(); + + useEffect(() => { + const onOpenMedia = (data: string) => { + try { + const { protocol, hostname, pathname, searchParams } = new URL(data); + if (protocol === CONSTANTS.PROTOCOL) { + if (hostname.length) { + const transportUrl = `https://${hostname}${pathname}`; + navigate(`/addons?addon=${encodeURIComponent(transportUrl)}`); + } else { + navigate(`${pathname}?${searchParams.toString()}`); + } + } + } catch (e) { + console.error('Failed to open media:', e); + } + }; + + shell.on('open-media', onOpenMedia); + return () => shell.off('open-media', onOpenMedia); + }, [navigate, shell]); + + return null; +}; + +export default OpenMediaHandler; diff --git a/src/App/withProtectedRoutes.js b/src/App/withProtectedRoutes.js deleted file mode 100644 index a16e0c0c7..000000000 --- a/src/App/withProtectedRoutes.js +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2017-2023 Smart code 203358507 - -const React = require('react'); -const { Intro } = require('stremio/routes'); -const { useProfile } = require('stremio/common'); - -const withProtectedRoutes = (Component) => { - return function withProtectedRoutes(props) { - const profile = useProfile(); - const previousAuthRef = React.useRef(profile.auth); - React.useEffect(() => { - if (previousAuthRef.current !== null && profile.auth === null) { - window.location = '#/intro'; - } - previousAuthRef.current = profile.auth; - }, [profile]); - const onRouteChange = React.useCallback((routeConfig) => { - if (profile.auth !== null && routeConfig.component === Intro) { - window.location.replace('#/'); - return true; - } - }, [profile]); - return ( - - ); - }; -}; - -module.exports = withProtectedRoutes; diff --git a/src/common/Shortcuts/Shortcuts.tsx b/src/common/Shortcuts/Shortcuts.tsx index c27f5b25e..d145e874c 100644 --- a/src/common/Shortcuts/Shortcuts.tsx +++ b/src/common/Shortcuts/Shortcuts.tsx @@ -4,7 +4,7 @@ import shortcuts from './shortcuts.json'; const SHORTCUTS = shortcuts.map(({ shortcuts }) => shortcuts).flat(); export type ShortcutName = string; -export type ShortcutListener = (combo: number) => void; +export type ShortcutListener = (combo: number, key: string) => void; interface ShortcutsContext { grouped: ShortcutGroup[], @@ -51,7 +51,7 @@ const ShortcutsProvider = ({ children, onShortcut }: Props) => { if (modifers && (keys.includes(code) || keys.includes(key.toUpperCase()))) { const combo = combos.indexOf(keys); - listeners.current.get(name)?.forEach((listener) => listener(combo)); + listeners.current.get(name)?.forEach((listener) => listener(combo, key)); onShortcut(name as ShortcutName, combo, key); } diff --git a/src/common/index.js b/src/common/index.js index b99b88c2f..870592e84 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -21,6 +21,7 @@ const useModelState = require('./useModelState'); const useNotifications = require('./useNotifications'); const useOnScrollToBottom = require('./useOnScrollToBottom'); const useProfile = require('./useProfile'); +const { default: useRouteFocused } = require('./useRouteFocused'); const { default: useSettings } = require('./useSettings'); const useStreamingServer = require('./useStreamingServer'); const { default: useTimeout } = require('./useTimeout'); @@ -61,6 +62,7 @@ module.exports = { useNotifications, useOnScrollToBottom, useProfile, + useRouteFocused, useSettings, useStreamingServer, useTimeout, diff --git a/src/common/routerPaths.tsx b/src/common/routerPaths.tsx new file mode 100644 index 000000000..d877b98c1 --- /dev/null +++ b/src/common/routerPaths.tsx @@ -0,0 +1,72 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React from 'react'; +import routes from 'stremio/routes'; + +export const routerPaths = [ + { + path: '/intro', + view: 1, + element: , + }, + { + path: '/discover/:transportUrl?/:type?/:catalogId?', + view: 1, + element: , + }, + { + path: '/library/:type?', + view: 1, + element: , + }, + { + path: '/calendar/:year?/:month?', + view: 1, + element: , + }, + { + path: '/continuewatching/:type?', + view: 1, + element: , + }, + { + path: '/search', + view: 1, + element: , + }, + { + path: '/metadetails/:type?/:id?/:videoId?', + view: 2, + element: , + }, + { + path: '/detail/:type?/:id?/:videoId?', + view: 2, + element: , + }, + { + path: '/addons/:type?/:transportUrl?/:catalogId?', + view: 3, + element: , + }, + { + path: '/settings', + view: 3, + element: , + }, + { + path: '/player/:stream?/:streamTransportUrl?/:metaTransportUrl?/:type?/:id?/:videoId?', + view: 4, + element: , + }, + { + path: '/', + view: 0, + element: , + }, + { + path: '*', + view: 1, + element: , + }, +]; diff --git a/src/common/toPath.ts b/src/common/toPath.ts new file mode 100644 index 000000000..62021c975 --- /dev/null +++ b/src/common/toPath.ts @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2026 Smart code 203358507 + +const toPath = (link: string): string => link.startsWith('#') ? link.slice(1) : link; + +export default toPath; diff --git a/src/common/useModelState.js b/src/common/useModelState.js index f9914553c..685f23430 100644 --- a/src/common/useModelState.js +++ b/src/common/useModelState.js @@ -6,7 +6,7 @@ const { deepEqual } = require('fast-equals'); const intersection = require('lodash.intersection'); const { useCore } = require('stremio/core'); const { useCoreSuspender } = require('stremio/common/CoreSuspender'); -const { useRouteFocused } = require('stremio-router'); +const { default: useRouteFocused } = require('stremio/common/useRouteFocused'); const useModelState = ({ action, ...args }) => { const core = useCore(); diff --git a/src/common/useNavigateWithOrigin.ts b/src/common/useNavigateWithOrigin.ts new file mode 100644 index 000000000..08eceb174 --- /dev/null +++ b/src/common/useNavigateWithOrigin.ts @@ -0,0 +1,39 @@ +import { useLocation, useNavigate, To, Location } from 'react-router-dom'; + +const ORIGIN_KEY = 'originPath'; + +export function useNavigateWithOrigin() { + const navigate = useNavigate(); + const location = useLocation(); + + function navigateWithOrigin(target: To) { + const origin: Location = location.state?.from || location; + + // Save origin in sessionStorage + sessionStorage.setItem(ORIGIN_KEY, origin.pathname + origin.search); + + // Navigate and propagate origin + navigate(target, { + state: { from: origin }, + }); + } + + function setOriginPath(path?: string) { + const finalPath = path ?? location.pathname + location.search; + sessionStorage.setItem(ORIGIN_KEY, finalPath); + } + + function getStoredOrigin(fallback?: string): string | undefined { + if (location.state?.from) { + const from = location.state.from as Location; + return from.pathname + (from.search || ''); + } + return sessionStorage.getItem(ORIGIN_KEY) || fallback; + } + + return { + navigateWithOrigin, + getStoredOrigin, + setOriginPath, + }; +} diff --git a/src/common/usePlayUrl.ts b/src/common/usePlayUrl.ts index 2ac0ac1ff..c2c28590a 100644 --- a/src/common/usePlayUrl.ts +++ b/src/common/usePlayUrl.ts @@ -1,4 +1,5 @@ import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; import magnet from 'magnet-uri'; import { useCore } from 'stremio/core'; import useToast from 'stremio/common/Toast/useToast'; @@ -8,6 +9,7 @@ import useStreamingServer from 'stremio/common/useStreamingServer'; const HTTP_REGEX = /^https?:\/\/.+/i; const usePlayUrl = () => { + const navigate = useNavigate(); const core = useCore(); const toast = useToast(); const { createTorrentFromMagnet } = useTorrent(); @@ -30,7 +32,7 @@ const usePlayUrl = () => { url: trimmed, }); if (typeof encoded === 'string') { - window.location.hash = `#/player/${encodeURIComponent(encoded)}`; + navigate(`/player/${encodeURIComponent(encoded)}`); return true; } } catch (e) { diff --git a/src/common/useRouteFocused.ts b/src/common/useRouteFocused.ts new file mode 100644 index 000000000..689e9851c --- /dev/null +++ b/src/common/useRouteFocused.ts @@ -0,0 +1,29 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React from 'react'; + +const RouteFocusedContext = React.createContext(true); + +const useRouteFocused = () => { + const routeFocused = React.useContext(RouteFocusedContext); + const [isFocused, setIsFocused] = React.useState(document.hasFocus()); + + React.useEffect(() => { + const handleFocus = () => setIsFocused(true); + const handleBlur = () => setIsFocused(false); + + window.addEventListener('focus', handleFocus); + window.addEventListener('blur', handleBlur); + + return () => { + window.removeEventListener('focus', handleFocus); + window.removeEventListener('blur', handleBlur); + }; + }, []); + + return routeFocused && isFocused; +}; + +export const RouteFocusedProvider = RouteFocusedContext.Provider; + +export default useRouteFocused; diff --git a/src/components/LibItem/LibItem.js b/src/components/LibItem/LibItem.js index 1d5e5c96f..827399788 100644 --- a/src/components/LibItem/LibItem.js +++ b/src/components/LibItem/LibItem.js @@ -1,12 +1,15 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); +const { default: toPath } = require('stremio/common/toPath'); const PropTypes = require('prop-types'); const { useCore } = require('stremio/core'); const MetaItem = require('stremio/components/MetaItem'); const { t } = require('i18next'); const LibItem = ({ _id, removable, notifications, watched, ...props }) => { + const navigate = useNavigate(); const core = useCore(); const newVideos = React.useMemo(() => { @@ -49,7 +52,7 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => { switch (event.value) { case 'play': { if (props.deepLinks && typeof props.deepLinks.player === 'string') { - window.location = props.deepLinks.player; + navigate(toPath(props.deepLinks.player)); } break; @@ -57,9 +60,9 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => { case 'details': { if (props.deepLinks) { if (typeof props.deepLinks.metaDetailsVideos === 'string') { - window.location = props.deepLinks.metaDetailsVideos; + navigate(toPath(props.deepLinks.metaDetailsVideos)); } else if (typeof props.deepLinks.metaDetailsStreams === 'string') { - window.location = props.deepLinks.metaDetailsStreams; + navigate(toPath(props.deepLinks.metaDetailsStreams)); } } @@ -122,7 +125,7 @@ const LibItem = ({ _id, removable, notifications, watched, ...props }) => { if (props.deepLinks && typeof props.deepLinks.player === 'string') { return (event) => { event.preventDefault(); - window.location = props.deepLinks.player; + navigate(toPath(props.deepLinks.player)); }; } return null; diff --git a/src/components/MainNavBars/MainNavBars.tsx b/src/components/MainNavBars/MainNavBars.tsx index 1dae27a16..1b8037eb5 100644 --- a/src/components/MainNavBars/MainNavBars.tsx +++ b/src/components/MainNavBars/MainNavBars.tsx @@ -7,12 +7,12 @@ import { useContentGamepadNavigation, useVerticalNavGamepadNavigation } from 'st import styles from './MainNavBars.less'; const TABS = [ - { id: 'board', label: 'Board', icon: 'home', href: '#/' }, - { id: 'discover', label: 'Discover', icon: 'discover', href: '#/discover' }, - { id: 'library', label: 'Library', icon: 'library', href: '#/library' }, - { id: 'calendar', label: 'Calendar', icon: 'calendar', href: '#/calendar' }, - { id: 'addons', label: 'ADDONS', icon: 'addons', href: '#/addons' }, - { id: 'settings', label: 'SETTINGS', icon: 'settings', href: '#/settings' }, + { id: 'board', label: 'Board', icon: 'home', href: '/' }, + { id: 'discover', label: 'Discover', icon: 'discover', href: '/discover' }, + { id: 'library', label: 'Library', icon: 'library', href: '/library' }, + { id: 'calendar', label: 'Calendar', icon: 'calendar', href: '/calendar' }, + { id: 'addons', label: 'ADDONS', icon: 'addons', href: '/addons' }, + { id: 'settings', label: 'SETTINGS', icon: 'settings', href: '/settings' }, ]; type Props = { diff --git a/src/components/MetaItem/MetaItem.js b/src/components/MetaItem/MetaItem.js index 0e66c6a9e..bd61ef964 100644 --- a/src/components/MetaItem/MetaItem.js +++ b/src/components/MetaItem/MetaItem.js @@ -6,6 +6,7 @@ const classnames = require('classnames'); const { useTranslation } = require('react-i18next'); const filterInvalidDOMProps = require('filter-invalid-dom-props').default; const { default: Icon } = require('@stremio/stremio-icons/react'); +const { useNavigateWithOrigin } = require('stremio/common/useNavigateWithOrigin'); const { default: Button } = require('stremio/components/Button'); const { default: Image } = require('stremio/components/Image'); const Multiselect = require('stremio/components/Multiselect'); @@ -15,6 +16,7 @@ const styles = require('./styles'); const MetaItem = React.memo(({ className, type, name, poster, posterShape, posterChangeCursor, progress, newVideos, options, deepLinks, dataset, optionOnSelect, onDismissClick, onPlayClick, watched, ...props }) => { const { t } = useTranslation(); + const { setOriginPath } = useNavigateWithOrigin(); const [menuOpen, onMenuOpen, onMenuClose] = useBinaryState(false); const href = React.useMemo(() => { return deepLinks ? @@ -32,6 +34,7 @@ const MetaItem = React.memo(({ className, type, name, poster, posterShape, poste null; }, [deepLinks]); const metaItemOnClick = React.useCallback((event) => { + setOriginPath(); if (event.nativeEvent.selectPrevented) { event.preventDefault(); } else if (typeof props.onClick === 'function') { diff --git a/src/components/ModalDialog/ModalDialog.js b/src/components/ModalDialog/ModalDialog.js index b07481f69..44f1f1fdb 100644 --- a/src/components/ModalDialog/ModalDialog.js +++ b/src/components/ModalDialog/ModalDialog.js @@ -4,10 +4,11 @@ const React = require('react'); const { useTranslation } = require('react-i18next'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { useRouteFocused, useModalsContainer } = require('stremio-router'); +const { useModalsContainer } = require('stremio/router/ModalsContainerContext'); +const Modal = require('stremio/router/Modal'); +const { default: useRouteFocused } = require('stremio/common/useRouteFocused'); const { default: Button } = require('stremio/components/Button'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Modal } = require('stremio-router'); const styles = require('./styles'); const ModalDialog = ({ className, title, buttons, children, dataset, onCloseRequest, background, ...props }) => { diff --git a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js index c9679ecf8..7130239b3 100644 --- a/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/components/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -1,6 +1,7 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); @@ -12,10 +13,15 @@ const NavMenu = require('./NavMenu'); const styles = require('./styles'); const { t } = require('i18next'); -const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, fullscreenButton, navMenu, hdrInfo, ...props }) => { +const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, fullscreenButton, navMenu, originPath, hdrInfo, ...props }) => { + const navigate = useNavigate(); const backButtonOnClick = React.useCallback(() => { - window.history.back(); - }, []); + if (originPath) { + navigate(originPath); + } else { + navigate(-1); + } + }, [originPath, navigate]); const [fullscreen, requestFullscreen, exitFullscreen, , supported] = useFullscreen(); const renderNavMenuLabel = React.useCallback(({ ref, className, onClick, children, }) => ( diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index e8f58bdff..7ce785213 100644 --- a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -1,12 +1,14 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); +const { useSearchParams } = require('react-router-dom'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const debounce = require('lodash.debounce'); const { useTranslation } = require('react-i18next'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { useRouteFocused } = require('stremio-router'); +const { default: useRouteFocused } = require('stremio/common/useRouteFocused'); const Button = require('stremio/components/Button').default; const TextInput = require('stremio/components/TextInput').default; const { default: usePlayUrl } = require('stremio/common/usePlayUrl'); @@ -21,17 +23,18 @@ const SearchBar = React.memo(({ className, query, active }) => { const routeFocused = useRouteFocused(); const searchHistory = useSearchHistory(); const localSearch = useLocalSearch(); + const navigate = useNavigate(); const { handlePlayUrl } = usePlayUrl(); const [historyOpen, openHistory, closeHistory, ] = useBinaryState(query === null ? true : false); const [currentQuery, setCurrentQuery] = React.useState(query || ''); - + const [, setSearchParams] = useSearchParams(); const searchInputRef = React.useRef(null); const containerRef = React.useRef(null); const searchBarOnClick = React.useCallback(() => { if (!active) { - window.location = '#/search'; + navigate('/search'); } }, [active]); @@ -66,7 +69,7 @@ const SearchBar = React.memo(({ className, query, active }) => { const searchValue = `/search?search=${encodeURIComponent(event.target.value)}`; setCurrentQuery(searchValue); if (searchInputRef.current && searchValue) { - window.location.hash = searchValue; + setSearchParams({ search: event.target.value }); closeHistory(); } }, []); @@ -74,7 +77,8 @@ const SearchBar = React.memo(({ className, query, active }) => { const queryInputClear = React.useCallback(() => { searchInputRef.current.value = ''; setCurrentQuery(''); - window.location.hash = '/search'; + setSearchParams({}); + navigate('/search'); }, []); const updateLocalSearchDebounced = React.useCallback(debounce((query) => { diff --git a/src/components/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js b/src/components/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js index 65c6a02a9..e59f106f7 100644 --- a/src/components/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js +++ b/src/components/NavBar/VerticalNavBar/NavTabButton/NavTabButton.js @@ -4,8 +4,9 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const { default: Icon } = require('@stremio/stremio-icons/react'); -const { Button, Image } = require('stremio/components'); +const { Image } = require('stremio/components'); const styles = require('./styles'); +const { Link } = require('react-router-dom'); const NavTabButton = ({ className, logo, icon, label, href, selected, onClick }) => { const renderLogoFallback = React.useCallback(() => ( @@ -24,7 +25,7 @@ const NavTabButton = ({ className, logo, icon, label, href, selected, onClick }) }); }; return ( -