mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
re-add similar artists carousel to artist page
This commit is contained in:
@@ -157,45 +157,65 @@ function BaseGridCarousel(props: GridCarouselProps) {
|
||||
{cq.isCalculated && (
|
||||
<>
|
||||
<div className={styles.navigation}>
|
||||
<Group gap="xs" justify="space-between" w="100%">
|
||||
<Group gap="xs">
|
||||
{typeof title === 'string' ? (
|
||||
{typeof title === 'string' ? (
|
||||
<Group gap="xs" justify="space-between" w="100%">
|
||||
<Group gap="xs">
|
||||
<TextTitle fw={700} isNoSelect order={3}>
|
||||
{title}
|
||||
</TextTitle>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
{enableRefresh && onRefresh && (
|
||||
{enableRefresh && onRefresh && (
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'xs' }}
|
||||
onClick={onRefresh}
|
||||
size="xs"
|
||||
tooltip={{ label: 'Refresh' }}
|
||||
variant="transparent"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
iconProps={{ size: 'xs' }}
|
||||
onClick={onRefresh}
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
tooltip={{ label: 'Refresh' }}
|
||||
variant="transparent"
|
||||
variant="subtle"
|
||||
/>
|
||||
)}
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
) : (
|
||||
<div className={styles.customTitleContainer}>
|
||||
<div className={styles.customTitleContent}>{title}</div>
|
||||
<Group gap="xs" justify="end">
|
||||
<ActionIcon
|
||||
disabled={isPrevDisabled}
|
||||
icon="arrowLeftS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handlePrevPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
<ActionIcon
|
||||
disabled={isNextDisabled}
|
||||
icon="arrowRightS"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleNextPage}
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -14,6 +14,19 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.custom-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.custom-title-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||
|
||||
@@ -30,11 +30,22 @@
|
||||
.album-section-divider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--theme-spacing-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.album-section-divider {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.similar-artists-title {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--theme-spacing-md);
|
||||
align-items: center;
|
||||
margin-bottom: var(--theme-spacing-md);
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table
|
||||
import { ItemControls } from '/@/renderer/components/item-list/types';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||
import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import {
|
||||
@@ -51,9 +52,12 @@ import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||
import {
|
||||
Album,
|
||||
AlbumArtist,
|
||||
AlbumArtistDetailResponse,
|
||||
AlbumListSort,
|
||||
LibraryItem,
|
||||
RelatedArtist,
|
||||
ServerType,
|
||||
Song,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
@@ -237,13 +241,14 @@ const AlbumArtistMetadataTopSongs = ({
|
||||
if (!tableConfig || columns.length === 0) {
|
||||
return (
|
||||
<section>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group align="flex-end" wrap="nowrap">
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<div className={styles.albumSectionTitle}>
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<div className={styles.albumSectionDividerContainer}>
|
||||
<div className={styles.albumSectionDivider} />
|
||||
<Button
|
||||
component={Link}
|
||||
size="compact-md"
|
||||
@@ -257,8 +262,8 @@ const AlbumArtistMetadataTopSongs = ({
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -268,13 +273,14 @@ const AlbumArtistMetadataTopSongs = ({
|
||||
return (
|
||||
<section>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group align="flex-end" wrap="nowrap">
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<div className={styles.albumSectionTitle}>
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<div className={styles.albumSectionDividerContainer}>
|
||||
<div className={styles.albumSectionDivider} />
|
||||
<Button
|
||||
component={Link}
|
||||
size="compact-md"
|
||||
@@ -288,8 +294,8 @@ const AlbumArtistMetadataTopSongs = ({
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</div>
|
||||
</div>
|
||||
<Group gap="sm" w="100%">
|
||||
<TextInput
|
||||
flex={1}
|
||||
@@ -426,6 +432,79 @@ const AlbumArtistMetadataExternalLinks = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface AlbumArtistMetadataSimilarArtistsProps {
|
||||
detailQuery: ReturnType<typeof useSuspenseQuery<AlbumArtistDetailResponse>>;
|
||||
routeId: string;
|
||||
}
|
||||
|
||||
const AlbumArtistMetadataSimilarArtists = ({
|
||||
detailQuery,
|
||||
routeId,
|
||||
}: AlbumArtistMetadataSimilarArtistsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const serverId = useCurrentServerId();
|
||||
|
||||
const similarArtists = useMemo(() => {
|
||||
const relatedArtists = detailQuery.data?.similarArtists;
|
||||
if (!relatedArtists || relatedArtists.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return relatedArtists.map(
|
||||
(relatedArtist: RelatedArtist): AlbumArtist => ({
|
||||
_itemType: LibraryItem.ALBUM_ARTIST,
|
||||
_serverId: serverId || '',
|
||||
_serverType: (server?.type as ServerType) || ServerType.JELLYFIN,
|
||||
albumCount: null,
|
||||
biography: null,
|
||||
duration: null,
|
||||
genres: [],
|
||||
id: relatedArtist.id,
|
||||
imageId: relatedArtist.imageId,
|
||||
imageUrl: relatedArtist.imageUrl,
|
||||
lastPlayedAt: null,
|
||||
mbz: null,
|
||||
name: relatedArtist.name,
|
||||
playCount: null,
|
||||
similarArtists: null,
|
||||
songCount: null,
|
||||
userFavorite: false,
|
||||
userRating: null,
|
||||
}),
|
||||
);
|
||||
}, [detailQuery.data?.similarArtists, server?.type, serverId]);
|
||||
|
||||
const carouselTitle = useMemo(
|
||||
() => (
|
||||
<div className={styles.similarArtistsTitle}>
|
||||
<TextTitle fw={700} order={3}>
|
||||
{t('page.albumArtistDetail.relatedArtists', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<div className={styles.albumSectionDividerContainer}>
|
||||
<div className={styles.albumSectionDivider} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[t],
|
||||
);
|
||||
|
||||
if (similarArtists.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AlbumArtistGridCarousel
|
||||
data={similarArtists}
|
||||
excludeIds={[routeId]}
|
||||
rowCount={1}
|
||||
title={carouselTitle}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlbumArtistDetailContent = () => {
|
||||
const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||
const { albumArtistId, artistId } = useParams() as {
|
||||
@@ -493,7 +572,7 @@ export const AlbumArtistDetailContent = () => {
|
||||
artistDiscographyLink={artistDiscographyLink}
|
||||
artistSongsLink={artistSongsLink}
|
||||
/>
|
||||
<Grid gutter="xl">
|
||||
<Grid gutter="2xl">
|
||||
{showGenres && (
|
||||
<Grid.Col order={genresOrder} span={12}>
|
||||
<AlbumArtistMetadataGenres genres={detailQuery.data?.genres} />
|
||||
@@ -521,6 +600,14 @@ export const AlbumArtistDetailContent = () => {
|
||||
<Grid.Col order={itemOrder.recentAlbums} span={12}>
|
||||
<ArtistAlbums />
|
||||
</Grid.Col>
|
||||
{enabledItem.similarArtists && (
|
||||
<Grid.Col order={itemOrder.similarArtists} span={12}>
|
||||
<AlbumArtistMetadataSimilarArtists
|
||||
detailQuery={detailQuery}
|
||||
routeId={routeId}
|
||||
/>
|
||||
</Grid.Col>
|
||||
)}
|
||||
{enabledItem.topSongs && (
|
||||
<Grid.Col order={itemOrder.topSongs} span={12}>
|
||||
<AlbumArtistMetadataTopSongs
|
||||
@@ -765,16 +852,17 @@ const ArtistAlbums = () => {
|
||||
/>
|
||||
</Group>
|
||||
<div className={styles.albumSectionContainer} ref={cq.ref}>
|
||||
{releaseTypeEntries.map(({ albums, displayName, releaseType }) => (
|
||||
<AlbumSection
|
||||
albums={albums}
|
||||
controls={controls}
|
||||
cq={cq}
|
||||
key={releaseType}
|
||||
rows={rows}
|
||||
title={displayName}
|
||||
/>
|
||||
))}
|
||||
{cq.isCalculated &&
|
||||
releaseTypeEntries.map(({ albums, displayName, releaseType }) => (
|
||||
<AlbumSection
|
||||
albums={albums}
|
||||
controls={controls}
|
||||
cq={cq}
|
||||
key={releaseType}
|
||||
rows={rows}
|
||||
title={displayName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -578,7 +578,6 @@ export const SettingsStateSchema = ValidationSettingsStateSchema.merge(
|
||||
|
||||
export enum ArtistItem {
|
||||
BIOGRAPHY = 'biography',
|
||||
COMPILATIONS = 'compilations',
|
||||
RECENT_ALBUMS = 'recentAlbums',
|
||||
SIMILAR_ARTISTS = 'similarArtists',
|
||||
TOP_SONGS = 'topSongs',
|
||||
|
||||
Reference in New Issue
Block a user