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 { 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,
@@ -179,8 +180,6 @@ export const ItemCardControls = ({
itemType,
type = 'default',
}: ItemCardControlsProps) => {
const isPlayerFetching = useIsPlayerFetching();
const playNowHandler = useMemo(
() => createPlayHandler(controls, item, internalState, itemType, Play.NOW),
[controls, item, internalState, itemType],
@@ -196,6 +195,21 @@ export const ItemCardControls = ({
[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(
() => createFavoriteHandler(controls, item, internalState, itemType),
[controls, item, internalState, itemType],
@@ -222,16 +236,18 @@ export const ItemCardControls = ({
<motion.div className={clsx(styles.container)} {...containerProps[type]}>
{controls?.onPlay && (
<>
<PlayButton disabled={isPlayerFetching} onClick={playNowHandler} />
<PlayButton onClick={playNowHandler} onLongPress={playShuffleHandler} />
<SecondaryPlayButton
className={styles.left}
icon="mediaPlayNext"
onClick={playNextHandler}
onLongPress={playNextShuffleHandler}
/>
<SecondaryPlayButton
className={styles.right}
icon="mediaPlayLast"
onClick={playLastHandler}
onLongPress={playLastShuffleHandler}
/>
</>
)}
@@ -326,26 +342,40 @@ const RatingButton = memo(
const PlayButton = memo(
({
disabled,
loading,
onClick,
onLongPress,
}: {
disabled?: 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>) => {
e.stopPropagation();
e.preventDefault();
if (disabled || loading) {
return;
}
onClick?.(e);
};
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();
e.preventDefault();
longPressHandlers.onMouseDown?.(e);
};
return (
@@ -354,8 +384,12 @@ const PlayButton = memo(
[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>
@@ -368,27 +402,47 @@ const SecondaryPlayButton = memo(
className,
icon,
onClick,
onLongPress,
}: {
className?: string;
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>) => {
e.stopPropagation();
e.preventDefault();
onClick?.(e);
};
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();
e.preventDefault();
longPressHandlers.onMouseDown?.(e);
};
return (
<button
className={clsx(styles.playButton, styles.secondary, className)}
onClick={handleClick}
className={clsx(styles.playButton, styles.secondary, className, {
[styles.disabled]: disabled,
})}
disabled={disabled}
onMouseDown={handleMouseDown}
onMouseLeave={longPressHandlers.onMouseLeave}
onMouseUp={longPressHandlers.onMouseUp}
onTouchCancel={longPressHandlers.onTouchCancel}
onTouchEnd={longPressHandlers.onTouchEnd}
onTouchStart={longPressHandlers.onTouchStart}
>
<Icon icon={icon} size="lg" />
</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,
};
};