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-image": "^4.1.0",
"react-loading-skeleton": "^3.5.0", "react-loading-skeleton": "^3.5.0",
"react-player": "^2.11.0", "react-player": "^2.11.0",
"react-router": "^7.9.4", "react-router": "^7.9.6",
"react-virtualized-auto-sizer": "^1.0.26", "react-virtualized-auto-sizer": "^1.0.26",
"react-window": "1.8.11", "react-window": "1.8.11",
"react-window-v2": "npm:react-window@^2.2.3", "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) 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': '@offlegacy/nuqs-hash-router':
specifier: ^0.1.1 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': '@radix-ui/react-context-menu':
specifier: ^2.2.16 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) 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 version: https://codeload.github.com/jeffvli/Node-MPV/tar.gz/32b4d64395289ad710c41d481d2707a7acfc228f
nuqs: nuqs:
specifier: ^2.7.1 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: overlayscrollbars:
specifier: ^2.11.1 specifier: ^2.11.1
version: 2.11.3 version: 2.11.3
@@ -189,8 +189,8 @@ importers:
specifier: ^2.11.0 specifier: ^2.11.0
version: 2.16.0(react@19.1.0) version: 2.16.0(react@19.1.0)
react-router: react-router:
specifier: ^7.9.4 specifier: ^7.9.6
version: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 7.9.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-virtualized-auto-sizer: react-virtualized-auto-sizer:
specifier: ^1.0.26 specifier: ^1.0.26
version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.0.26(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -4610,6 +4610,16 @@ packages:
react-dom: react-dom:
optional: true 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: react-style-singleton@2.2.3:
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -7101,9 +7111,9 @@ snapshots:
mkdirp: 1.0.4 mkdirp: 1.0.4
rimraf: 3.0.2 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: 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 react: 19.1.0
'@pkgjs/parseargs@0.11.0': '@pkgjs/parseargs@0.11.0':
@@ -10148,12 +10158,12 @@ snapshots:
dependencies: dependencies:
boolbase: 1.0.0 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: dependencies:
'@standard-schema/spec': 1.0.0 '@standard-schema/spec': 1.0.0
react: 19.1.0 react: 19.1.0
optionalDependencies: 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) react-router-dom: 7.9.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
object-assign@4.1.1: {} object-assign@4.1.1: {}
@@ -10571,6 +10581,15 @@ snapshots:
set-cookie-parser: 2.7.1 set-cookie-parser: 2.7.1
optionalDependencies: optionalDependencies:
react-dom: 19.1.0(react@19.1.0) 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): react-style-singleton@2.2.3(@types/react@19.2.5)(react@19.1.0):
dependencies: 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 { ServerRequired } from '/@/renderer/features/action-required/components/server-required';
import { ServerList } from '/@/renderer/features/servers/components/server-list'; import { ServerList } from '/@/renderer/features/servers/components/server-list';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; 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 { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServerWithCredential } from '/@/renderer/store'; import { useCurrentServerWithCredential } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button'; 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 { useLocation, useNavigate } from 'react-router';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; 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 { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center'; import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group'; 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'; } from '/@/renderer/features/shared/components/library-background-overlay';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; 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 { useFastAverageColor } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types'; 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 { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
const AlbumListRoute = () => { const AlbumListRoute = () => {
const { albumArtistId, genreId } = useParams(); 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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header'; 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 { PlayButton } from '/@/renderer/features/shared/components/play-button';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-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'; } from '/@/renderer/features/shared/components/library-background-overlay';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; 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 { useFastAverageColor } from '/@/renderer/hooks';
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store'; import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types'; 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 { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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 { useCurrentServer } from '/@/renderer/store/auth.store';
import { LibraryItem } from '/@/shared/types/domain-types'; 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 { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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'; import { ItemListKey } from '/@/shared/types/types';
const AlbumArtistListRoute = () => { 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 { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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'; import { ItemListKey } from '/@/shared/types/types';
const ArtistListRoute = () => { 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 { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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'; import { ItemListKey } from '/@/shared/types/types';
const GenreListRoute = () => { 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 { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { import {
HomeItem, HomeItem,
useCurrentServer, useCurrentServer,
@@ -154,12 +155,14 @@ const HomeRoute = () => {
); );
}; };
const SuspensedHomeRoute = () => { const HomeRouteWithBoundary = () => {
return ( return (
<PageErrorBoundary>
<Suspense fallback={<Spinner container />}> <Suspense fallback={<Spinner container />}>
<HomeRoute /> <HomeRoute />
</Suspense> </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 NavidromeIcon from '/@/renderer/features/servers/assets/navidrome.png';
import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png'; import SubsonicIcon from '/@/renderer/features/servers/assets/opensubsonic.png';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; 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 { AppRoute } from '/@/renderer/router/routes';
import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store'; import { useAuthStoreActions, useCurrentServer } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button'; 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 { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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'; import { ItemListKey } from '/@/shared/types/types';
const NowPlayingRoute = () => { 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 { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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 { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store'; import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; 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 { PlaylistListHeader } from '/@/renderer/features/playlists/components/playlist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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'; import { ItemListKey } from '/@/shared/types/types';
const PlaylistListRoute = () => { 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 { SearchHeader } from '/@/renderer/features/search/components/search-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; import { LibraryContainer } from '/@/renderer/features/shared/components/library-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
const SearchRoute = () => { const SearchRoute = () => {
const { state: locationState } = useLocation(); 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 { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Center } from '/@/shared/components/center/center'; import { Center } from '/@/shared/components/center/center';
import { Code } from '/@/shared/components/code/code';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
interface RouterErrorFallbackProps { interface RouterErrorFallbackProps {
@@ -41,32 +43,30 @@ const RouterErrorFallback = ({ error, resetErrorBoundary }: RouterErrorFallbackP
> >
<ServerSelector /> <ServerSelector />
</Box> </Box>
<Center style={{ height: '100vh' }}> <Center h="100vh" p="md" w="100%">
<Stack style={{ maxWidth: '50%' }}> <Stack maw="800px">
<Group gap="xs"> <Group gap="xs">
<Icon fill="error" icon="error" size="lg" /> <Icon fill="error" icon="error" size="lg" />
<Text size="lg"> <TextTitle fw={700} order={3}>
{t('error.genericError', { postProcess: 'sentenceCase' })} {t('error.genericError', { postProcess: 'sentenceCase' })}
</Text> </TextTitle>
</Group> </Group>
<Text size="sm" style={{ wordBreak: 'break-word' }}> <Text style={{ wordBreak: 'break-word' }}>
{error?.message || t('error.genericError', { postProcess: 'sentenceCase' })} {error?.message || t('error.genericError', { postProcess: 'sentenceCase' })}
</Text> </Text>
{process.env.NODE_ENV === 'development' && error?.stack && ( {process.env.NODE_ENV === 'development' && error?.stack && (
<Text <Code
size="xs" p="md"
style={{ style={{
backgroundColor: 'var(--theme-colors-error)', backgroundColor: 'var(--theme-colors-surface)',
color: 'var(--theme-colors-errorText)',
fontFamily: 'monospace', fontFamily: 'monospace',
maxHeight: '300px', maxHeight: '300px',
overflow: 'auto', overflow: 'auto',
padding: '10px',
wordBreak: 'break-word', wordBreak: 'break-word',
}} }}
> >
{error.stack} {error.stack}
</Text> </Code>
)} )}
<Group grow> <Group grow>
<Button onClick={resetErrorBoundary} size="md" variant="default"> <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 { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { LibraryContainer } from '/@/renderer/features/shared/components/library-container'; 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 { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header'; import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { GenreListSort, SortOrder } from '/@/shared/types/domain-types'; import { GenreListSort, SortOrder } from '/@/shared/types/domain-types';
const TrackListRoute = () => { const SongListRoute = () => {
const server = useCurrentServer(); const server = useCurrentServer();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { albumArtistId, genreId } = useParams(); const { albumArtistId, genreId } = useParams();
@@ -83,4 +84,12 @@ const TrackListRoute = () => {
); );
}; };
export default TrackListRoute; const SongListRouteWithBoundary = () => {
return (
<PageErrorBoundary>
<SongListRoute />
</PageErrorBoundary>
);
};
export default SongListRouteWithBoundary;
+9 -40
View File
@@ -1,9 +1,9 @@
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { HashRouter, Route, Routes } from 'react-router'; 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 { AddToPlaylistContextModal } from '/@/renderer/features/playlists/components/add-to-playlist-context-modal';
import { SettingsModal } from '/@/renderer/features/settings/components/settings-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 { ShareItemContextModal } from '/@/renderer/features/sharing/components/share-item-context-modal';
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';
@@ -65,14 +65,9 @@ const GenreListRoute = lazy(() => import('/@/renderer/features/genres/routes/gen
const SearchRoute = lazy(() => import('/@/renderer/features/search/routes/search-route')); 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 = () => { export const AppRouter = () => {
const router = ( const router = (
<HashRouter> <HashRouter>
<RouterErrorBoundary>
<ModalsProvider <ModalsProvider
modals={{ modals={{
addToPlaylist: AddToPlaylistContextModal, addToPlaylist: AddToPlaylistContextModal,
@@ -81,36 +76,20 @@ export const AppRouter = () => {
shareItem: ShareItemContextModal, shareItem: ShareItemContextModal,
}} }}
> >
<RouterErrorBoundary>
<Routes> <Routes>
<Route element={<TitlebarOutlet />}> <Route element={<TitlebarOutlet />}>
<Route element={<AppOutlet />} errorElement={<RouteErrorBoundary />}> <Route element={<AppOutlet />}>
<Route element={<ResponsiveLayout />}> <Route element={<ResponsiveLayout />}>
<Route <Route element={<HomeRoute />} index />
element={<HomeRoute />} <Route element={<HomeRoute />} path={AppRoute.HOME} />
errorElement={<RouteErrorBoundary />} <Route element={<SearchRoute />} path={AppRoute.SEARCH} />
index
/>
<Route
element={<HomeRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.HOME}
/>
<Route
element={<SearchRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.SEARCH}
/>
<Route <Route
element={<NowPlayingRoute />} element={<NowPlayingRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.NOW_PLAYING} path={AppRoute.NOW_PLAYING}
/> />
<Route path={AppRoute.LIBRARY_GENRES}> <Route path={AppRoute.LIBRARY_GENRES}>
<Route <Route element={<GenreListRoute />} index />
element={<GenreListRoute />}
errorElement={<RouteErrorBoundary />}
index
/>
<Route <Route
element={<AlbumListRoute />} element={<AlbumListRoute />}
path={AppRoute.LIBRARY_GENRES_ALBUMS} path={AppRoute.LIBRARY_GENRES_ALBUMS}
@@ -122,17 +101,14 @@ export const AppRouter = () => {
</Route> </Route>
<Route <Route
element={<AlbumListRoute />} element={<AlbumListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS} path={AppRoute.LIBRARY_ALBUMS}
/> />
<Route <Route
element={<AlbumDetailRoute />} element={<AlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS_DETAIL} path={AppRoute.LIBRARY_ALBUMS_DETAIL}
/> />
<Route <Route
element={<ArtistListRoute />} element={<ArtistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ARTISTS} path={AppRoute.LIBRARY_ARTISTS}
/> />
<Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}> <Route path={AppRoute.LIBRARY_ARTISTS_DETAIL}>
@@ -152,28 +128,21 @@ export const AppRouter = () => {
</Route> </Route>
<Route <Route
element={<DummyAlbumDetailRoute />} element={<DummyAlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.FAKE_LIBRARY_ALBUM_DETAILS} path={AppRoute.FAKE_LIBRARY_ALBUM_DETAILS}
/> />
<Route <Route
element={<SongListRoute />} element={<SongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_SONGS} path={AppRoute.LIBRARY_SONGS}
/> />
<Route <Route
element={<PlaylistListRoute />} element={<PlaylistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS} path={AppRoute.PLAYLISTS}
/> />
<Route <Route
element={<PlaylistDetailSongListRoute />} element={<PlaylistDetailSongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL_SONGS} path={AppRoute.PLAYLISTS_DETAIL_SONGS}
/> />
<Route <Route path={AppRoute.LIBRARY_ALBUM_ARTISTS}>
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS}
>
<Route element={<AlbumArtistListRoute />} index /> <Route element={<AlbumArtistListRoute />} index />
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}> <Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}>
<Route element={<AlbumArtistDetailRoute />} index /> <Route element={<AlbumArtistDetailRoute />} index />
@@ -209,8 +178,8 @@ export const AppRouter = () => {
</Route> </Route>
</Route> </Route>
</Routes> </Routes>
</ModalsProvider>
</RouterErrorBoundary> </RouterErrorBoundary>
</ModalsProvider>
</HashRouter> </HashRouter>
); );