handle image drag from item detail list

This commit is contained in:
jeffvli
2026-02-12 01:37:59 -08:00
parent dc957cb3cc
commit 1d8e1957ba
2 changed files with 109 additions and 31 deletions
@@ -196,6 +196,17 @@
min-width: 0; min-width: 0;
} }
.image-wrapper-outer {
position: relative;
display: block;
width: 100%;
aspect-ratio: 1;
}
.image-wrapper-outer.image-wrapper-dragging {
opacity: 0.5;
}
.image-wrapper { .image-wrapper {
position: relative; position: relative;
display: block; display: block;
@@ -32,10 +32,12 @@ import styles from './item-detail-list.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls';
import { ItemImage } from '/@/renderer/components/item-image/item-image'; import { ItemImage } from '/@/renderer/components/item-image/item-image';
import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { import {
ItemListStateActions, ItemListStateActions,
ItemListStateItemWithRequiredProperties, ItemListStateItemWithRequiredProperties,
useItemDraggingState,
useItemListState, useItemListState,
useItemSelectionState, useItemSelectionState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
@@ -62,6 +64,7 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useIsMutatingCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useIsMutatingDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useSettingsStore, useShowRatings } from '/@/renderer/store'; import { useSettingsStore, useShowRatings } from '/@/renderer/store';
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils'; import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
@@ -422,6 +425,61 @@ const MetadataSection = memo(
const [isImageHovered, setIsImageHovered] = useState(false); const [isImageHovered, setIsImageHovered] = useState(false);
const [isMetadataHovered, setIsMetadataHovered] = useState(false); const [isMetadataHovered, setIsMetadataHovered] = useState(false);
const getId = useCallback(() => {
const draggedItems = getDraggedItems(item, internalState, false);
return draggedItems.map((i) => i.id);
}, [item, internalState]);
const getItem = useCallback(() => {
return getDraggedItems(item, internalState, false);
}, [item, internalState]);
const onDragStart = useCallback(() => {
const draggedItems = getDraggedItems(item, internalState, false);
internalState?.setDragging(draggedItems);
}, [item, internalState]);
const onDrop = useCallback(() => {
internalState?.setDragging([]);
}, [internalState]);
const drag = useMemo(() => {
const playlistSongs = (item as { _playlistSongs?: Song[] })._playlistSongs;
if (playlistSongs && playlistSongs.length > 0) {
return {
getId,
getItem: () => playlistSongs,
itemType: LibraryItem.SONG,
onDragStart,
onDrop,
operation: [DragOperation.ADD],
target: DragTarget.SONG,
};
}
return {
getId,
getItem,
itemType: item._itemType,
onDragStart,
onDrop,
operation: [DragOperation.ADD],
target: DragTarget.ALBUM,
};
}, [getId, getItem, item, onDragStart, onDrop]);
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
drag,
isEnabled: !!item,
});
const isDraggingState = useItemDraggingState(internalState, item.id);
const isDragging = isDraggingState || isDraggingLocal;
const handleLinkDragStart = useCallback((e: React.DragEvent<HTMLAnchorElement>) => {
e.preventDefault();
e.stopPropagation();
}, []);
const isFavorite = item.userFavorite ?? false; const isFavorite = item.userFavorite ?? false;
const userRating = item.userRating ?? null; const userRating = item.userRating ?? null;
const hasRating = showRatings && userRating !== null && userRating > 0; const hasRating = showRatings && userRating !== null && userRating > 0;
@@ -483,39 +541,48 @@ const MetadataSection = memo(
onMouseEnter={() => setIsMetadataHovered(true)} onMouseEnter={() => setIsMetadataHovered(true)}
onMouseLeave={() => setIsMetadataHovered(false)} onMouseLeave={() => setIsMetadataHovered(false)}
> >
<Link <div
className={styles.imageWrapper} className={clsx(styles.imageWrapperOuter, {
onMouseEnter={() => setIsImageHovered(true)} [styles.imageWrapperDragging]: isDragging,
onMouseLeave={() => setIsImageHovered(false)}
state={{ item }}
to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
albumId: item.id,
})} })}
ref={dragRef ?? undefined}
> >
<ItemImage <Link
className={styles.image} className={styles.imageWrapper}
explicitStatus={item.explicitStatus} draggable={false}
id={item.imageId} onDragStart={handleLinkDragStart}
itemType={item._itemType} onMouseEnter={() => setIsImageHovered(true)}
serverId={item._serverId} onMouseLeave={() => setIsImageHovered(false)}
type="itemCard" state={{ item }}
/> to={generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, {
{isFavorite && <div className={styles.favoriteBadge} />} albumId: item.id,
{hasRating && <div className={styles.ratingBadge}>{userRating}</div>} })}
<AnimatePresence> >
{controls && isImageHovered && ( <ItemImage
<ItemCardControls className={styles.image}
controls={controls} explicitStatus={item.explicitStatus}
enableExpansion={false} id={item.imageId}
internalState={internalState} itemType={item._itemType}
item={item} serverId={item._serverId}
itemType={item._itemType} type="itemCard"
showRating={true} />
type="compact" {isFavorite && <div className={styles.favoriteBadge} />}
/> {hasRating && <div className={styles.ratingBadge}>{userRating}</div>}
)} <AnimatePresence>
</AnimatePresence> {controls && isImageHovered && (
</Link> <ItemCardControls
controls={controls}
enableExpansion={false}
internalState={internalState}
item={item}
itemType={item._itemType}
showRating={true}
type="compact"
/>
)}
</AnimatePresence>
</Link>
</div>
<Link <Link
className={styles.title} className={styles.title}
state={{ item }} state={{ item }}