diff --git a/src/renderer/components/item-card/item-card-controls.tsx b/src/renderer/components/item-card/item-card-controls.tsx index 4a32f10f8..3f6e9cdd7 100644 --- a/src/renderer/components/item-card/item-card-controls.tsx +++ b/src/renderer/components/item-card/item-card-controls.tsx @@ -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 = ({ {controls?.onPlay && ( <> - + )} @@ -326,26 +342,40 @@ const RatingButton = memo( const PlayButton = memo( ({ - disabled, loading, onClick, + onLongPress, }: { - disabled?: boolean; loading?: boolean; - onClick?: (e: MouseEvent) => void; + onClick?: (e: React.MouseEvent) => void; + onLongPress?: (e: React.MouseEvent) => void; }) => { - const handleClick = (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - if (disabled || loading) { - return; - } - onClick?.(e); - }; + const isPlayerFetching = useIsPlayerFetching(); + + const disabled = isPlayerFetching || loading; + + const longPressHandlers = useLongPress({ + onClick: (e) => { + if (disabled || loading) { + return; + } + e.stopPropagation(); + e.preventDefault(); + onClick?.(e as React.MouseEvent); + }, + onLongPress: (e) => { + if (disabled || loading) { + return; + } + e.stopPropagation(); + e.preventDefault(); + onLongPress?.(e as React.MouseEvent); + }, + }); const handleMouseDown = (e: React.MouseEvent) => { 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} > @@ -368,27 +402,47 @@ const SecondaryPlayButton = memo( className, icon, onClick, + onLongPress, }: { className?: string; icon: keyof typeof AppIcon; - onClick?: (e: MouseEvent) => void; + onClick?: (e: React.MouseEvent) => void; + onLongPress?: (e: React.MouseEvent) => void; }) => { - const handleClick = (e: MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - onClick?.(e); - }; + const isPlayerFetching = useIsPlayerFetching(); + + const disabled = isPlayerFetching; + + const longPressHandlers = useLongPress({ + onClick: (e) => { + e.stopPropagation(); + e.preventDefault(); + onClick?.(e as React.MouseEvent); + }, + onLongPress: (e) => { + e.stopPropagation(); + e.preventDefault(); + onLongPress?.(e as React.MouseEvent); + }, + }); const handleMouseDown = (e: React.MouseEvent) => { e.stopPropagation(); - e.preventDefault(); + longPressHandlers.onMouseDown?.(e); }; return ( diff --git a/src/shared/hooks/use-long-press.ts b/src/shared/hooks/use-long-press.ts index 01afac4ab..e23e8c2a8 100644 --- a/src/shared/hooks/use-long-press.ts +++ b/src/shared/hooks/use-long-press.ts @@ -1,3 +1,104 @@ -import { useLongPress as useMantineLongPress } from '@mantine/hooks'; +import { useCallback, useRef } from 'react'; -export const useLongPress = useMantineLongPress; +interface UseLongPressOptions { + delay?: number; + onClick?: (event: React.MouseEvent | React.TouchEvent) => void; + onLongPress?: (event: React.MouseEvent | React.TouchEvent) => 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 = ({ + delay = 500, + onClick, + onLongPress, +}: UseLongPressOptions): UseLongPressReturn => { + const timeoutRef = useRef(null); + const targetRef = useRef(null); + const longPressTriggeredRef = useRef(false); + const eventRef = useRef | React.TouchEvent>(null); + + const start = useCallback( + (event: React.MouseEvent | React.TouchEvent) => { + 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); + }, + [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); + }, + [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, + }; +};