normalize controls onto lists

This commit is contained in:
jeffvli
2025-11-08 14:28:22 -08:00
parent fb75717ae0
commit 3c996407d5
15 changed files with 401 additions and 347 deletions
@@ -75,6 +75,13 @@
}
}
.play-button.disabled,
.play-button.loading {
cursor: not-allowed;
opacity: 0.5;
transform: translate(-50%, -50%) scale(1);
}
.play-button.primary {
left: 50%;
width: 25%;
@@ -4,7 +4,9 @@ import { MouseEvent } from 'react';
import styles from './item-card-controls.module.css';
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { useIsPlayerFetching } from '/@/renderer/features/player/context/player-context';
import { animationVariants } from '/@/shared/components/animations/animation-variants';
import { AppIcon, Icon } from '/@/shared/components/icon/icon';
import { Rating } from '/@/shared/components/rating/rating';
@@ -19,7 +21,8 @@ import {
import { Play } from '/@/shared/types/types';
interface ItemCardControlsProps {
controls: ItemControls;
controls?: ItemControls;
internalState?: ItemListStateActions;
item: Album | AlbumArtist | Artist | Playlist | Song | undefined;
itemType: LibraryItem;
type?: 'compact' | 'default' | 'poster';
@@ -48,58 +51,152 @@ const containerProps = {
export const ItemCardControls = ({
controls,
internalState,
item,
itemType,
type = 'default',
}: ItemCardControlsProps) => {
const isPlayerFetching = useIsPlayerFetching();
return (
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
<PlayButton
onClick={(e) => {
e.stopPropagation();
controls?.onPlay?.(item, itemType, Play.NOW, e);
}}
/>
<SecondaryPlayButton
className={styles.left}
icon="mediaPlayNext"
onClick={(e) => {
e.stopPropagation();
controls?.onPlay?.(item, itemType, Play.NEXT, e);
}}
/>
<SecondaryPlayButton
className={styles.right}
icon="mediaPlayLast"
onClick={(e) => {
e.stopPropagation();
controls?.onPlay?.(item, itemType, Play.LAST, e);
}}
/>
<SecondaryButton
className={styles.favorite}
icon="favorite"
onClick={(e) => {
e.stopPropagation();
controls?.onFavorite?.(item, itemType, e);
}}
/>
<Rating className={styles.rating} size="xs" />
<SecondaryButton
className={styles.options}
icon="ellipsisHorizontal"
onClick={(e) => {
e.stopPropagation();
controls?.onMore?.(item, itemType, e);
}}
/>
{controls?.onItemExpand && (
{controls?.onPlay && (
<>
<PlayButton
disabled={isPlayerFetching}
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.NOW,
});
}}
/>
<SecondaryPlayButton
className={styles.left}
icon="mediaPlayNext"
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.NEXT,
});
}}
/>
<SecondaryPlayButton
className={styles.right}
icon="mediaPlayLast"
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
controls?.onPlay?.({
event: e,
internalState,
item,
itemType,
playType: Play.LAST,
});
}}
/>
</>
)}
{controls?.onFavorite && (
<SecondaryButton
className={styles.favorite}
icon="favorite"
onClick={(e) => {
e.stopPropagation();
if (!item) {
return;
}
const newFavorite = !(item as { userFavorite: boolean }).userFavorite;
controls?.onFavorite?.({
event: e,
favorite: newFavorite,
internalState,
item,
itemType,
});
}}
/>
)}
{controls?.onRating && (
<Rating
className={styles.rating}
onChange={(rating) => {
if (!item) {
return;
}
let newRating = rating;
if (rating === (item as { userRating: number }).userRating) {
newRating = 0;
}
controls?.onRating?.({
event: null,
internalState,
item,
itemType,
rating: newRating,
});
}}
onClick={(e) => {
e.stopPropagation();
}}
size="xs"
/>
)}
{controls?.onMore && (
<SecondaryButton
className={styles.options}
icon="ellipsisHorizontal"
onClick={(e) => {
e.stopPropagation();
controls?.onMore?.({
event: e,
internalState,
item,
itemType,
});
}}
/>
)}
{controls?.onExpand && (
<SecondaryButton
className={styles.expand}
icon="arrowDownS"
onClick={(e) => {
e.stopPropagation();
controls?.onItemExpand?.(item, itemType, e);
controls?.onExpand?.({
event: e,
internalState,
item,
itemType,
});
}}
/>
)}
@@ -107,12 +204,26 @@ export const ItemCardControls = ({
);
};
const PlayButton = ({ onClick }: { onClick?: (e: MouseEvent<HTMLButtonElement>) => void }) => {
const PlayButton = ({
disabled,
loading,
onClick,
}: {
disabled?: boolean;
loading?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void;
}) => {
return (
<button
className={clsx(styles.playButton, styles.primary)}
className={clsx(styles.playButton, styles.primary, {
[styles.disabled]: disabled,
})}
disabled={disabled}
onClick={(e) => {
e.stopPropagation();
if (disabled || loading) {
return;
}
onClick?.(e);
}}
>
+21 -12
View File
@@ -6,6 +6,7 @@ import { generatePath, Link } from 'react-router';
import styles from './item-card.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types';
import { AppRoute } from '/@/renderer/router/routes';
import { Image } from '/@/shared/components/image/image';
@@ -21,24 +22,26 @@ import {
Song,
} from '/@/shared/types/domain-types';
type DataRow = {
format: (data: Album | AlbumArtist | Artist | Playlist | Song) => ReactNode | string;
id: string;
isMuted?: boolean;
};
interface ItemCardProps {
controls: ItemControls;
export interface ItemCardProps {
controls?: ItemControls;
data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
internalState?: ItemListStateActions;
isRound?: boolean;
itemType: LibraryItem;
type?: 'compact' | 'default' | 'poster';
withControls?: boolean;
}
type DataRow = {
format: (data: Album | AlbumArtist | Artist | Playlist | Song) => ReactNode | string;
id: string;
isMuted?: boolean;
};
export const ItemCard = ({
controls,
data,
internalState,
isRound,
itemType,
type = 'poster',
@@ -54,6 +57,7 @@ export const ItemCard = ({
controls={controls}
data={data}
imageUrl={imageUrl}
internalState={internalState}
isRound={isRound}
itemType={itemType}
rows={rows}
@@ -66,6 +70,7 @@ export const ItemCard = ({
controls={controls}
data={data}
imageUrl={imageUrl}
internalState={internalState}
isRound={isRound}
itemType={itemType}
rows={rows}
@@ -79,6 +84,7 @@ export const ItemCard = ({
controls={controls}
data={data}
imageUrl={imageUrl}
internalState={internalState}
isRound={isRound}
itemType={itemType}
rows={rows}
@@ -89,8 +95,9 @@ export const ItemCard = ({
};
export interface ItemCardDerivativeProps extends Omit<ItemCardProps, 'type'> {
controls: ItemControls;
controls?: ItemControls;
imageUrl: string | undefined;
internalState?: ItemListStateActions;
rows: DataRow[];
}
@@ -119,7 +126,7 @@ const CompactItemCard = ({
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
controls?.onClick?.(data, itemType, e);
// controls?.onClick?.(data, itemType, e);
};
return (
@@ -195,7 +202,7 @@ const DefaultItemCard = ({
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
controls?.onClick?.(data, itemType, e);
// controls?.onClick?.(data, itemType, e);
};
return (
@@ -250,6 +257,7 @@ const PosterItemCard = ({
controls,
data,
imageUrl,
internalState,
isRound,
itemType,
rows,
@@ -271,7 +279,7 @@ const PosterItemCard = ({
};
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
controls?.onClick?.(data, itemType, e);
// controls?.onClick?.(data, itemType, e);
};
return (
@@ -289,6 +297,7 @@ const PosterItemCard = ({
{withControls && showControls && data && (
<ItemCardControls
controls={controls}
internalState={internalState}
item={data}
itemType={itemType}
type="poster"