optimize library headers (#1374)

This commit is contained in:
jeffvli
2025-12-14 02:33:19 -08:00
parent 4cc51c3700
commit b4b106222e
15 changed files with 247 additions and 155 deletions
@@ -14,20 +14,14 @@ import { ItemControls } from '/@/renderer/components/item-list/types';
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistGridCarousel } from '/@/renderer/features/artists/components/album-artist-grid-carousel';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { DefaultPlayButton } from '/@/renderer/features/shared/components/play-button';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { AppRoute } from '/@/renderer/router/routes';
import { ArtistItem, useCurrentServer, usePlayerSong } from '/@/renderer/store';
import {
useGeneralSettings,
usePlayButtonBehavior,
useSettingsStore,
} from '/@/renderer/store/settings.store';
import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
import { sanitize } from '/@/renderer/utils/sanitize';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
@@ -52,58 +46,35 @@ import {
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
interface AlbumArtistActionButtonsProps {
albumCount: null | number | undefined;
artistDiscographyLink: string;
artistSongsLink: string;
onFavorite: () => void;
onMoreOptions: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay: () => void;
userFavorite?: boolean;
}
const AlbumArtistActionButtons = ({
albumCount,
artistDiscographyLink,
artistSongsLink,
onFavorite,
onMoreOptions,
onPlay,
userFavorite,
}: AlbumArtistActionButtonsProps) => {
const { t } = useTranslation();
return (
<>
<Group gap="md">
<DefaultPlayButton disabled={albumCount === 0} onClick={onPlay} />
<Group gap="xs">
<ActionIcon
icon="favorite"
iconProps={{
fill: userFavorite ? 'primary' : undefined,
}}
onClick={onFavorite}
size="lg"
variant="transparent"
/>
<ActionIcon
icon="ellipsisHorizontal"
onClick={onMoreOptions}
size="lg"
variant="transparent"
/>
</Group>
</Group>
<Group gap="md">
<Group gap="lg">
<Button
component={Link}
p={0}
size="compact-md"
to={artistDiscographyLink}
variant="subtle"
variant="transparent"
>
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button>
<Button component={Link} size="compact-md" to={artistSongsLink} variant="subtle">
<Button
component={Link}
p={0}
size="compact-md"
to={artistSongsLink}
variant="transparent"
>
{String(t('page.albumArtistDetail.viewAllTracks')).toUpperCase()}
</Button>
</Group>
@@ -175,8 +146,8 @@ const AlbumArtistMetadataBiography = ({
artist: artistName,
})}
</TextTitle>
<Spoiler maxHeight={50}>
<div dangerouslySetInnerHTML={{ __html: sanitizedBiography }}></div>
<Spoiler>
<Text dangerouslySetInnerHTML={{ __html: sanitizedBiography }} />
</Spoiler>
</section>
);
@@ -440,7 +411,6 @@ export const AlbumArtistDetailContent = () => {
};
const routeId = (artistId || albumArtistId) as string;
const { ref, ...cq } = useContainerQuery();
const { addToQueueByFetch, setFavorite } = usePlayer();
const server = useCurrentServer();
const [enabledItem, itemOrder] = useMemo(() => {
@@ -506,21 +476,11 @@ export const AlbumArtistDetailContent = () => {
sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC,
title: (
<Group align="flex-end">
<TextTitle fw={700} order={2}>
{t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase',
})}
</TextTitle>
<Button
component={Link}
size="compact-md"
to={artistDiscographyLink}
variant="subtle"
>
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button>
</Group>
<TextTitle fw={700} order={2}>
{t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase',
})}
</TextTitle>
),
uniqueId: 'recentReleases',
},
@@ -560,7 +520,6 @@ export const AlbumArtistDetailContent = () => {
},
];
}, [
artistDiscographyLink,
detailQuery.data?.similarArtists,
enabledItem.compilations,
enabledItem.recentAlbums,
@@ -573,37 +532,6 @@ export const AlbumArtistDetailContent = () => {
t,
]);
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = async (playType?: Play) => {
if (!server?.id) return;
addToQueueByFetch(
server.id,
[routeId],
albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
playType || playButtonBehavior,
);
};
const handleFavorite = () => {
if (!detailQuery.data) return;
setFavorite(
detailQuery.data._serverId,
[detailQuery.data.id],
LibraryItem.ALBUM_ARTIST,
!detailQuery.data.userFavorite,
);
};
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery.data) return;
ContextMenuController.call({
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
event: e,
});
};
const albumCount = detailQuery.data?.albumCount;
const biography =
detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null;
const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false;
@@ -618,13 +546,8 @@ export const AlbumArtistDetailContent = () => {
<div className={styles.contentContainer} ref={ref}>
<div className={styles.detailContainer}>
<AlbumArtistActionButtons
albumCount={albumCount}
artistDiscographyLink={artistDiscographyLink}
artistSongsLink={artistSongsLink}
onFavorite={handleFavorite}
onMoreOptions={handleMoreOptions}
onPlay={() => handlePlay(playButtonBehavior)}
userFavorite={detailQuery.data?.userFavorite}
/>
<Grid gutter="xl">
{showGenres && (
@@ -0,0 +1,8 @@
.metadata-group {
justify-content: center;
width: 100%;
@container (min-width: $mantine-breakpoint-sm) {
justify-content: flex-start;
}
}
@@ -3,17 +3,24 @@ import { forwardRef, Fragment, Ref } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router';
import styles from './album-artist-detail-header.module.css';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { LibraryHeader } from '/@/renderer/features/shared/components/library-header';
import {
LibraryHeader,
LibraryHeaderMenu,
} from '/@/renderer/features/shared/components/library-header';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils';
import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivElement>) => {
const { albumArtistId, artistId } = useParams() as {
@@ -56,7 +63,28 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
},
];
const { setRating } = usePlayer();
const { addToQueueByFetch, setFavorite, setRating } = usePlayer();
const playButtonBehavior = usePlayButtonBehavior();
const handlePlay = (type?: Play) => {
if (!server?.id || !routeId) return;
addToQueueByFetch(
server.id,
[routeId],
LibraryItem.ALBUM_ARTIST,
type || playButtonBehavior,
);
};
const handleFavorite = () => {
if (!detailQuery?.data) return;
setFavorite(
detailQuery.data._serverId,
[detailQuery.data.id],
LibraryItem.ALBUM_ARTIST,
!detailQuery.data.userFavorite,
);
};
const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return;
@@ -78,6 +106,14 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
);
};
const handleMoreOptions = (e: React.MouseEvent<HTMLButtonElement>) => {
if (!detailQuery?.data) return;
ContextMenuController.call({
cmd: { items: [detailQuery.data], type: LibraryItem.ALBUM_ARTIST },
event: e,
});
};
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
return (
@@ -87,8 +123,8 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
ref={ref}
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group>
<Stack gap="md" w="100%">
<Group className={styles.metadataGroup}>
{metadataItems
.filter((i) => i.enabled)
.map((item, index) => (
@@ -97,17 +133,16 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
<Text isMuted={item.secondary}>{item.value}</Text>
</Fragment>
))}
{showRating && (
<>
<Text isNoSelect></Text>
<Rating
onChange={handleUpdateRating}
readOnly={detailQuery?.isFetching}
value={detailQuery?.data?.userRating || 0}
/>
</>
)}
</Group>
<LibraryHeaderMenu
favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite}
onMore={handleMoreOptions}
onPlay={(type) => handlePlay(type)}
onRating={showRating ? handleUpdateRating : undefined}
onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0}
/>
</Stack>
</LibraryHeader>
);