diff --git a/package-lock.json b/package-lock.json index 39a17e5aa..1fb9d968d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,8 @@ "react-focus-lock": "2.13.2", "react-i18next": "^15.1.3", "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#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", @@ -3105,6 +3107,15 @@ "node": ">= 8" } }, + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -12498,6 +12509,38 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-router": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz", + "integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz", + "integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.0", + "react-router": "6.30.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "dev": true, diff --git a/package.json b/package.json index 9599f3587..b7d1f108f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,8 @@ "react-focus-lock": "2.13.2", "react-i18next": "^15.1.3", "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#8212fa77c4febd22ddb611590e9fb574dc845416", "url": "0.11.4", diff --git a/src/App/App.js b/src/App/App.js index 3e816be9f..a1976b586 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -5,25 +5,19 @@ const React = require('react'); 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 { FileDropProvider, PlatformProvider, ToastProvider, TooltipProvider, CONSTANTS, withCoreSuspender, useShell } = require('stremio/common'); const ServicesToaster = require('./ServicesToaster'); const DeepLinkHandler = require('./DeepLinkHandler'); const SearchParamsHandler = require('./SearchParamsHandler'); const { default: UpdaterBanner } = require('./UpdaterBanner'); const ErrorDialog = require('./ErrorDialog'); -const withProtectedRoutes = require('./withProtectedRoutes'); -const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); -const RouterWithProtectedRoutes = withCoreSuspender(withProtectedRoutes(Router)); +const RouterWithProtectedRoutes = withCoreSuspender(Router); const App = () => { const { i18n } = useTranslation(); const shell = useShell(); - const onPathNotMatch = React.useCallback(() => { - return NotFound; - }, []); const services = React.useMemo(() => { const core = new Core({ appVersion: process.env.VERSION, @@ -207,11 +201,7 @@ const App = () => { - + 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/index.js b/src/common/index.js index 55ccfe045..a691d58c5 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -20,6 +20,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 { default: useShell } = require('./useShell'); const useStreamingServer = require('./useStreamingServer'); @@ -53,6 +54,7 @@ module.exports = { useNotifications, useOnScrollToBottom, useProfile, + useRouteFocused, useSettings, useShell, useStreamingServer, diff --git a/src/common/routerPaths.tsx b/src/common/routerPaths.tsx new file mode 100644 index 000000000..9cd98a9ec --- /dev/null +++ b/src/common/routerPaths.tsx @@ -0,0 +1,59 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React from 'react'; +import routes from 'stremio/routes'; + +export const routerPaths = [ + { + path: '/intro', + element: , + }, + { + path: '/discover/:transportUrl?/:type?/:catalogId?', + element: , + }, + { + path: '/library/:type?', + element: , + }, + { + path: '/calendar/:year?/:month?', + element: , + }, + { + path: '/continuewatching/:type?', + element: , + }, + { + path: '/search', + element: , + }, + { + path: '/metadetails/:type?/:id?/:videoId?', + element: , + }, + { + path: '/detail/:type?/:id?/:videoId?', + element: , + }, + { + path: '/addons/:type?/:transportUrl?/:catalogId?', + element: , + }, + { + path: '/settings', + element: , + }, + { + path: '/player/:stream?/:streamTransportUrl?/:metaTransportUrl?/:type?/:id?/:videoId?', + element: , + }, + { + path: '/', + element: , + }, + { + path: '*', + element: , + }, +]; diff --git a/src/common/useModelState.js b/src/common/useModelState.js index 42672dc22..8f22f2a02 100644 --- a/src/common/useModelState.js +++ b/src/common/useModelState.js @@ -5,7 +5,7 @@ const throttle = require('lodash.throttle'); const isEqual = require('lodash.isequal'); const intersection = require('lodash.intersection'); const { useCoreSuspender } = require('stremio/common/CoreSuspender'); -const { useRouteFocused } = require('stremio-router'); +const { default: useRouteFocused } = require('stremio/common/useRouteFocused'); const { useServices } = require('stremio/services'); const useModelState = ({ action, ...args }) => { @@ -32,17 +32,17 @@ const useModelState = ({ action, ...args }) => { } } ); - React.useInsertionEffect(() => { + React.useLayoutEffect(() => { if (action) { core.transport.dispatch(action, model); } }, [action]); - React.useInsertionEffect(() => { + React.useLayoutEffect(() => { return () => { core.transport.dispatch({ action: 'Unload' }, model); }; }, []); - React.useInsertionEffect(() => { + React.useLayoutEffect(() => { const onNewState = async (models) => { if (models.indexOf(model) === -1 && (!Array.isArray(deps) || intersection(deps, models).length === 0)) { return; @@ -67,7 +67,7 @@ const useModelState = ({ action, ...args }) => { core.transport.off('NewState', onNewStateThrottled); }; }, [routeFocused]); - React.useInsertionEffect(() => { + React.useLayoutEffect(() => { mountedRef.current = true; }, []); return state; diff --git a/src/common/useRouteFocused.ts b/src/common/useRouteFocused.ts new file mode 100644 index 000000000..80037f7d3 --- /dev/null +++ b/src/common/useRouteFocused.ts @@ -0,0 +1,24 @@ +// Copyright (C) 2017-2025 Smart code 203358507 + +import React from 'react'; + +const useRouteFocused = () => { + 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 isFocused; +}; + +export default useRouteFocused; diff --git a/src/components/ContinueWatchingItem/ContinueWatchingItem.js b/src/components/ContinueWatchingItem/ContinueWatchingItem.js index 8a0143619..8334cb796 100644 --- a/src/components/ContinueWatchingItem/ContinueWatchingItem.js +++ b/src/components/ContinueWatchingItem/ContinueWatchingItem.js @@ -1,23 +1,28 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); const PropTypes = require('prop-types'); const { useServices } = require('stremio/services'); const LibItem = require('stremio/components/LibItem'); const ContinueWatchingItem = ({ _id, notifications, deepLinks, ...props }) => { const { core } = useServices(); + const navigate = useNavigate(); const onClick = React.useCallback(() => { if (deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams) { - window.location = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams; + // TODO - remove # from deeplinks in core if possible + const navigateTo = deepLinks?.metaDetailsVideos ?? deepLinks?.metaDetailsStreams; + navigate(navigateTo.replace('#', '')); } }, [deepLinks]); const onPlayClick = React.useCallback((event) => { event.stopPropagation(); if (deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos) { - window.location = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos; + const navigateTo = deepLinks?.player ?? deepLinks?.metaDetailsStreams ?? deepLinks?.metaDetailsVideos; + navigate(navigateTo.replace('#', '')); } }, [deepLinks]); diff --git a/src/components/LibItem/LibItem.js b/src/components/LibItem/LibItem.js index a42def27f..e618ee804 100644 --- a/src/components/LibItem/LibItem.js +++ b/src/components/LibItem/LibItem.js @@ -1,14 +1,15 @@ // Copyright (C) 2017-2023 Smart code 203358507 const React = require('react'); +const { useNavigate } = require('react-router'); const { useServices } = require('stremio/services'); const PropTypes = require('prop-types'); const MetaItem = require('stremio/components/MetaItem'); const { t } = require('i18next'); const LibItem = ({ _id, removable, notifications, watched, ...props }) => { - const { core } = useServices(); + const navigate = useNavigate(); const newVideos = React.useMemo(() => { const count = notifications.items?.[_id]?.length ?? 0; @@ -50,7 +51,8 @@ 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; + // TODO: remove # from deeplinks in core for web? + navigate(props.deepLinks.player.replace('#', '')); } break; @@ -58,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(props.deepLinks.metaDetailsVideos.replace('#', '')); } else if (typeof props.deepLinks.metaDetailsStreams === 'string') { - window.location = props.deepLinks.metaDetailsStreams; + navigate(props.deepLinks.metaDetailsStreams.replace('#', '')); } } diff --git a/src/components/MainNavBars/MainNavBars.tsx b/src/components/MainNavBars/MainNavBars.tsx index 43d08f5c8..a42273eab 100644 --- a/src/components/MainNavBars/MainNavBars.tsx +++ b/src/components/MainNavBars/MainNavBars.tsx @@ -6,12 +6,12 @@ import { VerticalNavBar, HorizontalNavBar } from 'stremio/components/NavBar'; 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/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 6be35cd5d..1b3e9b3da 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'); @@ -13,8 +14,9 @@ const styles = require('./styles'); const { t } = require('i18next'); const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, fullscreenButton, navMenu, ...props }) => { + const navigate = useNavigate(); const backButtonOnClick = React.useCallback(() => { - window.history.back(); + navigate(-1); }, []); const [fullscreen, requestFullscreen, exitFullscreen] = useFullscreen(); const [isIOSPWA] = usePWA(); diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenu.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenu.js index 8d381f3ca..b5de0ee71 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenu.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenu.js @@ -3,7 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); -const { useRouteFocused } = require('stremio-router'); +const { default: useRouteFocused } = require('stremio/common/useRouteFocused'); const Popup = require('stremio/components/Popup'); const useBinaryState = require('stremio/common/useBinaryState'); const NavMenuContent = require('./NavMenuContent'); diff --git a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index de0a02212..72531ba6c 100644 --- a/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/components/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.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 { useTranslation } = require('react-i18next'); @@ -17,6 +18,7 @@ const styles = require('./styles'); const NavMenuContent = ({ onClick }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { core } = useServices(); const profile = useProfile(); const streamingServer = useStreamingServer(); @@ -45,6 +47,12 @@ const NavMenuContent = ({ onClick }) => { console.error(e); } }, []); + const handleAuth = React.useCallback(() => { + return profile.auth !== null + ? logoutButtonOnClick() + : navigate('/intro'); + }, [profile.auth, logoutButtonOnClick, navigate]); + return (
@@ -64,7 +72,7 @@ const NavMenuContent = ({ onClick }) => {
{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
-
diff --git a/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/components/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index 0bc95cd73..885dd196f 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 useTorrent = require('stremio/common/useTorrent'); @@ -22,16 +24,17 @@ const SearchBar = React.memo(({ className, query, active }) => { const searchHistory = useSearchHistory(); const localSearch = useLocalSearch(); const { createTorrentFromMagnet } = useTorrent(); + const navigate = useNavigate(); 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]); @@ -64,7 +67,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(); } }, []); @@ -72,7 +75,7 @@ const SearchBar = React.memo(({ className, query, active }) => { const queryInputClear = React.useCallback(() => { searchInputRef.current.value = ''; setCurrentQuery(''); - window.location.hash = '/search'; + navigate('/search', { 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 ( -