extract play button from item card and add long press animation

This commit is contained in:
jeffvli
2025-11-25 16:20:44 -08:00
parent 8ad5e26c2f
commit 2264fa0d29
10 changed files with 296 additions and 226 deletions
@@ -45,44 +45,7 @@
margin-bottom: var(--theme-spacing-lg);
}
.play-button {
all: unset;
position: absolute;
top: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
border: none;
border-radius: 100%;
opacity: 0.8;
transform: translate(-50%, -50%) scale(1);
transition: opacity 0.1s ease-in-out;
transition: transform 0.1s ease-in-out;
&:hover {
opacity: 1;
transform: translate(-50%, -50%) scale(1.1);
}
&:active {
opacity: 1;
transform: translate(-50%, -50%) scale(0.9);
}
svg {
stroke: rgb(0 0 0);
}
}
.play-button.disabled,
.play-button.loading {
cursor: not-allowed;
opacity: 0.5;
transform: translate(-50%, -50%) scale(1);
}
.play-button.primary {
.primary {
left: 50%;
width: 25%;
height: 25%;
@@ -92,36 +55,16 @@
}
}
.play-button.secondary {
.secondary {
width: 15%;
height: 15%;
}
.play-button.left {
.left {
left: 25%;
}
.play-button.right {
left: 75%;
}
.play-button.left-top {
top: 40%;
left: 25%;
}
.play-button.left-bottom {
top: 60%;
left: 25%;
}
.play-button.right-bottom {
top: 60%;
left: 75%;
}
.play-button.right-top {
top: 40%;
.right {
left: 75%;
}
@@ -130,7 +73,7 @@
position: absolute;
padding: var(--theme-spacing-md);
border-radius: var(--theme-radius-md);
opacity: 0.8;
opacity: 1;
transition: opacity 0.2s ease-in-out;
transition: scale 0.2s linear;
@@ -6,14 +6,13 @@ 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 { PlayButton } from '/@/renderer/features/shared/components/play-button';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
import { animationVariants } from '/@/shared/components/animations/animation-variants';
import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon';
import { Rating } from '/@/shared/components/rating/rating';
import { useLongPress } from '/@/shared/hooks/use-long-press';
import {
Album,
AlbumArtist,
@@ -236,15 +235,19 @@ export const ItemCardControls = ({
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && (
<>
<PlayButton onClick={playNowHandler} onLongPress={playShuffleHandler} />
<SecondaryPlayButton
className={styles.left}
<PlayButton
classNames={clsx(styles.primary)}
onClick={playNowHandler}
onLongPress={playShuffleHandler}
/>
<PlayButton
classNames={clsx(styles.secondary, styles.left)}
icon="mediaPlayNext"
onClick={playNextHandler}
onLongPress={playNextShuffleHandler}
/>
<SecondaryPlayButton
className={styles.right}
<PlayButton
classNames={clsx(styles.secondary, styles.right)}
icon="mediaPlayLast"
onClick={playLastHandler}
onLongPress={playLastShuffleHandler}
@@ -340,128 +343,6 @@ const RatingButton = memo(
(prev, next) => prev.rating === next.rating,
);
const PlayButton = memo(
({
loading,
onClick,
onLongPress,
}: {
loading?: boolean;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => {
const isPlayerFetching = useIsPlayerFetching();
const disabled = isPlayerFetching || loading;
const longPressHandlers = useLongPress<HTMLButtonElement>({
onClick: (e) => {
if (disabled || loading) {
return;
}
e.stopPropagation();
e.preventDefault();
onClick?.(e as React.MouseEvent<HTMLButtonElement>);
},
onLongPress: (e) => {
if (disabled || loading) {
return;
}
e.stopPropagation();
e.preventDefault();
onLongPress?.(e as React.MouseEvent<HTMLButtonElement>);
},
});
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
longPressHandlers.onMouseDown?.(e);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
return (
<button
className={clsx(styles.playButton, styles.primary, {
[styles.disabled]: disabled,
})}
disabled={disabled}
onClick={handleClick}
onMouseDown={handleMouseDown}
onMouseLeave={longPressHandlers.onMouseLeave}
onMouseUp={longPressHandlers.onMouseUp}
onTouchCancel={longPressHandlers.onTouchCancel}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchStart={longPressHandlers.onTouchStart}
>
<Icon icon="mediaPlay" size="lg" />
</button>
);
},
);
const SecondaryPlayButton = memo(
({
className,
icon,
onClick,
onLongPress,
}: {
className?: string;
icon: keyof typeof AppIcon;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => {
const isPlayerFetching = useIsPlayerFetching();
const disabled = isPlayerFetching;
const longPressHandlers = useLongPress<HTMLButtonElement>({
onClick: (e) => {
e.stopPropagation();
e.preventDefault();
onClick?.(e as React.MouseEvent<HTMLButtonElement>);
},
onLongPress: (e) => {
e.stopPropagation();
e.preventDefault();
onLongPress?.(e as React.MouseEvent<HTMLButtonElement>);
},
});
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
longPressHandlers.onMouseDown?.(e);
};
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};
return (
<button
className={clsx(styles.playButton, styles.secondary, className, {
[styles.disabled]: disabled,
})}
disabled={disabled}
onClick={handleClick}
onMouseDown={handleMouseDown}
onMouseLeave={longPressHandlers.onMouseLeave}
onMouseUp={longPressHandlers.onMouseUp}
onTouchCancel={longPressHandlers.onTouchCancel}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchStart={longPressHandlers.onTouchStart}
>
<Icon icon={icon} size="lg" />
</button>
);
},
);
interface SecondaryButtonProps {
className?: string;
disabled?: boolean;