decouple AlbumArtistInfo from AlbumArtistDetail (#1809)

This commit is contained in:
jeffvli
2026-03-08 22:06:18 -07:00
parent 7dbf8dd9fe
commit 17deac8d65
14 changed files with 386 additions and 156 deletions
@@ -7,6 +7,7 @@ import { getOptimizedListCount } from '/@/renderer/api/utils-list-count';
import { QueryHookArgs } from '/@/renderer/lib/react-query';
import {
AlbumArtistDetailQuery,
AlbumArtistInfoQuery,
AlbumArtistListQuery,
ArtistListQuery,
ListCountQuery,
@@ -28,6 +29,20 @@ export const artistsQueries = {
...args.options,
});
},
albumArtistInfo: (args: QueryHookArgs<AlbumArtistInfoQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return (
api.controller.getAlbumArtistInfo?.({
apiClientProps: { serverId: args.serverId, signal },
query: args.query,
}) ?? Promise.resolve(null)
);
},
queryKey: queryKeys.albumArtists.info(args.serverId, args.query),
...args.options,
});
},
albumArtistList: (args: QueryHookArgs<AlbumArtistListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
@@ -5,8 +5,7 @@ import {
UseSuspenseQueryResult,
} from '@tanstack/react-query';
import { LayoutGroup, motion } from 'motion/react';
import { Suspense } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { Suspense, useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createSearchParams, generatePath, Link, useLocation, useParams } from 'react-router';
@@ -70,6 +69,7 @@ import { Grid } from '/@/shared/components/grid/grid';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { SegmentedControl } from '/@/shared/components/segmented-control/segmented-control';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
import { Stack } from '/@/shared/components/stack/stack';
@@ -201,18 +201,57 @@ const AlbumArtistMetadataGenres = ({ genres }: AlbumArtistMetadataGenresProps) =
interface AlbumArtistMetadataBiographyProps {
artistName?: string;
biography: null | string | undefined;
routeId: string;
}
const AlbumArtistMetadataBiography = ({
artistName,
biography,
routeId,
}: AlbumArtistMetadataBiographyProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
if (!biography) return null;
const artistInfoQuery = useQuery({
...artistsQueries.albumArtistInfo({
query: { id: routeId, limit: 10 },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
});
const sanitizedBiography = sanitize(biography);
const detailQuery = useQuery({
...artistsQueries.albumArtistDetail({
query: { id: routeId },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
});
const biography = artistInfoQuery.data?.biography || detailQuery.data?.biography;
const isLoading = !biography && (artistInfoQuery.isLoading || detailQuery.isLoading);
const sanitizedBiography = biography ? sanitize(biography) : '';
if (isLoading) {
return (
<section style={{ maxWidth: '1280px' }}>
<TextTitle fw={700} order={3}>
{t('page.albumArtistDetail.about', {
artist: artistName,
})}
</TextTitle>
<Stack gap="xs">
<Skeleton enableAnimation height="1rem" width="100%" />
<Skeleton enableAnimation height="1rem" width="98%" />
<Skeleton enableAnimation height="1rem" width="60%" />
</Stack>
</section>
);
}
if (!biography) {
return null;
}
return (
<section style={{ maxWidth: '1280px' }}>
@@ -850,20 +889,25 @@ const AlbumArtistMetadataExternalLinks = ({
};
interface AlbumArtistMetadataSimilarArtistsProps {
detailQuery: ReturnType<typeof useSuspenseQuery<AlbumArtistDetailResponse>>;
routeId: string;
}
const AlbumArtistMetadataSimilarArtists = ({
detailQuery,
routeId,
}: AlbumArtistMetadataSimilarArtistsProps) => {
const AlbumArtistMetadataSimilarArtists = ({ routeId }: AlbumArtistMetadataSimilarArtistsProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const serverId = useCurrentServerId();
const artistInfoQuery = useQuery({
...artistsQueries.albumArtistInfo({
query: { id: routeId, limit: 10 },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
});
const relatedArtists = artistInfoQuery.data?.similarArtists ?? null;
const similarArtists = useMemo(() => {
const relatedArtists = detailQuery.data?.similarArtists;
if (!relatedArtists || relatedArtists.length === 0) {
return [];
}
@@ -890,7 +934,7 @@ const AlbumArtistMetadataSimilarArtists = ({
userRating: relatedArtist.userRating,
}),
);
}, [detailQuery.data?.similarArtists, server?.type, serverId]);
}, [relatedArtists, server?.type, serverId]);
const carouselTitle = useMemo(
() => (
@@ -908,7 +952,7 @@ const AlbumArtistMetadataSimilarArtists = ({
[t],
);
if (similarArtists.length === 0) {
if (!artistInfoQuery.isLoading && similarArtists.length === 0) {
return null;
}
@@ -916,6 +960,7 @@ const AlbumArtistMetadataSimilarArtists = ({
<AlbumArtistGridCarousel
data={similarArtists}
excludeIds={[routeId]}
isLoading={artistInfoQuery.isLoading}
rowCount={1}
title={carouselTitle}
/>
@@ -977,8 +1022,6 @@ export const AlbumArtistDetailContent = ({
[routeId, detailQuery.data?.name],
);
const biography =
detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null;
const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false;
const mbzId = detailQuery.data?.mbz;
@@ -1034,11 +1077,11 @@ export const AlbumArtistDetailContent = ({
/>
</Grid.Col>
)}
{biography && (
{enabledItem.biography && (
<Grid.Col order={itemOrder.biography} span={12}>
<AlbumArtistMetadataBiography
artistName={detailQuery.data?.name}
biography={biography}
routeId={routeId}
/>
</Grid.Col>
)}
@@ -1047,10 +1090,7 @@ export const AlbumArtistDetailContent = ({
</Grid.Col>
{enabledItem.similarArtists && (
<Grid.Col order={itemOrder.similarArtists} span={12}>
<AlbumArtistMetadataSimilarArtists
detailQuery={detailQuery}
routeId={routeId}
/>
<AlbumArtistMetadataSimilarArtists routeId={routeId} />
</Grid.Col>
)}
{enabledItem.topSongs && (
@@ -1,4 +1,4 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useQuery, useSuspenseQuery } from '@tanstack/react-query';
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
@@ -69,9 +69,9 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
];
const { addToQueueByFetch } = usePlayer();
const playButtonBehavior = usePlayButtonBehavior();
const setFavorite = useSetFavorite();
const setRating = useSetRating();
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = useCallback(
(type?: Play) => {
@@ -137,11 +137,19 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
type: 'itemCard',
});
const artistInfoQuery = useQuery({
...artistsQueries.albumArtistInfo({
query: { id: routeId, limit: 10 },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
});
const showRating = showRatings && detailQuery?.data?._serverType === ServerType.NAVIDROME;
const selectedImageUrl = useMemo(() => {
return detailQuery.data?.imageUrl || imageUrl;
}, [detailQuery.data?.imageUrl, imageUrl]);
return detailQuery.data?.imageUrl || artistInfoQuery.data?.imageUrl || imageUrl;
}, [artistInfoQuery.data?.imageUrl, detailQuery.data?.imageUrl, imageUrl]);
return (
<LibraryHeader
@@ -1,6 +1,9 @@
import { useMemo } from 'react';
import { GridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel-v2';
import {
GridCarousel,
GridCarouselSkeletonFallback,
} from '/@/renderer/components/grid-carousel/grid-carousel-v2';
import { MemoizedItemCard } from '/@/renderer/components/item-card/item-card';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
@@ -10,12 +13,13 @@ import { ItemListKey } from '/@/shared/types/types';
interface AlbumArtistGridCarouselProps {
data: AlbumArtist[];
excludeIds?: string[];
isLoading?: boolean;
rowCount?: number;
title: React.ReactNode | string;
}
export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
const { data, excludeIds, rowCount = 1, title } = props;
const { data, excludeIds, isLoading = false, rowCount = 1, title } = props;
const rows = useGridRows(LibraryItem.ALBUM_ARTIST, ItemListKey.ALBUM_ARTIST);
const controls = useDefaultItemListControls();
@@ -41,6 +45,18 @@ export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
}));
}, [data, excludeIds, controls, rows]);
if (isLoading) {
return (
<GridCarouselSkeletonFallback
placeholderItemType={LibraryItem.ALBUM_ARTIST}
placeholderRound
placeholderRows={rows}
rowCount={rowCount}
title={title}
/>
);
}
const handleNextPage = () => {};
const handlePrevPage = () => {};
@@ -1,7 +1,8 @@
import { useSuspenseQueries } from '@tanstack/react-query';
import { Suspense, useRef } from 'react';
import { useQuery, useQueryClient, useSuspenseQueries } from '@tanstack/react-query';
import { Suspense, useEffect, useRef } from 'react';
import { useParams } from 'react-router';
import { queryKeys } from '/@/renderer/api/query-keys';
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
import { NativeScrollArea } from '/@/renderer/components/native-scroll-area/native-scroll-area';
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
@@ -19,7 +20,12 @@ import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-e
import { useFastAverageColor } from '/@/renderer/hooks';
import { useArtistBackground, useCurrentServer, useCurrentServerId } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { AlbumListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
import {
AlbumArtistDetailResponse,
AlbumListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
const AlbumArtistDetailRouteContent = () => {
const scrollAreaRef = useRef<HTMLDivElement>(null);
@@ -27,6 +33,7 @@ const AlbumArtistDetailRouteContent = () => {
const server = useCurrentServer();
const serverId = useCurrentServerId();
const { artistBackground, artistBackgroundBlur } = useArtistBackground();
const queryClient = useQueryClient();
const { albumArtistId, artistId } = useParams() as {
albumArtistId?: string;
@@ -35,6 +42,26 @@ const AlbumArtistDetailRouteContent = () => {
const routeId = (artistId || albumArtistId) as string;
const artistInfoQuery = useQuery({
...artistsQueries.albumArtistInfo({
query: { id: routeId, limit: 10 },
serverId: server?.id,
}),
enabled: Boolean(server?.id && routeId),
});
useEffect(() => {
const data = artistInfoQuery.data;
if (!data?.imageUrl || !server?.id || !routeId) return;
queryClient.setQueryData(
queryKeys.albumArtists.detail(server.id, { id: routeId }),
(prev: AlbumArtistDetailResponse | undefined) => {
if (!prev) return prev;
return { ...prev, imageUrl: data.imageUrl ?? prev.imageUrl };
},
);
}, [artistInfoQuery.data, queryClient, routeId, server?.id]);
const [detailQuery, albumsQuery] = useSuspenseQueries({
queries: [
artistsQueries.albumArtistDetail({ query: { id: routeId }, serverId: server?.id }),
@@ -60,13 +87,14 @@ const AlbumArtistDetailRouteContent = () => {
const libraryBackgroundImageUrl = useItemImageUrl({
id: detailQuery.data?.imageId || undefined,
imageUrl: detailQuery.data?.imageUrl,
itemType: LibraryItem.ALBUM_ARTIST,
type: 'itemCard',
});
const selectedImageUrl = imageUrl || detailQuery.data?.imageUrl;
const { background: backgroundColor, isLoading: isColorLoading } = useFastAverageColor({
const { background: backgroundColor } = useFastAverageColor({
id: artistId,
src: selectedImageUrl,
srcLoaded: true,
@@ -76,9 +104,9 @@ const AlbumArtistDetailRouteContent = () => {
const showBlurredImage = artistBackground;
if (isColorLoading) {
return <Spinner container />;
}
// if (isColorLoading) {
// return <Spinner container />;
// }
return (
<AnimatedPage key={`album-artist-detail-${routeId}`}>
@@ -6,6 +6,7 @@ import {
Album,
AlbumArtist,
AlbumArtistDetailResponse,
AlbumArtistInfoResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
@@ -240,29 +241,44 @@ export const applyFavoriteOptimisticUpdates = (
return { ...prev, userFavorite: isFavorite };
}
if (prev.similarArtists && prev.similarArtists.length > 0) {
const hasMatchingSimilarArtist = prev.similarArtists.some(
(artist) => itemIdSet.has(artist.id),
);
if (hasMatchingSimilarArtist) {
return {
...prev,
similarArtists: prev.similarArtists.map((artist) =>
itemIdSet.has(artist.id)
? { ...artist, userFavorite: isFavorite }
: artist,
),
};
}
}
return prev;
},
});
}
});
const infoQueryKey = queryKeys.albumArtists.info(variables.apiClientProps.serverId);
const infoQueries = queryClient.getQueriesData({
exact: false,
queryKey: infoQueryKey,
});
infoQueries.forEach(([queryKey, data]) => {
if (data) {
pendingUpdates.push({
previousData: data,
queryKey,
updater: (prev: AlbumArtistInfoResponse | null | undefined) => {
if (!prev?.similarArtists?.length) return prev;
const hasMatching = prev.similarArtists.some((artist) =>
itemIdSet.has(artist.id),
);
if (!hasMatching) return prev;
return {
...prev,
similarArtists: prev.similarArtists.map((artist) =>
itemIdSet.has(artist.id)
? { ...artist, userFavorite: isFavorite }
: artist,
),
};
},
});
}
});
const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId);
const listQueries = queryClient.getQueriesData({
exact: false,
@@ -654,6 +670,10 @@ export const applyFavoriteOptimisticUpdatesDeferred = (
queryKeys.albumArtists.detail(variables.apiClientProps.serverId),
'album-artist-detail',
);
collectQueries(
queryKeys.albumArtists.info(variables.apiClientProps.serverId),
'album-artist-info',
);
collectQueries(
queryKeys.albumArtists.list(variables.apiClientProps.serverId),
'album-artist-list',
@@ -7,6 +7,7 @@ import {
Album,
AlbumArtist,
AlbumArtistDetailResponse,
AlbumArtistInfoResponse,
AlbumArtistListResponse,
AlbumDetailResponse,
AlbumListResponse,
@@ -235,29 +236,44 @@ export const applyRatingOptimisticUpdates = (
return { ...prev, userRating: rating };
}
if (prev.similarArtists && prev.similarArtists.length > 0) {
const hasMatchingSimilarArtist = prev.similarArtists.some(
(artist) => itemIdSet.has(artist.id),
);
if (hasMatchingSimilarArtist) {
return {
...prev,
similarArtists: prev.similarArtists.map((artist) =>
itemIdSet.has(artist.id)
? { ...artist, userRating: rating }
: artist,
),
};
}
}
return prev;
},
});
}
});
const infoQueryKey = queryKeys.albumArtists.info(variables.apiClientProps.serverId);
const infoQueries = queryClient.getQueriesData({
exact: false,
queryKey: infoQueryKey,
});
infoQueries.forEach(([queryKey, data]) => {
if (data) {
pendingUpdates.push({
previousData: data,
queryKey,
updater: (prev: AlbumArtistInfoResponse | null | undefined) => {
if (!prev?.similarArtists?.length) return prev;
const hasMatching = prev.similarArtists.some((artist) =>
itemIdSet.has(artist.id),
);
if (!hasMatching) return prev;
return {
...prev,
similarArtists: prev.similarArtists.map((artist) =>
itemIdSet.has(artist.id)
? { ...artist, userRating: rating }
: artist,
),
};
},
});
}
});
const listQueryKey = queryKeys.albumArtists.list(variables.apiClientProps.serverId);
const listQueries = queryClient.getQueriesData({
exact: false,
@@ -626,6 +642,10 @@ export const applyRatingOptimisticUpdatesDeferred = (
queryKeys.albumArtists.detail(variables.apiClientProps.serverId),
'album-artist-detail',
);
collectQueries(
queryKeys.albumArtists.info(variables.apiClientProps.serverId),
'album-artist-info',
);
collectQueries(
queryKeys.albumArtists.list(variables.apiClientProps.serverId),
'album-artist-list',
@@ -695,19 +715,6 @@ export const applyRatingOptimisticUpdatesDeferred = (
if (itemIdSet.has(prev.id)) {
return { ...prev, userRating: rating };
}
if (prev.similarArtists) {
const hasMatch = prev.similarArtists.some((a: any) =>
itemIdSet.has(a.id),
);
if (hasMatch) {
return {
...prev,
similarArtists: prev.similarArtists.map((a: any) =>
itemIdSet.has(a.id) ? { ...a, userRating: rating } : a,
),
};
}
}
return prev;
}
case 'album-artist-infinite-list':
@@ -732,6 +739,17 @@ export const applyRatingOptimisticUpdatesDeferred = (
}
return prev;
}
case 'album-artist-info': {
if (!prev?.similarArtists?.length) return prev;
const hasMatch = prev.similarArtists.some((a: any) => itemIdSet.has(a.id));
if (!hasMatch) return prev;
return {
...prev,
similarArtists: prev.similarArtists.map((a: any) =>
itemIdSet.has(a.id) ? { ...a, userRating: rating } : a,
),
};
}
case 'album-artist-list':
case 'album-list':
case 'artist-list':