diff --git a/src/apps/experimental/AppOverrides.scss b/src/apps/experimental/AppOverrides.scss index fa586829e9..f6ea674428 100644 --- a/src/apps/experimental/AppOverrides.scss +++ b/src/apps/experimental/AppOverrides.scss @@ -17,6 +17,10 @@ $drawer-width: 240px; left: $drawer-width; } } +// The fallback page has no drawer +#fallbackPage { + left: 0; +} // Hide some items from the user "settings" page that are in the drawer #myPreferencesMenuPage { diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx index 046f447d65..d0c5deab09 100644 --- a/src/apps/experimental/routes/routes.tsx +++ b/src/apps/experimental/routes/routes.tsx @@ -5,6 +5,7 @@ import ConnectionRequired from 'components/ConnectionRequired'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import ErrorBoundary from 'components/router/ErrorBoundary'; +import FallbackRoute from 'components/router/FallbackRoute'; import { ASYNC_USER_ROUTES } from './asyncRoutes'; import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes'; @@ -15,9 +16,11 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ path: '/*', lazy: () => import('../AppLayout'), children: [ + { index: true, element: }, + { - /* User routes: Any child route of this layout is authenticated */ - element: , + /* User routes */ + Component: ConnectionRequired, children: [ ...ASYNC_USER_ROUTES.map(toAsyncPageRoute), ...LEGACY_USER_ROUTES.map(toViewManagerPageRoute), @@ -25,15 +28,26 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [ // The video page is special since it combines new controls with the legacy view { path: 'video', - element: + Component: VideoPage } ], ErrorBoundary }, - /* Public routes */ - { index: true, element: }, - ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute) + { + /* Public routes */ + element: , + children: [ + ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute), + + /* Fallback route for invalid paths */ + { + path: '*', + Component: FallbackRoute + } + ] + } + ] } ]; diff --git a/src/apps/stable/routes/routes.tsx b/src/apps/stable/routes/routes.tsx index 787d89ac7f..f0e2718b27 100644 --- a/src/apps/stable/routes/routes.tsx +++ b/src/apps/stable/routes/routes.tsx @@ -5,6 +5,7 @@ import ConnectionRequired from 'components/ConnectionRequired'; import { toAsyncPageRoute } from 'components/router/AsyncRoute'; import { toViewManagerPageRoute } from 'components/router/LegacyRoute'; import ErrorBoundary from 'components/router/ErrorBoundary'; +import FallbackRoute from 'components/router/FallbackRoute'; import AppLayout from '../AppLayout'; @@ -16,9 +17,11 @@ export const STABLE_APP_ROUTES: RouteObject[] = [ path: '/*', Component: AppLayout, children: [ + { index: true, element: }, + { /* User routes */ - element: , + Component: ConnectionRequired, children: [ ...ASYNC_USER_ROUTES.map(toAsyncPageRoute), ...LEGACY_USER_ROUTES.map(toViewManagerPageRoute) @@ -26,9 +29,19 @@ export const STABLE_APP_ROUTES: RouteObject[] = [ ErrorBoundary }, - /* Public routes */ - { index: true, element: }, - ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute) + { + /* Public routes */ + element: , + children: [ + ...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute), + /* Fallback route for invalid paths */ + { + path: '*', + Component: FallbackRoute + } + ] + } + ] } ]; diff --git a/src/components/router/FallbackRoute.tsx b/src/components/router/FallbackRoute.tsx new file mode 100644 index 0000000000..ec91faa313 --- /dev/null +++ b/src/components/router/FallbackRoute.tsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; + +import Page from 'components/Page'; +import globalize from 'lib/globalize'; +import LinkButton from 'elements/emby-button/LinkButton'; + +const FallbackRoute = () => { + const location = useLocation(); + + // Check if the requested path should be redirected + const to = useMemo(() => { + const _to = { + search: location.search, + hash: location.hash + }; + + // If a path ends in ".html", redirect to the path with it removed + if (location.pathname.endsWith('.html')) { + return { ..._to, pathname: location.pathname.slice(0, -5) }; + } + }, [ location ]); + + if (to) { + console.warn('[FallbackRoute] You are using a deprecated URL format. This will stop working in a future Jellyfin update.'); + + return ( + + ); + } + + return ( + +
+

{globalize.translate('HeaderPageNotFound')}

+

{globalize.translate('PageNotFound')}

+ + {globalize.translate('GoHome')} + +
+
+ ); +}; + +export default FallbackRoute; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 71de76a2ab..bb5f9cf80a 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -473,6 +473,7 @@ "HeaderNoLyrics": "No lyrics found", "HeaderOnNow": "On Now", "HeaderOtherItems": "Other Items", + "HeaderPageNotFound": "Page not found", "HeaderParentalRatings": "Parental Ratings", "HeaderPassword": "Password", "HeaderPasswordReset": "Password Reset", @@ -1315,6 +1316,7 @@ "PackageInstallCancelled": "{0} (version {1}) installation cancelled.", "PackageInstallCompleted": "{0} (version {1}) installation completed.", "PackageInstallFailed": "{0} (version {1}) installation failed.", + "PageNotFound": "This is not the page you are looking for.", "ParentalRating": "Parental rating", "PasswordMatchError": "Password and password confirmation must match.", "PasswordRequiredForAdmin": "A password is required for admin accounts.",