refactor app error boundaries

This commit is contained in:
jeffvli
2025-11-23 14:12:00 -08:00
parent 84419820b8
commit a32f76720a
23 changed files with 323 additions and 178 deletions
+1 -1
View File
@@ -120,7 +120,7 @@
"react-image": "^4.1.0",
"react-loading-skeleton": "^3.5.0",
"react-player": "^2.11.0",
"react-router": "^7.9.4",
"react-router": "^7.9.6",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.11",
"react-window-v2": "npm:react-window@^2.2.3",
+27 -8
View File
@@ -46,7 +46,7 @@ importers:
version: 8.3.8(@mantine/core@8.3.8(@mantine/hooks@8.3.8(react@19.1.0))(@types/react@19.2.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mantine/hooks@8.3.8(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@offlegacy/nuqs-hash-router':
specifier: ^0.1.1
version: 0.1.1(nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0))(react@19.1.0)
version: 0.1.1(nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0))(react@19.1.0)
'@radix-ui/react-context-menu':
specifier: ^2.2.16
version: 2.2.16(@types/react-dom@19.2.3(@types/react@19.2.5))(@types/react@19.2.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -151,7 +151,7 @@ importers:
version: https://codeload.github.com/jeffvli/Node-MPV/tar.gz/32b4d64395289ad710c41d481d2707a7acfc228f
nuqs:
specifier: ^2.7.1
version: 2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
version: 2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
overlayscrollbars:
specifier: ^2.11.1
version: 2.11.3
@@ -189,8 +189,8 @@ importers:
specifier: ^2.11.0
version: 2.16.0(react@19.1.0)
react-router:
specifier: ^7.9.4
version: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
specifier: ^7.9.6
version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-virtualized-auto-sizer:
specifier: ^1.0.26
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4610,6 +4610,16 @@ packages:
react-dom:
optional: true
react-router@7.9.6:
resolution: {integrity: sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'}
@@ -7101,9 +7111,9 @@ snapshots:
mkdirp: 1.0.4
rimraf: 3.0.2
'@offlegacy/nuqs-hash-router@0.1.1(nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0))(react@19.1.0)':
'@offlegacy/nuqs-hash-router@0.1.1(nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0))(react@19.1.0)':
dependencies:
nuqs: 2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
nuqs: 2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
react: 19.1.0
'@pkgjs/parseargs@0.11.0':
@@ -10148,12 +10158,12 @@ snapshots:
dependencies:
boolbase: 1.0.0
nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
nuqs@2.7.1(react-router-dom@7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
'@standard-schema/spec': 1.0.0
react: 19.1.0
optionalDependencies:
react-router: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-router-dom: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
object-assign@4.1.1: {}
@@ -10571,6 +10581,15 @@ snapshots:
set-cookie-parser: 2.7.1
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
optional: true
react-router@7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
cookie: 1.0.2
react: 19.1.0
set-cookie-parser: 2.7.1
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0):
dependencies:
@@ -1,87 +0,0 @@
import { useTranslation } from 'react-i18next';
import { useNavigate, useRouteError } from 'react-router';
import { AppMenu } from '/@/renderer/features/titlebar/components/app-menu';
import { AppRoute } from '/@/renderer/router/routes';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Divider } from '/@/shared/components/divider/divider';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
const RouteErrorBoundary = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const error = useRouteError() as any;
console.error('error', error);
const handleReload = () => {
navigate(0);
};
const handleReturn = () => {
navigate(-1);
};
const handleHome = () => {
navigate(AppRoute.HOME);
};
return (
<div style={{ backgroundColor: 'var(--theme-colors-background)' }}>
<Center style={{ height: '100vh' }}>
<Stack style={{ maxWidth: '50%' }}>
<Group>
<ActionIcon
icon="arrowLeftS"
onClick={handleReturn}
px={10}
variant="subtle"
/>
<Icon fill="error" icon="error" size="lg" />
<Text size="lg">{t('error.genericError')}</Text>
</Group>
<Divider my={5} />
<Text size="sm">{error?.message}</Text>
<Group gap="sm" grow>
<Button
leftSection={<Icon icon="home" />}
onClick={handleHome}
size="md"
style={{ flex: 0.5 }}
variant="default"
>
{t('page.home.title')}
</Button>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
leftSection={<Icon icon="menu" />}
size="md"
style={{ flex: 0.5 }}
variant="default"
>
{t('common.menu')}
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<AppMenu />
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
<Group grow>
<Button onClick={handleReload} size="md" variant="filled">
{t('common.reload')}
</Button>
</Group>
</Stack>
</Center>
</div>
);
};
export default RouteErrorBoundary;
@@ -9,6 +9,7 @@ import { ServerCredentialRequired } from '/@/renderer/features/action-required/c
import { ServerRequired } from '/@/renderer/features/action-required/components/server-required';
import { ServerList } from '/@/renderer/features/servers/components/server-list';
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 { useCurrentServerWithCredential } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
@@ -84,4 +85,12 @@ const ActionRequiredRoute = () => {
);
};
export default ActionRequiredRoute;
const ActionRequiredRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<ActionRequiredRoute />
</PageErrorBoundary>
);
};
export default ActionRequiredRouteWithBoundary;
@@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
@@ -32,4 +33,12 @@ const InvalidRoute = () => {
);
};
export default InvalidRoute;
const InvalidRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<InvalidRoute />
</PageErrorBoundary>
);
};
export default InvalidRouteWithBoundary;
@@ -13,6 +13,7 @@ import {
} from '/@/renderer/features/shared/components/library-background-overlay';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { useFastAverageColor } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -83,4 +84,12 @@ const AlbumDetailRoute = () => {
);
};
export default AlbumDetailRoute;
const AlbumDetailRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<AlbumDetailRoute />
</PageErrorBoundary>
);
};
export default AlbumDetailRouteWithBoundary;
@@ -6,6 +6,7 @@ import { AlbumListContent } from '/@/renderer/features/albums/components/album-l
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
const AlbumListRoute = () => {
const { albumArtistId, genreId } = useParams();
@@ -34,4 +35,12 @@ const AlbumListRoute = () => {
);
};
export default AlbumListRoute;
const AlbumListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<AlbumListRoute />
</PageErrorBoundary>
);
};
export default AlbumListRouteWithBoundary;
@@ -11,6 +11,7 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { PlayButton } from '/@/renderer/features/shared/components/play-button';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
@@ -228,4 +229,12 @@ const DummyAlbumDetailRoute = () => {
);
};
export default DummyAlbumDetailRoute;
const DummyAlbumDetailRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<DummyAlbumDetailRoute />
</PageErrorBoundary>
);
};
export default DummyAlbumDetailRouteWithBoundary;
@@ -13,6 +13,7 @@ import {
} from '/@/renderer/features/shared/components/library-background-overlay';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { useFastAverageColor } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -89,4 +90,12 @@ const AlbumArtistDetailRoute = () => {
);
};
export default AlbumArtistDetailRoute;
const AlbumArtistDetailRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<AlbumArtistDetailRoute />
</PageErrorBoundary>
);
};
export default AlbumArtistDetailRouteWithBoundary;
@@ -8,6 +8,7 @@ import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { useCurrentServer } from '/@/renderer/store/auth.store';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -64,4 +65,12 @@ const AlbumArtistDetailTopSongsListRoute = () => {
);
};
export default AlbumArtistDetailTopSongsListRoute;
const AlbumArtistDetailTopSongsListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<AlbumArtistDetailTopSongsListRoute />
</PageErrorBoundary>
);
};
export default AlbumArtistDetailTopSongsListRouteWithBoundary;
@@ -5,6 +5,7 @@ import { AlbumArtistListContent } from '/@/renderer/features/artists/components/
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ItemListKey } from '/@/shared/types/types';
const AlbumArtistListRoute = () => {
@@ -33,4 +34,12 @@ const AlbumArtistListRoute = () => {
);
};
export default AlbumArtistListRoute;
const AlbumArtistListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<AlbumArtistListRoute />
</PageErrorBoundary>
);
};
export default AlbumArtistListRouteWithBoundary;
@@ -5,6 +5,7 @@ import { ArtistListContent } from '/@/renderer/features/artists/components/artis
import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ItemListKey } from '/@/shared/types/types';
const ArtistListRoute = () => {
@@ -33,4 +34,12 @@ const ArtistListRoute = () => {
);
};
export default ArtistListRoute;
const ArtistListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<ArtistListRoute />
</PageErrorBoundary>
);
};
export default ArtistListRouteWithBoundary;
@@ -5,6 +5,7 @@ import { GenreListContent } from '/@/renderer/features/genres/components/genre-l
import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ItemListKey } from '/@/shared/types/types';
const GenreListRoute = () => {
@@ -33,4 +34,12 @@ const GenreListRoute = () => {
);
};
export default GenreListRoute;
const GenreListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<GenreListRoute />
</PageErrorBoundary>
);
};
export default GenreListRouteWithBoundary;
@@ -10,6 +10,7 @@ import { FeaturedGenres } from '/@/renderer/features/home/components/featured-ge
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import {
HomeItem,
useCurrentServer,
@@ -154,12 +155,14 @@ const HomeRoute = () => {
);
};
const SuspensedHomeRoute = () => {
const HomeRouteWithBoundary = () => {
return (
<Suspense fallback={<Spinner container />}>
<HomeRoute />
</Suspense>
<PageErrorBoundary>
<Suspense fallback={<Spinner container />}>
<HomeRoute />
</Suspense>
</PageErrorBoundary>
);
};
export default SuspensedHomeRoute;
export default HomeRouteWithBoundary;
@@ -11,6 +11,7 @@ import JellyfinIcon from '/@/renderer/features/servers/assets/jellyfin.png';
import NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png';
import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png';
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 { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
@@ -200,4 +201,12 @@ const LoginRoute = () => {
);
};
export default LoginRoute;
const LoginRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<LoginRoute />
</PageErrorBoundary>
);
};
export default LoginRouteWithBoundary;
@@ -6,6 +6,7 @@ import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queu
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ItemListKey } from '/@/shared/types/types';
const NowPlayingRoute = () => {
@@ -28,4 +29,12 @@ const NowPlayingRoute = () => {
);
};
export default NowPlayingRoute;
const NowPlayingRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<NowPlayingRoute />
</PageErrorBoundary>
);
};
export default NowPlayingRouteWithBoundary;
@@ -14,6 +14,7 @@ import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/crea
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
@@ -233,4 +234,12 @@ const PlaylistDetailSongListRoute = () => {
);
};
export default PlaylistDetailSongListRoute;
const PlaylistDetailSongListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<PlaylistDetailSongListRoute />
</PageErrorBoundary>
);
};
export default PlaylistDetailSongListRouteWithBoundary;
@@ -6,6 +6,7 @@ import { PlaylistListContent } from '/@/renderer/features/playlists/components/p
import { PlaylistListHeader } from '/@/renderer/features/playlists/components/playlist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { ItemListKey } from '/@/shared/types/types';
const PlaylistListRoute = () => {
@@ -35,4 +36,12 @@ const PlaylistListRoute = () => {
);
};
export default PlaylistListRoute;
const PlaylistListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<PlaylistListRoute />
</PageErrorBoundary>
);
};
export default PlaylistListRouteWithBoundary;
@@ -5,6 +5,7 @@ import { SearchContent } from '/@/renderer/features/search/components/search-con
import { SearchHeader } from '/@/renderer/features/search/components/search-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
const SearchRoute = () => {
const { state: locationState } = useLocation();
@@ -22,4 +23,12 @@ const SearchRoute = () => {
);
};
export default SearchRoute;
const SearchRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<SearchRoute />
</PageErrorBoundary>
);
};
export default SearchRouteWithBoundary;
@@ -0,0 +1,97 @@
import { ErrorBoundary } from 'react-error-boundary';
import { useTranslation } from 'react-i18next';
import { ServerSelector } from '/@/renderer/features/sidebar/components/server-selector';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Code } from '/@/shared/components/code/code';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text';
interface PageErrorFallbackProps {
error: Error;
resetErrorBoundary: () => void;
}
const PageErrorFallback = ({ error, resetErrorBoundary }: PageErrorFallbackProps) => {
const { t } = useTranslation();
const handleRefresh = () => {
window.location.reload();
};
return (
<Box h="100%" pos="relative" w="100%">
<Box
style={{
padding: 'var(--theme-spacing-md)',
position: 'absolute',
right: 0,
top: 0,
zIndex: 1000,
}}
>
<ServerSelector />
</Box>
<Center h="100%" p="md" w="100%">
<Stack maw="800px">
<Group gap="xs">
<Icon fill="error" icon="error" size="lg" />
<TextTitle fw={700} order={3}>
{t('error.genericError', { postProcess: 'sentenceCase' })}
</TextTitle>
</Group>
<Text style={{ wordBreak: 'break-word' }}>
{error?.message || t('error.genericError', { postProcess: 'sentenceCase' })}
</Text>
{process.env.NODE_ENV === 'development' && error?.stack && (
<Code
p="md"
style={{
backgroundColor: 'var(--theme-colors-surface)',
fontFamily: 'monospace',
maxHeight: '300px',
overflow: 'auto',
wordBreak: 'break-word',
}}
>
{error.stack}
</Code>
)}
<Group grow>
<Button onClick={resetErrorBoundary} size="md" variant="default">
{t('common.reload', { postProcess: 'sentenceCase' })}
</Button>
<Button onClick={handleRefresh} size="md" variant="filled">
{t('common.refresh', { postProcess: 'sentenceCase' })}
</Button>
</Group>
</Stack>
</Center>
</Box>
);
};
interface PageErrorBoundaryProps {
children: React.ReactNode;
}
export const PageErrorBoundary = ({ children }: PageErrorBoundaryProps) => {
return (
<ErrorBoundary
FallbackComponent={PageErrorFallback}
onError={(error, errorInfo) => {
if (process.env.NODE_ENV === 'development') {
console.error('Page error boundary caught an error:', error, errorInfo);
}
}}
onReset={() => {}}
>
{children}
</ErrorBoundary>
);
};
@@ -5,9 +5,11 @@ import { ServerSelector } from '/@/renderer/features/sidebar/components/server-s
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center';
import { Code } from '/@/shared/components/code/code';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text';
interface RouterErrorFallbackProps {
@@ -41,32 +43,30 @@ const RouterErrorFallback = ({ error, resetErrorBoundary }: RouterErrorFallbackP
>
<ServerSelector />
</Box>
<Center style={{ height: '100vh' }}>
<Stack style={{ maxWidth: '50%' }}>
<Center h="100vh" p="md" w="100%">
<Stack maw="800px">
<Group gap="xs">
<Icon fill="error" icon="error" size="lg" />
<Text size="lg">
<TextTitle fw={700} order={3}>
{t('error.genericError', { postProcess: 'sentenceCase' })}
</Text>
</TextTitle>
</Group>
<Text size="sm" style={{ wordBreak: 'break-word' }}>
<Text style={{ wordBreak: 'break-word' }}>
{error?.message || t('error.genericError', { postProcess: 'sentenceCase' })}
</Text>
{process.env.NODE_ENV === 'development' && error?.stack && (
<Text
size="xs"
<Code
p="md"
style={{
backgroundColor: 'var(--theme-colors-error)',
color: 'var(--theme-colors-errorText)',
backgroundColor: 'var(--theme-colors-surface)',
fontFamily: 'monospace',
maxHeight: '300px',
overflow: 'auto',
padding: '10px',
wordBreak: 'break-word',
}}
>
{error.stack}
</Text>
</Code>
)}
<Group grow>
<Button onClick={resetErrorBoundary} size="md" variant="default">
@@ -6,12 +6,13 @@ import { ListContext } from '/@/renderer/context/list-context';
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
import { useCurrentServer } from '/@/renderer/store';
import { GenreListSort, SortOrder } from '/@/shared/types/domain-types';
const TrackListRoute = () => {
const SongListRoute = () => {
const server = useCurrentServer();
const [searchParams] = useSearchParams();
const { albumArtistId, genreId } = useParams();
@@ -83,4 +84,12 @@ const TrackListRoute = () => {
);
};
export default TrackListRoute;
const SongListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<SongListRoute />
</PageErrorBoundary>
);
};
export default SongListRouteWithBoundary;
+18 -49
View File
@@ -1,9 +1,9 @@
import { lazy, Suspense } from 'react';
import { HashRouter, Route, Routes } from 'react-router';
import { RouterErrorBoundary } from '/@/renderer/components/error-boundary/router-error-boundary';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-modal';
import { SettingsModal } from '/@/renderer/features/settings/components/settings-modal';
import { RouterErrorBoundary } from '/@/renderer/features/shared/components/router-error-boundary';
import { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
import { ResponsiveLayout } from '/@/renderer/layouts/responsive-layout';
import { AppOutlet } from '/@/renderer/router/app-outlet';
@@ -65,52 +65,31 @@ const GenreListRoute = lazy(() => import('/@/renderer/features/genres/routes/gen
const SearchRoute = lazy(() => import('/@/renderer/features/search/routes/search-route'));
const RouteErrorBoundary = lazy(
() => import('/@/renderer/features/action-required/components/route-error-boundary'),
);
export const AppRouter = () => {
const router = (
<HashRouter>
<RouterErrorBoundary>
<ModalsProvider
modals={{
addToPlaylist: AddToPlaylistContextModal,
base: BaseContextModal,
settings: SettingsModal,
shareItem: ShareItemContextModal,
}}
>
<ModalsProvider
modals={{
addToPlaylist: AddToPlaylistContextModal,
base: BaseContextModal,
settings: SettingsModal,
shareItem: ShareItemContextModal,
}}
>
<RouterErrorBoundary>
<Routes>
<Route element={<TitlebarOutlet />}>
<Route element={<AppOutlet />} errorElement={<RouteErrorBoundary />}>
<Route element={<AppOutlet />}>
<Route element={<ResponsiveLayout />}>
<Route
element={<HomeRoute />}
errorElement={<RouteErrorBoundary />}
index
/>
<Route
element={<HomeRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.HOME}
/>
<Route
element={<SearchRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.SEARCH}
/>
<Route element={<HomeRoute />} index />
<Route element={<HomeRoute />} path={AppRoute.HOME} />
<Route element={<SearchRoute />} path={AppRoute.SEARCH} />
<Route
element={<NowPlayingRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.NOW_PLAYING}
/>
<Route path={AppRoute.LIBRARY_GENRES}>
<Route
element={<GenreListRoute />}
errorElement={<RouteErrorBoundary />}
index
/>
<Route element={<GenreListRoute />} index />
<Route
element={<AlbumListRoute />}
path={AppRoute.LIBRARY_GENRES_ALBUMS}
@@ -122,17 +101,14 @@ export const AppRouter = () => {
</Route>
<Route
element={<AlbumListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS}
/>
<Route
element={<AlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
/>
<Route
element={<ArtistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ARTISTS}
/>
<Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}>
@@ -152,28 +128,21 @@ export const AppRouter = () => {
</Route>
<Route
element={<DummyAlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.FAKE_LIBRARY_ALBUM_DETAILS}
/>
<Route
element={<SongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_SONGS}
/>
<Route
element={<PlaylistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS}
/>
<Route
element={<PlaylistDetailSongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL_SONGS}
/>
<Route
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS}
>
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS}>
<Route element={<AlbumArtistListRoute />} index />
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}>
<Route element={<AlbumArtistDetailRoute />} index />
@@ -209,8 +178,8 @@ export const AppRouter = () => {
</Route>
</Route>
</Routes>
</ModalsProvider>
</RouterErrorBoundary>
</RouterErrorBoundary>
</ModalsProvider>
</HashRouter>
);