add long press to card controls for shuffle

This commit is contained in:
jeffvli
2025-11-23 15:58:36 -08:00
parent 1763f666b5
commit db110733a4
2 changed files with 182 additions and 27 deletions
@@ -13,6 +13,7 @@ import { useIsMutatingRating } from '/@/renderer/features/shared/mutations/set-r
import { animationVariants } from '/@/shared/components/animations/animation-variants'; import { animationVariants } from '/@/shared/components/animations/animation-variants';
import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon'; import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon';
import { Rating } from '/@/shared/components/rating/rating'; import { Rating } from '/@/shared/components/rating/rating';
import { useLongPress } from '/@/shared/hooks/use-long-press';
import { import {
Album, Album,
AlbumArtist, AlbumArtist,
@@ -179,8 +180,6 @@ export const ItemCardControls = ({
itemType, itemType,
type = 'default', type = 'default',
}: ItemCardControlsProps) => { }: ItemCardControlsProps) => {
const isPlayerFetching = useIsPlayerFetching();
const playNowHandler = useMemo( const playNowHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.NOW), () => createPlayHandler(controls, item, internalState, itemType, Play.NOW),
[controls, item, internalState, itemType], [controls, item, internalState, itemType],
@@ -196,6 +195,21 @@ export const ItemCardControls = ({
[controls, item, internalState, itemType], [controls, item, internalState, itemType],
); );
const playShuffleHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.SHUFFLE),
[controls, item, internalState, itemType],
);
const playNextShuffleHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.NEXT_SHUFFLE),
[controls, item, internalState, itemType],
);
const playLastShuffleHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.LAST_SHUFFLE),
[controls, item, internalState, itemType],
);
const favoriteHandler = useMemo( const favoriteHandler = useMemo(
() => createFavoriteHandler(controls, item, internalState, itemType), () => createFavoriteHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType], [controls, item, internalState, itemType],
@@ -222,16 +236,18 @@ export const ItemCardControls = ({
<motion.div className={clsx(styles.container)} {...containerProps[type]}> <motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && ( {controls?.onPlay && (
<> <>
<PlayButton disabled={isPlayerFetching} onClick={playNowHandler} /> <PlayButton onClick={playNowHandler} onLongPress={playShuffleHandler} />
<SecondaryPlayButton <SecondaryPlayButton
className={styles.left} className={styles.left}
icon="mediaPlayNext" icon="mediaPlayNext"
onClick={playNextHandler} onClick={playNextHandler}
onLongPress={playNextShuffleHandler}
/> />
<SecondaryPlayButton <SecondaryPlayButton
className={styles.right} className={styles.right}
icon="mediaPlayLast" icon="mediaPlayLast"
onClick={playLastHandler} onClick={playLastHandler}
onLongPress={playLastShuffleHandler}
/> />
</> </>
)} )}
@@ -326,26 +342,40 @@ const RatingButton = memo(
const PlayButton = memo( const PlayButton = memo(
({ ({
disabled,
loading, loading,
onClick, onClick,
onLongPress,
}: { }: {
disabled?: boolean;
loading?: boolean; loading?: boolean;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => { const isPlayerFetching = useIsPlayerFetching();
e.stopPropagation();
e.preventDefault(); const disabled = isPlayerFetching || loading;
if (disabled || loading) {
return; const longPressHandlers = useLongPress<HTMLButtonElement>({
} onClick: (e) => {
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>) => { const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); longPressHandlers.onMouseDown?.(e);
}; };
return ( return (
@@ -354,8 +384,12 @@ const PlayButton = memo(
[styles.disabled]: disabled, [styles.disabled]: disabled,
})} })}
disabled={disabled} disabled={disabled}
onClick={handleClick}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseLeave={longPressHandlers.onMouseLeave}
onMouseUp={longPressHandlers.onMouseUp}
onTouchCancel={longPressHandlers.onTouchCancel}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchStart={longPressHandlers.onTouchStart}
> >
<Icon icon="mediaPlay" size="lg" /> <Icon icon="mediaPlay" size="lg" />
</button> </button>
@@ -368,27 +402,47 @@ const SecondaryPlayButton = memo(
className, className,
icon, icon,
onClick, onClick,
onLongPress,
}: { }: {
className?: string; className?: string;
icon: keyof typeof AppIcon; icon: keyof typeof AppIcon;
onClick?: (e: MouseEvent<HTMLButtonElement>) => void; onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onLongPress?: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => { }) => {
const handleClick = (e: MouseEvent<HTMLButtonElement>) => { const isPlayerFetching = useIsPlayerFetching();
e.stopPropagation();
e.preventDefault(); const disabled = isPlayerFetching;
onClick?.(e);
}; 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>) => { const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); longPressHandlers.onMouseDown?.(e);
}; };
return ( return (
<button <button
className={clsx(styles.playButton, styles.secondary, className)} className={clsx(styles.playButton, styles.secondary, className, {
onClick={handleClick} [styles.disabled]: disabled,
})}
disabled={disabled}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseLeave={longPressHandlers.onMouseLeave}
onMouseUp={longPressHandlers.onMouseUp}
onTouchCancel={longPressHandlers.onTouchCancel}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchStart={longPressHandlers.onTouchStart}
> >
<Icon icon={icon} size="lg" /> <Icon icon={icon} size="lg" />
</button> </button>
+103 -2
View File
@@ -1,3 +1,104 @@
import { useLongPress as useMantineLongPress } from '@mantine/hooks'; import { useCallback, useRef } from 'react';
export const useLongPress = useMantineLongPress; interface UseLongPressOptions<T extends HTMLElement = HTMLElement> {
delay?: number;
onClick?: (event: React.MouseEvent<T> | React.TouchEvent<T>) => void;
onLongPress?: (event: React.MouseEvent<T> | React.TouchEvent<T>) => void;
}
interface UseLongPressReturn {
onMouseDown: (event: React.MouseEvent) => void;
onMouseLeave: (event: React.MouseEvent) => void;
onMouseUp: (event: React.MouseEvent) => void;
onTouchCancel: (event: React.TouchEvent) => void;
onTouchEnd: (event: React.TouchEvent) => void;
onTouchStart: (event: React.TouchEvent) => void;
}
export const useLongPress = <T extends HTMLElement = HTMLElement>({
delay = 500,
onClick,
onLongPress,
}: UseLongPressOptions<T>): UseLongPressReturn => {
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const targetRef = useRef<EventTarget | null>(null);
const longPressTriggeredRef = useRef(false);
const eventRef = useRef<null | React.MouseEvent<T> | React.TouchEvent<T>>(null);
const start = useCallback(
(event: React.MouseEvent<T> | React.TouchEvent<T>) => {
longPressTriggeredRef.current = false;
targetRef.current = event.target;
eventRef.current = event;
timeoutRef.current = setTimeout(() => {
longPressTriggeredRef.current = true;
if (eventRef.current) {
onLongPress?.(eventRef.current);
}
}, delay);
},
[onLongPress, delay],
);
const clear = useCallback(() => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
}, []);
const handleMouseDown = useCallback(
(event: React.MouseEvent) => {
event.preventDefault();
start(event as React.MouseEvent<T>);
},
[start],
);
const handleMouseUp = useCallback(() => {
clear();
if (!longPressTriggeredRef.current && onClick && eventRef.current) {
onClick(eventRef.current);
}
longPressTriggeredRef.current = false;
eventRef.current = null;
}, [clear, onClick]);
const handleMouseLeave = useCallback(() => {
clear();
longPressTriggeredRef.current = false;
eventRef.current = null;
}, [clear]);
const handleTouchStart = useCallback(
(event: React.TouchEvent) => {
start(event as React.TouchEvent<T>);
},
[start],
);
const handleTouchEnd = useCallback(() => {
clear();
if (!longPressTriggeredRef.current && onClick && eventRef.current) {
onClick(eventRef.current);
}
longPressTriggeredRef.current = false;
eventRef.current = null;
}, [clear, onClick]);
const handleTouchCancel = useCallback(() => {
clear();
longPressTriggeredRef.current = false;
eventRef.current = null;
}, [clear]);
return {
onMouseDown: handleMouseDown,
onMouseLeave: handleMouseLeave,
onMouseUp: handleMouseUp,
onTouchCancel: handleTouchCancel,
onTouchEnd: handleTouchEnd,
onTouchStart: handleTouchStart,
};
};