[enhancement]: Support react-router links in Modal (#586)

This commit is contained in:
Kendall Garner 2024-04-17 14:29:46 +00:00 committed by GitHub
parent 04b4d92f69
commit d03a3a11eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 198 additions and 147 deletions

View file

@ -3,10 +3,9 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod
import { ModuleRegistry } from '@ag-grid-community/core'; import { ModuleRegistry } from '@ag-grid-community/core';
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model'; import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
import { MantineProvider } from '@mantine/core'; import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { initSimpleImg } from 'react-simple-img'; import { initSimpleImg } from 'react-simple-img';
import { BaseContextModal, toast } from './components'; import { toast } from './components';
import { useTheme } from './hooks'; import { useTheme } from './hooks';
import { IsUpdatedDialog } from './is-updated-dialog'; import { IsUpdatedDialog } from './is-updated-dialog';
import { AppRouter } from './router/app-router'; import { AppRouter } from './router/app-router';
@ -20,7 +19,6 @@ import './styles/global.scss';
import { ContextMenuProvider } from '/@/renderer/features/context-menu'; import { ContextMenuProvider } from '/@/renderer/features/context-menu';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { PlayQueueHandlerContext } from '/@/renderer/features/player'; import { PlayQueueHandlerContext } from '/@/renderer/features/player';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store'; import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types'; import { FontType, PlaybackType, PlayerStatus } from '/@/renderer/types';
@ -246,27 +244,11 @@ export const App = () => {
}, },
}} }}
> >
<ModalsProvider <PlayQueueHandlerContext.Provider value={providerValue}>
modalProps={{ <ContextMenuProvider>
centered: true, <AppRouter />
styles: { </ContextMenuProvider>
body: { position: 'relative' }, </PlayQueueHandlerContext.Provider>
content: { overflow: 'auto' },
},
transitionProps: {
duration: 300,
exitDuration: 300,
transition: 'fade',
},
}}
modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }}
>
<PlayQueueHandlerContext.Provider value={providerValue}>
<ContextMenuProvider>
<AppRouter />
</ContextMenuProvider>
</PlayQueueHandlerContext.Provider>
</ModalsProvider>
<IsUpdatedDialog /> <IsUpdatedDialog />
</MantineProvider> </MantineProvider>
); );

View file

