From 19a51d82be5b50a60e2ed058a37d87034ecef510 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 22 Nov 2025 04:33:12 -0800 Subject: [PATCH] fix unneccesary navigation on grid card control click --- .../item-card/item-card-controls.tsx | 489 +++++++++++------- 1 file changed, 295 insertions(+), 194 deletions(-) diff --git a/src/renderer/components/item-card/item-card-controls.tsx b/src/renderer/components/item-card/item-card-controls.tsx index 75539bcd7..a7d345e87 100644 --- a/src/renderer/components/item-card/item-card-controls.tsx +++ b/src/renderer/components/item-card/item-card-controls.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { motion } from 'motion/react'; -import { MouseEvent } from 'react'; +import { memo, MouseEvent, useMemo } from 'react'; import styles from './item-card-controls.module.css'; @@ -50,6 +50,134 @@ const containerProps = { }, }; +const createPlayHandler = + ( + controls: ItemControls | undefined, + item: Album | AlbumArtist | Artist | Playlist | Song | undefined, + internalState: ItemListStateActions | undefined, + itemType: LibraryItem, + playType: Play, + ) => + (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!item) { + return; + } + + controls?.onPlay?.({ + event: e, + internalState, + item, + itemType, + playType, + }); + }; + +const createFavoriteHandler = + ( + controls: ItemControls | undefined, + item: Album | AlbumArtist | Artist | Playlist | Song | undefined, + internalState: ItemListStateActions | undefined, + itemType: LibraryItem, + ) => + (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + + if (!item) { + return; + } + + const newFavorite = !(item as { userFavorite: boolean }).userFavorite; + controls?.onFavorite?.({ + event: e, + favorite: newFavorite, + internalState, + item, + itemType, + }); + }; + +const createRatingChangeHandler = + ( + controls: ItemControls | undefined, + item: Album | AlbumArtist | Artist | Playlist | Song | undefined, + internalState: ItemListStateActions | undefined, + itemType: LibraryItem, + ) => + (rating: number) => { + if (!item) { + return; + } + + let newRating = rating; + + if (rating === (item as { userRating: number }).userRating) { + newRating = 0; + } + + controls?.onRating?.({ + event: null, + internalState, + item, + itemType, + rating: newRating, + }); + }; + +const ratingClickHandler = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); +}; + +const ratingMouseDownHandler = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); +}; + +const moreDoubleClickHandler = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); +}; + +const createMoreHandler = + ( + controls: ItemControls | undefined, + item: Album | AlbumArtist | Artist | Playlist | Song | undefined, + internalState: ItemListStateActions | undefined, + itemType: LibraryItem, + ) => + (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + controls?.onMore?.({ + event: e, + internalState, + item, + itemType, + }); + }; + +const createExpandHandler = + ( + controls: ItemControls | undefined, + item: Album | AlbumArtist | Artist | Playlist | Song | undefined, + internalState: ItemListStateActions | undefined, + itemType: LibraryItem, + ) => + (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + controls?.onExpand?.({ + event: e, + internalState, + item, + itemType, + }); + }; + export const ItemCardControls = ({ controls, enableExpansion, @@ -60,65 +188,65 @@ export const ItemCardControls = ({ }: ItemCardControlsProps) => { const isPlayerFetching = useIsPlayerFetching(); + const playNowHandler = useMemo( + () => createPlayHandler(controls, item, internalState, itemType, Play.NOW), + [controls, item, internalState, itemType], + ); + + const playNextHandler = useMemo( + () => createPlayHandler(controls, item, internalState, itemType, Play.NEXT), + [controls, item, internalState, itemType], + ); + + const playLastHandler = useMemo( + () => createPlayHandler(controls, item, internalState, itemType, Play.LAST), + [controls, item, internalState, itemType], + ); + + const favoriteHandler = useMemo( + () => createFavoriteHandler(controls, item, internalState, itemType), + [controls, item, internalState, itemType], + ); + + const ratingChangeHandler = useMemo( + () => createRatingChangeHandler(controls, item, internalState, itemType), + [controls, item, internalState, itemType], + ); + + const moreHandler = useMemo( + () => createMoreHandler(controls, item, internalState, itemType), + [controls, item, internalState, itemType], + ); + + const expandHandler = useMemo( + () => createExpandHandler(controls, item, internalState, itemType), + [controls, item, internalState, itemType], + ); + + const isFavorite = (item as { userFavorite?: boolean })?.userFavorite ?? false; + + const favoriteIconProps = useMemo>( + () => ({ + color: isFavorite ? ('primary' as const) : ('default' as const), + fill: isFavorite ? ('primary' as const) : undefined, + }), + [isFavorite], + ); + return ( {controls?.onPlay && ( <> - { - e.stopPropagation(); - - if (!item) { - return; - } - - controls?.onPlay?.({ - event: e, - internalState, - item, - itemType, - playType: Play.NOW, - }); - }} - /> + { - e.stopPropagation(); - - if (!item) { - return; - } - - controls?.onPlay?.({ - event: e, - internalState, - item, - itemType, - playType: Play.NEXT, - }); - }} + onClick={playNextHandler} /> { - e.stopPropagation(); - - if (!item) { - return; - } - - controls?.onPlay?.({ - event: e, - internalState, - item, - itemType, - playType: Play.LAST, - }); - }} + onClick={playLastHandler} /> )} @@ -126,57 +254,16 @@ export const ItemCardControls = ({ { - e.stopPropagation(); - - if (!item) { - return; - } - - const newFavorite = !(item as { userFavorite: boolean }).userFavorite; - controls?.onFavorite?.({ - event: e, - favorite: newFavorite, - internalState, - item, - itemType, - }); - }} + iconProps={favoriteIconProps} + onClick={favoriteHandler} /> )} {controls?.onRating && ( { - 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(); - }} + onChange={ratingChangeHandler} + onClick={ratingClickHandler} + onMouseDown={ratingMouseDownHandler} size="xs" /> )} @@ -184,90 +271,92 @@ export const ItemCardControls = ({ { - e.stopPropagation(); - e.preventDefault(); - controls?.onMore?.({ - event: e, - internalState, - item, - itemType, - }); - }} - onDoubleClick={(e) => { - e.stopPropagation(); - e.preventDefault(); - }} + onClick={moreHandler} + onDoubleClick={moreDoubleClickHandler} /> )} {controls?.onExpand && enableExpansion && ( { - e.stopPropagation(); - controls?.onExpand?.({ - event: e, - internalState, - item, - itemType, - }); - }} + onClick={expandHandler} /> )} ); }; -const PlayButton = ({ - disabled, - loading, - onClick, -}: { - disabled?: boolean; - loading?: boolean; - onClick?: (e: MouseEvent) => void; -}) => { - return ( - - ); -}; +const PlayButton = memo( + ({ + disabled, + loading, + onClick, + }: { + disabled?: boolean; + loading?: boolean; + onClick?: (e: MouseEvent) => void; + }) => { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + if (disabled || loading) { + return; + } + onClick?.(e); + }; -const SecondaryPlayButton = ({ - className, - icon, - onClick, -}: { - className?: string; - icon: keyof typeof AppIcon; - onClick?: (e: MouseEvent) => void; -}) => { - return ( - - ); -}; + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + return ( + + ); + }, +); + +const SecondaryPlayButton = memo( + ({ + className, + icon, + onClick, + }: { + className?: string; + icon: keyof typeof AppIcon; + onClick?: (e: MouseEvent) => void; + }) => { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onClick?.(e); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + return ( + + ); + }, +); interface SecondaryButtonProps { className?: string; @@ -275,31 +364,43 @@ interface SecondaryButtonProps { onClick?: (e: MouseEvent) => void; } -const SecondaryButton = ({ - className, - icon, - iconProps, - onClick, - onDoubleClick, -}: SecondaryButtonProps & { - iconProps?: Partial; - onDoubleClick?: (e: MouseEvent) => void; -}) => { - return ( - - ); -}; +const SecondaryButton = memo( + ({ + className, + icon, + iconProps, + onClick, + onDoubleClick, + }: SecondaryButtonProps & { + iconProps?: Partial; + onDoubleClick?: (e: MouseEvent) => void; + }) => { + const handleClick = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onClick?.(e); + }; + + const handleDoubleClick = (e: MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + onDoubleClick?.(e); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + return ( + + ); + }, +);