mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
optimize library headers (#1374)
This commit is contained in:
@@ -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%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user