@ -7,10 +7,13 @@ import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/render
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { formatSizeString } from '/@/renderer/utils/format-size-string'; import { formatSizeString } from '/@/renderer/utils/format-size-string';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { Rating, Spoiler } from '/@/renderer/components'; import { Rating, Spoiler, Text } from '/@/renderer/components';
import { sanitize } from '/@/renderer/utils/sanitize'; import { sanitize } from '/@/renderer/utils/sanitize';
import { SongPath } from '/@/renderer/features/item-details/components/song-path'; import { SongPath } from '/@/renderer/features/item-details/components/song-path';
import { SEPARATOR_STRING } from '/@/renderer/api/utils'; import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/renderer/components/separator';
export type ItemDetailsModalProps = { export type ItemDetailsModalProps = {
item: Album | AlbumArtist | Song; item: Album | AlbumArtist | Song;
@ -43,8 +46,28 @@ const handleRow = <T extends AnyLibraryItem>(t: TFunction, item: T, rule: ItemDe
); );
}; };
const formatArtists = (item: Album | Song) => const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) =>
item.albumArtists?.map((artist) => artist.name).join(SEPARATOR_STRING); (isAlbumArtist ? item.albumArtists : item.artists)?.map((artist, index) => (
<span key={artist.id}>
{index > 0 && <Separator />}
<Text
$link
component={Link}
overflow="visible"
size="md"
to={
artist.id
? generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
albumArtistId: artist.id,
})
: ''
}
weight={500}
>
{artist.name || '—'}
</Text>
</span>
));
const formatComment = (item: Album | Song) => const formatComment = (item: Album | Song) =>
item.comment ? <Spoiler maxHeight={50}>{replaceURLWithHTMLLinks(item.comment)}</Spoiler> : null; item.comment ? <Spoiler maxHeight={50}>{replaceURLWithHTMLLinks(item.comment)}</Spoiler> : null;
@ -52,7 +75,27 @@ const formatComment = (item: Album | Song) =>
const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : ''); const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : '');
const formatGenre = (item: Album | AlbumArtist | Song) => const formatGenre = (item: Album | AlbumArtist | Song) =>
item.genres?.map((genre) => genre.name).join(SEPARATOR_STRING); item.genres?.map((genre, index) => (
<span key={genre.id}>
{index > 0 && <Separator />}
<Text
$link
component={Link}
overflow="visible"
size="md"
to={
genre.id
? generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId: genre.id,
})
: ''
}
weight={500}
>
{genre.name || '—'}
</Text>
</span>
));
const formatRating = (item: Album | AlbumArtist | Song) => const formatRating = (item: Album | AlbumArtist | Song) =>
item.userRating !== null ? ( item.userRating !== null ? (
@ -67,7 +110,7 @@ const BoolField = (key: boolean) =>
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [ const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
{ key: 'name', label: 'common.title' }, { key: 'name', label: 'common.title' },
{ label: 'entity.albumArtist_one', render: formatArtists }, { label: 'entity.albumArtist_one', render: formatArtists(true) },
{ label: 'entity.genre_other', render: formatGenre }, { label: 'entity.genre_other', render: formatGenre },
{ {
label: 'common.duration', label: 'common.duration',
@ -159,13 +202,32 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
const SongPropertyMapping: ItemDetailRow<Song>[] = [ const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ key: 'name', label: 'common.title' }, { key: 'name', label: 'common.title' },
{ key: 'path', label: 'common.path', render: SongPath }, { key: 'path', label: 'common.path', render: SongPath },
{ label: 'entity.albumArtist_one', render: formatArtists }, { label: 'entity.albumArtist_one', render: formatArtists(true) },
{ key: 'artists', label: 'entity.artist_other', render: formatArtists(false) },
{ {
key: 'artists', key: 'album',
label: 'entity.artist_other', label: 'entity.album_one',
render: (song) => song.artists.map((artist) => artist.name).join(SEPARATOR_STRING), render: (song) =>
song.albumId &&
song.album && (
<Text
$link
component={Link}
overflow="visible"
size="md"
to={
song.albumId
? generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: song.albumId,
})
: ''
}
weight={500}
>
{song.album}
</Text>
),
}, },
{ key: 'album', label: 'entity.album_one' },
{ key: 'discNumber', label: 'common.disc' }, { key: 'discNumber', label: 'common.disc' },
{ key: 'trackNumber', label: 'common.trackNumber' }, { key: 'trackNumber', label: 'common.trackNumber' },
{ key: 'releaseYear', label: 'filter.releaseYear' }, { key: 'releaseYear', label: 'filter.releaseYear' },

View file

@ -1,14 +1,12 @@
import { lazy, Suspense } from 'react'; import { lazy, Suspense } from 'react';
import { import { Route, HashRouter, Routes } from 'react-router-dom';
Route,
createRoutesFromElements,
RouterProvider,
createHashRouter,
} from 'react-router-dom';
import { AppRoute } from './routes'; import { AppRoute } from './routes';
import { DefaultLayout } from '/@/renderer/layouts'; import { DefaultLayout } from '/@/renderer/layouts';
import { AppOutlet } from '/@/renderer/router/app-outlet'; import { AppOutlet } from '/@/renderer/router/app-outlet';
import { TitlebarOutlet } from '/@/renderer/router/titlebar-outlet'; import { TitlebarOutlet } from '/@/renderer/router/titlebar-outlet';
import { ModalsProvider } from '@mantine/modals';
import { BaseContextModal } from '/@/renderer/components';
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
const NowPlayingRoute = lazy( const NowPlayingRoute = lazy(
() => import('/@/renderer/features/now-playing/routes/now-playing-route'), () => import('/@/renderer/features/now-playing/routes/now-playing-route'),
@ -67,137 +65,146 @@ const RouteErrorBoundary = lazy(
); );
export const AppRouter = () => { export const AppRouter = () => {
const router = createHashRouter( const router = (
createRoutesFromElements( <HashRouter future={{ v7_startTransition: true }}>
<> <ModalsProvider
<Route element={<TitlebarOutlet />}> modalProps={{
<Route centered: true,
element={<AppOutlet />} styles: {
errorElement={<RouteErrorBoundary />} body: { position: 'relative' },
> content: { overflow: 'auto' },
<Route element={<DefaultLayout />}> },
<Route transitionProps: {
index duration: 300,
element={<HomeRoute />} exitDuration: 300,
errorElement={<RouteErrorBoundary />} transition: 'fade',
/> },
<Route }}
element={<HomeRoute />} modals={{ addToPlaylist: AddToPlaylistContextModal, base: BaseContextModal }}
errorElement={<RouteErrorBoundary />} >
path={AppRoute.HOME} <Routes>
/> <Route element={<TitlebarOutlet />}>
<Route <Route
element={<SearchRoute />} element={<AppOutlet />}
errorElement={<RouteErrorBoundary />} errorElement={<RouteErrorBoundary />}
path={AppRoute.SEARCH} >
/> <Route element={<DefaultLayout />}>
<Route
element={<SettingsRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.SETTINGS}
/>
<Route
element={<NowPlayingRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.NOW_PLAYING}
/>
<Route path={AppRoute.LIBRARY_GENRES}>
<Route <Route
index index
element={<GenreListRoute />} element={<HomeRoute />}
errorElement={<RouteErrorBoundary />} errorElement={<RouteErrorBoundary />}
/> />
<Route <Route
element={<AlbumListRoute />} element={<HomeRoute />}
path={AppRoute.LIBRARY_GENRES_ALBUMS} errorElement={<RouteErrorBoundary />}
path={AppRoute.HOME}
/> />
<Route <Route
element={<SongListRoute />} element={<SearchRoute />}
path={AppRoute.LIBRARY_GENRES_SONGS} errorElement={<RouteErrorBoundary />}
path={AppRoute.SEARCH}
/> />
</Route>
<Route
element={<AlbumListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS}
/>
<Route
element={<AlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
/>
<Route
element={<SongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_SONGS}
/>
<Route
element={<PlaylistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS}
/>
<Route
element={<PlaylistDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL}
/>
<Route
element={<PlaylistDetailSongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL_SONGS}
/>
<Route
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS}
>
<Route <Route
index element={<SettingsRoute />}
element={<AlbumArtistListRoute />} errorElement={<RouteErrorBoundary />}
path={AppRoute.SETTINGS}
/> />
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}> <Route
element={<NowPlayingRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.NOW_PLAYING}
/>
<Route path={AppRoute.LIBRARY_GENRES}>
<Route <Route
index index
element={<AlbumArtistDetailRoute />} element={<GenreListRoute />}
errorElement={<RouteErrorBoundary />}
/> />
<Route <Route
element={<AlbumListRoute />} element={<AlbumListRoute />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY} path={AppRoute.LIBRARY_GENRES_ALBUMS}
/> />
<Route <Route
element={<SongListRoute />} element={<SongListRoute />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS} path={AppRoute.LIBRARY_GENRES_SONGS}
/>
<Route
element={<AlbumArtistDetailTopSongsListRoute />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS}
/> />
</Route> </Route>
<Route
element={<AlbumListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS}
/>
<Route
element={<AlbumDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUMS_DETAIL}
/>
<Route
element={<SongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_SONGS}
/>
<Route
element={<PlaylistListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS}
/>
<Route
element={<PlaylistDetailRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL}
/>
<Route
element={<PlaylistDetailSongListRoute />}
errorElement={<RouteErrorBoundary />}
path={AppRoute.PLAYLISTS_DETAIL_SONGS}
/>
<Route
errorElement={<RouteErrorBoundary />}
path={AppRoute.LIBRARY_ALBUM_ARTISTS}
>
<Route
index
element={<AlbumArtistListRoute />}
/>
<Route path={AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL}>
<Route
index
element={<AlbumArtistDetailRoute />}
/>
<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>
</Route>
<Route element={<TitlebarOutlet />}>
<Route element={<DefaultLayout shell />}>
<Route <Route
element={<InvalidRoute />} element={<ActionRequiredRoute />}
path="*" path={AppRoute.ACTION_REQUIRED}
/> />
</Route> </Route>
</Route> </Route>
</Route> </Routes>
<Route element={<TitlebarOutlet />}> </ModalsProvider>
<Route element={<DefaultLayout shell />}> </HashRouter>
<Route
element={<ActionRequiredRoute />}
path={AppRoute.ACTION_REQUIRED}
/>
</Route>
</Route>
</>,
),
); );
return ( return <Suspense fallback={<></>}>{router}</Suspense>;
<Suspense fallback={<></>}>
<RouterProvider
future={{ v7_startTransition: true }}
router={router}
/>
</Suspense>
);
}; };