mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
add more metadata to album header / side
This commit is contained in:
@@ -102,6 +102,7 @@
|
||||
"minimize": "minimize",
|
||||
"modified": "modified",
|
||||
"mbid": "MusicBrainz ID",
|
||||
"mood": "mood",
|
||||
"name": "name",
|
||||
"no": "no",
|
||||
"none": "none",
|
||||
|
||||
@@ -27,7 +27,11 @@
|
||||
.metadata-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
grid-area: metadata;
|
||||
gap: var(--theme-spacing-xl);
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.songs-column {
|
||||
@@ -53,6 +57,47 @@
|
||||
}
|
||||
}
|
||||
|
||||
.external-links-group {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metadata-pill-group {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pill-group-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
& > div {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: $mantine-breakpoint-sm) {
|
||||
.metadata-column {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.external-links-group {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.metadata-pill-group {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.pill-group-wrapper {
|
||||
justify-content: flex-start;
|
||||
|
||||
& > div {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (min-width: $mantine-breakpoint-lg) {
|
||||
.content-layout {
|
||||
grid-template-areas: 'songs metadata';
|
||||
|
||||
@@ -22,18 +22,12 @@ import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, usePlayerSong } from '/@/renderer/store';
|
||||
import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import {
|
||||
formatDateAbsoluteUTC,
|
||||
formatDurationString,
|
||||
formatSizeString,
|
||||
titleCase,
|
||||
} from '/@/renderer/utils';
|
||||
import { titleCase } from '/@/renderer/utils';
|
||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
|
||||
import { sortSongList } from '/@/shared/api/utils';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Pill, PillLink } from '/@/shared/components/pill/pill';
|
||||
@@ -55,89 +49,85 @@ import {
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
|
||||
|
||||
const MetadataPillGroup = ({
|
||||
items,
|
||||
title,
|
||||
}: {
|
||||
items: undefined | { id: string; value: ReactNode | string | undefined }[];
|
||||
title: string;
|
||||
}) => {
|
||||
if (!items || items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Stack align="center" className={styles.metadataPillGroup} gap="xs">
|
||||
<Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||
{title}
|
||||
</Text>
|
||||
<div className={styles['pill-group-wrapper']}>
|
||||
<Pill.Group>
|
||||
{items.map((tag, index) => (
|
||||
<Pill key={`item-${tag.id}-${index}`} size="md">
|
||||
{tag.value}
|
||||
</Pill>
|
||||
))}
|
||||
</Pill.Group>
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface AlbumMetadataTagsProps {
|
||||
album: Album | undefined;
|
||||
}
|
||||
|
||||
const MOOD_TAG = 'mood';
|
||||
const RELEASE_COUNTRY_TAG = 'releasecountry';
|
||||
const RELEASE_STATUS_TAG = 'releasestatus';
|
||||
|
||||
const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const metadataItems = useMemo(() => {
|
||||
const defaultTagItems = useMemo(() => {
|
||||
if (!album) return [];
|
||||
|
||||
const originalDifferentFromRelease =
|
||||
album.originalDate && album.originalDate !== album.releaseDate;
|
||||
|
||||
const releasePrefix = originalDifferentFromRelease
|
||||
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||
: '♫';
|
||||
|
||||
const releaseTypes = normalizeReleaseTypes(album.releaseTypes ?? [], t).map((type) => ({
|
||||
id: type,
|
||||
value: titleCase(type),
|
||||
}));
|
||||
|
||||
const releaseCountries =
|
||||
album.tags?.[RELEASE_COUNTRY_TAG]?.map((country) => ({
|
||||
id: country,
|
||||
value: country,
|
||||
})) || [];
|
||||
|
||||
const releaseStatuses =
|
||||
album.tags?.[RELEASE_STATUS_TAG]?.map((status) => ({
|
||||
id: status,
|
||||
value: status,
|
||||
})) || [];
|
||||
|
||||
const recordLabels =
|
||||
album.recordLabels?.map((label) => ({
|
||||
id: label,
|
||||
value: label,
|
||||
})) || [];
|
||||
|
||||
console.log('album', album);
|
||||
|
||||
const items: Array<{ id: string; value: ReactNode | string | undefined }> = [];
|
||||
|
||||
if (originalDifferentFromRelease && album.originalDate) {
|
||||
items.push({
|
||||
id: 'originalDate',
|
||||
value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(...releaseTypes);
|
||||
|
||||
items.push(
|
||||
...releaseTypes,
|
||||
{
|
||||
id: 'isCompilation',
|
||||
value: album?.isCompilation
|
||||
? t('filter.isCompilation', { postProcess: 'sentenceCase' })
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'releaseDate',
|
||||
value: album.releaseDate
|
||||
? `${releasePrefix} ${formatDateAbsoluteUTC(album.releaseDate)}`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'releaseYear',
|
||||
value: album.releaseDate
|
||||
? undefined
|
||||
: album.releaseYear
|
||||
? album.releaseYear.toString()
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
value: album.songCount
|
||||
? t('entity.trackWithCount', {
|
||||
count: album.songCount,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
value: album.duration ? (
|
||||
<Flex align="center" gap="xs">
|
||||
<Icon icon="duration" size="md" /> {formatDurationString(album.duration)}
|
||||
</Flex>
|
||||
) : undefined,
|
||||
},
|
||||
{
|
||||
id: 'size',
|
||||
value: album.size ? formatSizeString(album.size) : undefined,
|
||||
},
|
||||
{
|
||||
id: 'playCount',
|
||||
value:
|
||||
typeof album.playCount === 'number'
|
||||
? t('entity.play', {
|
||||
count: album.playCount,
|
||||
})
|
||||
: undefined,
|
||||
},
|
||||
...releaseCountries,
|
||||
...releaseStatuses,
|
||||
...recordLabels,
|
||||
{
|
||||
id: 'explicitStatus',
|
||||
value:
|
||||
@@ -147,31 +137,32 @@ const AlbumMetadataTags = ({ album }: AlbumMetadataTagsProps) => {
|
||||
? t('common.clean', { postProcess: 'sentenceCase' })
|
||||
: undefined,
|
||||
},
|
||||
|
||||
{
|
||||
id: 'version',
|
||||
value: album.version || undefined,
|
||||
},
|
||||
);
|
||||
|
||||
return items.filter((item) => item.value);
|
||||
}, [album, t]);
|
||||
|
||||
if (metadataItems.length === 0) return null;
|
||||
const moodTagItems = useMemo(() => {
|
||||
if (!album) return [];
|
||||
|
||||
return album.tags?.[MOOD_TAG]?.map((tag) => ({
|
||||
id: tag,
|
||||
value: tag,
|
||||
}));
|
||||
}, [album]);
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||
{t('common.tags', { postProcess: 'sentenceCase' })}
|
||||
</Text>
|
||||
<Pill.Group>
|
||||
{metadataItems.map((item, index) => (
|
||||
<Pill key={`item-${item.id}-${index}`} size="md">
|
||||
{item.value}
|
||||
</Pill>
|
||||
))}
|
||||
</Pill.Group>
|
||||
</Stack>
|
||||
<>
|
||||
<MetadataPillGroup
|
||||
items={defaultTagItems}
|
||||
title={t('common.tags', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
|
||||
<MetadataPillGroup
|
||||
items={moodTagItems}
|
||||
title={t('common.mood', { postProcess: 'sentenceCase' })}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -208,38 +199,38 @@ const AlbumMetadataGenres = ({ genres }: AlbumMetadataGenresProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface AlbumMetadataArtistsProps {
|
||||
artists?: Array<{ id: string; name: string }>;
|
||||
}
|
||||
// interface AlbumMetadataArtistsProps {
|
||||
// artists?: Array<{ id: string; name: string }>;
|
||||
// }
|
||||
|
||||
const AlbumMetadataArtists = ({ artists }: AlbumMetadataArtistsProps) => {
|
||||
const { t } = useTranslation();
|
||||
// const AlbumMetadataArtists = ({ artists }: AlbumMetadataArtistsProps) => {
|
||||
// const { t } = useTranslation();
|
||||
|
||||
if (!artists || artists.length === 0) return null;
|
||||
// if (!artists || artists.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||
{t('entity.albumArtist', {
|
||||
count: artists.length,
|
||||
})}
|
||||
</Text>
|
||||
<Pill.Group>
|
||||
{artists.map((artist) => (
|
||||
<PillLink
|
||||
key={`artist-${artist.id}`}
|
||||
size="md"
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</PillLink>
|
||||
))}
|
||||
</Pill.Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
// return (
|
||||
// <Stack gap="xs">
|
||||
// <Text fw={600} isNoSelect size="sm" tt="uppercase">
|
||||
// {t('entity.albumArtist', {
|
||||
// count: artists.length,
|
||||
// })}
|
||||
// </Text>
|
||||
// <Pill.Group>
|
||||
// {artists.map((artist) => (
|
||||
// <PillLink
|
||||
// key={`artist-${artist.id}`}
|
||||
// size="md"
|
||||
// to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
// albumArtistId: artist.id,
|
||||
// })}
|
||||
// >
|
||||
// {artist.name}
|
||||
// </PillLink>
|
||||
// ))}
|
||||
// </Pill.Group>
|
||||
// </Stack>
|
||||
// );
|
||||
// };
|
||||
|
||||
interface AlbumMetadataExternalLinksProps {
|
||||
albumArtist?: string;
|
||||
@@ -269,7 +260,7 @@ const AlbumMetadataExternalLinks = ({
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Text>
|
||||
<Group gap="sm">
|
||||
<Group className={styles.externalLinksGroup} gap="sm">
|
||||
{lastFM && (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
@@ -382,19 +373,17 @@ export const AlbumDetailContent = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.metadataColumn}>
|
||||
<Stack gap="2xl">
|
||||
<AlbumMetadataArtists artists={detailQuery?.data?.albumArtists} />
|
||||
<AlbumMetadataGenres genres={detailQuery?.data?.genres} />
|
||||
<AlbumMetadataTags album={detailQuery?.data} />
|
||||
<AlbumMetadataExternalLinks
|
||||
albumArtist={detailQuery?.data?.albumArtist}
|
||||
albumName={detailQuery?.data?.name}
|
||||
externalLinks={externalLinks}
|
||||
lastFM={lastFM}
|
||||
mbzId={mbzId || undefined}
|
||||
musicBrainz={musicBrainz}
|
||||
/>
|
||||
</Stack>
|
||||
{/* <AlbumMetadataArtists artists={detailQuery?.data?.albumArtists} /> */}
|
||||
<AlbumMetadataGenres genres={detailQuery?.data?.genres} />
|
||||
<AlbumMetadataTags album={detailQuery?.data} />
|
||||
<AlbumMetadataExternalLinks
|
||||
albumArtist={detailQuery?.data?.albumArtist}
|
||||
albumName={detailQuery?.data?.name}
|
||||
externalLinks={externalLinks}
|
||||
lastFM={lastFM}
|
||||
mbzId={mbzId || undefined}
|
||||
musicBrainz={musicBrainz}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{labels && (
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { forwardRef } from 'react';
|
||||
import { generatePath, Link, useParams } from 'react-router';
|
||||
import { forwardRef, Fragment, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useParams } from 'react-router';
|
||||
|
||||
import styles from './album-detail-header.module.css';
|
||||
|
||||
import { useItemImageUrl } from '/@/renderer/components/item-image/item-image';
|
||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||
import { JoinedAlbumArtist } from '/@/renderer/features/albums/components/joined-album-artist';
|
||||
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import {
|
||||
@@ -15,7 +17,10 @@ import {
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, useGeneralSettings } from '/@/renderer/store';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
|
||||
import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
||||
@@ -23,6 +28,7 @@ import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const { showRatings } = useGeneralSettings();
|
||||
const detailQuery = useQuery(
|
||||
@@ -82,8 +88,8 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
});
|
||||
};
|
||||
|
||||
const firstAlbumArtist = detailQuery?.data?.albumArtists?.[0];
|
||||
const releaseYear = detailQuery?.data?.releaseYear;
|
||||
const releaseDate = detailQuery?.data?.releaseDate;
|
||||
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: detailQuery?.data?.imageId || undefined,
|
||||
@@ -91,43 +97,128 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
type: 'header',
|
||||
});
|
||||
|
||||
const releaseType = detailQuery?.data?.releaseType || undefined;
|
||||
const metadataItems = useMemo(() => {
|
||||
const items: Array<{ id: string; value: React.ReactNode | string | undefined }> = [];
|
||||
|
||||
const album = detailQuery?.data;
|
||||
|
||||
if (!album) return [];
|
||||
|
||||
const originalDifferentFromRelease =
|
||||
album?.originalDate && album?.originalDate !== album?.releaseDate;
|
||||
|
||||
const playCount = album?.playCount;
|
||||
|
||||
const releasePrefix = originalDifferentFromRelease
|
||||
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||
: '♫';
|
||||
|
||||
if (originalDifferentFromRelease && album.originalDate) {
|
||||
items.push({
|
||||
id: 'originalDate',
|
||||
value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`,
|
||||
});
|
||||
}
|
||||
|
||||
items.push(
|
||||
...[
|
||||
{
|
||||
id: 'releaseDate',
|
||||
value: releaseDate
|
||||
? `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`
|
||||
: releaseYear,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
||||
},
|
||||
{
|
||||
id: 'duration',
|
||||
value: formatDurationString(detailQuery?.data?.duration || 0),
|
||||
},
|
||||
{
|
||||
id: 'explicitStatus',
|
||||
value: detailQuery?.data?.explicitStatus,
|
||||
},
|
||||
{
|
||||
id: 'playCount',
|
||||
value: playCount ? t('entity.play', { count: playCount }) : undefined,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
return items.filter((item) => !!item.value);
|
||||
}, [detailQuery?.data, releaseDate, releaseYear, t]);
|
||||
|
||||
const headerItem = useMemo(() => {
|
||||
const album = detailQuery?.data;
|
||||
|
||||
if (!album) return null;
|
||||
|
||||
const releaseTypes = album.releaseType
|
||||
? normalizeReleaseTypes([album.releaseType], t)
|
||||
: null;
|
||||
|
||||
const releaseTypeText = releaseTypes?.length ? releaseTypes[0] : null;
|
||||
|
||||
if (releaseTypeText) {
|
||||
return (
|
||||
<Group gap="sm">
|
||||
<Text
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
size="md"
|
||||
to={AppRoute.LIBRARY_ALBUMS}
|
||||
tt="uppercase"
|
||||
>
|
||||
{releaseTypeText}
|
||||
</Text>
|
||||
{album.version && (
|
||||
<>
|
||||
<Text fw={600} isMuted>
|
||||
<Separator />
|
||||
</Text>
|
||||
<Text>{album.version}</Text>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [detailQuery?.data, t]);
|
||||
|
||||
return (
|
||||
<Stack ref={ref}>
|
||||
<LibraryHeader
|
||||
imageUrl={imageUrl}
|
||||
item={{ releaseType, route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
|
||||
item={{
|
||||
children: headerItem,
|
||||
route: AppRoute.LIBRARY_ALBUMS,
|
||||
type: LibraryItem.ALBUM,
|
||||
}}
|
||||
title={detailQuery?.data?.name || ''}
|
||||
>
|
||||
<Stack gap="md" w="100%">
|
||||
{(firstAlbumArtist || releaseYear) && (
|
||||
<Group className={styles.metadataGroup}>
|
||||
{firstAlbumArtist && (
|
||||
<Text
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
isNoSelect
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: firstAlbumArtist.id,
|
||||
})}
|
||||
>
|
||||
{firstAlbumArtist.name}
|
||||
</Text>
|
||||
)}
|
||||
{firstAlbumArtist && releaseYear && (
|
||||
<Text fw={600} isNoSelect>
|
||||
•
|
||||
</Text>
|
||||
)}
|
||||
{releaseYear && (
|
||||
<Text fw={600} isMuted isNoSelect>
|
||||
{releaseYear}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
<Group className={styles.metadataGroup} gap="xs">
|
||||
{metadataItems.map((item, index) => (
|
||||
<Fragment key={item.id}>
|
||||
{index > 0 && (
|
||||
<Text fw={400} isMuted isNoSelect>
|
||||
•
|
||||
</Text>
|
||||
)}
|
||||
<Text fw={400}>{item.value}</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
<Group className={styles.metadataGroup}>
|
||||
<JoinedAlbumArtist
|
||||
albumArtist={detailQuery?.data?.albumArtist || ''}
|
||||
albumArtists={detailQuery?.data?.albumArtists || []}
|
||||
/>
|
||||
</Group>
|
||||
<LibraryHeaderMenu
|
||||
favorite={detailQuery?.data?.userFavorite}
|
||||
onFavorite={handleFavorite}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { Fragment } from 'react';
|
||||
import { generatePath, Link } from 'react-router';
|
||||
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Separator } from '/@/shared/components/separator/separator';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { AlbumArtist, RelatedArtist } from '/@/shared/types/domain-types';
|
||||
|
||||
interface JoinedAlbumArtistProps {
|
||||
albumArtist: string;
|
||||
albumArtists: AlbumArtist[] | RelatedArtist[];
|
||||
}
|
||||
|
||||
export const JoinedAlbumArtist = ({ albumArtist, albumArtists }: JoinedAlbumArtistProps) => {
|
||||
const parts: (
|
||||
| string
|
||||
| { artist: AlbumArtist | RelatedArtist; end: number; start: number; text: string }
|
||||
)[] = [];
|
||||
const matches: Array<{
|
||||
artist: AlbumArtist | RelatedArtist;
|
||||
end: number;
|
||||
name: string;
|
||||
start: number;
|
||||
}> = [];
|
||||
|
||||
for (const artist of albumArtists) {
|
||||
const name = artist.name;
|
||||
const regex = new RegExp(escapeRegex(name), 'gi');
|
||||
let match: null | RegExpExecArray = null;
|
||||
while ((match = regex.exec(albumArtist)) !== null) {
|
||||
matches.push({
|
||||
artist,
|
||||
end: match.index + match[0].length,
|
||||
name: match[0],
|
||||
start: match.index,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
matches.sort((a, b) => {
|
||||
const lengthDiff = b.end - b.start - (a.end - a.start);
|
||||
if (lengthDiff !== 0) return lengthDiff;
|
||||
return a.start - b.start;
|
||||
});
|
||||
|
||||
const nonOverlappingMatches: typeof matches = [];
|
||||
for (const match of matches) {
|
||||
const overlaps = nonOverlappingMatches.some(
|
||||
(existing) =>
|
||||
(match.start >= existing.start && match.start < existing.end) ||
|
||||
(match.end > existing.start && match.end <= existing.end) ||
|
||||
(match.start <= existing.start && match.end >= existing.end),
|
||||
);
|
||||
|
||||
if (!overlaps) {
|
||||
nonOverlappingMatches.push(match);
|
||||
}
|
||||
}
|
||||
|
||||
nonOverlappingMatches.sort((a, b) => a.start - b.start);
|
||||
|
||||
let lastIndex = 0;
|
||||
for (const match of nonOverlappingMatches) {
|
||||
if (match.start > lastIndex) {
|
||||
parts.push(albumArtist.substring(lastIndex, match.start));
|
||||
}
|
||||
|
||||
parts.push({
|
||||
artist: match.artist,
|
||||
end: match.end,
|
||||
start: match.start,
|
||||
text: match.name,
|
||||
});
|
||||
|
||||
lastIndex = match.end;
|
||||
}
|
||||
|
||||
if (lastIndex < albumArtist.length) {
|
||||
parts.push(albumArtist.substring(lastIndex));
|
||||
}
|
||||
|
||||
const hasArtistMatches = parts.some((part) => typeof part !== 'string');
|
||||
|
||||
// If no matches found and there are album artists, return the album artists
|
||||
if (!hasArtistMatches && albumArtists.length > 0) {
|
||||
return (
|
||||
<Group gap="xs">
|
||||
{albumArtists.map((artist, index) => (
|
||||
<Fragment key={artist.id}>
|
||||
{index > 0 && <Separator />}
|
||||
<Text
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
// If no matches found and no albumArtists, return the original string
|
||||
if (!hasArtistMatches) {
|
||||
return (
|
||||
<Text fw={400} isNoSelect>
|
||||
{albumArtist}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text component="span" fw={400}>
|
||||
{parts.map((part, index) => {
|
||||
if (typeof part === 'string') {
|
||||
return <Fragment key={index}>{part}</Fragment>;
|
||||
}
|
||||
|
||||
const { artist, text } = part;
|
||||
return (
|
||||
<Text
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
key={`${artist.id}-${index}`}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
})}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
margin: var(--theme-spacing-sm) 0;
|
||||
margin: 0;
|
||||
font-size: clamp(1.75rem, 3dvw, 2.75rem);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-b
|
||||
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||
import { titleCase } from '/@/renderer/utils';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
@@ -35,7 +34,7 @@ interface LibraryHeaderProps {
|
||||
containerClassName?: string;
|
||||
imagePlaceholderUrl?: null | string;
|
||||
imageUrl?: null | string;
|
||||
item: { releaseType?: string; route: string; type?: LibraryItem };
|
||||
item: { children?: ReactNode; route: string; type?: LibraryItem };
|
||||
loading?: boolean;
|
||||
title: string;
|
||||
}
|
||||
@@ -53,85 +52,6 @@ export const LibraryHeader = forwardRef(
|
||||
};
|
||||
|
||||
const itemTypeString = (): string => {
|
||||
if (item.releaseType) {
|
||||
switch (item.releaseType) {
|
||||
case 'album':
|
||||
return t('releaseType.primary.album', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'appears-on':
|
||||
return t('page.albumArtistDetail.appearsOn', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'audiobook':
|
||||
return t('releaseType.secondary.audiobook', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'audio drama':
|
||||
return t('releaseType.secondary.audioDrama', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'broadcast':
|
||||
return t('releaseType.primary.broadcast', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'compilation':
|
||||
return t('releaseType.secondary.compilation', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'demo':
|
||||
return t('releaseType.secondary.demo', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'dj-mix':
|
||||
return t('releaseType.secondary.djMix', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'ep':
|
||||
return t('releaseType.primary.ep', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'field recording':
|
||||
return t('releaseType.secondary.fieldRecording', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'interview':
|
||||
return t('releaseType.secondary.interview', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'live':
|
||||
return t('releaseType.secondary.live', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'mixtape/street':
|
||||
return t('releaseType.secondary.mixtape', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'other':
|
||||
return t('releaseType.primary.other', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'remix':
|
||||
return t('releaseType.secondary.remix', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'single':
|
||||
return t('releaseType.primary.single', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'soundtrack':
|
||||
return t('releaseType.secondary.soundtrack', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
case 'spokenword':
|
||||
return t('releaseType.secondary.spokenWord', {
|
||||
postProcess: 'sentenceCase',
|
||||
});
|
||||
default:
|
||||
return titleCase(item.releaseType);
|
||||
}
|
||||
}
|
||||
|
||||
switch (item.type) {
|
||||
case LibraryItem.ALBUM:
|
||||
return t('entity.album', { count: 1 });
|
||||
@@ -203,18 +123,22 @@ export const LibraryHeader = forwardRef(
|
||||
</div>
|
||||
{title && (
|
||||
<div className={styles.metadataSection}>
|
||||
<Text
|
||||
className={styles.itemType}
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
size="md"
|
||||
style={{}}
|
||||
to={item.route}
|
||||
tt="uppercase"
|
||||
>
|
||||
{itemTypeString()}
|
||||
</Text>
|
||||
{item.children ? (
|
||||
<div className={styles.itemType}>{item.children}</div>
|
||||
) : (
|
||||
<Text
|
||||
className={styles.itemType}
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
size="md"
|
||||
to={item.route}
|
||||
tt="uppercase"
|
||||
>
|
||||
{itemTypeString()}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<h1
|
||||
className={styles.title}
|
||||
style={{
|
||||
|
||||
Reference in New Issue
Block a user