mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +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 && (
|
{cq.isCalculated && (
|
||||||
<>
|
<>
|
||||||
<div className={styles.navigation}>
|
<div className={styles.navigation}>
|
||||||
<Group gap="xs" justify="space-between" w="100%">
|
{typeof title === 'string' ? (
|
||||||
<Group gap="xs">
|
<Group gap="xs" justify="space-between" w="100%">
|
||||||
{typeof title === 'string' ? (
|
<Group gap="xs">
|
||||||
<TextTitle fw={700} isNoSelect order={3}>
|
<TextTitle fw={700} isNoSelect order={3}>
|
||||||
{title}
|
{title}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
) : (
|
{enableRefresh && onRefresh && (
|
||||||
title
|
<ActionIcon
|
||||||
)}
|
icon="refresh"
|
||||||
{enableRefresh && onRefresh && (
|
iconProps={{ size: 'xs' }}
|
||||||
|
onClick={onRefresh}
|
||||||
|
size="xs"
|
||||||
|
tooltip={{ label: 'Refresh' }}
|
||||||
|
variant="transparent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" justify="end">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
icon="refresh"
|
disabled={isPrevDisabled}
|
||||||
iconProps={{ size: 'xs' }}
|
icon="arrowLeftS"
|
||||||
onClick={onRefresh}
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handlePrevPage}
|
||||||
size="xs"
|
size="xs"
|
||||||
tooltip={{ label: 'Refresh' }}
|
variant="subtle"
|
||||||
variant="transparent"
|
|
||||||
/>
|
/>
|
||||||
)}
|
<ActionIcon
|
||||||
|
disabled={isNextDisabled}
|
||||||
|
icon="arrowRightS"
|
||||||
|
iconProps={{ size: 'lg' }}
|
||||||
|
onClick={handleNextPage}
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs" justify="end">
|
) : (
|
||||||
<ActionIcon
|
<div className={styles.customTitleContainer}>
|
||||||
disabled={isPrevDisabled}
|
<div className={styles.customTitleContent}>{title}</div>
|
||||||
icon="arrowLeftS"
|
<Group gap="xs" justify="end">
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handlePrevPage}
|
disabled={isPrevDisabled}
|
||||||
size="xs"
|
icon="arrowLeftS"
|
||||||
variant="subtle"
|
iconProps={{ size: 'lg' }}
|
||||||
/>
|
onClick={handlePrevPage}
|
||||||
<ActionIcon
|
size="xs"
|
||||||
disabled={isNextDisabled}
|
variant="subtle"
|
||||||
icon="arrowRightS"
|
/>
|
||||||
iconProps={{ size: 'lg' }}
|
<ActionIcon
|
||||||
onClick={handleNextPage}
|
disabled={isNextDisabled}
|
||||||
size="xs"
|
icon="arrowRightS"
|
||||||
variant="subtle"
|
iconProps={{ size: 'lg' }}
|
||||||
/>
|
onClick={handleNextPage}
|
||||||
</Group>
|
size="xs"
|
||||||
</Group>
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
<AnimatePresence custom={currentPage} initial={false} mode="wait">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -14,6 +14,19 @@
|
|||||||
justify-content: space-between;
|
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 {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
grid-template-columns: repeat(var(--cards-to-show, 2), minmax(0, 1fr));
|
||||||
|
|||||||
@@ -30,11 +30,22 @@
|
|||||||
.album-section-divider-container {
|
.album-section-divider-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: var(--theme-spacing-md);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.album-section-divider {
|
.album-section-divider {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
background: var(--theme-colors-border);
|
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 { ItemControls } from '/@/renderer/components/item-list/types';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-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 { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import {
|
import {
|
||||||
@@ -51,9 +52,12 @@ import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value';
|
|||||||
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
import { useHotkeys } from '/@/shared/hooks/use-hotkeys';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
|
AlbumArtist,
|
||||||
AlbumArtistDetailResponse,
|
AlbumArtistDetailResponse,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
|
RelatedArtist,
|
||||||
|
ServerType,
|
||||||
Song,
|
Song,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
@@ -237,13 +241,14 @@ const AlbumArtistMetadataTopSongs = ({
|
|||||||
if (!tableConfig || columns.length === 0) {
|
if (!tableConfig || columns.length === 0) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<div className={styles.albumSectionTitle}>
|
||||||
<Group align="flex-end" wrap="nowrap">
|
<TextTitle fw={700} order={3}>
|
||||||
<TextTitle fw={700} order={3}>
|
{t('page.albumArtistDetail.topSongs', {
|
||||||
{t('page.albumArtistDetail.topSongs', {
|
postProcess: 'sentenceCase',
|
||||||
postProcess: 'sentenceCase',
|
})}
|
||||||
})}
|
</TextTitle>
|
||||||
</TextTitle>
|
<div className={styles.albumSectionDividerContainer}>
|
||||||
|
<div className={styles.albumSectionDivider} />
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
size="compact-md"
|
size="compact-md"
|
||||||
@@ -257,8 +262,8 @@ const AlbumArtistMetadataTopSongs = ({
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</div>
|
||||||
</Group>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -268,13 +273,14 @@ const AlbumArtistMetadataTopSongs = ({
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group justify="space-between" wrap="nowrap">
|
<div className={styles.albumSectionTitle}>
|
||||||
<Group align="flex-end" wrap="nowrap">
|
<TextTitle fw={700} order={3}>
|
||||||
<TextTitle fw={700} order={3}>
|
{t('page.albumArtistDetail.topSongs', {
|
||||||
{t('page.albumArtistDetail.topSongs', {
|
postProcess: 'sentenceCase',
|
||||||
postProcess: 'sentenceCase',
|
})}
|
||||||
})}
|
</TextTitle>
|
||||||
</TextTitle>
|
<div className={styles.albumSectionDividerContainer}>
|
||||||
|
<div className={styles.albumSectionDivider} />
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
size="compact-md"
|
size="compact-md"
|
||||||
@@ -288,8 +294,8 @@ const AlbumArtistMetadataTopSongs = ({
|
|||||||
postProcess: 'sentenceCase',
|
postProcess: 'sentenceCase',
|
||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</div>
|
||||||
</Group>
|
</div>
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
<TextInput
|
<TextInput
|
||||||
flex={1}
|
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 = () => {
|
export const AlbumArtistDetailContent = () => {
|
||||||
const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
const { artistItems, externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||||
const { albumArtistId, artistId } = useParams() as {
|
const { albumArtistId, artistId } = useParams() as {
|
||||||
@@ -493,7 +572,7 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
artistDiscographyLink={artistDiscographyLink}
|
artistDiscographyLink={artistDiscographyLink}
|
||||||
artistSongsLink={artistSongsLink}
|
artistSongsLink={artistSongsLink}
|
||||||
/>
|
/>
|
||||||
<Grid gutter="xl">
|
<Grid gutter="2xl">
|
||||||
{showGenres && (
|
{showGenres && (
|
||||||
<Grid.Col order={genresOrder} span={12}>
|
<Grid.Col order={genresOrder} span={12}>
|
||||||
<AlbumArtistMetadataGenres genres={detailQuery.data?.genres} />
|
<AlbumArtistMetadataGenres genres={detailQuery.data?.genres} />
|
||||||
@@ -521,6 +600,14 @@ export const AlbumArtistDetailContent = () => {
|
|||||||
<Grid.Col order={itemOrder.recentAlbums} span={12}>
|
<Grid.Col order={itemOrder.recentAlbums} span={12}>
|
||||||
<ArtistAlbums />
|
<ArtistAlbums />
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
{enabledItem.similarArtists && (
|
||||||
|
<Grid.Col order={itemOrder.similarArtists} span={12}>
|
||||||
|
<AlbumArtistMetadataSimilarArtists
|
||||||
|
detailQuery={detailQuery}
|
||||||
|
routeId={routeId}
|
||||||
|
/>
|
||||||
|
</Grid.Col>
|
||||||
|
)}
|
||||||
{enabledItem.topSongs && (
|
{enabledItem.topSongs && (
|
||||||
<Grid.Col order={itemOrder.topSongs} span={12}>
|
<Grid.Col order={itemOrder.topSongs} span={12}>
|
||||||
<AlbumArtistMetadataTopSongs
|
<AlbumArtistMetadataTopSongs
|
||||||
@@ -765,16 +852,17 @@ const ArtistAlbums = () => {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<div className={styles.albumSectionContainer} ref={cq.ref}>
|
<div className={styles.albumSectionContainer} ref={cq.ref}>
|
||||||
{releaseTypeEntries.map(({ albums, displayName, releaseType }) => (
|
{cq.isCalculated &&
|
||||||
<AlbumSection
|
releaseTypeEntries.map(({ albums, displayName, releaseType }) => (
|
||||||
albums={albums}
|
<AlbumSection
|
||||||
controls={controls}
|
albums={albums}
|
||||||
cq={cq}
|
controls={controls}
|
||||||
key={releaseType}
|
cq={cq}
|
||||||
rows={rows}
|
key={releaseType}
|
||||||
title={displayName}
|
rows={rows}
|
||||||
/>
|
title={displayName}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -578,7 +578,6 @@ export const SettingsStateSchema = ValidationSettingsStateSchema.merge(
|
|||||||
|
|
||||||
export enum ArtistItem {
|
export enum ArtistItem {
|
||||||
BIOGRAPHY = 'biography',
|
BIOGRAPHY = 'biography',
|
||||||
COMPILATIONS = 'compilations',
|
|
||||||
RECENT_ALBUMS = 'recentAlbums',
|
RECENT_ALBUMS = 'recentAlbums',
|
||||||
SIMILAR_ARTISTS = 'similarArtists',
|
SIMILAR_ARTISTS = 'similarArtists',
|
||||||
TOP_SONGS = 'topSongs',
|
TOP_SONGS = 'topSongs',
|
||||||
|
|||||||
Reference in New Issue
Block a user