diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx
index bed7a9e4a..837c2f38b 100644
--- a/src/RootAppRouter.tsx
+++ b/src/RootAppRouter.tsx
@@ -10,6 +10,7 @@ import {
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 { WIZARD_APP_ROUTES } from 'apps/wizard/routes/routes';
import AppHeader from 'components/AppHeader';
import Backdrop from 'components/Backdrop';
import BangRedirect from 'components/router/BangRedirect';
@@ -25,6 +26,7 @@ const router = createHashRouter([
children: [
...(isExperimentalLayout ? EXPERIMENTAL_APP_ROUTES : STABLE_APP_ROUTES),
...DASHBOARD_APP_ROUTES,
+ ...WIZARD_APP_ROUTES,
{
path: '!/*',
Component: BangRedirect
diff --git a/src/apps/dashboard/routes/routes.tsx b/src/apps/dashboard/routes/routes.tsx
index 576bdd620..035bcc352 100644
--- a/src/apps/dashboard/routes/routes.tsx
+++ b/src/apps/dashboard/routes/routes.tsx
@@ -17,7 +17,7 @@ export const DASHBOARD_APP_PATHS = {
export const DASHBOARD_APP_ROUTES: RouteObject[] = [
{
- element: ,
+ element: ,
children: [
{
lazy: () => import('../AppLayout'),
diff --git a/src/apps/experimental/routes/legacyRoutes/public.ts b/src/apps/experimental/routes/legacyRoutes/public.ts
index 89fc17935..1808c1bfa 100644
--- a/src/apps/experimental/routes/legacyRoutes/public.ts
+++ b/src/apps/experimental/routes/legacyRoutes/public.ts
@@ -35,47 +35,5 @@ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
controller: 'session/resetPassword/index',
view: 'session/resetPassword/index.html'
}
- },
- {
- path: 'wizardremoteaccess',
- pageProps: {
- controller: 'wizard/remote/index',
- view: 'wizard/remote/index.html'
- }
- },
- {
- path: 'wizardfinish',
- pageProps: {
- controller: 'wizard/finish/index',
- view: 'wizard/finish/index.html'
- }
- },
- {
- path: 'wizardlibrary',
- pageProps: {
- controller: 'wizard/library',
- view: 'wizard/library.html'
- }
- },
- {
- path: 'wizardsettings',
- pageProps: {
- controller: 'wizard/settings/index',
- view: 'wizard/settings/index.html'
- }
- },
- {
- path: 'wizardstart',
- pageProps: {
- controller: 'wizard/start/index',
- view: 'wizard/start/index.html'
- }
- },
- {
- path: 'wizarduser',
- pageProps: {
- controller: 'wizard/user/index',
- view: 'wizard/user/index.html'
- }
}
];
diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx
index 533972b89..f783acb49 100644
--- a/src/apps/experimental/routes/routes.tsx
+++ b/src/apps/experimental/routes/routes.tsx
@@ -36,7 +36,7 @@ export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
{
/* Public routes */
- element: ,
+ element: ,
children: [
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
diff --git a/src/apps/stable/routes/legacyRoutes/public.ts b/src/apps/stable/routes/legacyRoutes/public.ts
index 89fc17935..abf6439bc 100644
--- a/src/apps/stable/routes/legacyRoutes/public.ts
+++ b/src/apps/stable/routes/legacyRoutes/public.ts
@@ -1,4 +1,4 @@
-import { LegacyRoute } from '../../../../components/router/LegacyRoute';
+import type { LegacyRoute } from 'components/router/LegacyRoute';
export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
{
@@ -35,47 +35,5 @@ export const LEGACY_PUBLIC_ROUTES: LegacyRoute[] = [
controller: 'session/resetPassword/index',
view: 'session/resetPassword/index.html'
}
- },
- {
- path: 'wizardremoteaccess',
- pageProps: {
- controller: 'wizard/remote/index',
- view: 'wizard/remote/index.html'
- }
- },
- {
- path: 'wizardfinish',
- pageProps: {
- controller: 'wizard/finish/index',
- view: 'wizard/finish/index.html'
- }
- },
- {
- path: 'wizardlibrary',
- pageProps: {
- controller: 'wizard/library',
- view: 'wizard/library.html'
- }
- },
- {
- path: 'wizardsettings',
- pageProps: {
- controller: 'wizard/settings/index',
- view: 'wizard/settings/index.html'
- }
- },
- {
- path: 'wizardstart',
- pageProps: {
- controller: 'wizard/start/index',
- view: 'wizard/start/index.html'
- }
- },
- {
- path: 'wizarduser',
- pageProps: {
- controller: 'wizard/user/index',
- view: 'wizard/user/index.html'
- }
}
];
diff --git a/src/apps/stable/routes/routes.tsx b/src/apps/stable/routes/routes.tsx
index 230a41953..bcd328ace 100644
--- a/src/apps/stable/routes/routes.tsx
+++ b/src/apps/stable/routes/routes.tsx
@@ -31,7 +31,7 @@ export const STABLE_APP_ROUTES: RouteObject[] = [
{
/* Public routes */
- element: ,
+ element: ,
children: [
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
/* Fallback route for invalid paths */
diff --git a/src/controllers/wizard/finish/index.html b/src/apps/wizard/controllers/finish/index.html
similarity index 100%
rename from src/controllers/wizard/finish/index.html
rename to src/apps/wizard/controllers/finish/index.html
diff --git a/src/controllers/wizard/finish/index.js b/src/apps/wizard/controllers/finish/index.js
similarity index 85%
rename from src/controllers/wizard/finish/index.js
rename to src/apps/wizard/controllers/finish/index.js
index e60653fd7..51a3f8ad3 100644
--- a/src/controllers/wizard/finish/index.js
+++ b/src/apps/wizard/controllers/finish/index.js
@@ -1,4 +1,4 @@
-import loading from '../../../components/loading/loading';
+import loading from 'components/loading/loading';
function onFinish() {
loading.show();
diff --git a/src/controllers/wizard/library.html b/src/apps/wizard/controllers/library.html
similarity index 100%
rename from src/controllers/wizard/library.html
rename to src/apps/wizard/controllers/library.html
diff --git a/src/controllers/wizard/library.js b/src/apps/wizard/controllers/library.js
similarity index 99%
rename from src/controllers/wizard/library.js
rename to src/apps/wizard/controllers/library.js
index 162e5b100..19ff51118 100644
--- a/src/controllers/wizard/library.js
+++ b/src/apps/wizard/controllers/library.js
@@ -361,7 +361,7 @@ function getVirtualFolderHtml(page, virtualFolder, index) {
window.WizardLibraryPage = {
next: function () {
- Dashboard.navigate('wizardsettings');
+ Dashboard.navigate('wizard/settings');
}
};
pageClassOn('pageshow', 'mediaLibraryPage', function () {
diff --git a/src/controllers/wizard/remote/index.html b/src/apps/wizard/controllers/remote/index.html
similarity index 100%
rename from src/controllers/wizard/remote/index.html
rename to src/apps/wizard/controllers/remote/index.html
diff --git a/src/controllers/wizard/remote/index.js b/src/apps/wizard/controllers/remote/index.js
similarity index 75%
rename from src/controllers/wizard/remote/index.js
rename to src/apps/wizard/controllers/remote/index.js
index 65b176488..2948a0824 100644
--- a/src/controllers/wizard/remote/index.js
+++ b/src/apps/wizard/controllers/remote/index.js
@@ -1,8 +1,8 @@
-import loading from '../../../components/loading/loading';
-import '../../../elements/emby-checkbox/emby-checkbox';
-import '../../../elements/emby-button/emby-button';
-import '../../../elements/emby-select/emby-select';
-import Dashboard from '../../../utils/dashboard';
+import loading from 'components/loading/loading';
+import 'elements/emby-checkbox/emby-checkbox';
+import 'elements/emby-button/emby-button';
+import 'elements/emby-select/emby-select';
+import Dashboard from 'utils/dashboard';
function save(page) {
loading.show();
@@ -23,7 +23,7 @@ function save(page) {
}
function navigateToNextPage() {
- Dashboard.navigate('wizardfinish');
+ Dashboard.navigate('wizard/finish');
}
function onSubmit(e) {
diff --git a/src/controllers/wizard/settings/index.html b/src/apps/wizard/controllers/settings/index.html
similarity index 100%
rename from src/controllers/wizard/settings/index.html
rename to src/apps/wizard/controllers/settings/index.html
diff --git a/src/controllers/wizard/settings/index.js b/src/apps/wizard/controllers/settings/index.js
similarity index 89%
rename from src/controllers/wizard/settings/index.js
rename to src/apps/wizard/controllers/settings/index.js
index e37abceaa..1bf069ec0 100644
--- a/src/controllers/wizard/settings/index.js
+++ b/src/apps/wizard/controllers/settings/index.js
@@ -1,8 +1,8 @@
-import loading from '../../../components/loading/loading';
-import '../../../elements/emby-checkbox/emby-checkbox';
-import '../../../elements/emby-button/emby-button';
-import '../../../elements/emby-select/emby-select';
-import Dashboard from '../../../utils/dashboard';
+import loading from 'components/loading/loading';
+import 'elements/emby-checkbox/emby-checkbox';
+import 'elements/emby-button/emby-button';
+import 'elements/emby-select/emby-select';
+import Dashboard from 'utils/dashboard';
function save(page) {
loading.show();
@@ -66,7 +66,7 @@ function reload(page) {
}
function navigateToNextPage() {
- Dashboard.navigate('wizardremoteaccess');
+ Dashboard.navigate('wizard/remoteaccess');
}
function onSubmit(e) {
diff --git a/src/controllers/wizard/start/index.html b/src/apps/wizard/controllers/start/index.html
similarity index 100%
rename from src/controllers/wizard/start/index.html
rename to src/apps/wizard/controllers/start/index.html
diff --git a/src/controllers/wizard/start/index.js b/src/apps/wizard/controllers/start/index.js
similarity index 87%
rename from src/controllers/wizard/start/index.js
rename to src/apps/wizard/controllers/start/index.js
index 08d3fcd2d..1ba18df01 100644
--- a/src/controllers/wizard/start/index.js
+++ b/src/apps/wizard/controllers/start/index.js
@@ -1,7 +1,7 @@
-import loading from '../../../components/loading/loading';
-import '../../../elements/emby-button/emby-button';
-import '../../../elements/emby-select/emby-select';
-import Dashboard from '../../../utils/dashboard';
+import loading from 'components/loading/loading';
+import 'elements/emby-button/emby-button';
+import 'elements/emby-select/emby-select';
+import Dashboard from 'utils/dashboard';
import dom from 'scripts/dom';
function loadPage(page, config, languageOptions) {
@@ -24,7 +24,7 @@ function save(page) {
url: apiClient.getUrl('Startup/Configuration'),
contentType: 'application/json'
}).then(function () {
- Dashboard.navigate('wizarduser');
+ Dashboard.navigate('wizard/user');
});
});
}
diff --git a/src/controllers/wizard/user/index.html b/src/apps/wizard/controllers/user/index.html
similarity index 100%
rename from src/controllers/wizard/user/index.html
rename to src/apps/wizard/controllers/user/index.html
diff --git a/src/controllers/wizard/user/index.js b/src/apps/wizard/controllers/user/index.js
similarity index 84%
rename from src/controllers/wizard/user/index.js
rename to src/apps/wizard/controllers/user/index.js
index 1d4836f35..9e2fb68ba 100644
--- a/src/controllers/wizard/user/index.js
+++ b/src/apps/wizard/controllers/user/index.js
@@ -1,17 +1,17 @@
-import loading from '../../../components/loading/loading';
-import globalize from '../../../lib/globalize';
-import '../../../styles/dashboard.scss';
-import '../../../elements/emby-input/emby-input';
-import '../../../elements/emby-button/emby-button';
-import Dashboard from '../../../utils/dashboard';
-import toast from '../../../components/toast/toast';
+import loading from 'components/loading/loading';
+import globalize from 'lib/globalize';
+import 'styles/dashboard.scss';
+import 'elements/emby-input/emby-input';
+import 'elements/emby-button/emby-button';
+import Dashboard from 'utils/dashboard';
+import toast from 'components/toast/toast';
function getApiClient() {
return ApiClient;
}
function nextWizardPage() {
- Dashboard.navigate('wizardlibrary')
+ Dashboard.navigate('wizard/library')
.catch(err => {
console.error('[Wizard > User] error navigating to library setup', err);
});
diff --git a/src/apps/wizard/routes/routes.tsx b/src/apps/wizard/routes/routes.tsx
new file mode 100644
index 000000000..c328d7aae
--- /dev/null
+++ b/src/apps/wizard/routes/routes.tsx
@@ -0,0 +1,76 @@
+import React from 'react';
+import { Navigate, RouteObject } from 'react-router-dom';
+
+import AppLayout from 'apps/stable/AppLayout';
+import { AppType } from 'constants/appType';
+import ConnectionRequired from 'components/ConnectionRequired';
+import ErrorBoundary from 'components/router/ErrorBoundary';
+import { type LegacyRoute, toViewManagerPageRoute } from 'components/router/LegacyRoute';
+
+const ROUTES: LegacyRoute[] = [
+ {
+ path: 'remoteaccess',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'remote/index',
+ view: 'remote/index.html'
+ }
+ },
+ {
+ path: 'finish',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'finish/index',
+ view: 'finish/index.html'
+ }
+ },
+ {
+ path: 'library',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'library',
+ view: 'library.html'
+ }
+ },
+ {
+ path: 'settings',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'settings/index',
+ view: 'settings/index.html'
+ }
+ },
+ {
+ path: 'start',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'start/index',
+ view: 'start/index.html'
+ }
+ },
+ {
+ path: 'user',
+ pageProps: {
+ appType: AppType.Wizard,
+ controller: 'user/index',
+ view: 'user/index.html'
+ }
+ }
+];
+
+export const WIZARD_APP_ROUTES: RouteObject[] = [
+ {
+ element: ,
+ children: [
+ {
+ Component: AppLayout,
+ path: 'wizard',
+ children: [
+ { index: true, element: },
+ ...ROUTES.map(toViewManagerPageRoute)
+ ],
+ ErrorBoundary
+ }
+ ]
+ }
+];
diff --git a/src/components/ConnectionRequired.tsx b/src/components/ConnectionRequired.tsx
index 8106135e6..443a5e1a0 100644
--- a/src/components/ConnectionRequired.tsx
+++ b/src/components/ConnectionRequired.tsx
@@ -1,23 +1,49 @@
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
-import type { ConnectResponse } from 'jellyfin-apiclient';
+import type { ApiClient, ConnectResponse } from 'jellyfin-apiclient';
+
+import globalize from 'lib/globalize';
+import { ConnectionState } from 'utils/jellyfin-apiclient/ConnectionState';
import alert from './alert';
import Loading from './loading/LoadingComponent';
import ServerConnections from './ServerConnections';
-import globalize from '../lib/globalize';
-import { ConnectionState } from '../utils/jellyfin-apiclient/ConnectionState';
+
+enum AccessLevel {
+ /** Requires a user with administrator access */
+ Admin = 'admin',
+ /** No access restrictions */
+ Public = 'public',
+ /** Requires a valid user session */
+ User = 'user',
+ /** Requires the startup wizard to NOT be completed */
+ Wizard = 'wizard'
+};
+
+type AccessLevelValue = `${AccessLevel}`;
enum BounceRoutes {
Home = '/home',
Login = '/login',
SelectServer = '/selectserver',
- StartWizard = '/wizardstart'
+ StartWizard = '/wizard/start'
}
type ConnectionRequiredProps = {
- isAdminRequired?: boolean,
- isUserRequired?: boolean
+ level?: AccessLevelValue
+};
+
+const fetchPublicSystemInfo = async (apiClient: ApiClient) => {
+ const infoResponse = await fetch(
+ `${apiClient.serverAddress()}/System/Info/Public`,
+ { cache: 'no-cache' }
+ );
+
+ if (!infoResponse.ok) {
+ throw new Error('Public system info request failed');
+ }
+
+ return infoResponse.json();
};
/**
@@ -26,8 +52,7 @@ type ConnectionRequiredProps = {
* If a condition fails, this component will navigate to the appropriate page.
*/
const ConnectionRequired: FunctionComponent = ({
- isAdminRequired = false,
- isUserRequired = true
+ level = 'user'
}) => {
const navigate = useNavigate();
const location = useLocation();
@@ -76,17 +101,27 @@ const ConnectionRequired: FunctionComponent = ({
}
console.warn('[ConnectionRequired] unhandled connection state', connectionResponse.State);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, navigate]);
+ const handleWizard = useCallback(async (firstConnection: ConnectResponse | null) => {
+ const apiClient = firstConnection?.ApiClient || ServerConnections.currentApiClient();
+ if (!apiClient) {
+ throw new Error('No ApiClient available');
+ }
+
+ const systemInfo = await fetchPublicSystemInfo(apiClient);
+ if (systemInfo?.StartupWizardCompleted) {
+ console.info('[ConnectionRequired] startup wizard is complete, redirecting home');
+ navigate(BounceRoutes.Home);
+ }
+ }, [ navigate ]);
+
const handleIncompleteWizard = useCallback(async (firstConnection: ConnectResponse) => {
if (firstConnection.State === ConnectionState.ServerSignIn) {
// Verify the wizard is complete
try {
- const infoResponse = await fetch(`${firstConnection.ApiClient.serverAddress()}/System/Info/Public`, { cache: 'no-cache' });
- if (!infoResponse.ok) {
- throw new Error('Public system info request failed');
- }
- const systemInfo = await infoResponse.json();
+ const systemInfo = await fetchPublicSystemInfo(firstConnection.ApiClient);
if (!systemInfo?.StartupWizardCompleted) {
// Update the current ApiClient
// TODO: Is there a better place to handle this?
@@ -113,7 +148,7 @@ const ConnectionRequired: FunctionComponent = ({
const client = ServerConnections.currentApiClient();
// If this is a user route, ensure a user is logged in
- if ((isAdminRequired || isUserRequired) && !client?.isLoggedIn()) {
+ if ((level === AccessLevel.Admin || level === AccessLevel.User) && !client?.isLoggedIn()) {
try {
console.warn('[ConnectionRequired] unauthenticated user attempted to access user route');
bounce(await ServerConnections.connect())
@@ -127,7 +162,7 @@ const ConnectionRequired: FunctionComponent = ({
}
// If this is an admin route, ensure the user has access
- if (isAdminRequired) {
+ if (level === AccessLevel.Admin) {
try {
const user = await client?.getCurrentUser();
if (!user?.Policy?.IsAdministrator) {
@@ -145,7 +180,7 @@ const ConnectionRequired: FunctionComponent = ({
}
setIsLoading(false);
- }, [bounce, isAdminRequired, isUserRequired]);
+ }, [bounce, level]);
useEffect(() => {
// Check connection status on initial page load
@@ -155,7 +190,14 @@ const ConnectionRequired: FunctionComponent = ({
console.debug('[ConnectionRequired] connection state', firstConnection?.State);
ServerConnections.firstConnection = true;
- if (firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn()) {
+ if (level === AccessLevel.Wizard) {
+ handleWizard(firstConnection)
+ .catch(err => {
+ console.error('[ConnectionRequired] could not validate wizard status', err);
+ });
+ } else if (
+ firstConnection && firstConnection.State !== ConnectionState.SignedIn && !apiClient?.isLoggedIn()
+ ) {
handleIncompleteWizard(firstConnection)
.catch(err => {
console.error('[ConnectionRequired] could not start wizard', err);
@@ -169,7 +211,7 @@ const ConnectionRequired: FunctionComponent = ({
}).catch(err => {
console.error('[ConnectionRequired] failed to connect', err);
});
- }, [handleIncompleteWizard, validateUserAccess]);
+ }, [handleIncompleteWizard, handleWizard, level, validateUserAccess]);
if (isLoading) {
return ;
diff --git a/src/components/router/FallbackRoute.tsx b/src/components/router/FallbackRoute.tsx
index 7f832c166..e2f3f8d77 100644
--- a/src/components/router/FallbackRoute.tsx
+++ b/src/components/router/FallbackRoute.tsx
@@ -15,6 +15,11 @@ const FallbackRoute = () => {
hash: location.hash
};
+ // Redirect old wizard paths
+ if (RegExp(/^\/wizard[a-z]+\.html/i).test(location.pathname)) {
+ return { ..._to, pathname: `/wizard/${location.pathname.slice(7, -5)}` };
+ }
+
// 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) };
diff --git a/src/components/viewManager/ViewManagerPage.tsx b/src/components/viewManager/ViewManagerPage.tsx
index 1aea49ba6..51f76c327 100644
--- a/src/components/viewManager/ViewManagerPage.tsx
+++ b/src/components/viewManager/ViewManagerPage.tsx
@@ -38,19 +38,26 @@ const importController = (
controller: string,
view: string
) => {
- if (appType === AppType.Dashboard) {
- return Promise.all([
- import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${controller}`),
- import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${view}`)
- .then(html => globalize.translateHtml(html))
- ]);
+ switch (appType) {
+ case AppType.Dashboard:
+ return Promise.all([
+ import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${controller}`),
+ import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/controllers/${view}`)
+ .then(html => globalize.translateHtml(html))
+ ]);
+ case AppType.Wizard:
+ return Promise.all([
+ import(/* webpackChunkName: "[request]" */ `../../apps/wizard/controllers/${controller}`),
+ import(/* webpackChunkName: "[request]" */ `../../apps/wizard/controllers/${view}`)
+ .then(html => globalize.translateHtml(html))
+ ]);
+ default:
+ return Promise.all([
+ import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
+ import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
+ .then(html => globalize.translateHtml(html))
+ ]);
}
-
- return Promise.all([
- import(/* webpackChunkName: "[request]" */ `../../controllers/${controller}`),
- import(/* webpackChunkName: "[request]" */ `../../controllers/${view}`)
- .then(html => globalize.translateHtml(html))
- ]);
};
const loadView = async (
diff --git a/src/constants/appType.ts b/src/constants/appType.ts
index efed1f5ff..9a2fa12d1 100644
--- a/src/constants/appType.ts
+++ b/src/constants/appType.ts
@@ -4,5 +4,6 @@
export enum AppType {
Dashboard = 'dashboard',
Experimental = 'experimental',
- Stable = 'stable'
+ Stable = 'stable',
+ Wizard = 'wizard'
}