redesign album detail page

This commit is contained in:
jeffvli
2025-11-20 03:47:56 -08:00
parent da82581eb0
commit 7fb0dffc40
14 changed files with 662 additions and 301 deletions
@@ -28,6 +28,7 @@ interface HeaderPlayButtonProps {
itemType: LibraryItem;
listQuery?: Record<string, any>;
songs?: Song[];
variant?: 'default' | 'filled';
}
interface TitleProps {
@@ -40,6 +41,7 @@ const HeaderPlayButton = ({
itemType,
listQuery,
songs,
variant = 'filled',
...props
}: HeaderPlayButtonProps) => {
const serverId = useCurrentServerId();
@@ -73,14 +75,19 @@ const HeaderPlayButton = ({
return (
<div className={styles.playButtonContainer}>
<PlayButton className={className} onClick={openPlayTypeModal} {...props} />
<PlayButton
className={className}
onClick={openPlayTypeModal}
variant={variant}
{...props}
/>
</div>
);
};
const Title = ({ children }: TitleProps) => {
return (
<TextTitle fw={700} order={1} overflow="hidden">
<TextTitle fw={700} order={2} overflow="hidden">
{children}
</TextTitle>
);
@@ -22,10 +22,8 @@
width: 250px !important;
height: 250px;
}
}
@container (min-width: 768px) {
.library-header {
@container (min-width: $mantine-breakpoint-sm) {
grid-template-areas: 'image info';
grid-template-rows: auto;
grid-template-columns: 225px minmax(0, 1fr);
@@ -45,10 +43,8 @@
height: 225px;
}
}
}
@container (min-width: 1200px) {
.library-header {
@container (min-width: $mantine-breakpoint-lg) {
grid-template-columns: 250px minmax(0, 1fr);
.image {
@@ -70,6 +66,10 @@
align-items: center;
justify-content: center;
filter: drop-shadow(0 0 8px rgb(0 0 0 / 50%));
@container (min-width: $mantine-breakpoint-sm) {
align-items: flex-end;
}
}
.metadata-section {
@@ -81,25 +81,12 @@
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
text-align: center;
& > div {
align-items: center;
justify-content: center;
text-align: center;
}
}
@container (min-width: 768px) {
.image-section {
align-items: flex-end;
}
.metadata-section,
.metadata-section > div:first-of-type,
.metadata-section > div:last-of-type {
@container (min-width: $mantine-breakpoint-sm) {
align-items: flex-start;
justify-content: flex-start;
justify-content: flex-end;
text-align: left;
}
}
@@ -146,3 +133,18 @@
color: var(--theme-colors-foreground);
-webkit-box-orient: vertical;
}
.library-header-menu {
display: flex;
flex-direction: column;
gap: var(--theme-spacing-sm);
align-items: center;
justify-content: center;
width: 100%;
@container (min-width: $mantine-breakpoint-sm) {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
@@ -7,9 +7,16 @@ import { Link } from 'react-router';
import styles from './library-header.module.css';
import {
WidePlayButton,
WideShuffleButton,
} from '/@/renderer/features/shared/components/play-button';
import { useGeneralSettings } from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Center } from '/@/shared/components/center/center';
import { Group } from '/@/shared/components/group/group';
import { Image } from '/@/shared/components/image/image';
import { Rating } from '/@/shared/components/rating/rating';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -117,6 +124,7 @@ export const LibraryHeader = forwardRef(
{title && (
<div className={styles.metadataSection}>
<Text
className={styles.itemType}
component={Link}
fw={600}
isLink
@@ -127,7 +135,7 @@ export const LibraryHeader = forwardRef(
{itemTypeString()}
</Text>
<h1 className={styles.title}>
<AutoTextSize maxFontSizePx={80} minFontSizePx={36} mode="box">
<AutoTextSize maxFontSizePx={80} minFontSizePx={32} mode="box">
{title}
</AutoTextSize>
</h1>
@@ -138,3 +146,54 @@ export const LibraryHeader = forwardRef(
);
},
);
interface LibraryHeaderMenuProps {
favorite?: boolean;
onFavorite?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onMore?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onPlay?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onRating?: (rating: number) => void;
onShuffle?: (e: React.MouseEvent<HTMLButtonElement>) => void;
rating?: number;
}
export const LibraryHeaderMenu = ({
favorite,
onFavorite,
onMore,
onPlay,
onRating,
onShuffle,
rating,
}: LibraryHeaderMenuProps) => {
return (
<div className={styles.libraryHeaderMenu}>
<Group wrap="nowrap">
{onPlay && <WidePlayButton onClick={onPlay} />}
{onShuffle && <WideShuffleButton onClick={onShuffle} />}
</Group>
<Group gap="sm" wrap="nowrap">
{onRating && <Rating onChange={onRating} size="lg" value={rating || 0} />}
{onFavorite && (
<ActionIcon
icon="favorite"
iconProps={{
fill: favorite ? 'primary' : undefined,
}}
onClick={onFavorite}
size="lg"
variant="transparent"
/>
)}
{onMore && (
<ActionIcon
icon="ellipsisHorizontal"
onClick={onMore}
size="lg"
variant="transparent"
/>
)}
</Group>
</div>
);
};
@@ -14,3 +14,80 @@
opacity: 0.6;
}
}
.button.unthemed {
@mixin light {
color: white;
background: black;
svg {
color: white;
fill: white;
}
&:hover {
background: lighten(black, 10%);
}
}
@mixin dark {
color: black;
background: white;
svg {
color: black;
fill: black;
}
&:hover {
background: darken(white, 20%);
}
}
}
.wide-button {
padding-right: var(--theme-spacing-xl);
padding-left: var(--theme-spacing-xl);
background: white;
border-radius: var(--theme-radius-xl);
transition: background-color 0.2s ease-in-out;
}
.wide-button.unthemed {
@mixin light {
background: black;
svg {
color: white;
fill: white;
}
&:hover {
background: lighten(black, 10%);
}
}
@mixin dark {
background: white;
svg {
color: black;
fill: black;
}
&:hover {
background: darken(white, 20%);
}
}
}
.wide-button-label {
font-size: var(--theme-font-size-md);
font-weight: 600;
color: black;
svg {
color: black;
fill: black;
}
}
@@ -1,23 +1,69 @@
import clsx from 'clsx';
import { t } from 'i18next';
import styles from './play-button.module.css';
import { ActionIcon, ActionIconProps } from '/@/shared/components/action-icon/action-icon';
import { Button, ButtonProps } from '/@/shared/components/button/button';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
export interface PlayButtonProps extends ActionIconProps {
size?: number | string;
}
export const PlayButton = ({ className, ...props }: PlayButtonProps) => {
export const PlayButton = ({ className, variant = 'filled', ...props }: PlayButtonProps) => {
return (
<ActionIcon
className={clsx(styles.button, className)}
className={clsx(styles.button, className, {
[styles.unthemed]: variant !== 'filled',
})}
icon="mediaPlay"
iconProps={{
size: 'xl',
}}
variant="filled"
variant={variant}
{...props}
/>
);
};
interface WidePlayButtonProps extends ButtonProps {}
export const WidePlayButton = ({
className,
variant = 'default',
...props
}: WidePlayButtonProps) => {
return (
<Button
className={clsx(styles.wideButton, className, {
[styles.unthemed]: variant !== 'filled',
})}
classNames={{
label: styles.wideButtonLabel,
root: styles.wideButton,
}}
variant="subtle"
{...props}
>
{props.children || (
<Group gap="sm" wrap="nowrap">
<Icon fill="default" icon="mediaPlay" size="lg" />
{t('player.play', { postProcess: 'sentenceCase' })}
</Group>
)}
</Button>
);
};
export const WideShuffleButton = ({ ...props }: WidePlayButtonProps) => {
return (
<WidePlayButton {...props}>
<Group gap="sm" wrap="nowrap">
<Icon fill="default" icon="mediaShuffle" size="lg" />
{t('action.shuffle', { postProcess: 'sentenceCase' })}
</Group>
</WidePlayButton>
);
};