From 3235e2e594e7b070ca0ac7b7f35bc52d09d79d2f Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 24 Jul 2024 14:59:16 -0400 Subject: [PATCH 1/3] Unify app routers --- src/RootApp.tsx | 12 ++----- src/RootAppRouter.tsx | 17 +++++++--- src/apps/experimental/routes/routes.tsx | 4 ++- src/apps/stable/AppRouter.tsx | 43 ------------------------- 4 files changed, 18 insertions(+), 58 deletions(-) delete mode 100644 src/apps/stable/AppRouter.tsx diff --git a/src/RootApp.tsx b/src/RootApp.tsx index afd98314749..bf5f4f76a56 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -1,4 +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'; @@ -8,21 +7,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')); +import RootAppRouter from './RootAppRouter'; const RootApp = ({ history }: Readonly<{ history: History }>) => { - const layoutMode = localStorage.getItem('layout'); - const isExperimentalLayout = layoutMode === 'experimental'; - return ( - {isExperimentalLayout ? - : - - } + diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx index 1fc2293703d..ae683fe9463 100644 --- a/src/RootAppRouter.tsx +++ b/src/RootAppRouter.tsx @@ -4,21 +4,26 @@ 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 UserThemeProvider from 'themes/UserThemeProvider'; +const layoutMode = localStorage.getItem('layout'); +const isExperimentalLayout = layoutMode === 'experimental'; + const router = createHashRouter([ { element: , children: [ - ...EXPERIMENTAL_APP_ROUTES, + ...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES), ...DASHBOARD_APP_ROUTES ] } @@ -35,10 +40,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 ( - + diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx index 47e0a9f7f52..beff0629acb 100644 --- a/src/apps/experimental/routes/routes.tsx +++ b/src/apps/experimental/routes/routes.tsx @@ -6,11 +6,13 @@ 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'; + +const AppLayout = loadable(() => import('../AppLayout')); export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ { diff --git a/src/apps/stable/AppRouter.tsx b/src/apps/stable/AppRouter.tsx deleted file mode 100644 index 40b5c2aa39d..00000000000 --- a/src/apps/stable/AppRouter.tsx +++ /dev/null @@ -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: , - children: [ - ...STABLE_APP_ROUTES, - ...DASHBOARD_APP_ROUTES - ] -}]); - -export default function StableAppRouter({ history }: Readonly<{ history: History }>) { - useLegacyRouterSync({ router, history }); - - return ; -} - -/** - * 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 ( - - - - - - - ); -} From 1adaf00cb3d002370bd9943443750a70e78cd052 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 24 Jul 2024 15:12:10 -0400 Subject: [PATCH 2/3] Add RouterHistory to replace syncing for compatibility --- src/RootApp.tsx | 5 +- src/RootAppRouter.tsx | 7 +- src/components/dialogHelper/dialogHelper.js | 3 +- src/components/router/appRouter.js | 5 +- src/components/router/routerHistory.ts | 73 +++++++++++++++++++++ src/hooks/useLegacyRouterSync.ts | 71 -------------------- src/index.jsx | 3 +- 7 files changed, 84 insertions(+), 83 deletions(-) create mode 100644 src/components/router/routerHistory.ts delete mode 100644 src/hooks/useLegacyRouterSync.ts diff --git a/src/RootApp.tsx b/src/RootApp.tsx index bf5f4f76a56..cf2a97ca264 100644 --- a/src/RootApp.tsx +++ b/src/RootApp.tsx @@ -1,4 +1,3 @@ -import { History } from '@remix-run/router'; import { QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import React from 'react'; @@ -9,12 +8,12 @@ import { queryClient } from 'utils/query/queryClient'; import RootAppRouter from './RootAppRouter'; -const RootApp = ({ history }: Readonly<{ history: History }>) => { +const RootApp = () => { return ( - + diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx index ae683fe9463..bdd7333f16c 100644 --- a/src/RootAppRouter.tsx +++ b/src/RootAppRouter.tsx @@ -1,5 +1,4 @@ -import { History } from '@remix-run/router'; import React from 'react'; import { RouterProvider, @@ -13,7 +12,7 @@ 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 { createRouterHistory } from 'components/router/routerHistory'; import UserThemeProvider from 'themes/UserThemeProvider'; const layoutMode = localStorage.getItem('layout'); @@ -29,9 +28,9 @@ const router = createHashRouter([ } ]); -export default function RootAppRouter({ history }: Readonly<{ history: History}>) { - useLegacyRouterSync({ router, history }); +export const history = createRouterHistory(router); +export default function RootAppRouter() { return ; } diff --git a/src/components/dialogHelper/dialogHelper.js b/src/components/dialogHelper/dialogHelper.js index b03ff9052ce..8942431a667 100644 --- a/src/components/dialogHelper/dialogHelper.js +++ b/src/components/dialogHelper/dialogHelper.js @@ -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'; diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 3b07a5571cf..50ab0a61210 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -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). diff --git a/src/components/router/routerHistory.ts b/src/components/router/routerHistory.ts new file mode 100644 index 00000000000..698af920e49 --- /dev/null +++ b/src/components/router/routerHistory.ts @@ -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 */ diff --git a/src/hooks/useLegacyRouterSync.ts b/src/hooks/useLegacyRouterSync.ts deleted file mode 100644 index c1af8fa3102..00000000000 --- a/src/hooks/useLegacyRouterSync.ts +++ /dev/null @@ -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; - }); - }); - }); -} diff --git a/src/index.jsx b/src/index.jsx index f0d72d43caf..693e4793a6a 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -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'; From 8ddd9ecd9db4faea662958ac29a78ef9ce5468f3 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 24 Jul 2024 15:12:33 -0400 Subject: [PATCH 3/3] Add legacy bang url redirects --- src/apps/experimental/routes/routes.tsx | 6 +++++ src/apps/stable/routes/routes.tsx | 6 +++++ src/components/router/BangRedirect.tsx | 34 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 src/components/router/BangRedirect.tsx diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx index beff0629acb..a35fc3d0ec9 100644 --- a/src/apps/experimental/routes/routes.tsx +++ b/src/apps/experimental/routes/routes.tsx @@ -11,6 +11,7 @@ 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')); @@ -40,6 +41,11 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ ] }, + { + path: '!/*', + Component: BangRedirect + }, + /* Redirects for old paths */ ...REDIRECTS.map(toRedirectRoute) ]; diff --git a/src/apps/stable/routes/routes.tsx b/src/apps/stable/routes/routes.tsx index 300092c9fee..f8f720a5b07 100644 --- a/src/apps/stable/routes/routes.tsx +++ b/src/apps/stable/routes/routes.tsx @@ -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) ]; diff --git a/src/components/router/BangRedirect.tsx b/src/components/router/BangRedirect.tsx new file mode 100644 index 00000000000..1ee18e9dcb9 --- /dev/null +++ b/src/components/router/BangRedirect.tsx @@ -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 ( + + ); +}; + +export default BangRedirect;