From 99b2bd4f6eddf7ecaa4cfb86beb3df7015b9ac8e Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Wed, 4 Sep 2024 17:06:37 -0400 Subject: [PATCH] Remove app router routing --- src/components/ConnectionRequired.tsx | 31 +- src/components/nowPlayingBar/nowPlayingBar.js | 6 +- src/components/router/appRouter.js | 351 ++---------------- src/index.jsx | 13 +- 4 files changed, 55 insertions(+), 346 deletions(-) diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx index e46a8e4ab39..960ead7d76a 100644 --- a/src/components/ConnectionRequired.tsx +++ b/src/components/ConnectionRequired.tsx @@ -3,7 +3,6 @@ import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import type { ConnectResponse } from 'jellyfin-apiclient'; import alert from './alert'; -import { appRouter } from './router/appRouter'; import Loading from './loading/LoadingComponent'; import ServerConnections from './ServerConnections'; import globalize from '../lib/globalize'; @@ -149,24 +148,20 @@ const ConnectionRequired: FunctionComponent = ({ }, [bounce, isAdminRequired, isUserRequired]); useEffect(() => { - // TODO: appRouter will call appHost.exit() if navigating back when you are already at the default route. - // This case will need to be handled elsewhere before appRouter can be killed. - // Check connection status on initial page load - const firstConnection = appRouter.firstConnectionResult; - appRouter.firstConnectionResult = null; - - if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) { - handleIncompleteWizard(firstConnection) - .catch(err => { - console.error('[ConnectionRequired] failed to start wizard', err); - }); - } else { - validateUserAccess() - .catch(err => { - console.error('[ConnectionRequired] failed to validate user access', err); - }); - } + ServerConnections.connect() + .then(firstConnection => { + console.debug('[ConnectionRequired] connection state', firstConnection?.State); + + if (firstConnection && firstConnection.State !== ConnectionState.SignedIn) { + return handleIncompleteWizard(firstConnection); + } else { + return validateUserAccess(); + } + }) + .catch(err => { + console.error('[ConnectionRequired] failed to connect to server', err); + }); }, [handleIncompleteWizard, validateUserAccess]); if (isLoading) { diff --git a/src/components/nowPlayingBar/nowPlayingBar.js b/src/components/nowPlayingBar/nowPlayingBar.js index 4140fcf50b3..9c3bdaca408 100644 --- a/src/components/nowPlayingBar/nowPlayingBar.js +++ b/src/components/nowPlayingBar/nowPlayingBar.js @@ -1,3 +1,4 @@ +import { appRouter, isLyricsPage } from 'components/router/appRouter'; import datetime from '../../scripts/datetime'; import Events from '../../utils/events.ts'; import browser from '../../scripts/browser'; @@ -16,7 +17,6 @@ import appFooter from '../appFooter/appFooter'; import itemShortcuts from '../shortcuts'; import './nowPlayingBar.scss'; import '../../elements/emby-slider/emby-slider'; -import { appRouter } from '../router/appRouter'; let currentPlayer; let currentPlayerSupportedCommands = []; @@ -773,7 +773,7 @@ function refreshFromPlayer(player, type) { } function bindToPlayer(player) { - isLyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics'; + isLyricPageActive = isLyricsPage(); if (player === currentPlayer) { return; } @@ -806,7 +806,7 @@ Events.on(playbackManager, 'playerchange', function () { bindToPlayer(playbackManager.getCurrentPlayer()); document.addEventListener('viewbeforeshow', function (e) { - isLyricPageActive = appRouter.currentRouteInfo.path.toLowerCase() === '/lyrics'; + isLyricPageActive = isLyricsPage(); setLyricButtonActiveStatus(); if (!e.detail.options.enableMediaControl) { if (isVisibilityAllowed) { diff --git a/src/components/router/appRouter.js b/src/components/router/appRouter.js index 5688ecd6032..e3f0b494099 100644 --- a/src/components/router/appRouter.js +++ b/src/components/router/appRouter.js @@ -1,33 +1,36 @@ import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type'; -import { Action } from 'history'; -import { appHost } from '../apphost'; -import { clearBackdrop, setBackdropTransparency } from '../backdrop/backdrop'; +import { setBackdropTransparency } from '../backdrop/backdrop'; import globalize from '../../lib/globalize'; -import Events from '../../utils/events.ts'; import itemHelper from '../itemHelper'; import loading from '../loading/loading'; -import viewManager from '../viewManager/viewManager'; import ServerConnections from '../ServerConnections'; import alert from '../alert'; 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'; import { history } from 'RootAppRouter'; -/** - * Page types of "no return" (when "Go back" should behave differently, probably quitting the application). - */ -const START_PAGE_TYPES = ['home', 'login', 'selectserver']; +/** Pages of "no return" (when "Go back" should behave differently, probably quitting the application). */ const START_PAGE_PATHS = ['/home.html', '/login.html', '/selectserver.html']; +/** Pages that do not require a user to be logged in to view. */ +const PUBLIC_PATHS = [ + '/addserver.html', + '/selectserver.html', + '/login.html', + '/forgotpassword.html', + '/forgotpasswordpin.html', + '/wizardremoteaccess.html', + '/wizardfinish.html', + '/wizardlibrary.html', + '/wizardsettings.html', + '/wizardstart.html', + '/wizarduser.html' +]; + class AppRouter { - allRoutes = new Map(); - currentRouteInfo = { route: {} }; - currentViewLoadRequest; - firstConnectionResult; forcedLogoutMsg; msgTimeout; promiseShow; @@ -45,21 +48,6 @@ class AppRouter { } } - addRoute(path, route) { - this.allRoutes.set(path, { - route, - handler: this.#getHandler(route) - }); - } - - #beginConnectionWizard() { - clearBackdrop(); - loading.show(); - ServerConnections.connect().then(result => { - this.#handleConnectionResult(result); - }); - } - ready() { return this.promiseShow || Promise.resolve(); } @@ -98,7 +86,7 @@ class AppRouter { path = path.replace(this.baseUrl(), ''); // can't use this with home right now due to the back menu - if (this.currentRouteInfo?.path === path && this.currentRouteInfo.route.type !== 'home') { + if (history.location.pathname === path && path !== '/home.html') { loading.hide(); return Promise.resolve(); } @@ -112,72 +100,17 @@ class AppRouter { return this.promiseShow; } - #goToRoute({ location, action }) { - // Strip the leading "!" if present - const normalizedPath = location.pathname.replace(/^!/, ''); - - const route = this.allRoutes.get(normalizedPath); - if (route) { - console.debug('[appRouter] "%s" route found', normalizedPath, location, route); - route.handler({ - // Recreate the default context used by page.js: https://github.com/visionmedia/page.js#context - path: normalizedPath + location.search, - pathname: normalizedPath, - querystring: location.search.replace(/^\?/, ''), - state: location.state, - // Custom context variables - isBack: action === Action.Pop - }); - } else { - // The route is not registered here, so it should be handled by react-router - this.currentRouteInfo = { - route: {}, - path: normalizedPath + location.search - }; - } - } - - start() { - loading.show(); - - ServerConnections.getApiClients().forEach(apiClient => { - Events.off(apiClient, 'requestfail', this.onRequestFail); - Events.on(apiClient, 'requestfail', this.onRequestFail); - }); - - Events.on(ServerConnections, 'apiclientcreated', (_e, apiClient) => { - Events.off(apiClient, 'requestfail', this.onRequestFail); - Events.on(apiClient, 'requestfail', this.onRequestFail); - }); - - return ServerConnections.connect().then(result => { - this.firstConnectionResult = result; - - // Handle the initial route - this.#goToRoute({ location: history.location }); - - // Handle route changes - history.listen(params => { - this.#goToRoute(params); - }); - }).catch().then(() => { - loading.hide(); - }); - } - baseUrl() { return this.baseRoute; } canGoBack() { - const { path, route } = this.currentRouteInfo; - const pathOnly = path?.split('?')[0] ?? ''; + const path = history.location.pathname; - if (!route) { - return false; - } - - if (!document.querySelector('.dialogContainer') && (START_PAGE_TYPES.includes(route.type) || START_PAGE_PATHS.includes(pathOnly))) { + if ( + !document.querySelector('.dialogContainer') + && START_PAGE_PATHS.includes(path) + ) { return false; } @@ -219,126 +152,6 @@ class AppRouter { setBackdropTransparency(level); } - #handleConnectionResult(result) { - switch (result.State) { - case ConnectionState.SignedIn: - loading.hide(); - this.goHome(); - break; - case ConnectionState.ServerSignIn: - this.showLocalLogin(result.ApiClient.serverId()); - break; - case ConnectionState.ServerSelection: - this.showSelectServer(); - break; - case ConnectionState.ServerUpdateNeeded: - alert({ - text: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin'), - html: globalize.translate('ServerUpdateNeeded', 'https://github.com/jellyfin/jellyfin') - }).then(() => { - this.showSelectServer(); - }); - break; - default: - break; - } - } - - #loadContentUrl(ctx, _next, route, request) { - let url; - if (route.contentPath && typeof (route.contentPath) === 'function') { - url = route.contentPath(ctx.querystring); - } else { - url = route.contentPath || route.path; - } - - if (ctx.querystring && route.enableContentQueryString) { - url += '?' + ctx.querystring; - } - - let promise; - if (route.serverRequest) { - const apiClient = ServerConnections.currentApiClient(); - url = apiClient.getUrl(`/web${url}`); - promise = apiClient.get(url); - } else { - promise = import(/* webpackChunkName: "[request]" */ `../../controllers/${url}`); - } - - promise.then((html) => { - this.#loadContent(ctx, route, html, request); - }); - } - - #handleRoute(ctx, next, route) { - this.#authenticate(ctx, route, () => { - this.#initRoute(ctx, next, route); - }); - } - - #initRoute(ctx, next, route) { - const onInitComplete = (controllerFactory) => { - this.#sendRouteToViewManager(ctx, next, route, controllerFactory); - }; - - if (route.controller) { - import(/* webpackChunkName: "[request]" */ '../../controllers/' + route.controller).then(onInitComplete); - } else { - onInitComplete(); - } - } - - #cancelCurrentLoadRequest() { - const currentRequest = this.currentViewLoadRequest; - if (currentRequest) { - currentRequest.cancel = true; - } - } - - #sendRouteToViewManager(ctx, next, route, controllerFactory) { - this.#cancelCurrentLoadRequest(); - const isBackNav = ctx.isBack; - - const currentRequest = { - url: this.baseUrl() + ctx.path, - transition: route.transition, - isBack: isBackNav, - state: ctx.state, - type: route.type, - fullscreen: route.fullscreen, - controllerFactory: controllerFactory, - options: { - supportsThemeMedia: route.supportsThemeMedia || false, - enableMediaControl: route.enableMediaControl !== false - }, - autoFocus: route.autoFocus - }; - this.currentViewLoadRequest = currentRequest; - - const onNewViewNeeded = () => { - if (typeof route.path === 'string') { - this.#loadContentUrl(ctx, next, route, currentRequest); - } else { - next(); - } - }; - - if (!isBackNav) { - onNewViewNeeded(); - return; - } - viewManager.tryRestoreView(currentRequest, () => { - this.currentRouteInfo = { - route: route, - path: ctx.path - }; - }).catch((result) => { - if (!result?.cancelled) { - onNewViewNeeded(); - } - }); - } - onViewShow() { const resolve = this.resolveOnNextShow; if (resolve) { @@ -370,114 +183,16 @@ class AppRouter { const apiClient = this; if (data.status === 403 && data.errorCode === 'ParentalControl') { - const isCurrentAllowed = appRouter.currentRouteInfo ? (appRouter.currentRouteInfo.route.anonymous || appRouter.currentRouteInfo.route.startup) : true; + const isPublicPage = PUBLIC_PATHS.includes(history.location.pathname); // Bounce to the login screen, but not if a password entry fails, obviously - if (!isCurrentAllowed) { + if (!isPublicPage) { appRouter.showForcedLogoutMessage(globalize.translate('AccessRestrictedTryAgainLater')); appRouter.showLocalLogin(apiClient.serverId()); } } } - #authenticate(ctx, route, callback) { - const firstResult = this.firstConnectionResult; - - this.firstConnectionResult = null; - if (firstResult) { - if (firstResult.State === ConnectionState.ServerSignIn) { - const url = firstResult.ApiClient.serverAddress() + '/System/Info/Public'; - fetch(url, { cache: 'no-cache' }).then(response => { - if (!response.ok) return Promise.reject(new Error('fetch failed')); - return response.json(); - }).then(data => { - if (data !== null && data.StartupWizardCompleted === false) { - ServerConnections.setLocalApiClient(firstResult.ApiClient); - this.show('wizardstart.html'); - } else { - this.#handleConnectionResult(firstResult); - } - }).catch(error => { - console.error(error); - }); - - return; - } else if (firstResult.State !== ConnectionState.SignedIn) { - this.#handleConnectionResult(firstResult); - return; - } - } - - const apiClient = ServerConnections.currentApiClient(); - const pathname = ctx.pathname.toLowerCase(); - - console.debug('[appRouter] processing path request: ' + pathname); - const isCurrentRouteStartup = this.currentRouteInfo ? this.currentRouteInfo.route.startup : true; - const shouldExitApp = ctx.isBack && route.isDefaultRoute && isCurrentRouteStartup; - - if (!shouldExitApp && (!apiClient?.isLoggedIn()) && !route.anonymous) { - console.debug('[appRouter] route does not allow anonymous access: redirecting to login'); - this.#beginConnectionWizard(); - return; - } - - if (shouldExitApp) { - if (appHost.supports('exit')) { - appHost.exit(); - } - - return; - } - - if (apiClient?.isLoggedIn()) { - console.debug('[appRouter] user is authenticated'); - - if (route.roles) { - this.#validateRoles(apiClient, route.roles).then(() => { - callback(); - }, this.#beginConnectionWizard.bind(this)); - return; - } - } - - console.debug('[appRouter] proceeding to page: ' + pathname); - callback(); - } - - #validateRoles(apiClient, roles) { - return Promise.all(roles.split(',').map((role) => { - return this.#validateRole(apiClient, role); - })); - } - - #validateRole(apiClient, role) { - if (role === 'admin') { - return apiClient.getCurrentUser().then((user) => { - if (user.Policy.IsAdministrator) { - return Promise.resolve(); - } - return Promise.reject(); - }); - } - - // Unknown role - return Promise.resolve(); - } - - #loadContent(ctx, route, html, request) { - html = globalize.translateHtml(html, route.dictionary); - request.view = html; - - viewManager.loadView(request); - - this.currentRouteInfo = { - route: route, - path: ctx.path - }; - - ctx.handled = true; - } - #getRequestFile() { let path = window.location.pathname || ''; @@ -495,20 +210,6 @@ class AppRouter { return path; } - #getHandler(route) { - return (ctx, next) => { - const ignore = ctx.path === this.currentRouteInfo.path; - if (ignore) { - console.debug('[appRouter] path did not change, ignoring route change'); - // Resolve 'show' promise - this.onViewShow(); - return; - } - - this.#handleRoute(ctx, next, route); - }; - } - getRouteUrl(item, options) { if (!item) { throw new Error('item cannot be null'); @@ -783,5 +484,7 @@ class AppRouter { export const appRouter = new AppRouter(); +export const isLyricsPage = () => history.location.pathname.toLowerCase() === '/lyrics'; + window.Emby = window.Emby || {}; window.Emby.Page = appRouter; diff --git a/src/index.jsx b/src/index.jsx index 51104084d5a..0e1ff4f6680 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -9,6 +9,7 @@ import ServerConnections from './components/ServerConnections'; import { appHost } from './components/apphost'; import autoFocuser from './components/autoFocuser'; +import loading from 'components/loading/loading'; import { pluginManager } from './components/pluginManager'; import { appRouter } from './components/router/appRouter'; import globalize from './lib/globalize'; @@ -99,6 +100,16 @@ build: ${__JF_BUILD_VERSION__}`); ServerConnections.currentApiClient()?.ensureWebSocket(); }); + // Register API request error handlers + ServerConnections.getApiClients().forEach(apiClient => { + Events.off(apiClient, 'requestfail', appRouter.onRequestFail); + Events.on(apiClient, 'requestfail', appRouter.onRequestFail); + }); + Events.on(ServerConnections, 'apiclientcreated', (_e, apiClient) => { + Events.off(apiClient, 'requestfail', appRouter.onRequestFail); + Events.on(apiClient, 'requestfail', appRouter.onRequestFail); + }); + // Render the app await renderApp(); @@ -250,7 +261,7 @@ async function renderApp() { // Remove the splash logo container.innerHTML = ''; - await appRouter.start(); + loading.show(); const root = createRoot(container); root.render(