mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
decouple AlbumArtistInfo from AlbumArtistDetail (#1809)
This commit is contained in:
@@ -200,6 +200,18 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
|
||||
},
|
||||
getAlbumArtistInfo(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const fn = apiController('getAlbumArtistInfo', server.type);
|
||||
return fn
|
||||
? fn(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }))
|
||||
: Promise.resolve(null);
|
||||
},
|
||||
getAlbumArtistList(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -258,34 +258,54 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
throw new Error('No userId found');
|
||||
}
|
||||
|
||||
const [res, similarArtistsRes] = await Promise.all([
|
||||
jfApiClient(apiClientProps).getAlbumArtistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
Fields: JF_FIELDS.ALBUM_ARTIST_DETAIL,
|
||||
},
|
||||
}),
|
||||
jfApiClient(apiClientProps).getSimilarArtistList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Limit: 10,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const res = await jfApiClient(apiClientProps).getAlbumArtistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
Fields: ['Genres', 'Overview', 'SortName'],
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200 || similarArtistsRes.status !== 200) {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get album artist detail');
|
||||
}
|
||||
|
||||
return jfNormalize.albumArtist(
|
||||
{ ...res.body, similarArtists: similarArtistsRes.body },
|
||||
apiClientProps.server,
|
||||
return jfNormalize.albumArtist(res.body, apiClientProps.server);
|
||||
},
|
||||
getAlbumArtistInfo: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const similarArtistsRes = await jfApiClient(apiClientProps).getSimilarArtistList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Limit: query.limit ?? 10,
|
||||
},
|
||||
});
|
||||
|
||||
if (similarArtistsRes.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = similarArtistsRes.body?.Items?.filter(
|
||||
(entry) => entry.Name !== 'Various Artists',
|
||||
);
|
||||
const similarArtists =
|
||||
items?.map((entry) => ({
|
||||
id: entry.Id,
|
||||
imageId: entry.ImageTags?.Primary ? entry.Id : null,
|
||||
imageUrl: null,
|
||||
name: entry.Name,
|
||||
userFavorite: entry.UserData?.IsFavorite || false,
|
||||
userRating: null,
|
||||
})) ?? null;
|
||||
|
||||
return {
|
||||
similarArtists,
|
||||
};
|
||||
},
|
||||
getAlbumArtistList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -190,19 +190,11 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const [res, artistInfoRes] = await Promise.all([
|
||||
ndApiClient(apiClientProps).getAlbumArtistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
}),
|
||||
ssApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
count: 10,
|
||||
id: query.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const res = await ndApiClient(apiClientProps).getAlbumArtistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get album artist detail');
|
||||
@@ -212,22 +204,42 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
throw new Error('Server is required');
|
||||
}
|
||||
|
||||
// Prefer images from getArtistInfo first (which should be proxied)
|
||||
// Prioritize large > medium > small
|
||||
return ndNormalize.albumArtist(
|
||||
{
|
||||
...res.body.data,
|
||||
...(artistInfoRes.status === 200 && {
|
||||
largeImageUrl:
|
||||
artistInfoRes.body.artistInfo.largeImageUrl ||
|
||||
artistInfoRes.body.artistInfo.mediumImageUrl ||
|
||||
artistInfoRes.body.artistInfo.smallImageUrl ||
|
||||
res.body.data.largeImageUrl,
|
||||
similarArtists: artistInfoRes.body.artistInfo.similarArtist,
|
||||
}),
|
||||
return ndNormalize.albumArtist(res.body.data, apiClientProps.server);
|
||||
},
|
||||
getAlbumArtistInfo: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
id: query.id,
|
||||
...(query.limit != null && { count: query.limit }),
|
||||
},
|
||||
apiClientProps.server,
|
||||
);
|
||||
});
|
||||
|
||||
if (artistInfoRes.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artistInfo = artistInfoRes.body.artistInfo;
|
||||
const imageUrl =
|
||||
artistInfo?.largeImageUrl ||
|
||||
artistInfo?.mediumImageUrl ||
|
||||
artistInfo?.smallImageUrl ||
|
||||
null;
|
||||
|
||||
return {
|
||||
biography: artistInfo?.biography || null,
|
||||
imageUrl,
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageUrl: artist?.artistImageUrl?.replace(/\?size=\d+/, '') ?? null,
|
||||
name: artist.name,
|
||||
userFavorite: Boolean(artist.starred) || false,
|
||||
userRating: artist.userRating ?? null,
|
||||
})) ?? null,
|
||||
};
|
||||
},
|
||||
getAlbumArtistList: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
AlbumArtistDetailQuery,
|
||||
AlbumArtistInfoQuery,
|
||||
AlbumArtistListQuery,
|
||||
AlbumDetailQuery,
|
||||
AlbumListQuery,
|
||||
@@ -93,6 +94,13 @@ export const queryKeys: Record<
|
||||
|
||||
return [serverId, 'albumArtists', 'infiniteList'] as const;
|
||||
},
|
||||
info: (serverId: string, query?: AlbumArtistInfoQuery) => {
|
||||
if (query) {
|
||||
return [serverId, 'albumArtists', 'info', query] as const;
|
||||
}
|
||||
|
||||
return [serverId, 'albumArtists', 'info'] as const;
|
||||
},
|
||||
list: (serverId: string, query?: AlbumArtistListQuery) => {
|
||||
const { filter, pagination } = splitPaginatedQuery(query);
|
||||
if (query && pagination) {
|
||||
|
||||
@@ -258,18 +258,11 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
getAlbumArtistDetail: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const [artistInfoRes, res] = await Promise.all([
|
||||
ssApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
}),
|
||||
ssApiClient(apiClientProps).getArtist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const res = await ssApiClient(apiClientProps).getArtist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error('Failed to get album artist detail');
|
||||
@@ -277,11 +270,6 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
const artist = res.body.artist;
|
||||
|
||||
let artistInfo;
|
||||
if (artistInfoRes.status === 200) {
|
||||
artistInfo = artistInfoRes.body.artistInfo;
|
||||
}
|
||||
|
||||
return {
|
||||
...ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
albums: artist.album?.map((album) =>
|
||||
@@ -292,10 +280,36 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
args.context?.pathReplaceWith,
|
||||
),
|
||||
),
|
||||
similarArtists: null,
|
||||
};
|
||||
},
|
||||
getAlbumArtistInfo: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
const artistInfoRes = await ssApiClient(apiClientProps).getArtistInfo({
|
||||
query: {
|
||||
id: query.id,
|
||||
...(query.limit != null && { count: query.limit }),
|
||||
},
|
||||
});
|
||||
|
||||
if (artistInfoRes.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artistInfo = artistInfoRes.body.artistInfo;
|
||||
|
||||
return {
|
||||
biography: artistInfo?.biography || null,
|
||||
similarArtists:
|
||||
artistInfo?.similarArtist?.map((artist) =>
|
||||
ssNormalize.albumArtist(artist, apiClientProps.server),
|
||||
) || null,
|
||||
artistInfo?.similarArtist?.map((artist) => ({
|
||||
id: artist.id,
|
||||
imageId: null,
|
||||
imageUrl: null,
|
||||
name: artist.name,
|
||||
userFavorite: Boolean(artist.starred) || false,
|
||||
userRating: artist.userRating ?? null,
|
||||
})) ?? null,
|
||||
};
|
||||
},
|
||||
getAlbumArtistList: async (args) => {
|
||||
|
||||
@@ -365,6 +365,7 @@ interface GridCarouselSkeletonProps {
|
||||
containerQuery?: ReturnType<typeof useGridCarouselContainerQuery>;
|
||||
enableRefresh?: boolean;
|
||||
placeholderItemType: LibraryItem;
|
||||
placeholderRound?: boolean;
|
||||
placeholderRows: DataRow[];
|
||||
rowCount?: number;
|
||||
title?: ReactNode | string;
|
||||
@@ -375,12 +376,15 @@ const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => {
|
||||
containerQuery: providedContainerQuery,
|
||||
enableRefresh = false,
|
||||
placeholderItemType,
|
||||
placeholderRound = false,
|
||||
placeholderRows,
|
||||
rowCount = 1,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const { ...cq } = providedContainerQuery;
|
||||
const defaultContainerQuery = useGridCarouselContainerQuery();
|
||||
const containerQuery = providedContainerQuery ?? defaultContainerQuery;
|
||||
const { ...cq } = containerQuery;
|
||||
|
||||
const cardsToShow = cq.isCalculated
|
||||
? getCardsToShow({
|
||||
@@ -399,6 +403,7 @@ const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => {
|
||||
content: (
|
||||
<MemoizedItemCard
|
||||
data={undefined}
|
||||
isRound={placeholderRound}
|
||||
itemType={placeholderItemType}
|
||||
rows={placeholderRows}
|
||||
type="poster"
|
||||
@@ -406,12 +411,12 @@ const GridCarouselSkeleton = (props: GridCarouselSkeletonProps) => {
|
||||
),
|
||||
id: `skeleton-${index}`,
|
||||
}));
|
||||
}, [cardsToShow, rowCount, placeholderItemType, placeholderRows]);
|
||||
}, [cardsToShow, placeholderRound, rowCount, placeholderItemType, placeholderRows]);
|
||||
|
||||
return (
|
||||
<GridCarousel
|
||||
cards={placeholderCards}
|
||||
containerQuery={providedContainerQuery}
|
||||
containerQuery={containerQuery}
|
||||
enableRefresh={enableRefresh}
|
||||
hasNextPage={false}
|
||||
isFetchingNextPage={false}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -820,6 +820,16 @@ export type AlbumArtistDetailQuery = { id: string };
|
||||
|
||||
export type AlbumArtistDetailResponse = AlbumArtist | null;
|
||||
|
||||
export type AlbumArtistInfoArgs = BaseEndpointArgs & { query: AlbumArtistInfoQuery };
|
||||
|
||||
export type AlbumArtistInfoQuery = { id: string; limit?: number };
|
||||
|
||||
export type AlbumArtistInfoResponse = {
|
||||
biography?: null | string;
|
||||
imageUrl?: null | string;
|
||||
similarArtists: null | RelatedArtist[];
|
||||
};
|
||||
|
||||
export type ArtistListArgs = BaseEndpointArgs & { query: ArtistListQuery };
|
||||
|
||||
export type ArtistListCountArgs = BaseEndpointArgs & { query: ListCountQuery<ArtistListQuery> };
|
||||
@@ -1373,6 +1383,7 @@ export type ControllerEndpoint = {
|
||||
) => Promise<DeleteInternetRadioStationResponse>;
|
||||
deletePlaylist: (args: DeletePlaylistArgs) => Promise<DeletePlaylistResponse>;
|
||||
getAlbumArtistDetail: (args: AlbumArtistDetailArgs) => Promise<AlbumArtistDetailResponse>;
|
||||
getAlbumArtistInfo?: (args: AlbumArtistInfoArgs) => Promise<AlbumArtistInfoResponse | null>;
|
||||
getAlbumArtistList: (args: AlbumArtistListArgs) => Promise<AlbumArtistListResponse>;
|
||||
getAlbumArtistListCount: (args: AlbumArtistListCountArgs) => Promise<number>;
|
||||
getAlbumDetail: (args: AlbumDetailArgs) => Promise<AlbumDetailResponse>;
|
||||
@@ -1491,6 +1502,9 @@ export type InternalControllerEndpoint = {
|
||||
getAlbumArtistDetail: (
|
||||
args: ReplaceApiClientProps<AlbumArtistDetailArgs>,
|
||||
) => Promise<AlbumArtistDetailResponse>;
|
||||
getAlbumArtistInfo?: (
|
||||
args: ReplaceApiClientProps<AlbumArtistInfoArgs>,
|
||||
) => Promise<AlbumArtistInfoResponse | null>;
|
||||
getAlbumArtistList: (
|
||||
args: ReplaceApiClientProps<AlbumArtistListArgs>,
|
||||
) => Promise<AlbumArtistListResponse>;
|
||||
|
||||
Reference in New Issue
Block a user