add favorite songs section to artist page (#1604)

This commit is contained in:
jeffvli
2026-02-02 22:23:38 -08:00
parent 50c3dbc0a0
commit ac5611fdca
16 changed files with 545 additions and 43 deletions
+2
View File
@@ -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})"
},
+7
View File
@@ -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>
@@ -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>
);
};
@@ -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,
@@ -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,
});
};
+20
View File
@@ -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="*" />
+2
View File
@@ -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',
+17 -1
View File
@@ -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,
},
),
);