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 { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -8,21 +6,14 @@ import { ApiProvider } from 'hooks/useApi';
|
||||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||||
import { queryClient } from 'utils/query/queryClient';
|
import { queryClient } from 'utils/query/queryClient';
|
||||||
|
|
||||||
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
import RootAppRouter from './RootAppRouter';
|
||||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
|
||||||
|
|
||||||
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
|
||||||
const layoutMode = localStorage.getItem('layout');
|
|
||||||
const isExperimentalLayout = layoutMode === 'experimental';
|
|
||||||
|
|
||||||
|
const RootApp = () => {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ApiProvider>
|
<ApiProvider>
|
||||||
<WebConfigProvider>
|
<WebConfigProvider>
|
||||||
{isExperimentalLayout ?
|
<RootAppRouter />
|
||||||
<RootAppRouter history={history} /> :
|
|
||||||
<StableAppRouter history={history} />
|
|
||||||
}
|
|
||||||
</WebConfigProvider>
|
</WebConfigProvider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
|
|
@ -1,32 +1,36 @@
|
||||||
|
|
||||||
import { History } from '@remix-run/router';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
createHashRouter,
|
createHashRouter,
|
||||||
Outlet
|
Outlet,
|
||||||
|
useLocation
|
||||||
} from 'react-router-dom';
|
} 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 { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||||
|
import { STABLE_APP_ROUTES } from 'apps/stable/routes/routes';
|
||||||
import AppHeader from 'components/AppHeader';
|
import AppHeader from 'components/AppHeader';
|
||||||
import Backdrop from 'components/Backdrop';
|
import Backdrop from 'components/Backdrop';
|
||||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
import { createRouterHistory } from 'components/router/routerHistory';
|
||||||
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
|
||||||
import UserThemeProvider from 'themes/UserThemeProvider';
|
import UserThemeProvider from 'themes/UserThemeProvider';
|
||||||
|
|
||||||
|
const layoutMode = localStorage.getItem('layout');
|
||||||
|
const isExperimentalLayout = layoutMode === 'experimental';
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
{
|
{
|
||||||
element: <RootAppLayout />,
|
element: <RootAppLayout />,
|
||||||
children: [
|
children: [
|
||||||
...EXPERIMENTAL_APP_ROUTES,
|
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
|
||||||
...DASHBOARD_APP_ROUTES
|
...DASHBOARD_APP_ROUTES
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
|
export const history = createRouterHistory(router);
|
||||||
useLegacyRouterSync({ router, history });
|
|
||||||
|
|
||||||
|
export default function RootAppRouter() {
|
||||||
return <RouterProvider router={router} />;
|
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.
|
* NOTE: The app will crash if these get removed from the DOM.
|
||||||
*/
|
*/
|
||||||
function RootAppLayout() {
|
function RootAppLayout() {
|
||||||
|
const location = useLocation();
|
||||||
|
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||||
|
.some(path => location.pathname.startsWith(`/${path}`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UserThemeProvider>
|
<UserThemeProvider>
|
||||||
<Backdrop />
|
<Backdrop />
|
||||||
<AppHeader isHidden />
|
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</UserThemeProvider>
|
</UserThemeProvider>
|
||||||
|
|
|
@ -6,11 +6,14 @@ import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
import { toRedirectRoute } from 'components/router/Redirect';
|
import { toRedirectRoute } from 'components/router/Redirect';
|
||||||
import AppLayout from '../AppLayout';
|
|
||||||
|
|
||||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||||
import VideoPage from './video';
|
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[] = [
|
export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||||
{
|
{
|
||||||
|
@ -38,6 +41,11 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '!/*',
|
||||||
|
Component: BangRedirect
|
||||||
|
},
|
||||||
|
|
||||||
/* Redirects for old paths */
|
/* Redirects for old paths */
|
||||||
...REDIRECTS.map(toRedirectRoute)
|
...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 { REDIRECTS } from './_redirects';
|
||||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||||
|
import BangRedirect from 'components/router/BangRedirect';
|
||||||
|
|
||||||
export const STABLE_APP_ROUTES: RouteObject[] = [
|
export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||||
{
|
{
|
||||||
|
@ -32,6 +33,11 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '!/*',
|
||||||
|
Component: BangRedirect
|
||||||
|
},
|
||||||
|
|
||||||
/* Redirects for old paths */
|
/* Redirects for old paths */
|
||||||
...REDIRECTS.map(toRedirectRoute)
|
...REDIRECTS.map(toRedirectRoute)
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { history } from '../router/appRouter';
|
|
||||||
import focusManager from '../focusManager';
|
import focusManager from '../focusManager';
|
||||||
import browser from '../../scripts/browser';
|
import browser from '../../scripts/browser';
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
|
@ -6,6 +5,8 @@ import inputManager from '../../scripts/inputManager';
|
||||||
import { toBoolean } from '../../utils/string.ts';
|
import { toBoolean } from '../../utils/string.ts';
|
||||||
import dom from '../../scripts/dom';
|
import dom from '../../scripts/dom';
|
||||||
|
|
||||||
|
import { history } from 'RootAppRouter';
|
||||||
|
|
||||||
import './dialoghelper.scss';
|
import './dialoghelper.scss';
|
||||||
import '../../styles/scrollstyles.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 { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import { Action, createHashHistory } from 'history';
|
import { Action } from 'history';
|
||||||
|
|
||||||
import { appHost } from '../apphost';
|
import { appHost } from '../apphost';
|
||||||
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
|
import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop';
|
||||||
|
@ -15,8 +15,7 @@ import { queryClient } from 'utils/query/queryClient';
|
||||||
import { getItemQuery } from 'hooks/useItem';
|
import { getItemQuery } from 'hooks/useItem';
|
||||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||||
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts';
|
import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState.ts';
|
||||||
|
import { history } from 'RootAppRouter';
|
||||||
export const history = createHashHistory();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Page types of "no return" (when "Go back" should behave differently, probably quitting the application).
|
* 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 { pluginManager } from './components/pluginManager';
|
||||||
import packageManager from './components/packageManager';
|
import packageManager from './components/packageManager';
|
||||||
import './components/playback/displayMirrorManager.ts';
|
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 './elements/emby-button/emby-button';
|
||||||
import './scripts/autoThemes';
|
import './scripts/autoThemes';
|
||||||
import './components/themeMediaPlayer';
|
import './components/themeMediaPlayer';
|
||||||
|
@ -39,6 +39,7 @@ import './legacy/vendorStyles';
|
||||||
import { currentSettings } from './scripts/settings/userSettings';
|
import { currentSettings } from './scripts/settings/userSettings';
|
||||||
import taskButton from './scripts/taskbutton';
|
import taskButton from './scripts/taskbutton';
|
||||||
import RootApp from './RootApp.tsx';
|
import RootApp from './RootApp.tsx';
|
||||||
|
import { history } from 'RootAppRouter';
|
||||||
|
|
||||||
import './styles/livetv.scss';
|
import './styles/livetv.scss';
|
||||||
import './styles/dashboard.scss';
|
import './styles/dashboard.scss';
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue