mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add favorite songs section to artist page (#1604)
This commit is contained in:
@@ -418,6 +418,7 @@
|
||||
"albumArtistDetail": {
|
||||
"about": "About {{artist}}",
|
||||
"appearsOn": "appears on",
|
||||
"favoriteSongs": "favorite songs",
|
||||
"groupingTypeAll": "all release types",
|
||||
"groupingTypePrimary": "primary release types",
|
||||
"recentReleases": "recent releases",
|
||||
@@ -425,6 +426,7 @@
|
||||
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
||||
"topSongs": "top songs",
|
||||
"topSongsFrom": "top songs from {{title}}",
|
||||
"favoriteSongsFrom": "favorite songs from {{title}}",
|
||||
"viewAll": "view all",
|
||||
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
||||
},
|
||||
|
||||
@@ -73,6 +73,13 @@ export const queryKeys: Record<
|
||||
|
||||
return [serverId, 'albumArtists', 'detail'] as const;
|
||||
},
|
||||
favoriteSongs: (serverId: string, artistId?: string) => {
|
||||
if (artistId) {
|
||||
return [serverId, 'albumArtists', 'favoriteSongs', artistId] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'favoriteSongs'] as const;
|
||||
},
|
||||
infiniteList: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
AlbumArtistListQuery,
|
||||
ArtistListQuery,
|
||||
ListCountQuery,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
TopSongListQuery,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
@@ -120,6 +122,24 @@ export const artistsQueries = {
|
||||
...args.options,
|
||||
});
|
||||
},
|
||||
favoriteSongs: (args: QueryHookArgs<{ artistId: string }>) => {
|
||||
return queryOptions({
|
||||
queryFn: ({ signal }) => {
|
||||
return api.controller.getSongList({
|
||||
apiClientProps: { serverId: args.serverId, signal },
|
||||
query: {
|
||||
artistIds: [args.query.artistId],
|
||||
favorite: true,
|
||||
limit: -1,
|
||||
sortBy: SongListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
});
|
||||
},
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(args.serverId, args.query.artistId),
|
||||
});
|
||||
},
|
||||
topSongs: (args: QueryHookArgs<TopSongListQuery>) => {
|
||||
return queryOptions({
|
||||
queryFn: ({ signal }) => {
|
||||
|
||||
@@ -450,6 +450,213 @@ const AlbumArtistMetadataTopSongs = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface AlbumArtistMetadataFavoriteSongsProps {
|
||||
routeId: string;
|
||||
}
|
||||
|
||||
const AlbumArtistMetadataFavoriteSongs = ({ routeId }: AlbumArtistMetadataFavoriteSongsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
|
||||
const currentSong = usePlayerSong();
|
||||
const player = usePlayer();
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const favoriteSongsQuery = useQuery({
|
||||
...artistsQueries.favoriteSongs({
|
||||
query: {
|
||||
artistId: routeId,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
});
|
||||
|
||||
const songs = useMemo(
|
||||
() => favoriteSongsQuery.data?.items || [],
|
||||
[favoriteSongsQuery.data?.items],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return tableConfig?.columns || [];
|
||||
}, [tableConfig?.columns]);
|
||||
|
||||
const filteredSongs = useMemo(() => {
|
||||
const filtered = searchLibraryItems(songs, debouncedSearchTerm, LibraryItem.SONG);
|
||||
// When searching, show all results. Otherwise, limit to 5 if not showing all
|
||||
if (debouncedSearchTerm?.trim() || showAll) {
|
||||
return filtered;
|
||||
}
|
||||
return filtered.slice(0, 5);
|
||||
}, [songs, debouncedSearchTerm, showAll]);
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.SONG,
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.SONG,
|
||||
});
|
||||
|
||||
const overrideControls: Partial<ItemControls> = useMemo(() => {
|
||||
return {
|
||||
onDoubleClick: ({ index, internalState, item, meta }) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
if (favoriteSongsQuery.isLoading || !favoriteSongsQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!favoriteSongsQuery?.data?.items?.length) return null;
|
||||
|
||||
if (!tableConfig || columns.length === 0) {
|
||||
return (
|
||||
<section>
|
||||
<div className={styles.albumSectionTitle}>
|
||||
<Group>
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.favoriteSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<Badge>{favoriteSongsQuery.data?.items?.length}</Badge>
|
||||
</Group>
|
||||
<div className={styles.albumSectionDividerContainer}>
|
||||
<div className={styles.albumSectionDivider} />
|
||||
<Button
|
||||
component={Link}
|
||||
size="compact-md"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_FAVORITE_SONGS, {
|
||||
albumArtistId: routeId,
|
||||
})}
|
||||
uppercase
|
||||
variant="subtle"
|
||||
>
|
||||
{t('page.albumArtistDetail.viewAll', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const currentSongId = currentSong?.id;
|
||||
|
||||
return (
|
||||
<section>
|
||||
<Stack gap="md">
|
||||
<div className={styles.albumSectionTitle}>
|
||||
<Group>
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.favoriteSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<Badge>{favoriteSongsQuery.data?.items?.length}</Badge>
|
||||
</Group>
|
||||
<div className={styles.albumSectionDividerContainer}>
|
||||
<div className={styles.albumSectionDivider} />
|
||||
<Button
|
||||
component={Link}
|
||||
size="compact-md"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_FAVORITE_SONGS, {
|
||||
albumArtistId: routeId,
|
||||
})}
|
||||
uppercase
|
||||
variant="subtle"
|
||||
>
|
||||
{t('page.albumArtistDetail.viewAll', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Group gap="sm" w="100%">
|
||||
<TextInput
|
||||
flex={1}
|
||||
leftSection={<Icon icon="search" />}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
placeholder={t('common.search', { postProcess: 'sentenceCase' })}
|
||||
radius="xl"
|
||||
rightSection={
|
||||
searchTerm ? (
|
||||
<ActionIcon
|
||||
icon="x"
|
||||
onClick={() => setSearchTerm('')}
|
||||
size="sm"
|
||||
variant="transparent"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
styles={{
|
||||
input: {
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
}}
|
||||
value={searchTerm}
|
||||
/>
|
||||
<ListConfigMenu
|
||||
displayTypes={[{ hidden: true, value: ListDisplayType.GRID }]}
|
||||
listKey={ItemListKey.SONG}
|
||||
optionsConfig={{
|
||||
table: {
|
||||
itemsPerPage: { hidden: true },
|
||||
pagination: { hidden: true },
|
||||
},
|
||||
}}
|
||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
<ItemTableList
|
||||
activeRowId={currentSongId}
|
||||
autoFitColumns={tableConfig.autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={filteredSongs}
|
||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||
enableDrag
|
||||
enableDragScroll={false}
|
||||
enableExpansion={false}
|
||||
enableHeader={tableConfig.enableHeader}
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
itemType={LibraryItem.SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
overrideControls={overrideControls}
|
||||
size={tableConfig.size}
|
||||
/>
|
||||
{!searchTerm.trim() && songs.length > 5 && !showAll && (
|
||||
<Group justify="center" w="100%">
|
||||
<Button onClick={() => setShowAll(true)} variant="subtle">
|
||||
{t('action.viewMore', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
interface AlbumArtistMetadataExternalLinksProps {
|
||||
artistName?: string;
|
||||
externalLinks: boolean;
|
||||
@@ -728,6 +935,11 @@ export const AlbumArtistDetailContent = ({
|
||||
/>
|
||||
</Grid.Col>
|
||||
)}
|
||||
{enabledItem.favoriteSongs && (
|
||||
<Grid.Col order={itemOrder.favoriteSongs} span={12}>
|
||||
<AlbumArtistMetadataFavoriteSongs routeId={routeId} />
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
|
||||
interface AlbumArtistDetailFavoriteSongsListHeaderProps {
|
||||
data: Song[];
|
||||
itemCount?: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const AlbumArtistDetailFavoriteSongsListHeader = ({
|
||||
data,
|
||||
itemCount,
|
||||
title,
|
||||
}: AlbumArtistDetailFavoriteSongsListHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar ignoreMaxWidth>
|
||||
<LibraryHeaderBar.PlayButton itemType={LibraryItem.SONG} songs={data} />
|
||||
<LibraryHeaderBar.Title order={2}>
|
||||
{t('page.albumArtistDetail.favoriteSongsFrom', {
|
||||
postProcess: 'titleCase',
|
||||
title,
|
||||
})}
|
||||
</LibraryHeaderBar.Title>
|
||||
<Badge>
|
||||
{itemCount === null || itemCount === undefined ? <SpinnerIcon /> : itemCount}
|
||||
</Badge>
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
);
|
||||
};
|
||||
+3
-3
@@ -20,10 +20,10 @@ export const AlbumArtistDetailTopSongsListHeader = ({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<PageHeader p="1rem">
|
||||
<LibraryHeaderBar>
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar ignoreMaxWidth>
|
||||
<LibraryHeaderBar.PlayButton itemType={LibraryItem.SONG} songs={data} />
|
||||
<LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Title order={2}>
|
||||
{t('page.albumArtistDetail.topSongsFrom', {
|
||||
postProcess: 'titleCase',
|
||||
title,
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { useItemListColumnReorder } from '/@/renderer/components/item-list/helpers/use-item-list-column-reorder';
|
||||
import { useItemListColumnResize } from '/@/renderer/components/item-list/helpers/use-item-list-column-resize';
|
||||
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||
import { AlbumArtistDetailFavoriteSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-favorite-songs-list-header';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, Play } from '/@/shared/types/types';
|
||||
|
||||
const AlbumArtistDetailFavoriteSongsListRoute = () => {
|
||||
const { albumArtistId, artistId } = useParams() as {
|
||||
albumArtistId?: string;
|
||||
artistId?: string;
|
||||
};
|
||||
const routeId = (artistId || albumArtistId) as string;
|
||||
const server = useCurrentServer();
|
||||
const pageKey = LibraryItem.SONG;
|
||||
|
||||
const detailQuery = useQuery(
|
||||
artistsQueries.albumArtistDetail({
|
||||
query: { id: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const favoriteSongsQuery = useQuery(
|
||||
artistsQueries.favoriteSongs({
|
||||
options: { enabled: !!detailQuery?.data?.name },
|
||||
query: { artistId: routeId },
|
||||
serverId: server?.id,
|
||||
}),
|
||||
);
|
||||
|
||||
const itemCount = favoriteSongsQuery?.data?.items?.length || 0;
|
||||
const songs = useMemo(
|
||||
() => favoriteSongsQuery?.data?.items || [],
|
||||
[favoriteSongsQuery?.data?.items],
|
||||
);
|
||||
|
||||
const tableConfig = useSettingsStore((state) => state.lists[ItemListKey.SONG]?.table);
|
||||
const currentSong = usePlayerSong();
|
||||
const player = usePlayer();
|
||||
|
||||
const columns = useMemo(() => {
|
||||
return tableConfig?.columns || [];
|
||||
}, [tableConfig?.columns]);
|
||||
|
||||
const { handleColumnReordered } = useItemListColumnReorder({
|
||||
itemListKey: ItemListKey.SONG,
|
||||
});
|
||||
|
||||
const { handleColumnResized } = useItemListColumnResize({
|
||||
itemListKey: ItemListKey.SONG,
|
||||
});
|
||||
|
||||
const overrideControls: Partial<ItemControls> = useMemo(() => {
|
||||
return {
|
||||
onDoubleClick: ({ index, internalState, item, meta }) => {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const playType = (meta?.playType as Play) || Play.NOW;
|
||||
const items = internalState?.getData() as Song[];
|
||||
|
||||
if (index !== undefined) {
|
||||
player.addToQueueByData(items, playType, item.id);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [player]);
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return {
|
||||
id: routeId,
|
||||
pageKey,
|
||||
};
|
||||
}, [routeId, pageKey]);
|
||||
|
||||
const currentSongId = currentSong?.id;
|
||||
|
||||
if (!tableConfig || columns.length === 0) {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<AlbumArtistDetailFavoriteSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<AlbumArtistDetailFavoriteSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
<ItemTableList
|
||||
activeRowId={currentSongId}
|
||||
autoFitColumns={tableConfig.autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={songs}
|
||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||
enableDrag
|
||||
enableExpansion={false}
|
||||
enableHeader={tableConfig.enableHeader}
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
itemType={LibraryItem.SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
overrideControls={overrideControls}
|
||||
size={tableConfig.size}
|
||||
/>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
};
|
||||
|
||||
const AlbumArtistDetailTopSongsListRouteWithBoundary = () => {
|
||||
return (
|
||||
<PageErrorBoundary>
|
||||
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||
</PageErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumArtistDetailTopSongsListRouteWithBoundary;
|
||||
@@ -12,7 +12,6 @@ import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
|
||||
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 { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||
@@ -93,13 +92,11 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<LibraryContainer>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
</LibraryContainer>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
@@ -108,34 +105,32 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
<LibraryContainer>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
<ItemTableList
|
||||
activeRowId={currentSongId}
|
||||
autoFitColumns={tableConfig.autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={songs}
|
||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||
enableDrag
|
||||
enableExpansion={false}
|
||||
enableHeader={tableConfig.enableHeader}
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
itemType={LibraryItem.SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
overrideControls={overrideControls}
|
||||
size={tableConfig.size}
|
||||
/>
|
||||
</LibraryContainer>
|
||||
<AlbumArtistDetailTopSongsListHeader
|
||||
data={songs}
|
||||
itemCount={itemCount}
|
||||
title={detailQuery?.data?.name || 'Unknown'}
|
||||
/>
|
||||
<ItemTableList
|
||||
activeRowId={currentSongId}
|
||||
autoFitColumns={tableConfig.autoFitColumns}
|
||||
CellComponent={ItemTableListColumn}
|
||||
columns={columns}
|
||||
data={songs}
|
||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||
enableDrag
|
||||
enableExpansion={false}
|
||||
enableHeader={tableConfig.enableHeader}
|
||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||
enableSelection
|
||||
enableSelectionDialog={false}
|
||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||
itemType={LibraryItem.SONG}
|
||||
onColumnReordered={handleColumnReordered}
|
||||
onColumnResized={handleColumnResized}
|
||||
overrideControls={overrideControls}
|
||||
size={tableConfig.size}
|
||||
/>
|
||||
</ListContext.Provider>
|
||||
</AnimatedPage>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
|
||||
const ARTIST_ITEMS: Array<[ArtistItem, string]> = [
|
||||
[ArtistItem.BIOGRAPHY, 'table.column.biography'],
|
||||
[ArtistItem.FAVORITE_SONGS, 'page.albumArtistDetail.favoriteSongs'],
|
||||
[ArtistItem.TOP_SONGS, 'page.albumArtistDetail.topSongs'],
|
||||
[ArtistItem.RECENT_ALBUMS, 'page.albumArtistDetail.recentReleases'],
|
||||
[ArtistItem.SIMILAR_ARTISTS, 'page.albumArtistDetail.relatedArtists'],
|
||||
|
||||
@@ -41,6 +41,7 @@ interface HeaderPlayButtonProps {
|
||||
|
||||
interface TitleProps {
|
||||
children: ReactNode;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
const HeaderPlayButton = ({
|
||||
@@ -100,9 +101,9 @@ const HeaderPlayButton = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Title = ({ children }: TitleProps) => {
|
||||
const Title = ({ children, order = 1 }: TitleProps) => {
|
||||
return (
|
||||
<TextTitle fw={700} order={1} overflow="hidden">
|
||||
<TextTitle fw={700} order={order as any} overflow="hidden">
|
||||
{children}
|
||||
</TextTitle>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import {
|
||||
applyFavoriteOptimisticUpdates,
|
||||
@@ -63,6 +64,17 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
||||
if (variables.query.type === LibraryItem.SONG) {
|
||||
remote?.updateFavorite(true, variables.apiClientProps.serverId, variables.query.id);
|
||||
}
|
||||
if (
|
||||
variables.query.type === LibraryItem.SONG ||
|
||||
variables.query.type === LibraryItem.PLAYLIST_SONG ||
|
||||
variables.query.type === LibraryItem.QUEUE_SONG
|
||||
) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(
|
||||
variables.apiClientProps.serverId,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import {
|
||||
applyFavoriteOptimisticUpdates,
|
||||
@@ -67,6 +68,17 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
||||
variables.query.id,
|
||||
);
|
||||
}
|
||||
if (
|
||||
variables.query.type === LibraryItem.SONG ||
|
||||
variables.query.type === LibraryItem.PLAYLIST_SONG ||
|
||||
variables.query.type === LibraryItem.QUEUE_SONG
|
||||
) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(
|
||||
variables.apiClientProps.serverId,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AxiosError } from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||
import { PreviousQueryData } from '/@/renderer/features/shared/mutations/favorite-optimistic-updates';
|
||||
import {
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
} from '/@/renderer/features/shared/mutations/rating-optimistic-updates';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { RatingResponse, SetRatingArgs } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, RatingResponse, SetRatingArgs } from '/@/shared/types/domain-types';
|
||||
|
||||
const setRatingMutationKey = ['set-rating'];
|
||||
|
||||
@@ -56,6 +57,19 @@ export const useSetRatingMutation = (args: MutationHookArgs) => {
|
||||
|
||||
return applyRatingOptimisticUpdates(queryClient, variables, variables.query.rating);
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
if (
|
||||
variables.query.type === LibraryItem.SONG ||
|
||||
variables.query.type === LibraryItem.PLAYLIST_SONG ||
|
||||
variables.query.type === LibraryItem.QUEUE_SONG
|
||||
) {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.albumArtists.favoriteSongs(
|
||||
variables.apiClientProps.serverId,
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -55,6 +55,10 @@ const AlbumArtistDetailTopSongsListRoute = lazy(
|
||||
() => import('../features/artists/routes/album-artist-detail-top-songs-list-route'),
|
||||
);
|
||||
|
||||
const AlbumArtistDetailFavoriteSongsListRoute = lazy(
|
||||
() => import('../features/artists/routes/album-artist-detail-favorite-songs-list-route'),
|
||||
);
|
||||
|
||||
const AlbumDetailRoute = lazy(
|
||||
() => import('/@/renderer/features/albums/routes/album-detail-route'),
|
||||
);
|
||||
@@ -251,6 +255,14 @@ export const AppRouter = () => {
|
||||
element={<AlbumArtistDetailTopSongsListRoute />}
|
||||
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||
}
|
||||
path={
|
||||
AppRoute.LIBRARY_ARTISTS_DETAIL_FAVORITE_SONGS
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
element={<DummyAlbumDetailRoute />}
|
||||
@@ -295,6 +307,14 @@ export const AppRouter = () => {
|
||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||
}
|
||||
path={
|
||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_FAVORITE_SONGS
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
<Route element={<InvalidRoute />} path="*" />
|
||||
|
||||
@@ -7,6 +7,7 @@ export enum AppRoute {
|
||||
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
||||
LIBRARY_ALBUM_ARTISTS_DETAIL = '/library/album-artists/:albumArtistId',
|
||||
LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY = '/library/album-artists/:albumArtistId/discography',
|
||||
LIBRARY_ALBUM_ARTISTS_DETAIL_FAVORITE_SONGS = '/library/album-artists/:albumArtistId/favorite-songs',
|
||||
LIBRARY_ALBUM_ARTISTS_DETAIL_SONGS = '/library/album-artists/:albumArtistId/songs',
|
||||
LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS = '/library/album-artists/:albumArtistId/top-songs',
|
||||
LIBRARY_ALBUMS = '/library/albums',
|
||||
@@ -14,6 +15,7 @@ export enum AppRoute {
|
||||
LIBRARY_ARTISTS = '/library/artists',
|
||||
LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId',
|
||||
LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY = '/library/artists/:artistId/discography',
|
||||
LIBRARY_ARTISTS_DETAIL_FAVORITE_SONGS = '/library/artists/:artistId/favorite-songs',
|
||||
LIBRARY_ARTISTS_DETAIL_SONGS = '/library/artists/:artistId/songs',
|
||||
LIBRARY_ARTISTS_DETAIL_TOP_SONGS = '/library/artists/:artistId/top-songs',
|
||||
LIBRARY_FOLDERS = '/library/folders',
|
||||
|
||||
@@ -77,6 +77,7 @@ const HomeItemSchema = z.enum([
|
||||
const ArtistItemSchema = z.enum([
|
||||
'biography',
|
||||
'compilations',
|
||||
'favoriteSongs',
|
||||
'recentAlbums',
|
||||
'similarArtists',
|
||||
'topSongs',
|
||||
@@ -655,6 +656,7 @@ export const SettingsStateSchema = ValidationSettingsStateSchema.merge(
|
||||
|
||||
export enum ArtistItem {
|
||||
BIOGRAPHY = 'biography',
|
||||
FAVORITE_SONGS = 'favoriteSongs',
|
||||
RECENT_ALBUMS = 'recentAlbums',
|
||||
SIMILAR_ARTISTS = 'similarArtists',
|
||||
TOP_SONGS = 'topSongs',
|
||||
@@ -2071,10 +2073,24 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
||||
});
|
||||
}
|
||||
|
||||
if (version <= 23) {
|
||||
// Add FAVORITE_SONGS to album artist page configuration
|
||||
const hasFavoriteSongs = state.general.artistItems?.some(
|
||||
(item) => item.id === ArtistItem.FAVORITE_SONGS,
|
||||
);
|
||||
|
||||
if (!hasFavoriteSongs) {
|
||||
state.general.artistItems.push({
|
||||
disabled: false,
|
||||
id: ArtistItem.FAVORITE_SONGS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return persistedState;
|
||||
},
|
||||
name: 'store_settings',
|
||||
version: 23,
|
||||
version: 24,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user