mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add favorite songs section to artist page (#1604)
This commit is contained in:
@@ -418,6 +418,7 @@
|
|||||||
"albumArtistDetail": {
|
"albumArtistDetail": {
|
||||||
"about": "About {{artist}}",
|
"about": "About {{artist}}",
|
||||||
"appearsOn": "appears on",
|
"appearsOn": "appears on",
|
||||||
|
"favoriteSongs": "favorite songs",
|
||||||
"groupingTypeAll": "all release types",
|
"groupingTypeAll": "all release types",
|
||||||
"groupingTypePrimary": "primary release types",
|
"groupingTypePrimary": "primary release types",
|
||||||
"recentReleases": "recent releases",
|
"recentReleases": "recent releases",
|
||||||
@@ -425,6 +426,7 @@
|
|||||||
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
"relatedArtists": "related $t(entity.artist, {\"count\": 2})",
|
||||||
"topSongs": "top songs",
|
"topSongs": "top songs",
|
||||||
"topSongsFrom": "top songs from {{title}}",
|
"topSongsFrom": "top songs from {{title}}",
|
||||||
|
"favoriteSongsFrom": "favorite songs from {{title}}",
|
||||||
"viewAll": "view all",
|
"viewAll": "view all",
|
||||||
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
"viewAllTracks": "view all $t(entity.track, {\"count\": 2})"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,6 +73,13 @@ export const queryKeys: Record<
|
|||||||
|
|
||||||
return [serverId, 'albumArtists', 'detail'] as const;
|
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) => {
|
infiniteList: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||||
const { filter, pagination } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import {
|
|||||||
AlbumArtistListQuery,
|
AlbumArtistListQuery,
|
||||||
ArtistListQuery,
|
ArtistListQuery,
|
||||||
ListCountQuery,
|
ListCountQuery,
|
||||||
|
SongListSort,
|
||||||
|
SortOrder,
|
||||||
TopSongListQuery,
|
TopSongListQuery,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -120,6 +122,24 @@ export const artistsQueries = {
|
|||||||
...args.options,
|
...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>) => {
|
topSongs: (args: QueryHookArgs<TopSongListQuery>) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
queryFn: ({ signal }) => {
|
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 {
|
interface AlbumArtistMetadataExternalLinksProps {
|
||||||
artistName?: string;
|
artistName?: string;
|
||||||
externalLinks: boolean;
|
externalLinks: boolean;
|
||||||
@@ -728,6 +935,11 @@ export const AlbumArtistDetailContent = ({
|
|||||||
/>
|
/>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
)}
|
)}
|
||||||
|
{enabledItem.favoriteSongs && (
|
||||||
|
<Grid.Col order={itemOrder.favoriteSongs} span={12}>
|
||||||
|
<AlbumArtistMetadataFavoriteSongs routeId={routeId} />
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
</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();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader p="1rem">
|
<PageHeader>
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar ignoreMaxWidth>
|
||||||
<LibraryHeaderBar.PlayButton itemType={LibraryItem.SONG} songs={data} />
|
<LibraryHeaderBar.PlayButton itemType={LibraryItem.SONG} songs={data} />
|
||||||
<LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title order={2}>
|
||||||
{t('page.albumArtistDetail.topSongsFrom', {
|
{t('page.albumArtistDetail.topSongsFrom', {
|
||||||
postProcess: 'titleCase',
|
postProcess: 'titleCase',
|
||||||
title,
|
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 { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
|
||||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
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 { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { usePlayerSong } from '/@/renderer/store';
|
import { usePlayerSong } from '/@/renderer/store';
|
||||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||||
@@ -93,13 +92,11 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<LibraryContainer>
|
<AlbumArtistDetailTopSongsListHeader
|
||||||
<AlbumArtistDetailTopSongsListHeader
|
data={songs}
|
||||||
data={songs}
|
itemCount={itemCount}
|
||||||
itemCount={itemCount}
|
title={detailQuery?.data?.name || 'Unknown'}
|
||||||
title={detailQuery?.data?.name || 'Unknown'}
|
/>
|
||||||
/>
|
|
||||||
</LibraryContainer>
|
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
@@ -108,34 +105,32 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<LibraryContainer>
|
<AlbumArtistDetailTopSongsListHeader
|
||||||
<AlbumArtistDetailTopSongsListHeader
|
data={songs}
|
||||||
data={songs}
|
itemCount={itemCount}
|
||||||
itemCount={itemCount}
|
title={detailQuery?.data?.name || 'Unknown'}
|
||||||
title={detailQuery?.data?.name || 'Unknown'}
|
/>
|
||||||
/>
|
<ItemTableList
|
||||||
<ItemTableList
|
activeRowId={currentSongId}
|
||||||
activeRowId={currentSongId}
|
autoFitColumns={tableConfig.autoFitColumns}
|
||||||
autoFitColumns={tableConfig.autoFitColumns}
|
CellComponent={ItemTableListColumn}
|
||||||
CellComponent={ItemTableListColumn}
|
columns={columns}
|
||||||
columns={columns}
|
data={songs}
|
||||||
data={songs}
|
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
||||||
enableAlternateRowColors={tableConfig.enableAlternateRowColors}
|
enableDrag
|
||||||
enableDrag
|
enableExpansion={false}
|
||||||
enableExpansion={false}
|
enableHeader={tableConfig.enableHeader}
|
||||||
enableHeader={tableConfig.enableHeader}
|
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
||||||
enableHorizontalBorders={tableConfig.enableHorizontalBorders}
|
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
||||||
enableRowHoverHighlight={tableConfig.enableRowHoverHighlight}
|
enableSelection
|
||||||
enableSelection
|
enableSelectionDialog={false}
|
||||||
enableSelectionDialog={false}
|
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
||||||
enableVerticalBorders={tableConfig.enableVerticalBorders}
|
itemType={LibraryItem.SONG}
|
||||||
itemType={LibraryItem.SONG}
|
onColumnReordered={handleColumnReordered}
|
||||||
onColumnReordered={handleColumnReordered}
|
onColumnResized={handleColumnResized}
|
||||||
onColumnResized={handleColumnResized}
|
overrideControls={overrideControls}
|
||||||
overrideControls={overrideControls}
|
size={tableConfig.size}
|
||||||
size={tableConfig.size}
|
/>
|
||||||
/>
|
|
||||||
</LibraryContainer>
|
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
|
|
||||||
const ARTIST_ITEMS: Array<[ArtistItem, string]> = [
|
const ARTIST_ITEMS: Array<[ArtistItem, string]> = [
|
||||||
[ArtistItem.BIOGRAPHY, 'table.column.biography'],
|
[ArtistItem.BIOGRAPHY, 'table.column.biography'],
|
||||||
|
[ArtistItem.FAVORITE_SONGS, 'page.albumArtistDetail.favoriteSongs'],
|
||||||
[ArtistItem.TOP_SONGS, 'page.albumArtistDetail.topSongs'],
|
[ArtistItem.TOP_SONGS, 'page.albumArtistDetail.topSongs'],
|
||||||
[ArtistItem.RECENT_ALBUMS, 'page.albumArtistDetail.recentReleases'],
|
[ArtistItem.RECENT_ALBUMS, 'page.albumArtistDetail.recentReleases'],
|
||||||
[ArtistItem.SIMILAR_ARTISTS, 'page.albumArtistDetail.relatedArtists'],
|
[ArtistItem.SIMILAR_ARTISTS, 'page.albumArtistDetail.relatedArtists'],
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface HeaderPlayButtonProps {
|
|||||||
|
|
||||||
interface TitleProps {
|
interface TitleProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
order?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderPlayButton = ({
|
const HeaderPlayButton = ({
|
||||||
@@ -100,9 +101,9 @@ const HeaderPlayButton = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Title = ({ children }: TitleProps) => {
|
const Title = ({ children, order = 1 }: TitleProps) => {
|
||||||
return (
|
return (
|
||||||
<TextTitle fw={700} order={1} overflow="hidden">
|
<TextTitle fw={700} order={order as any} overflow="hidden">
|
||||||
{children}
|
{children}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import {
|
import {
|
||||||
applyFavoriteOptimisticUpdates,
|
applyFavoriteOptimisticUpdates,
|
||||||
@@ -63,6 +64,17 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
|||||||
if (variables.query.type === LibraryItem.SONG) {
|
if (variables.query.type === LibraryItem.SONG) {
|
||||||
remote?.updateFavorite(true, variables.apiClientProps.serverId, variables.query.id);
|
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,
|
...options,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import isElectron from 'is-electron';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import {
|
import {
|
||||||
applyFavoriteOptimisticUpdates,
|
applyFavoriteOptimisticUpdates,
|
||||||
@@ -67,6 +68,17 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
|||||||
variables.query.id,
|
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,
|
...options,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AxiosError } from 'axios';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
import { eventEmitter } from '/@/renderer/events/event-emitter';
|
||||||
import { PreviousQueryData } from '/@/renderer/features/shared/mutations/favorite-optimistic-updates';
|
import { PreviousQueryData } from '/@/renderer/features/shared/mutations/favorite-optimistic-updates';
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +12,7 @@ import {
|
|||||||
} from '/@/renderer/features/shared/mutations/rating-optimistic-updates';
|
} from '/@/renderer/features/shared/mutations/rating-optimistic-updates';
|
||||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { toast } from '/@/shared/components/toast/toast';
|
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'];
|
const setRatingMutationKey = ['set-rating'];
|
||||||
|
|
||||||
@@ -56,6 +57,19 @@ export const useSetRatingMutation = (args: MutationHookArgs) => {
|
|||||||
|
|
||||||
return applyRatingOptimisticUpdates(queryClient, variables, variables.query.rating);
|
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,
|
...options,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const AlbumArtistDetailTopSongsListRoute = lazy(
|
|||||||
() => import('../features/artists/routes/album-artist-detail-top-songs-list-route'),
|
() => 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(
|
const AlbumDetailRoute = lazy(
|
||||||
() => import('/@/renderer/features/albums/routes/album-detail-route'),
|
() => import('/@/renderer/features/albums/routes/album-detail-route'),
|
||||||
);
|
);
|
||||||
@@ -251,6 +255,14 @@ export const AppRouter = () => {
|
|||||||
element={<AlbumArtistDetailTopSongsListRoute />}
|
element={<AlbumArtistDetailTopSongsListRoute />}
|
||||||
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
path={AppRoute.LIBRARY_ARTISTS_DETAIL_TOP_SONGS}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||||
|
}
|
||||||
|
path={
|
||||||
|
AppRoute.LIBRARY_ARTISTS_DETAIL_FAVORITE_SONGS
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
element={<DummyAlbumDetailRoute />}
|
element={<DummyAlbumDetailRoute />}
|
||||||
@@ -295,6 +307,14 @@ export const AppRouter = () => {
|
|||||||
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
element={
|
||||||
|
<AlbumArtistDetailFavoriteSongsListRoute />
|
||||||
|
}
|
||||||
|
path={
|
||||||
|
AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_FAVORITE_SONGS
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<InvalidRoute />} path="*" />
|
<Route element={<InvalidRoute />} path="*" />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum AppRoute {
|
|||||||
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
||||||
LIBRARY_ALBUM_ARTISTS_DETAIL = '/library/album-artists/:albumArtistId',
|
LIBRARY_ALBUM_ARTISTS_DETAIL = '/library/album-artists/:albumArtistId',
|
||||||
LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY = '/library/album-artists/:albumArtistId/discography',
|
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_SONGS = '/library/album-artists/:albumArtistId/songs',
|
||||||
LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS = '/library/album-artists/:albumArtistId/top-songs',
|
LIBRARY_ALBUM_ARTISTS_DETAIL_TOP_SONGS = '/library/album-artists/:albumArtistId/top-songs',
|
||||||
LIBRARY_ALBUMS = '/library/albums',
|
LIBRARY_ALBUMS = '/library/albums',
|
||||||
@@ -14,6 +15,7 @@ export enum AppRoute {
|
|||||||
LIBRARY_ARTISTS = '/library/artists',
|
LIBRARY_ARTISTS = '/library/artists',
|
||||||
LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId',
|
LIBRARY_ARTISTS_DETAIL = '/library/artists/:artistId',
|
||||||
LIBRARY_ARTISTS_DETAIL_DISCOGRAPHY = '/library/artists/:artistId/discography',
|
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_SONGS = '/library/artists/:artistId/songs',
|
||||||
LIBRARY_ARTISTS_DETAIL_TOP_SONGS = '/library/artists/:artistId/top-songs',
|
LIBRARY_ARTISTS_DETAIL_TOP_SONGS = '/library/artists/:artistId/top-songs',
|
||||||
LIBRARY_FOLDERS = '/library/folders',
|
LIBRARY_FOLDERS = '/library/folders',
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const HomeItemSchema = z.enum([
|
|||||||
const ArtistItemSchema = z.enum([
|
const ArtistItemSchema = z.enum([
|
||||||
'biography',
|
'biography',
|
||||||
'compilations',
|
'compilations',
|
||||||
|
'favoriteSongs',
|
||||||
'recentAlbums',
|
'recentAlbums',
|
||||||
'similarArtists',
|
'similarArtists',
|
||||||
'topSongs',
|
'topSongs',
|
||||||
@@ -655,6 +656,7 @@ export const SettingsStateSchema = ValidationSettingsStateSchema.merge(
|
|||||||
|
|
||||||
export enum ArtistItem {
|
export enum ArtistItem {
|
||||||
BIOGRAPHY = 'biography',
|
BIOGRAPHY = 'biography',
|
||||||
|
FAVORITE_SONGS = 'favoriteSongs',
|
||||||
RECENT_ALBUMS = 'recentAlbums',
|
RECENT_ALBUMS = 'recentAlbums',
|
||||||
SIMILAR_ARTISTS = 'similarArtists',
|
SIMILAR_ARTISTS = 'similarArtists',
|
||||||
TOP_SONGS = 'topSongs',
|
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;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 23,
|
version: 24,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user