mirror of
https://github.com/jeffvli/feishin.git
synced 2025-12-23 14:00:14 -06:00
adjust network request waterfall to force authentication first, add fallback for no network (#1028)
This commit is contained in:
@@ -11,7 +11,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
|
||||||
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
import { useSyncSettingsToMain } from '/@/renderer/hooks/use-sync-settings-to-main';
|
||||||
import { ReleaseNotesModal } from './release-notes-modal';
|
import { ReleaseNotesModal } from './release-notes-modal';
|
||||||
import { AppRouter } from '/@/renderer/router/app-router';
|
import { AppRouter } from '/@/renderer/router/app-router';
|
||||||
@@ -32,7 +31,7 @@ export const App = () => {
|
|||||||
const { content, enabled } = useCssSettings();
|
const { content, enabled } = useCssSettings();
|
||||||
const { bindings } = useHotkeySettings();
|
const { bindings } = useHotkeySettings();
|
||||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||||
useServerVersion();
|
|
||||||
useSyncSettingsToMain();
|
useSyncSettingsToMain();
|
||||||
|
|
||||||
const [webAudio, setWebAudio] = useState<WebAudio>();
|
const [webAudio, setWebAudio] = useState<WebAudio>();
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { Button } from '/@/shared/components/button/button';
|
||||||
|
import { Center } from '/@/shared/components/center/center';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
|
||||||
|
const NoNetworkRoute = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleRetry = () => {
|
||||||
|
// Navigate to home which will trigger authentication again
|
||||||
|
navigate(AppRoute.HOME);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedPage>
|
||||||
|
<PageHeader />
|
||||||
|
<Center style={{ height: '100%', width: '100vw' }}>
|
||||||
|
<Stack gap="xl" style={{ maxWidth: '50%', textAlign: 'center' }}>
|
||||||
|
<Icon icon="wifiOff" size="4rem" />
|
||||||
|
<Stack gap="md">
|
||||||
|
<Text size="xl" weight={600}>
|
||||||
|
{t('error.noNetwork', { postProcess: 'sentenceCase' })}
|
||||||
|
</Text>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
{t('error.noNetworkDescription', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
leftSection={<Icon icon="refresh" />}
|
||||||
|
onClick={handleRetry}
|
||||||
|
variant="filled"
|
||||||
|
>
|
||||||
|
{t('common.retry', { postProcess: 'sentenceCase' })}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
</AnimatedPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoNetworkRouteWithBoundary = () => {
|
||||||
|
return (
|
||||||
|
<PageErrorBoundary>
|
||||||
|
<NoNetworkRoute />
|
||||||
|
</PageErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NoNetworkRouteWithBoundary;
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
|
import { isAxiosError } from 'axios';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import isEqual from 'lodash/isEqual';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { controller } from '/@/renderer/api/controller';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
import { getServerById, useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
||||||
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
import { LogCategory, logFn } from '/@/renderer/utils/logger';
|
||||||
import { logMsg } from '/@/renderer/utils/logger-message';
|
import { logMsg } from '/@/renderer/utils/logger-message';
|
||||||
@@ -11,15 +16,35 @@ import { AuthState } from '/@/shared/types/types';
|
|||||||
|
|
||||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||||
|
|
||||||
|
const MIN_AUTH_DELAY_MS = 1000;
|
||||||
|
const MAX_NETWORK_RETRIES = 3;
|
||||||
|
const NETWORK_RETRY_DELAY_MS = 2000;
|
||||||
|
|
||||||
|
const isNetworkError = (error: any): boolean => {
|
||||||
|
return (
|
||||||
|
isAxiosError(error) &&
|
||||||
|
(error.code === 'ERR_NETWORK' ||
|
||||||
|
error.code === 'ECONNABORTED' ||
|
||||||
|
error.code === 'ETIMEDOUT' ||
|
||||||
|
error.message?.toLowerCase().includes('network') ||
|
||||||
|
error.message?.toLowerCase().includes('timeout') ||
|
||||||
|
!navigator.onLine)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const useServerAuthenticated = () => {
|
export const useServerAuthenticated = () => {
|
||||||
const priorServerId = useRef<string | undefined>(undefined);
|
const priorServerId = useRef<string | undefined>(undefined);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const [ready, setReady] = useState(AuthState.VALID);
|
const [ready, setReady] = useState(AuthState.LOADING);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const retryCountRef = useRef<number>(0);
|
||||||
|
|
||||||
const { setCurrentServer, updateServer } = useAuthStoreActions();
|
const { setCurrentServer, updateServer } = useAuthStoreActions();
|
||||||
|
|
||||||
const authenticateServer = useCallback(
|
const authenticateServer = useCallback(
|
||||||
async (serverWithAuth: NonNullable<ReturnType<typeof getServerById>>) => {
|
async (serverWithAuth: NonNullable<ReturnType<typeof getServerById>>, retryAttempt = 0) => {
|
||||||
|
const authStartTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setReady(AuthState.LOADING);
|
setReady(AuthState.LOADING);
|
||||||
|
|
||||||
@@ -61,6 +86,42 @@ export const useServerAuthenticated = () => {
|
|||||||
isAdmin: userInfo.isAdmin,
|
isAdmin: userInfo.isAdmin,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch and update server version and features
|
||||||
|
try {
|
||||||
|
const serverInfo = await controller.getServerInfo({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serverInfo && serverInfo.id === serverWithAuth.id) {
|
||||||
|
const { features, version } = serverInfo;
|
||||||
|
const currentServer = getServerById(serverWithAuth.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentServer &&
|
||||||
|
(version !== currentServer.version ||
|
||||||
|
!isEqual(features, currentServer.features))
|
||||||
|
) {
|
||||||
|
updateServer(serverWithAuth.id, {
|
||||||
|
features,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (serverInfoError) {
|
||||||
|
// Log but don't fail authentication if server info fetch fails
|
||||||
|
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
action: 'server_info_fetch_failed',
|
||||||
|
error: (serverInfoError as Error).message,
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
category: LogCategory.SYSTEM,
|
category: LogCategory.SYSTEM,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -73,6 +134,13 @@ export const useServerAuthenticated = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const elapsedTime = Date.now() - authStartTime;
|
||||||
|
const remainingDelay = Math.max(0, MIN_AUTH_DELAY_MS - elapsedTime);
|
||||||
|
|
||||||
|
if (remainingDelay > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, remainingDelay));
|
||||||
|
}
|
||||||
|
|
||||||
setReady(AuthState.VALID);
|
setReady(AuthState.VALID);
|
||||||
return;
|
return;
|
||||||
} catch (getUserInfoError: any) {
|
} catch (getUserInfoError: any) {
|
||||||
@@ -128,6 +196,42 @@ export const useServerAuthenticated = () => {
|
|||||||
|
|
||||||
updateServer(serverWithAuth.id, updatedServer);
|
updateServer(serverWithAuth.id, updatedServer);
|
||||||
|
|
||||||
|
// Fetch and update server version and features
|
||||||
|
try {
|
||||||
|
const serverInfo = await controller.getServerInfo({
|
||||||
|
apiClientProps: {
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (serverInfo && serverInfo.id === serverWithAuth.id) {
|
||||||
|
const { features, version } = serverInfo;
|
||||||
|
const currentServer = getServerById(serverWithAuth.id);
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentServer &&
|
||||||
|
(version !== currentServer.version ||
|
||||||
|
!isEqual(features, currentServer.features))
|
||||||
|
) {
|
||||||
|
updateServer(serverWithAuth.id, {
|
||||||
|
features,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (serverInfoError) {
|
||||||
|
// Log but don't fail authentication if server info fetch fails
|
||||||
|
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
action: 'server_info_fetch_failed',
|
||||||
|
error: (serverInfoError as Error).message,
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
logFn.info(logMsg[LogCategory.SYSTEM].serverAuthenticationSuccess, {
|
||||||
category: LogCategory.SYSTEM,
|
category: LogCategory.SYSTEM,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -141,6 +245,14 @@ export const useServerAuthenticated = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ensure minimum delay before completing authentication
|
||||||
|
const elapsedTime = Date.now() - authStartTime;
|
||||||
|
const remainingDelay = Math.max(0, MIN_AUTH_DELAY_MS - elapsedTime);
|
||||||
|
|
||||||
|
if (remainingDelay > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, remainingDelay));
|
||||||
|
}
|
||||||
|
|
||||||
setReady(AuthState.VALID);
|
setReady(AuthState.VALID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +263,54 @@ export const useServerAuthenticated = () => {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = (error as Error).message || 'Authentication failed';
|
const errorMessage = (error as Error).message || 'Authentication failed';
|
||||||
|
const isNetwork = isNetworkError(error);
|
||||||
|
|
||||||
|
// If it's a network error and we haven't exhausted retries, retry
|
||||||
|
if (isNetwork && retryAttempt < MAX_NETWORK_RETRIES) {
|
||||||
|
const nextRetry = retryAttempt + 1;
|
||||||
|
|
||||||
|
logFn.warn(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
action: 'network_error_retry',
|
||||||
|
attempt: nextRetry,
|
||||||
|
error: errorMessage,
|
||||||
|
maxRetries: MAX_NETWORK_RETRIES,
|
||||||
|
retryDelayMs: NETWORK_RETRY_DELAY_MS,
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, NETWORK_RETRY_DELAY_MS));
|
||||||
|
|
||||||
|
// Retry authentication
|
||||||
|
return authenticateServer(serverWithAuth, nextRetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If network error and retries exhausted, redirect to no-network page
|
||||||
|
if (isNetwork && retryAttempt >= MAX_NETWORK_RETRIES) {
|
||||||
|
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||||
|
category: LogCategory.SYSTEM,
|
||||||
|
meta: {
|
||||||
|
action: 'network_error_max_retries_exceeded',
|
||||||
|
attempts: retryAttempt + 1,
|
||||||
|
error: errorMessage,
|
||||||
|
serverId: serverWithAuth.id,
|
||||||
|
serverName: serverWithAuth.name,
|
||||||
|
serverType: serverWithAuth.type,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Don't clear credentials on network failure - preserve them for when network returns
|
||||||
|
setReady(AuthState.INVALID);
|
||||||
|
navigate(AppRoute.NO_NETWORK, { replace: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-network errors, handle normally
|
||||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationFailed, {
|
||||||
category: LogCategory.SYSTEM,
|
category: LogCategory.SYSTEM,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -176,7 +335,7 @@ export const useServerAuthenticated = () => {
|
|||||||
setReady(AuthState.INVALID);
|
setReady(AuthState.INVALID);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[updateServer, setCurrentServer],
|
[updateServer, setCurrentServer, navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
const debouncedAuth = debounce(
|
const debouncedAuth = debounce(
|
||||||
@@ -201,6 +360,7 @@ export const useServerAuthenticated = () => {
|
|||||||
if (priorServerId.current !== server.id) {
|
if (priorServerId.current !== server.id) {
|
||||||
const serverWithAuth = getServerById(server.id);
|
const serverWithAuth = getServerById(server.id);
|
||||||
priorServerId.current = server.id;
|
priorServerId.current = server.id;
|
||||||
|
retryCountRef.current = 0; // Reset retry count when server changes
|
||||||
|
|
||||||
if (!serverWithAuth) {
|
if (!serverWithAuth) {
|
||||||
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
|
logFn.error(logMsg[LogCategory.SYSTEM].serverAuthenticationError, {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import isEqual from 'lodash/isEqual';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { controller } from '/@/renderer/api/controller';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
|
|
||||||
|
|
||||||
export const useServerVersion = () => {
|
|
||||||
const { updateServer } = useAuthStoreActions();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
|
|
||||||
const serverInfo = useQuery({
|
|
||||||
enabled: !!server,
|
|
||||||
queryFn: async ({ signal }) => {
|
|
||||||
return controller.getServerInfo({
|
|
||||||
apiClientProps: {
|
|
||||||
serverId: server?.id || '',
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
queryKey: queryKeys.server.root(server?.id),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!server?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (server?.id === serverInfo.data?.id) {
|
|
||||||
const { features, version } = serverInfo.data || {};
|
|
||||||
if (version !== server?.version || !isEqual(features, server?.features)) {
|
|
||||||
updateServer(server.id, {
|
|
||||||
features,
|
|
||||||
version,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [serverInfo?.data, server?.features, server?.id, server?.version, updateServer]);
|
|
||||||
};
|
|
||||||
20
src/renderer/layouts/authentication-outlet.tsx
Normal file
20
src/renderer/layouts/authentication-outlet.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Outlet } from 'react-router';
|
||||||
|
|
||||||
|
import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated';
|
||||||
|
import { Center } from '/@/shared/components/center/center';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { AuthState } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const AuthenticationOutlet = () => {
|
||||||
|
const authState = useServerAuthenticated();
|
||||||
|
|
||||||
|
if (authState === AuthState.LOADING) {
|
||||||
|
return (
|
||||||
|
<Center h="100vh" w="100%">
|
||||||
|
<Spinner container />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Navigate, Outlet } from 'react-router';
|
import { Navigate, Outlet } from 'react-router';
|
||||||
|
|
||||||
import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
import { Center } from '/@/shared/components/center/center';
|
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
|
||||||
import { AuthState } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export const AppOutlet = () => {
|
export const AppOutlet = () => {
|
||||||
const currentServer = useCurrentServer();
|
const currentServer = useCurrentServer();
|
||||||
const authState = useServerAuthenticated();
|
|
||||||
|
|
||||||
const isActionsRequired = useMemo(() => {
|
const isActionsRequired = useMemo(() => {
|
||||||
const isServerRequired = !currentServer;
|
const isServerRequired = !currentServer;
|
||||||
@@ -21,15 +16,7 @@ export const AppOutlet = () => {
|
|||||||
return isActionRequired;
|
return isActionRequired;
|
||||||
}, [currentServer]);
|
}, [currentServer]);
|
||||||
|
|
||||||
if (authState === AuthState.LOADING) {
|
if (isActionsRequired) {
|
||||||
return (
|
|
||||||
<Center h="100vh" w="100%">
|
|
||||||
<Spinner container />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActionsRequired || authState === AuthState.INVALID) {
|
|
||||||
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
return <Navigate replace to={AppRoute.ACTION_REQUIRED} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { UpdatePlaylistContextModal } from '/@/renderer/features/playlists/compo
|
|||||||
import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
|
import { SettingsContextModal } from '/@/renderer/features/settings/components/settings-modal';
|
||||||
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
|
||||||
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
|
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
|
||||||
|
import { AuthenticationOutlet } from '/@/renderer/layouts/authentication-outlet';
|
||||||
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
|
||||||
import { AppOutlet } from '/@/renderer/router/app-outlet';
|
import { AppOutlet } from '/@/renderer/router/app-outlet';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
@@ -40,6 +41,10 @@ const InvalidRoute = lazy(
|
|||||||
|
|
||||||
const LoginRoute = lazy(() => import('/@/renderer/features/login/routes/login-route'));
|
const LoginRoute = lazy(() => import('/@/renderer/features/login/routes/login-route'));
|
||||||
|
|
||||||
|
const NoNetworkRoute = lazy(
|
||||||
|
() => import('/@/renderer/features/action-required/routes/no-network-route'),
|
||||||
|
);
|
||||||
|
|
||||||
const HomeRoute = lazy(() => import('/@/renderer/features/home/routes/home-route'));
|
const HomeRoute = lazy(() => import('/@/renderer/features/home/routes/home-route'));
|
||||||
|
|
||||||
const ArtistListRoute = lazy(() => import('/@/renderer/features/artists/routes/artist-list-route'));
|
const ArtistListRoute = lazy(() => import('/@/renderer/features/artists/routes/artist-list-route'));
|
||||||
@@ -96,96 +101,106 @@ export const AppRouter = () => {
|
|||||||
>
|
>
|
||||||
<RouterErrorBoundary>
|
<RouterErrorBoundary>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<TitlebarOutlet />}>
|
<Route element={<AuthenticationOutlet />}>
|
||||||
<Route element={<AppOutlet />}>
|
<Route element={<TitlebarOutlet />}>
|
||||||
<Route element={<ResponsiveLayout />}>
|
<Route element={<AppOutlet />}>
|
||||||
<Route element={<HomeRoute />} index />
|
<Route element={<ResponsiveLayout />}>
|
||||||
<Route element={<HomeRoute />} path={AppRoute.HOME} />
|
<Route element={<HomeRoute />} index />
|
||||||
<Route element={<SearchRoute />} path={AppRoute.SEARCH} />
|
<Route element={<HomeRoute />} path={AppRoute.HOME} />
|
||||||
<Route element={<FavoritesRoute />} path={AppRoute.FAVORITES} />
|
<Route element={<SearchRoute />} path={AppRoute.SEARCH} />
|
||||||
<Route element={<SettingsRoute />} path={AppRoute.SETTINGS} />
|
|
||||||
<Route
|
|
||||||
element={<NowPlayingRoute />}
|
|
||||||
path={AppRoute.NOW_PLAYING}
|
|
||||||
/>
|
|
||||||
<Route path={AppRoute.LIBRARY_GENRES}>
|
|
||||||
<Route element={<GenreListRoute />} index />
|
|
||||||
<Route
|
<Route
|
||||||
element={<GenreDetailRoute />}
|
element={<FavoritesRoute />}
|
||||||
path={AppRoute.LIBRARY_GENRES_DETAIL}
|
path={AppRoute.FAVORITES}
|
||||||
/>
|
/>
|
||||||
</Route>
|
<Route
|
||||||
<Route
|
element={<SettingsRoute />}
|
||||||
element={<AlbumListRoute />}
|
path={AppRoute.SETTINGS}
|
||||||
path={AppRoute.LIBRARY_ALBUMS}
|
/>
|
||||||
/>
|
<Route
|
||||||
<Route
|
element={<NowPlayingRoute />}
|
||||||
element={<AlbumDetailRoute />}
|
path={AppRoute.NOW_PLAYING}
|
||||||
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
|
/>
|
||||||
/>
|
<Route path={AppRoute.LIBRARY_GENRES}>
|
||||||
<Route
|
<Route element={<GenreListRoute />} index />
|
||||||
element={<ArtistListRoute />}
|
<Route
|
||||||
path={AppRoute.LIBRARY_ARTISTS}
|
element={<GenreDetailRoute />}
|
||||||
/>
|
path={AppRoute.LIBRARY_GENRES_DETAIL}
|
||||||
<Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}>
|
/>
|
||||||
<Route element={<AlbumArtistDetailRoute />} index />
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
element={<AlbumListRoute />}
|
element={<AlbumListRoute />}
|
||||||
path={AppRoute.LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY}
|
path={AppRoute.LIBRARY_ALBUMS}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={<SongListRoute />}
|
element={<AlbumDetailRoute />}
|
||||||
path={AppRoute.LIBRARY_ARTISTS_DETAIL_SONGS}
|
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={<AlbumArtistDetailTopSongsListRoute />}
|
element={<ArtistListRoute />}
|
||||||
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
path={AppRoute.LIBRARY_ARTISTS}
|
||||||
/>
|
/>
|
||||||
</Route>
|
<Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}>
|
||||||
<Route
|
|
||||||
element={<DummyAlbumDetailRoute />}
|
|
||||||
path={AppRoute.FAKE_LIBRARY_ALBUM_DETAILS}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
element={<SongListRoute />}
|
|
||||||
path={AppRoute.LIBRARY_SONGS}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
element={<FolderListRoute />}
|
|
||||||
path={AppRoute.LIBRARY_FOLDERS}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
element={<PlaylistListRoute />}
|
|
||||||
path={AppRoute.PLAYLISTS}
|
|
||||||
/>
|
|
||||||
<Route element={<RadioListRoute />} path={AppRoute.RADIO} />
|
|
||||||
<Route
|
|
||||||
element={<PlaylistDetailSongListRoute />}
|
|
||||||
path={AppRoute.PLAYLISTS_DETAIL_SONGS}
|
|
||||||
/>
|
|
||||||
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS}>
|
|
||||||
<Route element={<AlbumArtistListRoute />} index />
|
|
||||||
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}>
|
|
||||||
<Route element={<AlbumArtistDetailRoute />} index />
|
<Route element={<AlbumArtistDetailRoute />} index />
|
||||||
<Route
|
<Route
|
||||||
element={<AlbumListRoute />}
|
element={<AlbumListRoute />}
|
||||||
path={
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY}
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={<SongListRoute />}
|
element={<SongListRoute />}
|
||||||
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS}
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_SONGS}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
element={<AlbumArtistDetailTopSongsListRoute />}
|
element={<AlbumArtistDetailTopSongsListRoute />}
|
||||||
path={
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
element={<DummyAlbumDetailRoute />}
|
||||||
|
path={AppRoute.FAKE_LIBRARY_ALBUM_DETAILS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<SongListRoute />}
|
||||||
|
path={AppRoute.LIBRARY_SONGS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<FolderListRoute />}
|
||||||
|
path={AppRoute.LIBRARY_FOLDERS}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<PlaylistListRoute />}
|
||||||
|
path={AppRoute.PLAYLISTS}
|
||||||
|
/>
|
||||||
|
<Route element={<RadioListRoute />} path={AppRoute.RADIO} />
|
||||||
|
<Route
|
||||||
|
element={<PlaylistDetailSongListRoute />}
|
||||||
|
path={AppRoute.PLAYLISTS_DETAIL_SONGS}
|
||||||
|
/>
|
||||||
|
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS}>
|
||||||
|
<Route element={<AlbumArtistListRoute />} index />
|
||||||
|
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}>
|
||||||
|
<Route element={<AlbumArtistDetailRoute />} index />
|
||||||
|
<Route
|
||||||
|
element={<AlbumListRoute />}
|
||||||
|
path={
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<SongListRoute />}
|
||||||
|
path={
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
element={<AlbumArtistDetailTopSongsListRoute />}
|
||||||
|
path={
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
<Route element={<InvalidRoute />} path="*" />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<InvalidRoute />} path="*" />
|
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
@@ -196,6 +211,7 @@ export const AppRouter = () => {
|
|||||||
path={AppRoute.ACTION_REQUIRED}
|
path={AppRoute.ACTION_REQUIRED}
|
||||||
/>
|
/>
|
||||||
<Route element={<LoginRoute />} path={AppRoute.LOGIN} />
|
<Route element={<LoginRoute />} path={AppRoute.LOGIN} />
|
||||||
|
<Route element={<NoNetworkRoute />} path={AppRoute.NO_NETWORK} />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export enum AppRoute {
|
|||||||
LIBRARY_GENRES_DETAIL = '/library/genres/:genreId',
|
LIBRARY_GENRES_DETAIL = '/library/genres/:genreId',
|
||||||
LIBRARY_SONGS = '/library/songs',
|
LIBRARY_SONGS = '/library/songs',
|
||||||
LOGIN = '/login',
|
LOGIN = '/login',
|
||||||
|
NO_NETWORK = '/no-network',
|
||||||
NOW_PLAYING = '/now-playing',
|
NOW_PLAYING = '/now-playing',
|
||||||
PLAYING = '/playing',
|
PLAYING = '/playing',
|
||||||
PLAYLISTS = '/playlists',
|
PLAYLISTS = '/playlists',
|
||||||
|
|||||||
@@ -109,6 +109,8 @@ import {
|
|||||||
LuVolume1,
|
LuVolume1,
|
||||||
LuVolume2,
|
LuVolume2,
|
||||||
LuVolumeX,
|
LuVolumeX,
|
||||||
|
LuWifi,
|
||||||
|
LuWifiOff,
|
||||||
LuX,
|
LuX,
|
||||||
} from 'react-icons/lu';
|
} from 'react-icons/lu';
|
||||||
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
||||||
@@ -241,6 +243,8 @@ export const AppIcon = {
|
|||||||
volumeMute: LuVolumeX,
|
volumeMute: LuVolumeX,
|
||||||
volumeNormal: LuVolume1,
|
volumeNormal: LuVolume1,
|
||||||
warn: LuTriangleAlert,
|
warn: LuTriangleAlert,
|
||||||
|
wifiOff: LuWifiOff,
|
||||||
|
wifiOn: LuWifi,
|
||||||
x: LuX,
|
x: LuX,
|
||||||
xCircle: LuCircleX,
|
xCircle: LuCircleX,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -4,4 +4,5 @@
|
|||||||
|
|
||||||
.th {
|
.th {
|
||||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||||
|
background-color: initial;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user