From ad409fecfacf910db78a70c34deaaf8c4cf3f4ac Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 9 Nov 2025 00:36:35 -0800 Subject: [PATCH] add drag state to item grid --- .../components/item-card/item-card.module.css | 10 +- .../components/item-card/item-card.tsx | 48 ++++++ .../item-list/expanded-list-item.tsx | 4 +- .../item-list/helpers/get-dragged-items.ts | 69 ++++++++ .../item-list/helpers/item-list-controls.ts | 6 +- .../helpers/item-list-reducer-utils.ts | 49 +++++- .../item-list/helpers/item-list-state.ts | 128 ++++++++++++--- .../item-grid-list/item-grid-list.tsx | 23 +-- .../item-table-list/item-table-list.tsx | 10 +- .../components/expanded-album-list-item.tsx | 4 +- .../playlist-detail-song-list-route.tsx | 14 +- src/renderer/hooks/use-drag-drop.tsx | 155 ++++++++++++++++++ src/shared/types/drag-and-drop.ts | 2 +- 13 files changed, 451 insertions(+), 71 deletions(-) create mode 100644 src/renderer/components/item-list/helpers/get-dragged-items.ts create mode 100644 src/renderer/hooks/use-drag-drop.tsx diff --git a/src/renderer/components/item-card/item-card.module.css b/src/renderer/components/item-card/item-card.module.css index 702219dd5..9a5728c93 100644 --- a/src/renderer/components/item-card/item-card.module.css +++ b/src/renderer/components/item-card/item-card.module.css @@ -10,17 +10,15 @@ border-radius: var(--theme-radius-md); } -.container.previewed { - outline: 2px dashed var(--theme-colors-primary); - outline-offset: 2px; - opacity: 0.7; -} - .container.selected { outline: 2px solid var(--theme-colors-primary); outline-offset: 2px; } +.container.dragging { + opacity: 0.5; +} + .image-container { position: relative; width: 100%; diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 9cc3a7730..e9a837e9c 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -6,8 +6,10 @@ import { generatePath, Link } from 'react-router'; import styles from './item-card.module.css'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; +import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; import { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls } from '/@/renderer/components/item-list/types'; +import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; import { Image } from '/@/shared/components/image/image'; import { Separator } from '/@/shared/components/separator/separator'; @@ -21,10 +23,12 @@ import { Playlist, Song, } from '/@/shared/types/domain-types'; +import { DragTarget } from '/@/shared/types/drag-and-drop'; export interface ItemCardProps { controls?: ItemControls; data: Album | AlbumArtist | Artist | Playlist | Song | undefined; + enableDrag?: boolean; internalState?: ItemListStateActions; isRound?: boolean; itemType: LibraryItem; @@ -41,6 +45,7 @@ type DataRow = { export const ItemCard = ({ controls, data, + enableDrag, internalState, isRound, itemType, @@ -56,6 +61,7 @@ export const ItemCard = ({ ({ + drag: { + getId: () => { + if (!data) { + return []; + } + + const draggedItems = getDraggedItems(data, itemType, internalState); + return draggedItems.map((item) => item.id); + }, + getItem: () => { + if (!data) { + return []; + } + + return [data]; + }, + onDragStart: () => { + if (!data || !internalState) { + return; + } + + const draggedItems = getDraggedItems(data, itemType, internalState); + internalState.setDragging(draggedItems); + }, + onDrop: () => { + if (internalState) { + internalState.setDragging([]); + } + }, + target: DragTarget.ALBUM, + }, + isEnabled: !!enableDrag && !!data, + }); + + const isDragging = data && internalState ? internalState.isDragging(data.id) : isDraggingLocal; + if (data) { const handleMouseEnter = () => { if (withControls) { @@ -364,8 +410,10 @@ const PosterItemCard = ({ return (
{ + return { + _serverId: data._serverId, + id: data.id, + itemType, + }; +}; + +/** + * Gets the items that should be dragged based on the current data and selection state. + * If the current item is already selected, drag all selected items. + * Otherwise, select and drag only the current item. + * + * @param data - The item data to drag (Album, AlbumArtist, Artist, Playlist, or Song) + * @param itemType - The type of library item + * @param internalState - The item list state actions (optional) + * @param updateSelection - Whether to update the selection state (default: true) + * @returns Array of ItemListItem objects that should be dragged + */ +export const getDraggedItems = ( + data: Album | AlbumArtist | Artist | Playlist | Song | undefined, + itemType: LibraryItem, + internalState?: ItemListStateActions, + updateSelection: boolean = true, +): ItemListStateItem[] => { + if (!data || !internalState) { + return []; + } + + // Convert data to ItemListStateItem format + const draggedItem = convertToItemListItem(data, itemType); + + const previouslySelected = internalState.getSelected(); + const isDraggingSelectedItem = previouslySelected.some((selected) => selected.id === data.id); + + const draggedItems: ItemListStateItem[] = []; + + if (isDraggingSelectedItem) { + // If dragging a selected item, drag all selected items + draggedItems.push(...previouslySelected); + } else { + // If dragging an unselected item, select it and drag only it + if (updateSelection) { + internalState.setSelected([draggedItem]); + } + draggedItems.push(draggedItem); + } + + return draggedItems; +}; diff --git a/src/renderer/components/item-list/helpers/item-list-controls.ts b/src/renderer/components/item-list/helpers/item-list-controls.ts index cbf14581f..3a44418c7 100644 --- a/src/renderer/components/item-list/helpers/item-list-controls.ts +++ b/src/renderer/components/item-list/helpers/item-list-controls.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react'; -import { ItemListItem } from '/@/renderer/components/item-list/helpers/item-list-state'; +import { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state'; import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types'; import { usePlayerContext } from '/@/renderer/features/player/context/player-context'; import { Play } from '/@/shared/types/types'; @@ -15,7 +15,7 @@ export const useDefaultItemListControls = () => { return; } - const itemListItem: ItemListItem = { + const itemListItem: ItemListStateItem = { _serverId: item._serverId, id: item.id, itemType, @@ -61,7 +61,7 @@ export const useDefaultItemListControls = () => { const startIndex = Math.min(lastIndex, currentIndex); const stopIndex = Math.max(lastIndex, currentIndex); - const rangeItems: ItemListItem[] = []; + const rangeItems: ItemListStateItem[] = []; for (let i = startIndex; i <= stopIndex; i++) { const rangeItem = validData[i]; if ( diff --git a/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts b/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts index bca85dd58..065800157 100644 --- a/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts +++ b/src/renderer/components/item-list/helpers/item-list-reducer-utils.ts @@ -1,4 +1,4 @@ -import { ItemListAction, ItemListItem, ItemListState } from './item-list-state'; +import { ItemListAction, ItemListStateItem, ItemListState } from './item-list-state'; /** * Action creators for item grid state management @@ -17,22 +17,27 @@ export const itemGridActions = { type: 'CLEAR_SELECTED', }), - setExpanded: (items: ItemListItem[]): ItemListAction => ({ + setDragging: (items: ItemListStateItem[]): ItemListAction => ({ + payload: items, + type: 'SET_DRAGGING', + }), + + setExpanded: (items: ItemListStateItem[]): ItemListAction => ({ payload: items, type: 'SET_EXPANDED', }), - setSelected: (items: ItemListItem[]): ItemListAction => ({ + setSelected: (items: ItemListStateItem[]): ItemListAction => ({ payload: items, type: 'SET_SELECTED', }), - toggleExpanded: (item: ItemListItem): ItemListAction => ({ + toggleExpanded: (item: ItemListStateItem): ItemListAction => ({ payload: item, type: 'TOGGLE_EXPANDED', }), - toggleSelected: (item: ItemListItem): ItemListAction => ({ + toggleSelected: (item: ItemListStateItem): ItemListAction => ({ payload: item, type: 'TOGGLE_SELECTED', }), @@ -43,7 +48,19 @@ export const itemGridActions = { * These can be reused to extract specific data from state */ export const itemGridSelectors = { - getExpanded: (state: ItemListState): ItemListItem[] => { + getDragging: (state: ItemListState): ItemListStateItem[] => { + return Array.from(state.draggingItems.values()); + }, + + getDraggingCount: (state: ItemListState): number => { + return state.dragging.size; + }, + + getDraggingIds: (state: ItemListState): string[] => { + return Array.from(state.dragging); + }, + + getExpanded: (state: ItemListState): ItemListStateItem[] => { return Array.from(state.expandedItems.values()); }, @@ -55,7 +72,7 @@ export const itemGridSelectors = { return Array.from(state.expanded); }, - getSelected: (state: ItemListState): ItemListItem[] => { + getSelected: (state: ItemListState): ItemListStateItem[] => { return Array.from(state.selectedItems.values()); }, @@ -71,6 +88,10 @@ export const itemGridSelectors = { return state.version; }, + hasAnyDragging: (state: ItemListState): boolean => { + return state.dragging.size > 0; + }, + hasAnyExpanded: (state: ItemListState): boolean => { return state.expanded.size > 0; }, @@ -79,6 +100,10 @@ export const itemGridSelectors = { return state.selected.size > 0; }, + isDragging: (state: ItemListState, itemId: string): boolean => { + return state.dragging.has(itemId); + }, + isExpanded: (state: ItemListState, itemId: string): boolean => { return state.expanded.has(itemId); }, @@ -120,7 +145,10 @@ export const itemListUtils = { /** * Toggle expansion of all items in a list */ - toggleAllExpanded: (items: ItemListItem[], currentState: ItemListState): ItemListAction => { + toggleAllExpanded: ( + items: ItemListStateItem[], + currentState: ItemListState, + ): ItemListAction => { const allExpanded = items.every((item) => currentState.expanded.has(item.id)); return allExpanded ? itemGridActions.clearExpanded() : itemGridActions.setExpanded(items); }, @@ -128,7 +156,10 @@ export const itemListUtils = { /** * Toggle selection of all items in a list */ - toggleAllSelected: (items: ItemListItem[], currentState: ItemListState): ItemListAction => { + toggleAllSelected: ( + items: ItemListStateItem[], + currentState: ItemListState, + ): ItemListAction => { const allSelected = items.every((item) => currentState.selected.has(item.id)); return allSelected ? itemGridActions.clearSelected() : itemGridActions.setSelected(items); }, diff --git a/src/renderer/components/item-list/helpers/item-list-state.ts b/src/renderer/components/item-list/helpers/item-list-state.ts index 22890cb41..548ffac4a 100644 --- a/src/renderer/components/item-list/helpers/item-list-state.ts +++ b/src/renderer/components/item-list/helpers/item-list-state.ts @@ -4,47 +4,57 @@ import { itemGridSelectors } from '/@/renderer/components/item-list/helpers/item import { LibraryItem } from '/@/shared/types/domain-types'; export type ItemListAction = - | { payload: ItemListItem; type: 'TOGGLE_EXPANDED' } - | { payload: ItemListItem; type: 'TOGGLE_SELECTED' } - | { payload: ItemListItem[]; type: 'SET_EXPANDED' } - | { payload: ItemListItem[]; type: 'SET_SELECTED' } + | { payload: ItemListStateItem; type: 'TOGGLE_EXPANDED' } + | { payload: ItemListStateItem; type: 'TOGGLE_SELECTED' } + | { payload: ItemListStateItem[]; type: 'SET_DRAGGING' } + | { payload: ItemListStateItem[]; type: 'SET_EXPANDED' } + | { payload: ItemListStateItem[]; type: 'SET_SELECTED' } | { type: 'CLEAR_ALL' } + | { type: 'CLEAR_DRAGGING' } | { type: 'CLEAR_EXPANDED' } | { type: 'CLEAR_SELECTED' }; -export interface ItemListItem { - _serverId: string; - id: string; - itemType: LibraryItem; -} - export interface ItemListState { + dragging: Set; + draggingItems: Map; expanded: Set; - expandedItems: Map; + expandedItems: Map; selected: Set; - selectedItems: Map; + selectedItems: Map; version: number; } export interface ItemListStateActions { clearAll: () => void; + clearDragging: () => void; clearExpanded: () => void; clearSelected: () => void; findItemIndex: (itemId: string) => number; getData: () => unknown[]; - getExpanded: () => ItemListItem[]; + getDragging: () => ItemListStateItem[]; + getDraggingIds: () => string[]; + getExpanded: () => ItemListStateItem[]; getExpandedIds: () => string[]; - getSelected: () => ItemListItem[]; + getSelected: () => ItemListStateItem[]; getSelectedIds: () => string[]; getVersion: () => number; + hasDragging: () => boolean; hasExpanded: () => boolean; hasSelected: () => boolean; + isDragging: (itemId: string) => boolean; isExpanded: (itemId: string) => boolean; isSelected: (itemId: string) => boolean; - setExpanded: (items: ItemListItem[]) => void; - setSelected: (items: ItemListItem[]) => void; - toggleExpanded: (item: ItemListItem) => void; - toggleSelected: (item: ItemListItem) => void; + setDragging: (items: ItemListStateItem[]) => void; + setExpanded: (items: ItemListStateItem[]) => void; + setSelected: (items: ItemListStateItem[]) => void; + toggleExpanded: (item: ItemListStateItem) => void; + toggleSelected: (item: ItemListStateItem) => void; +} + +export interface ItemListStateItem { + _serverId: string; + id: string; + itemType: LibraryItem; } /** @@ -56,6 +66,8 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I case 'CLEAR_ALL': return { ...state, + dragging: new Set(), + draggingItems: new Map(), expanded: new Set(), expandedItems: new Map(), selected: new Set(), @@ -63,6 +75,14 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I version: state.version + 1, }; + case 'CLEAR_DRAGGING': + return { + ...state, + dragging: new Set(), + draggingItems: new Map(), + version: state.version + 1, + }; + case 'CLEAR_EXPANDED': return { ...state, @@ -79,9 +99,26 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I version: state.version + 1, }; + case 'SET_DRAGGING': { + const newDragging = new Set(); + const newDraggingItems = new Map(); + + action.payload.forEach((item) => { + newDragging.add(item.id); + newDraggingItems.set(item.id, item); + }); + + return { + ...state, + dragging: newDragging, + draggingItems: newDraggingItems, + version: state.version + 1, + }; + } + case 'SET_EXPANDED': { const newExpanded = new Set(); - const newExpandedItems = new Map(); + const newExpandedItems = new Map(); console.log('SET_EXPANDED', action.payload); @@ -101,7 +138,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I case 'SET_SELECTED': { const newSelected = new Set(); - const newSelectedItems = new Map(); + const newSelectedItems = new Map(); action.payload.forEach((item) => { newSelected.add(item.id); @@ -118,7 +155,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I case 'TOGGLE_EXPANDED': { const newExpanded = new Set(); - const newExpandedItems = new Map(); + const newExpandedItems = new Map(); // If the item is already expanded, collapse it if (state.expanded.has(action.payload.id)) { @@ -163,6 +200,8 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I }; export const initialItemListState: ItemListState = { + dragging: new Set(), + draggingItems: new Map(), expanded: new Set(), expandedItems: new Map(), selected: new Set(), @@ -173,19 +212,23 @@ export const initialItemListState: ItemListState = { export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActions => { const [state, dispatch] = useReducer(itemListReducer, initialItemListState); - const setExpanded = useCallback((items: ItemListItem[]) => { + const setExpanded = useCallback((items: ItemListStateItem[]) => { dispatch({ payload: items, type: 'SET_EXPANDED' }); }, []); - const setSelected = useCallback((items: ItemListItem[]) => { + const setDragging = useCallback((items: ItemListStateItem[]) => { + dispatch({ payload: items, type: 'SET_DRAGGING' }); + }, []); + + const setSelected = useCallback((items: ItemListStateItem[]) => { dispatch({ payload: items, type: 'SET_SELECTED' }); }, []); - const toggleExpanded = useCallback((item: ItemListItem) => { + const toggleExpanded = useCallback((item: ItemListStateItem) => { dispatch({ payload: item, type: 'TOGGLE_EXPANDED' }); }, []); - const toggleSelected = useCallback((item: ItemListItem) => { + const toggleSelected = useCallback((item: ItemListStateItem) => { dispatch({ payload: item, type: 'TOGGLE_SELECTED' }); }, []); @@ -207,10 +250,18 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi return itemGridSelectors.getExpanded(state); }, [state]); + const getDragging = useCallback(() => { + return itemGridSelectors.getDragging(state); + }, [state]); + const getSelected = useCallback(() => { return itemGridSelectors.getSelected(state); }, [state]); + const getDraggingIds = useCallback(() => { + return Array.from(state.dragging); + }, [state.dragging]); + const getExpandedIds = useCallback(() => { return Array.from(state.expanded); }, [state.expanded]); @@ -223,6 +274,10 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi dispatch({ type: 'CLEAR_EXPANDED' }); }, []); + const clearDragging = useCallback(() => { + dispatch({ type: 'CLEAR_DRAGGING' }); + }, []); + const clearSelected = useCallback(() => { dispatch({ type: 'CLEAR_SELECTED' }); }, []); @@ -239,10 +294,21 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi return itemGridSelectors.hasAnyExpanded(state); }, [state]); + const hasDragging = useCallback(() => { + return itemGridSelectors.hasAnyDragging(state); + }, [state]); + const hasSelected = useCallback(() => { return itemGridSelectors.hasAnySelected(state); }, [state]); + const isDragging = useCallback( + (itemId: string) => { + return itemGridSelectors.isDragging(state, itemId); + }, + [state], + ); + const getData = useCallback(() => { return getDataFn ? getDataFn() : []; }, [getDataFn]); @@ -260,19 +326,25 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi return useMemo( () => ({ clearAll, + clearDragging, clearExpanded, clearSelected, findItemIndex, getData, + getDragging, + getDraggingIds, getExpanded, getExpandedIds, getSelected, getSelectedIds, getVersion, + hasDragging, hasExpanded, hasSelected, + isDragging, isExpanded, isSelected, + setDragging, setExpanded, setSelected, toggleExpanded, @@ -280,19 +352,25 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi }), [ clearAll, + clearDragging, clearExpanded, clearSelected, findItemIndex, getData, + getDragging, + getDraggingIds, getExpanded, getExpandedIds, getSelected, getSelectedIds, getVersion, + hasDragging, hasExpanded, hasSelected, + isDragging, isExpanded, isSelected, + setDragging, setExpanded, setSelected, toggleExpanded, diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx index 47c09b18a..8e105f09f 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx @@ -36,8 +36,8 @@ import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { - ItemListItem, ItemListStateActions, + ItemListStateItem, useItemListState, } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; @@ -46,6 +46,7 @@ import { LibraryItem } from '/@/shared/types/domain-types'; interface VirtualizedGridListProps { controls: ItemControls; data: unknown[]; + enableDrag?: boolean; enableExpansion: boolean; enableSelection: boolean; gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; @@ -68,6 +69,7 @@ const VirtualizedGridList = React.memo( ({ controls, data, + enableDrag, enableExpansion, enableSelection, gap, @@ -86,6 +88,7 @@ const VirtualizedGridList = React.memo( columns: tableMeta?.columnCount || 0, controls, data, + enableDrag, enableExpansion, enableSelection, gap, @@ -97,6 +100,7 @@ const VirtualizedGridList = React.memo( tableMeta, controls, data, + enableDrag, enableExpansion, enableSelection, gap, @@ -229,6 +233,7 @@ export interface GridItemProps { columns: number; controls: ItemCardProps['controls']; data: any[]; + enableDrag?: boolean; enableExpansion?: boolean; enableSelection?: boolean; gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; @@ -244,6 +249,7 @@ export interface GridItemProps { export interface ItemGridListProps { currentPage?: number; data: unknown[]; + enableDrag?: boolean; enableExpansion?: boolean; enableSelection?: boolean; gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; @@ -258,6 +264,7 @@ export interface ItemGridListProps { export const ItemGridList = ({ data, + enableDrag = true, enableExpansion = true, enableSelection = true, gap = 'sm', @@ -336,7 +343,6 @@ export const ItemGridList = ({ const controls = useDefaultItemListControls(); - // Scroll to a specific index const scrollToIndex = useCallback( (index: number) => { if (!listRef.current || !tableMeta) return; @@ -346,7 +352,6 @@ export const ItemGridList = ({ [tableMeta], ); - // Scroll to a specific offset const scrollToOffset = useCallback((offset: number) => { if (!listRef.current) return; listRef.current.scrollTo(offset); @@ -355,7 +360,6 @@ export const ItemGridList = ({ // Handle keyboard navigation const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { - console.log('handleKeyDown', e.key); if (!enableSelection || !tableMeta) return; if ( e.key !== 'ArrowDown' && @@ -480,7 +484,7 @@ export const ItemGridList = ({ const startIndex = Math.min(lastIndex, newIndex); const stopIndex = Math.max(lastIndex, newIndex); - const rangeItems: ItemListItem[] = []; + const rangeItems: ItemListStateItem[] = []; for (let i = startIndex; i <= stopIndex; i++) { const rangeItem = data[i]; if ( @@ -507,7 +511,7 @@ export const ItemGridList = ({ }); // Ensure the last item in selection is the item at newIndex for incremental extension - const newItemListItem: ItemListItem = { + const newItemListItem: ItemListStateItem = { _serverId: newItem.serverId, id: newItem.id, itemType, @@ -546,7 +550,6 @@ export const ItemGridList = ({ [data, enableSelection, internalState, itemType, tableMeta, scrollToIndex], ); - // Create imperative handle const imperativeHandle: ItemListHandle = useMemo(() => { return { clearExpanded: () => { @@ -568,12 +571,10 @@ export const ItemGridList = ({ }; }, [data, internalState, scrollToIndex, scrollToOffset]); - // Expose handle via ref useEffect(() => { handleRef.current = imperativeHandle; }, [imperativeHandle]); - // Expose handle via forwardRef useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]); return ( @@ -588,6 +589,7 @@ export const ItemGridList = ({ ) => { const { index, style } = props; - const { columns, controls, data, gap, itemType } = props.data; + const { columns, controls, data, enableDrag, gap, itemType } = props.data; const items: ReactNode[] = []; const itemCount = data.length; @@ -640,6 +642,7 @@ const ListComponent = memo((props: ListChildComponentProps) => { { diff --git a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx index b2f3b25f1..b05afa55c 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx @@ -7,7 +7,6 @@ import { useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, useNavigate, useParams } from 'react-router'; -import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add'; import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api'; import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header'; import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder'; @@ -32,7 +31,6 @@ const PlaylistDetailSongListRoute = () => { const tableRef = useRef(null); const { playlistId } = useParams() as { playlistId: string }; const server = useCurrentServer(); - const handlePlayQueueAdd = useHandlePlayQueueAdd(); const detailQuery = useQuery( playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }), @@ -159,13 +157,13 @@ const PlaylistDetailSongListRoute = () => { ); const filterSortedSongs = useMemo(() => { - let items = playlistSongs.data?.items; + const items = playlistSongs.data?.items; if (items) { const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm; if (searchTerm) { - items = searchSongs(items, searchTerm); + // items = searchSongs(items, searchTerm); } const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID; @@ -182,10 +180,10 @@ const PlaylistDetailSongListRoute = () => { : undefined; const handlePlay = (play: Play) => { - handlePlayQueueAdd?.({ - byData: filterSortedSongs, - playType: play, - }); + // handlePlayQueueAdd?.({ + // byData: filterSortedSongs, + // playType: play, + // }); }; return ( diff --git a/src/renderer/hooks/use-drag-drop.tsx b/src/renderer/hooks/use-drag-drop.tsx new file mode 100644 index 000000000..e65f8f662 --- /dev/null +++ b/src/renderer/hooks/use-drag-drop.tsx @@ -0,0 +1,155 @@ +import { + attachClosestEdge, + type Edge, + extractClosestEdge, +} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge'; +import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine'; +import { + BaseEventPayload, + CleanupFn, + ElementDragType, + Input, +} from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types'; +import { + draggable, + 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 { values } from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import { createRoot } from 'react-dom/client'; + +import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; + +interface UseDraggableProps { + drag?: { + getId: () => string[]; + getItem: () => unknown[]; + onDragStart?: () => void; + onDrop?: () => void; + onGenerateDragPreview?: (data: BaseEventPayload) => void; + target: DragTarget | string; + }; + drop?: { + canDrop: (args: { source: DragData }) => boolean; + getData: (args: { element: HTMLElement; input: Input }) => DragData; + onDrag: (args: { self: DragData }) => void; + onDragLeave: () => void; + onDrop: (args: { self: DragData }) => void; + }; + isEnabled: boolean; +} + +export const useDragDrop = ({ + drag, + drop, + isEnabled, +}: UseDraggableProps) => { + const ref = useRef(null); + + const [isDragging, setIsDragging] = useState(false); + + useEffect(() => { + if (!ref.current || !isEnabled) return; + + const functions: CleanupFn[] = []; + + if (drag) { + functions.push( + draggable({ + element: ref.current, + getInitialData: () => { + const id = drag.getId(); + const item = drag.getItem(); + + const data = dndUtils.generateDragData({ + id, + item, + type: drag.target, + }); + return data; + }, + onDragStart: () => { + setIsDragging(true); + drag.onDragStart?.(); + }, + onDrop: () => { + setIsDragging(false); + drag.onDrop?.(); + }, + onGenerateDragPreview: (data) => { + if (drag.onGenerateDragPreview) { + return drag.onGenerateDragPreview(data); + } + + disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage }); + // setCustomNativeDragPreview({ + // nativeSetDragImage: data.nativeSetDragImage, + // // render: ({ container }) => { + // // const root = createRoot(container); + // // root.render(); + // // }, + // }); + }, + }), + ); + } + + // if (drop) { + // functions.push( + // dropTargetForElements({ + // canDrop: (args) => { + // const data = args.source.data as unknown as DragData; + // const isSelf = (args.source.data.id as string[])[0] === option.value; + // return dndUtils.isDropTarget(data.type, [DragTarget.GENERIC]) && !isSelf; + // }, + // element: ref.current, + // getData: ({ element, input }) => { + // const data = dndUtils.generateDragData({ + // id: [option.value], + // operation: [DragOperation.REORDER], + // type: DragTarget.GENERIC, + // }); + + // return attachClosestEdge(data, { + // allowedEdges: ['top', 'bottom'], + // element, + // input, + // }); + // }, + // onDrag: (args) => { + // const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); + // setIsDraggedOver(closestEdgeOfTarget); + // }, + // onDragLeave: () => { + // setIsDraggedOver(null); + // }, + // onDrop: (args) => { + // const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data); + + // const from = args.source.data.id as string[]; + // const to = args.self.data.id as string[]; + + // const newOrder = dndUtils.reorderById({ + // edge: closestEdgeOfTarget, + // idFrom: from[0], + // idTo: to[0], + // list: values, + // }); + + // onChange(newOrder); + // setIsDraggedOver(null); + // }, + // }), + // ); + // } + + return combine(...functions); + }, [drag, drop, isDragging]); + + return { + isDragging, + ref, + }; +}; diff --git a/src/shared/types/drag-and-drop.ts b/src/shared/types/drag-and-drop.ts index 169855b1c..f1c84fd18 100644 --- a/src/shared/types/drag-and-drop.ts +++ b/src/shared/types/drag-and-drop.ts @@ -53,7 +53,7 @@ export const dndUtils = { id: string[]; item?: TDataType[]; operation?: DragOperation[]; - type: DragTarget; + type: DragTarget | string; }, metadata?: T, ) => {