mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-04-18 12:52:52 +00:00
feat(Router): added HashRouter to manage routes in app
This commit is contained in:
parent
f7c1c82670
commit
3c5e75431b
6 changed files with 115 additions and 100 deletions
|
|
@ -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();
|
||||
|
|
|
|||
59
src/common/routerPaths.tsx
Normal file
59
src/common/routerPaths.tsx
Normal 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 />,
|
||||
},
|
||||
];
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
40
src/router/Router/Routes.tsx
Normal file
40
src/router/Router/Routes.tsx
Normal 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;
|
||||
Loading…
Reference in a new issue