feat(Router): added HashRouter to manage routes in app

This commit is contained in:
Botzy 2025-04-29 15:34:25 +03:00
parent f7c1c82670
commit 3c5e75431b
6 changed files with 115 additions and 100 deletions

View file

@ -12,11 +12,10 @@ 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();

View file

@ -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: <routes.Intro />,
},
{
path: '/discover/:transportUrl?/:type?/:catalogId?',
element: <routes.Discover />,
},
{
path: '/library/:type?',
element: <routes.Library />,
},
{
path: '/calendar/:year?/:month?',
element: <routes.Calendar />,
},
{
path: '/continuewatching/:type?',
element: <routes.Library />,
},
{
path: '/search',
element: <routes.Search />,
},
{
path: '/metadetails/:type?/:id?/:videoId?',
element: <routes.MetaDetails />,
},
{
path: '/detail/:type?/:id?/:videoId?',
element: <routes.MetaDetails />,
},
{
path: '/addons/:type?/:transportUrl?/:catalogId?',
element: <routes.Addons />,
},
{
path: '/settings',
element: <routes.Settings />,
},
{
path: '/player/:stream?/:streamTransportUrl?/:metaTransportUrl?/:type?/:id?/:videoId?',
element: <routes.Player />,
},
{
path: '/',
element: <routes.Board />,
},
{
path: '*',
element: <routes.NotFound />,
},
];

View file

@ -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 = {

View file

@ -4,12 +4,12 @@ const React = require('react');
const PropTypes = require('prop-types');
const { ModalsContainerProvider } = require('../ModalsContainerContext');
const Route = ({ children }) => {
const Route = ({ component }) => {
return (
<div className={'route-container'}>
<ModalsContainerProvider>
<div className={'route-content'}>
{children}
{component}
</div>
</ModalsContainerProvider>
</div>
@ -17,7 +17,7 @@ const Route = ({ children }) => {
};
Route.propTypes = {
children: PropTypes.node
component: PropTypes.node
};
module.exports = Route;

View file

@ -1,107 +1,24 @@
// Copyright (C) 2017-2023 Smart code 203358507
const React = require('react');
const ReactIs = require('react-is');
const { HashRouter } = require('react-router-dom');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const UrlUtils = require('url');
const isEqual = require('lodash.isequal');
const { RouteFocusedProvider } = require('../RouteFocusedContext');
const Route = require('../Route');
const routeConfigForPath = require('./routeConfigForPath');
const urlParamsForPath = require('./urlParamsForPath');
const { default: Routes } = require('./Routes');
const Router = ({ className, onPathNotMatch, onRouteChange, ...props }) => {
const viewsConfig = React.useMemo(() => props.viewsConfig, []);
const [views, setViews] = React.useState(() => {
return Array(viewsConfig.length).fill(null);
});
React.useLayoutEffect(() => {
const onLocationHashChange = () => {
const { pathname, query } = UrlUtils.parse(window.location.hash.slice(1));
const queryParams = new URLSearchParams(typeof query === 'string' ? query : '');
const routeConfig = routeConfigForPath(viewsConfig, typeof pathname === 'string' ? pathname : '');
if (routeConfig === null) {
if (typeof onPathNotMatch === 'function') {
const component = onPathNotMatch();
if (ReactIs.isValidElementType(component)) {
setViews((views) => {
return views
.slice(0, viewsConfig.length)
.concat({
key: '-1',
component
});
});
}
}
const Router = ({ className }) => {
return;
}
const urlParams = urlParamsForPath(routeConfig, typeof pathname === 'string' ? pathname : '');
const routeViewIndex = viewsConfig.findIndex((vc) => vc.includes(routeConfig));
const routeIndex = viewsConfig[routeViewIndex].findIndex((rc) => rc === routeConfig);
const handled = typeof onRouteChange === 'function' && onRouteChange(routeConfig, urlParams, queryParams);
if (!handled) {
setViews((views) => {
return views
.slice(0, viewsConfig.length)
.map((view, index) => {
if (index < routeViewIndex) {
return view;
} else if (index === routeViewIndex) {
return {
key: `${routeViewIndex}${routeIndex}`,
component: routeConfig.component,
urlParams: view !== null && isEqual(view.urlParams, urlParams) ?
view.urlParams
:
urlParams,
queryParams: view !== null && isEqual(Array.from(view.queryParams.entries()), Array.from(queryParams.entries())) ?
view.queryParams
:
queryParams
};
} else {
return null;
}
});
});
}
};
window.addEventListener('hashchange', onLocationHashChange);
onLocationHashChange();
return () => {
window.removeEventListener('hashchange', onLocationHashChange);
};
}, [onPathNotMatch, onRouteChange]);
return (
<div className={classnames(className, 'routes-container')}>
{
views
.filter((view) => view !== null)
.map(({ key, component, urlParams, queryParams }, index, views) => (
<RouteFocusedProvider key={key} value={index === views.length - 1}>
<Route>
{React.createElement(component, { urlParams, queryParams })}
</Route>
</RouteFocusedProvider>
))
}
<HashRouter>
<Routes />
</HashRouter>
</div>
);
};
Router.propTypes = {
className: PropTypes.string,
onPathNotMatch: PropTypes.func,
onRouteChange: PropTypes.func,
viewsConfig: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.exact({
regexp: PropTypes.instanceOf(RegExp).isRequired,
urlParamsNames: PropTypes.arrayOf(PropTypes.string).isRequired,
component: PropTypes.elementType.isRequired
}))).isRequired
};
module.exports = Router;

View file

@ -0,0 +1,40 @@
// Copyright (C) 2017-2025 Smart code 203358507
import React from 'react';
import { Routes as RRoutes, Route as RRoute, useLocation, useNavigate } from 'react-router';
import { routerPaths } from 'stremio/common/routerPaths';
import Route from '../Route/Route';
import { useProfile } from 'stremio/common';
const Routes = () => {
const location = useLocation();
const navigate = useNavigate();
const profile = useProfile();
const previousAuthRef = React.useRef(profile.auth);
React.useEffect(() => {
if (previousAuthRef.current !== null && profile.auth === null) {
previousAuthRef.current = profile.auth;
navigate('/intro', { replace: true });
}
}, [profile]);
/**
* Replaced onRouteChange with following useEffect:
*/
React.useEffect(() => {
if (profile.auth !== null && location.pathname === '/intro') {
navigate('/', { replace: true });
}
}, [location, profile.auth, navigate]);
const routes = routerPaths.map((route) =>
<RRoute key={route.path} path={route.path} element={<Route component={route.element} />} />
);
return <RRoutes location={location}>
{routes}
</RRoutes>;
};
export default Routes;