diff --git a/src/renderer/hooks/use-drag-drop.tsx b/src/renderer/hooks/use-drag-drop.tsx index 76aa3ea89..cc5883517 100644 --- a/src/renderer/hooks/use-drag-drop.tsx +++ b/src/renderer/hooks/use-drag-drop.tsx @@ -14,8 +14,11 @@ import { dropTargetForElements, } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'; +import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; import { useEffect, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; +import { DragPreview } from '/@/shared/components/drag-preview/drag-preview'; import { LibraryItem } from '/@/shared/types/domain-types'; import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; @@ -85,14 +88,22 @@ export const useDragDrop = ({ return drag.onGenerateDragPreview(data); } + const dragData = dndUtils.generateDragData({ + id: drag.getId(), + item: drag.getItem(), + itemType: drag.itemType, + operation: drag.operation, + type: drag.target, + }) as DragData; + disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage }); - // setCustomNativeDragPreview({ - // nativeSetDragImage: data.nativeSetDragImage, - // // render: ({ container }) => { - // // const root = createRoot(container); - // // root.render(); - // // }, - // }); + setCustomNativeDragPreview({ + nativeSetDragImage: data.nativeSetDragImage, + render: ({ container }) => { + const root = createRoot(container); + root.render(); + }, + }); }, }), ); diff --git a/src/shared/components/drag-preview/drag-preview.module.css b/src/shared/components/drag-preview/drag-preview.module.css new file mode 100644 index 000000000..7d37019e4 --- /dev/null +++ b/src/shared/components/drag-preview/drag-preview.module.css @@ -0,0 +1,121 @@ +.container { + position: relative; + pointer-events: none; + user-select: none; + transform-style: preserve-3d; + perspective: 1000px; +} + +.preview { + position: relative; + display: flex; + align-items: center; + min-width: 200px; + max-width: 300px; + padding: var(--theme-spacing-md); + color: var(--theme-colors-foreground); + background: var(--theme-colors-background); + border: 1px solid var(--theme-colors-border); + border-radius: var(--theme-radius-lg); + box-shadow: + 0 25px 70px -15px rgb(0 0 0 / 40%), + 0 15px 40px -10px rgb(0 0 0 / 30%), + 0 0 0 1px rgb(255 255 255 / 8%), + inset 0 1px 0 rgb(255 255 255 / 10%); + backdrop-filter: blur(12px) saturate(180%); + transform: rotateX(-8deg) rotateY(8deg) translateZ(30px) scale(0.92); + transform-style: preserve-3d; + transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1); + animation: float 3s ease-in-out infinite; +} + +@keyframes float { + 0%, + 100% { + transform: rotateX(-8deg) rotateY(8deg) translateZ(30px) scale(0.92) translateY(0); + } + + 50% { + transform: rotateX(-8deg) rotateY(8deg) translateZ(30px) scale(0.92) translateY(-4px); + } +} + +.content { + display: flex; + gap: var(--theme-spacing-md); + align-items: center; + width: 100%; +} + +.image-container { + position: relative; + flex-shrink: 0; + width: 48px; + height: 48px; + overflow: hidden; + border-radius: var(--theme-radius-md); + box-shadow: + 0 8px 16px rgb(0 0 0 / 25%), + 0 0 0 1px rgb(255 255 255 / 5%); + transform: translateZ(15px) scale(1.05); + transition: transform 0.2s ease-out; +} + +.image { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-overlay { + position: absolute; + inset: 0; + pointer-events: none; + background: linear-gradient(135deg, rgb(255 255 255 / 10%) 0%, rgb(0 0 0 / 10%) 100%); +} + +.icon-container { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + background: linear-gradient( + 135deg, + var(--theme-colors-surface) 0%, + var(--theme-colors-background) 100% + ); + border: 1px solid var(--theme-colors-border); + border-radius: var(--theme-radius-md); + box-shadow: + 0 8px 16px rgb(0 0 0 / 25%), + 0 0 0 1px rgb(255 255 255 / 5%); + transform: translateZ(15px) scale(1.05); + transition: transform 0.2s ease-out; +} + +.text-container { + display: flex; + flex: 1; + flex-direction: column; + gap: var(--theme-spacing-xs); + min-width: 0; +} + +.name { + overflow: hidden; + text-overflow: ellipsis; + font-size: var(--theme-font-size-sm); + font-weight: 600; + line-height: 1.4; + color: var(--theme-colors-foreground); + white-space: nowrap; +} + +.count { + font-size: var(--theme-font-size-sm); + font-weight: 500; + line-height: 1.2; + color: var(--theme-colors-foreground-muted); +} diff --git a/src/shared/components/drag-preview/drag-preview.tsx b/src/shared/components/drag-preview/drag-preview.tsx new file mode 100644 index 000000000..7a963c252 --- /dev/null +++ b/src/shared/components/drag-preview/drag-preview.tsx @@ -0,0 +1,77 @@ +import { memo } from 'react'; + +import styles from './drag-preview.module.css'; + +import { Icon } from '/@/shared/components/icon/icon'; +import { LibraryItem } from '/@/shared/types/domain-types'; +import { DragData } from '/@/shared/types/drag-and-drop'; + +interface DragPreviewProps { + data: DragData; +} + +const getItemName = (item: unknown): string => { + if (item && typeof item === 'object') { + if ('name' in item && typeof item.name === 'string') { + return item.name; + } + if ('title' in item && typeof item.title === 'string') { + return item.title; + } + } + return 'Item'; +}; + +const getItemImage = (item: unknown): null | string => { + if (item && typeof item === 'object') { + if ('imageUrl' in item && typeof item.imageUrl === 'string') { + return item.imageUrl; + } + } + return null; +}; + +export const DragPreview = memo(({ data }: DragPreviewProps) => { + const items = data.item || []; + const itemCount = items.length; + const firstItem = items[0]; + const itemName = firstItem ? getItemName(firstItem) : 'Item'; + const itemImage = firstItem ? getItemImage(firstItem) : null; + const isMultiple = itemCount > 1; + + return ( +
+
+
+ {itemImage ? ( +
+ {itemName} +
+
+ ) : ( +
+ {data.itemType === LibraryItem.ALBUM && } + {data.itemType === LibraryItem.SONG && ( + + )} + {data.itemType === LibraryItem.ARTIST && ( + + )} + {data.itemType === LibraryItem.PLAYLIST && ( + + )} + {data.itemType === LibraryItem.GENRE && } + {!data.itemType && } +
+ )} +
+
{itemName}
+ {isMultiple &&
+{itemCount - 1} more
} +
+
+
+
+ ); +}); + +DragPreview.displayName = 'DragPreview';