mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #5825 from thornbill/router-v13
This commit is contained in:
commit
7acdb66e14
11 changed files with 147 additions and 140 deletions
|
@ -1,5 +1,3 @@
|
|||
import loadable from '@loadable/component';
|
||||
import { History } from '@remix-run/router';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
|
@ -8,21 +6,14 @@ import { ApiProvider } from 'hooks/useApi';
|
|||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||
|
||||
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
import RootAppRouter from './RootAppRouter';
|
||||
|
||||
const RootApp = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
{isExperimentalLayout ?
|
||||
<RootAppRouter history={history} /> :
|
||||
<StableAppRouter history={history} />
|
||||
}
|
||||
<RootAppRouter />
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
|
|
|
@ -1,32 +1,36 @@
|
|||
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import {
|
||||
RouterProvider,
|
||||
createHashRouter,
|
||||
Outlet
|
||||
Outlet,
|
||||
useLocation
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import { createRouterHistory } from 'components/router/routerHistory';
|
||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
element: <RootAppLayout />,
|
||||
children: [
|
||||
...EXPERIMENTAL_APP_ROUTES,
|
||||
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||
...DASHBOARD_APP_ROUTES
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
export const history = createRouterHistory(router);
|
||||
|
||||
export default function RootAppRouter() {
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
|
@ -35,10 +39,14 @@ export default function RootAppRouter({ history }: Readonly<{ history: History}>
|
|||
* NOTE: The app will crash if these get removed from the DOM.
|
||||
*/
|
||||
function RootAppLayout() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<UserThemeProvider>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</UserThemeProvider>
|
||||
|
|
|
@ -6,11 +6,14 @@ import ConnectionRequired from 'components/ConnectionRequired';
|
|||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
import AppLayout from '../AppLayout';
|
||||
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
import VideoPage from './video';
|
||||
import loadable from '@loadable/component';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
|
||||
const AppLayout = loadable(() => import('../AppLayout'));
|
||||
|
||||
export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
|
@ -38,6 +41,11 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
|||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
},
|
||||
|
||||
/* Redirects for old paths */
|
||||
...REDIRECTS.map(toRedirectRoute)
|
||||
];
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { Outlet, RouterProvider, createHashRouter, useLocation } from 'react-router-dom';
|
||||
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
import { STABLE_APP_ROUTES } from './routes/routes';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import { DASHBOARD_APP_PATHS, DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||
|
||||
const router = createHashRouter([{
|
||||
element: <StableAppLayout />,
|
||||
children: [
|
||||
...STABLE_APP_ROUTES,
|
||||
...DASHBOARD_APP_ROUTES
|
||||
]
|
||||
}]);
|
||||
|
||||
export default function StableAppRouter({ history }: Readonly<{ history: History }>) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout component that renders legacy components required on all pages.
|
||||
* NOTE: The app will crash if these get removed from the DOM.
|
||||
*/
|
||||
function StableAppLayout() {
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<UserThemeProvider>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isNewLayoutPath} />
|
||||
|
||||
<Outlet />
|
||||
</UserThemeProvider>
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ import AppLayout from '../AppLayout';
|
|||
import { REDIRECTS } from './_redirects';
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
import BangRedirect from 'components/router/BangRedirect';
|
||||
|
||||
export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
|
@ -32,6 +33,11 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
|
|||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '!/*',
|
||||
Component: BangRedirect
|
||||
},
|
||||
|
||||
/* Redirects for old paths */
|
||||
...REDIRECTS.map(toRedirectRoute)
|
||||
];
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { history } from '../router/appRouter';
|
||||
import focusManager from '../focusManager';
|
||||
import browser from '../../scripts/browser';
|
||||
import layoutManager from '../layoutManager';
|
||||
|
@ -6,6 +5,8 @@ import inputManager from '../../scripts/inputManager';
|
|||
import { toBoolean } from '../../utils/string.ts';
|
||||
import dom from '../../scripts/dom';
|
||||
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
import './dialoghelper.scss';
|
||||
import '../../styles/scrollstyles.scss';
|
||||
|
||||
|
|
34
src/components/router/BangRedirect.tsx
Normal file
34
src/components/router/BangRedirect.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
|
||||
const BangRedirect = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const to = useMemo(() => {
|
||||
const _to = {
|
||||
search: location.search,
|
||||
hash: location.hash
|
||||
};
|
||||
|
||||
if (location.pathname.startsWith('/!/')) {
|
||||
return { ..._to, pathname: location.pathname.substring(2) };
|
||||
} else if (location.pathname.startsWith('/!')) {
|
||||
return { ..._to, pathname: location.pathname.replace(/^\/!/, '/') };
|
||||
} else if (location.pathname.startsWith('!')) {
|
||||
return { ..._to, pathname: location.pathname.substring(1) };
|
||||
}
|
||||
}, [ location ]);
|
||||
|
||||
if (!to) return null;
|
||||
|
||||
console.warn('[BangRedirect] You are using a deprecated URL format. This will stop working in a future Jellyfin update.');
|
||||
|
||||
return (
|
||||
<Navigate
|
||||
replace
|
||||
to={to}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BangRedirect;
|
|
@ -1,5 +1,5 @@
|
|||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||
import { Action, createHashHistory } from 'history';
|
||||
import { Action } from 'history';
|
||||
|
||||
import { appHost } from '../apphost';
|
||||
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
|
||||
|
@ -15,8 +15,7 @@ import { queryClient } from 'utils/query/queryClient';
|
|||
import { getItemQuery } from 'hooks/useItem';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts';
|
||||
|
||||
export const history = createHashHistory();
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
/**
|
||||
* Page types of "no return" (when "Go back" should behave differently, probably quitting the application).
|
||||
|
|
73
src/components/router/routerHistory.ts
Normal file
73
src/components/router/routerHistory.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Router, RouterState } from '@remix-run/router';
|
||||
import type { History, Listener, To } from 'history';
|
||||
|
||||
import Events, { type Event } from 'utils/events';
|
||||
|
||||
const HISTORY_UPDATE_EVENT = 'HISTORY_UPDATE';
|
||||
|
||||
export class RouterHistory implements History {
|
||||
_router: Router;
|
||||
createHref: (arg: any) => string;
|
||||
|
||||
constructor(router: Router) {
|
||||
this._router = router;
|
||||
|
||||
this._router.subscribe(state => {
|
||||
console.debug('[RouterHistory] history update', state);
|
||||
Events.trigger(document, HISTORY_UPDATE_EVENT, [ state ]);
|
||||
});
|
||||
|
||||
this.createHref = router.createHref;
|
||||
}
|
||||
|
||||
get action() {
|
||||
return this._router.state.historyAction;
|
||||
}
|
||||
|
||||
get location() {
|
||||
return this._router.state.location;
|
||||
}
|
||||
|
||||
back() {
|
||||
void this._router.navigate(-1);
|
||||
}
|
||||
|
||||
forward() {
|
||||
void this._router.navigate(1);
|
||||
}
|
||||
|
||||
go(delta: number) {
|
||||
void this._router.navigate(delta);
|
||||
}
|
||||
|
||||
push(to: To, state?: any) {
|
||||
void this._router.navigate(to, { state });
|
||||
}
|
||||
|
||||
replace(to: To, state?: any): void {
|
||||
void this._router.navigate(to, { state, replace: true });
|
||||
}
|
||||
|
||||
block() {
|
||||
// NOTE: We don't seem to use this functionality, so leaving it unimplemented.
|
||||
throw new Error('`history.block()` is not implemented');
|
||||
return () => undefined;
|
||||
}
|
||||
|
||||
listen(listener: Listener) {
|
||||
const compatListener = (_e: Event, state: RouterState) => {
|
||||
return listener({ action: state.historyAction, location: state.location });
|
||||
};
|
||||
|
||||
Events.on(document, HISTORY_UPDATE_EVENT, compatListener);
|
||||
|
||||
return () => Events.off(document, HISTORY_UPDATE_EVENT, compatListener);
|
||||
}
|
||||
}
|
||||
|
||||
export const createRouterHistory = (router: Router): History => {
|
||||
return new RouterHistory(router);
|
||||
};
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
@ -1,71 +0,0 @@
|
|||
import { Update } from 'history';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import type { History, Router } from '@remix-run/router';
|
||||
|
||||
interface UseLegacyRouterSyncProps {
|
||||
router: Router;
|
||||
history: History;
|
||||
}
|
||||
export function useLegacyRouterSync({ router, history }: UseLegacyRouterSyncProps) {
|
||||
const [routerLocation, setRouterLocation] = useState(router.state.location);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const onHistoryChange = async (update: Update) => {
|
||||
const isSynced = router.createHref(router.state.location) === router.createHref(update.location);
|
||||
|
||||
/**
|
||||
* Some legacy codepaths may still use the `#!` routing scheme which is unsupported with the React routing
|
||||
* implementation, so we need to remove the leading `!` from the pathname. React Router already removes the
|
||||
* hash for us.
|
||||
*/
|
||||
if (update.location.pathname.startsWith('/!/')) {
|
||||
history.replace(
|
||||
{ ...update.location, pathname: update.location.pathname.replace(/^\/!/, '') },
|
||||
update.location.state);
|
||||
} else if (update.location.pathname.startsWith('/!')) {
|
||||
history.replace(
|
||||
{ ...update.location, pathname: update.location.pathname.replace(/^\/!/, '/') },
|
||||
update.location.state);
|
||||
} else if (update.location.pathname.startsWith('!')) {
|
||||
history.replace(
|
||||
{ ...update.location, pathname: update.location.pathname.replace(/^!/, '') },
|
||||
update.location.state);
|
||||
} else if (!isSynced) {
|
||||
await router.navigate(update.location, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
const unlisten = history.listen(onHistoryChange);
|
||||
|
||||
return () => {
|
||||
unlisten();
|
||||
};
|
||||
}, [history, router]);
|
||||
|
||||
/**
|
||||
* Because the router subscription needs to be in a zero-dependencies effect, syncing changes to the router back to
|
||||
* the legacy history API needs to be in a separate effect. This should run any time the router location changes.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
const isSynced = router.createHref(routerLocation) === router.createHref(history.location);
|
||||
if (!isSynced) {
|
||||
history.replace(routerLocation);
|
||||
}
|
||||
}, [history, router, routerLocation]);
|
||||
|
||||
/**
|
||||
* We want to use an effect with no dependencies here when we set up the router subscription to ensure that we only
|
||||
* subscribe to the router state once. The router doesn't provide a way to remove subscribers, so we need to be
|
||||
* careful to not create multiple subscribers.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
router.subscribe((newState) => {
|
||||
setRouterLocation((prevLocation) => {
|
||||
if (newState.location !== prevLocation) {
|
||||
return newState.location;
|
||||
}
|
||||
return prevLocation;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -22,7 +22,7 @@ import { getPlugins } from './scripts/settings/webSettings';
|
|||
import { pluginManager } from './components/pluginManager';
|
||||
import packageManager from './components/packageManager';
|
||||
import './components/playback/displayMirrorManager.ts';
|
||||
import { appRouter, history } from './components/router/appRouter';
|
||||
import { appRouter } from './components/router/appRouter';
|
||||
import './elements/emby-button/emby-button';
|
||||
import './scripts/autoThemes';
|
||||
import './components/themeMediaPlayer';
|
||||
|
@ -39,6 +39,7 @@ import './legacy/vendorStyles';
|
|||
import { currentSettings } from './scripts/settings/userSettings';
|
||||
import taskButton from './scripts/taskbutton';
|
||||
import RootApp from './RootApp.tsx';
|
||||
import { history } from 'RootAppRouter';
|
||||
|
||||
import './styles/livetv.scss';
|
||||
import './styles/dashboard.scss';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue