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
+4 -4
View File
@@ -578,10 +578,10 @@
} }
}, },
"player": { "player": {
"addLast": "add last", "addLast": "last",
"addNext": "add next", "addNext": "next",
"addLastShuffled": "add last (shuffled)", "addLastShuffled": "last (shuffled)",
"addNextShuffled": "add next (shuffled)", "addNextShuffled": "next (shuffled)",
"holdToShuffle": "hold to shuffle", "holdToShuffle": "hold to shuffle",
"favorite": "favorite", "favorite": "favorite",
"lyrics": "lyrics", "lyrics": "lyrics",
@@ -368,8 +368,10 @@ export const AlbumDetailContent = () => {
<div className={styles.contentContainer} ref={ref}> <div className={styles.contentContainer} ref={ref}>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
{comment && ( {comment && (
<Spoiler hideLabel={true} maxHeight={32} showLabel={true}> <Spoiler maxHeight={75}>
{replaceURLWithHTMLLinks(comment)} <Text
dangerouslySetInnerHTML={{ __html: replaceURLWithHTMLLinks(comment) }}
/>
</Spoiler> </Spoiler>
)} )}
<div className={styles.contentLayout}> <div className={styles.contentLayout}>
@@ -89,7 +89,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }} item={{ route: AppRoute.LIBRARY_ALBUMS, type: LibraryItem.ALBUM }}
title={detailQuery?.data?.name || ''} title={detailQuery?.data?.name || ''}
> >
<Stack gap="xl" w="100%"> <Stack gap="md" w="100%">
{(firstAlbumArtist || releaseYear) && ( {(firstAlbumArtist || releaseYear) && (
<Group className={styles.metadataGroup}> <Group className={styles.metadataGroup}>
{firstAlbumArtist && ( {firstAlbumArtist && (
@@ -121,7 +121,7 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
favorite={detailQuery?.data?.userFavorite} favorite={detailQuery?.data?.userFavorite}
onFavorite={handleFavorite} onFavorite={handleFavorite}
onMore={handleMoreOptions} onMore={handleMoreOptions}
onPlay={() => handlePlay(Play.NOW)} onPlay={(type) => handlePlay(type)}
onRating={handleUpdateRating} onRating={handleUpdateRating}
onShuffle={() => handlePlay(Play.SHUFFLE)} onShuffle={() => handlePlay(Play.SHUFFLE)}
rating={detailQuery?.data?.userRating || 0} rating={detailQuery?.data?.userRating || 0}
@@ -211,7 +211,13 @@ const DummyAlbumDetailRoute = () => {
)} )}
{comment && ( {comment && (
<section> <section>
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler> <Spoiler maxHeight={75}>
<Text
dangerouslySetInnerHTML={{
__html: replaceURLWithHTMLLinks(comment),
}}
/>
</Spoiler>
</section> </section>
)} )}
<section> <section>
@@ -14,20 +14,14 @@ import { ItemControls } from '/@/renderer/components/item-list/types';
import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel'; import { AlbumInfiniteCarousel } from '/@/renderer/features/albums/components/album-infinite-carousel';
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 { 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 { 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 { DefaultPlayButton } from '/@/renderer/features/shared/components/play-button';
import { searchLibraryItems } from '/@/renderer/features/shared/utils'; import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ArtistItem, useCurrentServer, usePlayerSong } from '/@/renderer/store'; import { ArtistItem, useCurrentServer, usePlayerSong } from '/@/renderer/store';
import { import { useGeneralSettings, useSettingsStore } from '/@/renderer/store/settings.store';
useGeneralSettings,
usePlayButtonBehavior,
useSettingsStore,
} from '/@/renderer/store/settings.store';
import { sanitize } from '/@/renderer/utils/sanitize'; import { sanitize } from '/@/renderer/utils/sanitize';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
@@ -52,58 +46,35 @@ import {
import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types'; import { ItemListKey, ListDisplayType, Play } from '/@/shared/types/types';
interface AlbumArtistActionButtonsProps { interface AlbumArtistActionButtonsProps {
albumCount: null | number | undefined;
artistDiscographyLink: string; artistDiscographyLink: string;
artistSongsLink: string; artistSongsLink: string;
onFavorite: () => void;
onMoreOptions: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay: () => void;
userFavorite?: boolean;
} }
const AlbumArtistActionButtons = ({ const AlbumArtistActionButtons = ({
albumCount,
artistDiscographyLink, artistDiscographyLink,
artistSongsLink, artistSongsLink,
onFavorite,
onMoreOptions,
onPlay,
userFavorite,
}: AlbumArtistActionButtonsProps) => { }: AlbumArtistActionButtonsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<> <>
<Group gap="md"> <Group gap="lg">
<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">
<Button <Button
component={Link} component={Link}
p={0}
size="compact-md" size="compact-md"
to={artistDiscographyLink} to={artistDiscographyLink}
variant="subtle" variant="transparent"
> >
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()} {String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button> </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()} {String(t('page.albumArtistDetail.viewAllTracks')).toUpperCase()}
</Button> </Button>
</Group> </Group>
@@ -175,8 +146,8 @@ const AlbumArtistMetadataBiography = ({
artist: artistName, artist: artistName,
})} })}
</TextTitle> </TextTitle>
<Spoiler maxHeight={50}> <Spoiler>
<div dangerouslySetInnerHTML={{ __html: sanitizedBiography }}></div> <Text dangerouslySetInnerHTML={{ __html: sanitizedBiography }} />
</Spoiler> </Spoiler>
</section> </section>
); );
@@ -440,7 +411,6 @@ export const AlbumArtistDetailContent = () => {
}; };
const routeId = (artistId || albumArtistId) as string; const routeId = (artistId || albumArtistId) as string;
const { ref, ...cq } = useContainerQuery(); const { ref, ...cq } = useContainerQuery();
const { addToQueueByFetch, setFavorite } = usePlayer();
const server = useCurrentServer(); const server = useCurrentServer();
const [enabledItem, itemOrder] = useMemo(() => { const [enabledItem, itemOrder] = useMemo(() => {
@@ -506,21 +476,11 @@ export const AlbumArtistDetailContent = () => {
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
title: ( title: (
<Group align="flex-end">
<TextTitle fw={700} order={2}> <TextTitle fw={700} order={2}>
{t('page.albumArtistDetail.recentReleases', { {t('page.albumArtistDetail.recentReleases', {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
})} })}
</TextTitle> </TextTitle>
<Button
component={Link}
size="compact-md"
to={artistDiscographyLink}
variant="subtle"
>
{String(t('page.albumArtistDetail.viewDiscography')).toUpperCase()}
</Button>
</Group>
), ),
uniqueId: 'recentReleases', uniqueId: 'recentReleases',
}, },
@@ -560,7 +520,6 @@ export const AlbumArtistDetailContent = () => {
}, },
]; ];
}, [ }, [
artistDiscographyLink,
detailQuery.data?.similarArtists, detailQuery.data?.similarArtists,
enabledItem.compilations, enabledItem.compilations,
enabledItem.recentAlbums, enabledItem.recentAlbums,
@@ -573,37 +532,6 @@ export const AlbumArtistDetailContent = () => {
t, 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 = const biography =
detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null; detailQuery.data?.biography && enabledItem.biography ? detailQuery.data.biography : null;
const showGenres = detailQuery.data?.genres ? detailQuery.data.genres.length !== 0 : false; 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.contentContainer} ref={ref}>
<div className={styles.detailContainer}> <div className={styles.detailContainer}>
<AlbumArtistActionButtons <AlbumArtistActionButtons
albumCount={albumCount}
artistDiscographyLink={artistDiscographyLink} artistDiscographyLink={artistDiscographyLink}
artistSongsLink={artistSongsLink} artistSongsLink={artistSongsLink}
onFavorite={handleFavorite}
onMoreOptions={handleMoreOptions}
onPlay={() => handlePlay(playButtonBehavior)}
userFavorite={detailQuery.data?.userFavorite}
/> />
<Grid gutter="xl"> <Grid gutter="xl">
{showGenres && ( {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 { useTranslation } from 'react-i18next';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import styles from './album-artist-detail-header.module.css';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; 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 { 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 { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Rating } from '/@/shared/components/rating/rating';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivElement>) => { export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivElement>) => {
const { albumArtistId, artistId } = useParams() as { 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) => { const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return; 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; const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
return ( return (
@@ -87,8 +123,8 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
ref={ref} ref={ref}
title={detailQuery?.data?.name || ''} title={detailQuery?.data?.name || ''}
> >
<Stack> <Stack gap="md" w="100%">
<Group> <Group className={styles.metadataGroup}>
{metadataItems {metadataItems
.filter((i) => i.enabled) .filter((i) => i.enabled)
.map((item, index) => ( .map((item, index) => (
@@ -97,17 +133,16 @@ export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivEleme
<Text isMuted={item.secondary}>{item.value}</Text> <Text isMuted={item.secondary}>{item.value}</Text>
</Fragment> </Fragment>
))} ))}
{showRating && (
<>
<Text isNoSelect></Text>
<Rating
onChange={handleUpdateRating}
readOnly={detailQuery?.isFetching}
value={detailQuery?.data?.userRating || 0}
/>
</>
)}
</Group> </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> </Stack>
</LibraryHeader> </LibraryHeader>
); );
@@ -228,8 +228,8 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
label: 'common.biography', label: 'common.biography',
render: (artist) => render: (artist) =>
artist.biography ? ( artist.biography ? (
<Spoiler maxHeight={50}> <Spoiler>
<div dangerouslySetInnerHTML={{ __html: sanitize(artist.biography) }} /> <Text dangerouslySetInnerHTML={{ __html: sanitize(artist.biography) }} />
</Spoiler> </Spoiler>
) : null, ) : null,
}, },
@@ -91,7 +91,7 @@ export const PlaylistDetailSongListHeader = ({
title={detailQuery?.data?.name} title={detailQuery?.data?.name}
> >
<LibraryHeaderMenu <LibraryHeaderMenu
onPlay={() => handlePlay(Play.NOW)} onPlay={(type) => handlePlay(type)}
onShuffle={() => handlePlay(Play.SHUFFLE)} onShuffle={() => handlePlay(Play.SHUFFLE)}
/> />
</LibraryHeader> </LibraryHeader>
@@ -26,7 +26,7 @@
grid-template-areas: 'image info'; grid-template-areas: 'image info';
grid-template-rows: auto; grid-template-rows: auto;
grid-template-columns: 225px minmax(0, 1fr); grid-template-columns: 225px minmax(0, 1fr);
align-items: center; align-items: flex-end;
justify-items: start; justify-items: start;
height: auto; height: auto;
min-height: 340px; min-height: 340px;
@@ -98,7 +98,7 @@
.title { .title {
display: flex; display: flex;
margin: var(--theme-spacing-sm) 0; margin: var(--theme-spacing-sm) 0;
font-size: clamp(2rem, 3.5dvw, 3.25rem); font-size: clamp(1.75rem, 3dvw, 2.75rem);
line-height: 1.2; line-height: 1.2;
} }
@@ -7,9 +7,12 @@ import { Link } from 'react-router';
import styles from './library-header.module.css'; import styles from './library-header.module.css';
import { import {
PlayLastTextButton,
PlayNextTextButton,
PlayTextButton, PlayTextButton,
WideShuffleButton,
} from '/@/renderer/features/shared/components/play-button'; } from '/@/renderer/features/shared/components/play-button';
import { LONG_PRESS_PLAY_BEHAVIOR } from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
@@ -20,6 +23,7 @@ import { Image } from '/@/shared/components/image/image';
import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
interface LibraryHeaderProps { interface LibraryHeaderProps {
children?: ReactNode; children?: ReactNode;
@@ -145,36 +149,36 @@ export const LibraryHeader = forwardRef(
const calculateTitleSize = (title: string) => { const calculateTitleSize = (title: string) => {
const titleLength = title.length; const titleLength = title.length;
let baseSize = '3.5dvw'; let baseSize = '3dvw';
if (titleLength > 20) { if (titleLength > 20) {
baseSize = '3dvw';
}
if (titleLength > 30) {
baseSize = '2.75dvw';
}
if (titleLength > 40) {
baseSize = '2.5dvw'; baseSize = '2.5dvw';
} }
if (titleLength > 50) { if (titleLength > 30) {
baseSize = '2.25dvw'; baseSize = '2.25dvw';
} }
if (titleLength > 60) { if (titleLength > 40) {
baseSize = '2dvw'; baseSize = '2dvw';
} }
return `clamp(2rem, ${baseSize}, 3.25rem)`; if (titleLength > 50) {
baseSize = '1.875dvw';
}
if (titleLength > 60) {
baseSize = '1.75dvw';
}
return `clamp(1.75rem, ${baseSize}, 2.75rem)`;
}; };
interface LibraryHeaderMenuProps { interface LibraryHeaderMenuProps {
favorite?: boolean; favorite?: boolean;
onFavorite?: (e: React.MouseEvent<HTMLButtonElement>) => void; onFavorite?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onMore?: (e: React.MouseEvent<HTMLButtonElement>) => void; onMore?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay?: (e: React.MouseEvent<HTMLButtonElement>) => void; onPlay?: (type: Play) => void;
onRating?: (rating: number) => void; onRating?: (rating: number) => void;
onShuffle?: (e: React.MouseEvent<HTMLButtonElement>) => void; onShuffle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
rating?: number; rating?: number;
@@ -186,7 +190,6 @@ export const LibraryHeaderMenu = ({
onMore, onMore,
onPlay, onPlay,
onRating, onRating,
onShuffle,
rating, rating,
}: LibraryHeaderMenuProps) => { }: LibraryHeaderMenuProps) => {
const isMutatingRating = useIsMutatingRating(); const isMutatingRating = useIsMutatingRating();
@@ -194,11 +197,43 @@ export const LibraryHeaderMenu = ({
const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite(); const isMutatingDeleteFavorite = useIsMutatingDeleteFavorite();
const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite; const isMutatingFavorite = isMutatingCreateFavorite || isMutatingDeleteFavorite;
const handlePlayNow = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.NOW);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.NOW]);
},
});
const handlePlayNext = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.NEXT);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.NEXT]);
},
});
const handlePlayLast = usePlayButtonClick({
onClick: () => {
onPlay?.(Play.LAST);
},
onLongPress: () => {
onPlay?.(LONG_PRESS_PLAY_BEHAVIOR[Play.LAST]);
},
});
return ( return (
<div className={styles.libraryHeaderMenu}> <div className={styles.libraryHeaderMenu}>
<Group wrap="nowrap"> <Group wrap="nowrap">
{onPlay && <PlayTextButton onClick={onPlay} />} {onPlay && <PlayTextButton {...handlePlayNow.handlers} {...handlePlayNow.props} />}
{onShuffle && <WideShuffleButton onClick={onShuffle} />} {onPlay && (
<PlayNextTextButton {...handlePlayNext.handlers} {...handlePlayNext.props} />
)}
{onPlay && (
<PlayLastTextButton {...handlePlayLast.handlers} {...handlePlayLast.props} />
)}
</Group> </Group>
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
{onRating && ( {onRating && (
@@ -50,10 +50,26 @@
padding-left: var(--theme-spacing-xl); padding-left: var(--theme-spacing-xl);
background: white; background: white;
border-radius: var(--theme-radius-xl); border-radius: var(--theme-radius-xl);
transition: background-color 0.2s ease-in-out; transition: background-color 0.2s ease-in-out !important;
&[data-variant='subtle'] {
transition: background-color 0.2s ease-in-out !important;
&:hover,
&:active,
&:focus-visible {
transition: background-color 0.2s ease-in-out !important;
}
}
} }
.wide-text-button.unthemed { .wide-text-button.unthemed {
transition: background-color 0.2s ease-in-out !important;
&[data-variant='subtle'] {
transition: background-color 0.2s ease-in-out !important;
}
@mixin light { @mixin light {
background: black; background: black;
@@ -62,8 +78,11 @@
fill: white; fill: white;
} }
&:hover { &[data-variant='subtle']:hover,
background: lighten(black, 10%); &[data-variant='subtle']:active,
&[data-variant='subtle']:focus-visible {
background: lighten(black, 10%) !important;
transition: background-color 0.2s ease-in-out !important;
} }
} }
@@ -75,8 +94,11 @@
fill: black; fill: black;
} }
&:hover { &[data-variant='subtle']:hover,
background: darken(white, 20%); &[data-variant='subtle']:active,
&[data-variant='subtle']:focus-visible {
background: darken(white, 20%) !important;
transition: background-color 0.2s ease-in-out !important;
} }
} }
} }
@@ -90,14 +112,16 @@
color: white; color: white;
} }
svg { svg {
color: black; color: black;
fill: black; fill: black;
} }
} }
.no-fill {
fill: none !important;
}
.play-button { .play-button {
all: unset; all: unset;
display: flex; display: flex;
@@ -4,12 +4,14 @@ import { forwardRef, memo } from 'react';
import styles from './play-button.module.css'; import styles from './play-button.module.css';
import { PlayTooltip } from '/@/renderer/features/shared/components/play-button-group';
import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click'; import { usePlayButtonClick } from '/@/renderer/features/shared/hooks/use-play-button-click';
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon'; import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
import { Button, ButtonProps } from '/@/shared/components/button/button'; import { Button, ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { AppIcon, Icon } from '/@/shared/components/icon/icon'; import { AppIcon, Icon } from '/@/shared/components/icon/icon';
import { Spinner } from '/@/shared/components/spinner/spinner'; import { Spinner } from '/@/shared/components/spinner/spinner';
import { Play } from '/@/shared/types/types';
export interface DefaultPlayButtonProps extends ActionIconProps { export interface DefaultPlayButtonProps extends ActionIconProps {
size?: number | string; size?: number | string;
@@ -36,14 +38,18 @@ export const DefaultPlayButton = forwardRef<HTMLButtonElement, DefaultPlayButton
DefaultPlayButton.displayName = 'DefaultPlayButton'; DefaultPlayButton.displayName = 'DefaultPlayButton';
interface TextPlayButtonProps extends ButtonProps {} interface TextPlayButtonProps extends ButtonProps {
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
showTooltip?: boolean;
}
export const PlayTextButton = ({ export const PlayTextButton = ({
className, className,
showTooltip = true,
variant = 'default', variant = 'default',
...props ...props
}: TextPlayButtonProps) => { }: TextPlayButtonProps) => {
return ( const button = (
<Button <Button
className={clsx(styles.wideTextButton, className, { className={clsx(styles.wideTextButton, className, {
[styles.unthemed]: variant !== 'filled', [styles.unthemed]: variant !== 'filled',
@@ -57,12 +63,64 @@ export const PlayTextButton = ({
> >
{props.children || ( {props.children || (
<Group gap="sm" wrap="nowrap"> <Group gap="sm" wrap="nowrap">
<Icon fill="default" icon="mediaPlay" size="lg" /> <Icon icon="mediaPlay" size="lg" />
{t('player.play', { postProcess: 'sentenceCase' })} {t('player.play', { postProcess: 'sentenceCase' })}
</Group> </Group>
)} )}
</Button> </Button>
); );
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress && showTooltip) {
return <PlayTooltip type={Play.NOW}>{button}</PlayTooltip>;
}
return button;
};
export const PlayNextTextButton = ({ ...props }: TextPlayButtonProps) => {
const button = (
<PlayTextButton {...props} showTooltip={false}>
<Group gap="sm" wrap="nowrap">
<Icon className={styles.noFill} icon="mediaPlayNext" size="lg" />
{t('player.addNext', { postProcess: 'sentenceCase' })}
</Group>
</PlayTextButton>
);
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress) {
return <PlayTooltip type={Play.NEXT}>{button}</PlayTooltip>;
}
return button;
};
export const PlayLastTextButton = ({ ...props }: TextPlayButtonProps) => {
const button = (
<PlayTextButton {...props} showTooltip={false}>
<Group gap="sm" wrap="nowrap">
<Icon className={styles.noFill} icon="mediaPlayLast" size="lg" />
{t('player.addLast', { postProcess: 'sentenceCase' })}
</Group>
</PlayTextButton>
);
const hasLongPress = Boolean(
props.onLongPress || (props as any).onMouseDown || (props as any).onTouchStart,
);
if (hasLongPress) {
return <PlayTooltip type={Play.LAST}>{button}</PlayTooltip>;
}
return button;
}; };
export const WideShuffleButton = ({ ...props }: TextPlayButtonProps) => { export const WideShuffleButton = ({ ...props }: TextPlayButtonProps) => {
@@ -143,11 +143,11 @@
background-color: transparent; background-color: transparent;
@mixin dark { @mixin dark {
color: lighten(var(--theme-colors-foreground), 10%); color: darken(var(--theme-colors-foreground), 15%);
} }
@mixin light { @mixin light {
color: darken(var(--theme-colors-foreground), 10%); color: lighten(var(--theme-colors-foreground), 10%);
} }
} }
+3 -2
View File
@@ -5,17 +5,18 @@ import styles from './spoiler.module.css';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
interface SpoilerProps extends MantineSpoilerProps { interface SpoilerProps extends Omit<MantineSpoilerProps, 'hideLabel' | 'showLabel'> {
children?: ReactNode; children?: ReactNode;
} }
export const Spoiler = ({ children, ...props }: SpoilerProps) => { export const Spoiler = ({ children, maxHeight = 56, ...props }: SpoilerProps) => {
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
return ( return (
<MantineSpoiler <MantineSpoiler
classNames={{ content: styles.spoiler, control: styles.control }} classNames={{ content: styles.spoiler, control: styles.control }}
expanded={expanded} expanded={expanded}
maxHeight={maxHeight}
{...props} {...props}
hideLabel={<Icon icon="arrowUpS" size="lg" />} hideLabel={<Icon icon="arrowUpS" size="lg" />}
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}