1
0
Fork 0
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:
Bill Thornton 2024-08-01 01:07:05 -04:00 committed by GitHub
commit 7acdb66e14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 147 additions and 140 deletions

View file

@ -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} />

View file

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

View file

@ -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)
];

View file

@ -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>
);
}

View file

@ -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)
];

View file

@ -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';

View 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;

View file

@ -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).

View 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 */

View file

@ -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;
});
});
});
}

View file

@ -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';