add drag state to item grid

This commit is contained in:
jeffvli
2025-11-09 00:36:35 -08:00
parent 91e99a6eb6
commit 7e30312d2a
13 changed files with 451 additions and 71 deletions
@@ -10,17 +10,15 @@
border-radius: var(--theme-radius-md); border-radius: var(--theme-radius-md);
} }
.container.previewed {
outline: 2px dashed var(--theme-colors-primary);
outline-offset: 2px;
opacity: 0.7;
}
.container.selected { .container.selected {
outline: 2px solid var(--theme-colors-primary); outline: 2px solid var(--theme-colors-primary);
outline-offset: 2px; outline-offset: 2px;
} }
.container.dragging {
opacity: 0.5;
}
.image-container { .image-container {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -6,8 +6,10 @@ import { generatePath, Link } from 'react-router';
import styles from './item-card.module.css'; import styles from './item-card.module.css';
import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; 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 { ItemListStateActions } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls } from '/@/renderer/components/item-list/types'; import { ItemControls } from '/@/renderer/components/item-list/types';
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { Image } from '/@/shared/components/image/image'; import { Image } from '/@/shared/components/image/image';
import { Separator } from '/@/shared/components/separator/separator'; import { Separator } from '/@/shared/components/separator/separator';
@@ -21,10 +23,12 @@ import {
Playlist, Playlist,
Song, Song,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { DragTarget } from '/@/shared/types/drag-and-drop';
export interface ItemCardProps { export interface ItemCardProps {
controls?: ItemControls; controls?: ItemControls;
data: Album | AlbumArtist | Artist | Playlist | Song | undefined; data: Album | AlbumArtist | Artist | Playlist | Song | undefined;
enableDrag?: boolean;
internalState?: ItemListStateActions; internalState?: ItemListStateActions;
isRound?: boolean; isRound?: boolean;
itemType: LibraryItem; itemType: LibraryItem;
@@ -41,6 +45,7 @@ type DataRow = {
export const ItemCard = ({ export const ItemCard = ({
controls, controls,
data, data,
enableDrag,
internalState, internalState,
isRound, isRound,
itemType, itemType,
@@ -56,6 +61,7 @@ export const ItemCard = ({
<CompactItemCard <CompactItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag}
imageUrl={imageUrl} imageUrl={imageUrl}
internalState={internalState} internalState={internalState}
isRound={isRound} isRound={isRound}
@@ -69,6 +75,7 @@ export const ItemCard = ({
<PosterItemCard <PosterItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag}
imageUrl={imageUrl} imageUrl={imageUrl}
internalState={internalState} internalState={internalState}
isRound={isRound} isRound={isRound}
@@ -83,6 +90,7 @@ export const ItemCard = ({
<DefaultItemCard <DefaultItemCard
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag}
imageUrl={imageUrl} imageUrl={imageUrl}
internalState={internalState} internalState={internalState}
isRound={isRound} isRound={isRound}
@@ -312,6 +320,7 @@ const DefaultItemCard = ({
const PosterItemCard = ({ const PosterItemCard = ({
controls, controls,
data, data,
enableDrag,
imageUrl, imageUrl,
internalState, internalState,
isRound, isRound,
@@ -325,6 +334,43 @@ const PosterItemCard = ({
? internalState.isSelected((data as any).id) ? internalState.isSelected((data as any).id)
: false; : false;
const { isDragging: isDraggingLocal, ref } = useDragDrop<HTMLDivElement>({
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) { if (data) {
const handleMouseEnter = () => { const handleMouseEnter = () => {
if (withControls) { if (withControls) {
@@ -364,8 +410,10 @@ const PosterItemCard = ({
return ( return (
<div <div
className={clsx(styles.container, styles.poster, { className={clsx(styles.container, styles.poster, {
[styles.dragging]: isDragging,
[styles.selected]: isSelected, [styles.selected]: isSelected,
})} })}
ref={ref}
> >
<div <div
className={clsx(styles.imageContainer, { [styles.isRound]: isRound })} className={clsx(styles.imageContainer, { [styles.isRound]: isRound })}
@@ -3,7 +3,7 @@ import { Suspense } from 'react';
import styles from './expanded-list-item.module.css'; import styles from './expanded-list-item.module.css';
import { import {
ItemListItem, ItemListStateItem,
ItemListStateActions, ItemListStateActions,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ExpandedAlbumListItem } from '/@/renderer/features/albums/components/expanded-album-list-item'; import { ExpandedAlbumListItem } from '/@/renderer/features/albums/components/expanded-album-list-item';
@@ -35,7 +35,7 @@ export const ExpandedListItem = ({ internalState, itemType }: ExpandedListItemPr
}; };
interface SelectedItemProps { interface SelectedItemProps {
item: ItemListItem; item: ItemListStateItem;
itemType: LibraryItem; itemType: LibraryItem;
} }
@@ -0,0 +1,69 @@
import {
ItemListStateActions,
ItemListStateItem,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import {
Album,
AlbumArtist,
Artist,
LibraryItem,
Playlist,
Song,
} from '/@/shared/types/domain-types';
/**
* Converts domain data to ItemListStateItem format
*/
const convertToItemListItem = (
data: Album | AlbumArtist | Artist | Playlist | Song,
itemType: LibraryItem,
): ItemListStateItem => {
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;
};
@@ -1,6 +1,6 @@
import { useMemo } from 'react'; 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 { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context'; import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { Play } from '/@/shared/types/types'; import { Play } from '/@/shared/types/types';
@@ -15,7 +15,7 @@ export const useDefaultItemListControls = () => {
return; return;
} }
const itemListItem: ItemListItem = { const itemListItem: ItemListStateItem = {
_serverId: item._serverId, _serverId: item._serverId,
id: item.id, id: item.id,
itemType, itemType,
@@ -61,7 +61,7 @@ export const useDefaultItemListControls = () => {
const startIndex = Math.min(lastIndex, currentIndex); const startIndex = Math.min(lastIndex, currentIndex);
const stopIndex = Math.max(lastIndex, currentIndex); const stopIndex = Math.max(lastIndex, currentIndex);
const rangeItems: ItemListItem[] = []; const rangeItems: ItemListStateItem[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = validData[i]; const rangeItem = validData[i];
if ( if (
@@ -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 * Action creators for item grid state management
@@ -17,22 +17,27 @@ export const itemGridActions = {
type: 'CLEAR_SELECTED', type: 'CLEAR_SELECTED',
}), }),
setExpanded: (items: ItemListItem[]): ItemListAction => ({ setDragging: (items: ItemListStateItem[]): ItemListAction => ({
payload: items,
type: 'SET_DRAGGING',
}),
setExpanded: (items: ItemListStateItem[]): ItemListAction => ({
payload: items, payload: items,
type: 'SET_EXPANDED', type: 'SET_EXPANDED',
}), }),
setSelected: (items: ItemListItem[]): ItemListAction => ({ setSelected: (items: ItemListStateItem[]): ItemListAction => ({
payload: items, payload: items,
type: 'SET_SELECTED', type: 'SET_SELECTED',
}), }),
toggleExpanded: (item: ItemListItem): ItemListAction => ({ toggleExpanded: (item: ItemListStateItem): ItemListAction => ({
payload: item, payload: item,
type: 'TOGGLE_EXPANDED', type: 'TOGGLE_EXPANDED',
}), }),
toggleSelected: (item: ItemListItem): ItemListAction => ({ toggleSelected: (item: ItemListStateItem): ItemListAction => ({
payload: item, payload: item,
type: 'TOGGLE_SELECTED', type: 'TOGGLE_SELECTED',
}), }),
@@ -43,7 +48,19 @@ export const itemGridActions = {
* These can be reused to extract specific data from state * These can be reused to extract specific data from state
*/ */
export const itemGridSelectors = { 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()); return Array.from(state.expandedItems.values());
}, },
@@ -55,7 +72,7 @@ export const itemGridSelectors = {
return Array.from(state.expanded); return Array.from(state.expanded);
}, },
getSelected: (state: ItemListState): ItemListItem[] => { getSelected: (state: ItemListState): ItemListStateItem[] => {
return Array.from(state.selectedItems.values()); return Array.from(state.selectedItems.values());
}, },
@@ -71,6 +88,10 @@ export const itemGridSelectors = {
return state.version; return state.version;
}, },
hasAnyDragging: (state: ItemListState): boolean => {
return state.dragging.size > 0;
},
hasAnyExpanded: (state: ItemListState): boolean => { hasAnyExpanded: (state: ItemListState): boolean => {
return state.expanded.size > 0; return state.expanded.size > 0;
}, },
@@ -79,6 +100,10 @@ export const itemGridSelectors = {
return state.selected.size > 0; return state.selected.size > 0;
}, },
isDragging: (state: ItemListState, itemId: string): boolean => {
return state.dragging.has(itemId);
},
isExpanded: (state: ItemListState, itemId: string): boolean => { isExpanded: (state: ItemListState, itemId: string): boolean => {
return state.expanded.has(itemId); return state.expanded.has(itemId);
}, },
@@ -120,7 +145,10 @@ export const itemListUtils = {
/** /**
* Toggle expansion of all items in a list * 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)); const allExpanded = items.every((item) => currentState.expanded.has(item.id));
return allExpanded ? itemGridActions.clearExpanded() : itemGridActions.setExpanded(items); return allExpanded ? itemGridActions.clearExpanded() : itemGridActions.setExpanded(items);
}, },
@@ -128,7 +156,10 @@ export const itemListUtils = {
/** /**
* Toggle selection of all items in a list * 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)); const allSelected = items.every((item) => currentState.selected.has(item.id));
return allSelected ? itemGridActions.clearSelected() : itemGridActions.setSelected(items); return allSelected ? itemGridActions.clearSelected() : itemGridActions.setSelected(items);
}, },
@@ -4,47 +4,57 @@ import { itemGridSelectors } from '/@/renderer/components/item-list/helpers/item
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
export type ItemListAction = export type ItemListAction =
| { payload: ItemListItem; type: 'TOGGLE_EXPANDED' } | { payload: ItemListStateItem; type: 'TOGGLE_EXPANDED' }
| { payload: ItemListItem; type: 'TOGGLE_SELECTED' } | { payload: ItemListStateItem; type: 'TOGGLE_SELECTED' }
| { payload: ItemListItem[]; type: 'SET_EXPANDED' } | { payload: ItemListStateItem[]; type: 'SET_DRAGGING' }
| { payload: ItemListItem[]; type: 'SET_SELECTED' } | { payload: ItemListStateItem[]; type: 'SET_EXPANDED' }
| { payload: ItemListStateItem[]; type: 'SET_SELECTED' }
| { type: 'CLEAR_ALL' } | { type: 'CLEAR_ALL' }
| { type: 'CLEAR_DRAGGING' }
| { type: 'CLEAR_EXPANDED' } | { type: 'CLEAR_EXPANDED' }
| { type: 'CLEAR_SELECTED' }; | { type: 'CLEAR_SELECTED' };
export interface ItemListItem {
_serverId: string;
id: string;
itemType: LibraryItem;
}
export interface ItemListState { export interface ItemListState {
dragging: Set<string>;
draggingItems: Map<string, ItemListStateItem>;
expanded: Set<string>; expanded: Set<string>;
expandedItems: Map<string, ItemListItem>; expandedItems: Map<string, ItemListStateItem>;
selected: Set<string>; selected: Set<string>;
selectedItems: Map<string, ItemListItem>; selectedItems: Map<string, ItemListStateItem>;
version: number; version: number;
} }
export interface ItemListStateActions { export interface ItemListStateActions {
clearAll: () => void; clearAll: () => void;
clearDragging: () => void;
clearExpanded: () => void; clearExpanded: () => void;
clearSelected: () => void; clearSelected: () => void;
findItemIndex: (itemId: string) => number; findItemIndex: (itemId: string) => number;
getData: () => unknown[]; getData: () => unknown[];
getExpanded: () => ItemListItem[]; getDragging: () => ItemListStateItem[];
getDraggingIds: () => string[];
getExpanded: () => ItemListStateItem[];
getExpandedIds: () => string[]; getExpandedIds: () => string[];
getSelected: () => ItemListItem[]; getSelected: () => ItemListStateItem[];
getSelectedIds: () => string[]; getSelectedIds: () => string[];
getVersion: () => number; getVersion: () => number;
hasDragging: () => boolean;
hasExpanded: () => boolean; hasExpanded: () => boolean;
hasSelected: () => boolean; hasSelected: () => boolean;
isDragging: (itemId: string) => boolean;
isExpanded: (itemId: string) => boolean; isExpanded: (itemId: string) => boolean;
isSelected: (itemId: string) => boolean; isSelected: (itemId: string) => boolean;
setExpanded: (items: ItemListItem[]) => void; setDragging: (items: ItemListStateItem[]) => void;
setSelected: (items: ItemListItem[]) => void; setExpanded: (items: ItemListStateItem[]) => void;
toggleExpanded: (item: ItemListItem) => void; setSelected: (items: ItemListStateItem[]) => void;
toggleSelected: (item: ItemListItem) => 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': case 'CLEAR_ALL':
return { return {
...state, ...state,
dragging: new Set(),
draggingItems: new Map(),
expanded: new Set(), expanded: new Set(),
expandedItems: new Map(), expandedItems: new Map(),
selected: new Set(), selected: new Set(),
@@ -63,6 +75,14 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
version: state.version + 1, version: state.version + 1,
}; };
case 'CLEAR_DRAGGING':
return {
...state,
dragging: new Set(),
draggingItems: new Map(),
version: state.version + 1,
};
case 'CLEAR_EXPANDED': case 'CLEAR_EXPANDED':
return { return {
...state, ...state,
@@ -79,9 +99,26 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
version: state.version + 1, version: state.version + 1,
}; };
case 'SET_DRAGGING': {
const newDragging = new Set<string>();
const newDraggingItems = new Map<string, ItemListStateItem>();
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': { case 'SET_EXPANDED': {
const newExpanded = new Set<string>(); const newExpanded = new Set<string>();
const newExpandedItems = new Map<string, ItemListItem>(); const newExpandedItems = new Map<string, ItemListStateItem>();
console.log('SET_EXPANDED', action.payload); console.log('SET_EXPANDED', action.payload);
@@ -101,7 +138,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
case 'SET_SELECTED': { case 'SET_SELECTED': {
const newSelected = new Set<string>(); const newSelected = new Set<string>();
const newSelectedItems = new Map<string, ItemListItem>(); const newSelectedItems = new Map<string, ItemListStateItem>();
action.payload.forEach((item) => { action.payload.forEach((item) => {
newSelected.add(item.id); newSelected.add(item.id);
@@ -118,7 +155,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
case 'TOGGLE_EXPANDED': { case 'TOGGLE_EXPANDED': {
const newExpanded = new Set<string>(); const newExpanded = new Set<string>();
const newExpandedItems = new Map<string, ItemListItem>(); const newExpandedItems = new Map<string, ItemListStateItem>();
// If the item is already expanded, collapse it // If the item is already expanded, collapse it
if (state.expanded.has(action.payload.id)) { if (state.expanded.has(action.payload.id)) {
@@ -163,6 +200,8 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
}; };
export const initialItemListState: ItemListState = { export const initialItemListState: ItemListState = {
dragging: new Set(),
draggingItems: new Map(),
expanded: new Set(), expanded: new Set(),
expandedItems: new Map(), expandedItems: new Map(),
selected: new Set(), selected: new Set(),
@@ -173,19 +212,23 @@ export const initialItemListState: ItemListState = {
export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActions => { export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActions => {
const [state, dispatch] = useReducer(itemListReducer, initialItemListState); const [state, dispatch] = useReducer(itemListReducer, initialItemListState);
const setExpanded = useCallback((items: ItemListItem[]) => { const setExpanded = useCallback((items: ItemListStateItem[]) => {
dispatch({ payload: items, type: 'SET_EXPANDED' }); 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' }); dispatch({ payload: items, type: 'SET_SELECTED' });
}, []); }, []);
const toggleExpanded = useCallback((item: ItemListItem) => { const toggleExpanded = useCallback((item: ItemListStateItem) => {
dispatch({ payload: item, type: 'TOGGLE_EXPANDED' }); dispatch({ payload: item, type: 'TOGGLE_EXPANDED' });
}, []); }, []);
const toggleSelected = useCallback((item: ItemListItem) => { const toggleSelected = useCallback((item: ItemListStateItem) => {
dispatch({ payload: item, type: 'TOGGLE_SELECTED' }); dispatch({ payload: item, type: 'TOGGLE_SELECTED' });
}, []); }, []);
@@ -207,10 +250,18 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi
return itemGridSelectors.getExpanded(state); return itemGridSelectors.getExpanded(state);
}, [state]); }, [state]);
const getDragging = useCallback(() => {
return itemGridSelectors.getDragging(state);
}, [state]);
const getSelected = useCallback(() => { const getSelected = useCallback(() => {
return itemGridSelectors.getSelected(state); return itemGridSelectors.getSelected(state);
}, [state]); }, [state]);
const getDraggingIds = useCallback(() => {
return Array.from(state.dragging);
}, [state.dragging]);
const getExpandedIds = useCallback(() => { const getExpandedIds = useCallback(() => {
return Array.from(state.expanded); return Array.from(state.expanded);
}, [state.expanded]); }, [state.expanded]);
@@ -223,6 +274,10 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi
dispatch({ type: 'CLEAR_EXPANDED' }); dispatch({ type: 'CLEAR_EXPANDED' });
}, []); }, []);
const clearDragging = useCallback(() => {
dispatch({ type: 'CLEAR_DRAGGING' });
}, []);
const clearSelected = useCallback(() => { const clearSelected = useCallback(() => {
dispatch({ type: 'CLEAR_SELECTED' }); dispatch({ type: 'CLEAR_SELECTED' });
}, []); }, []);
@@ -239,10 +294,21 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi
return itemGridSelectors.hasAnyExpanded(state); return itemGridSelectors.hasAnyExpanded(state);
}, [state]); }, [state]);
const hasDragging = useCallback(() => {
return itemGridSelectors.hasAnyDragging(state);
}, [state]);
const hasSelected = useCallback(() => { const hasSelected = useCallback(() => {
return itemGridSelectors.hasAnySelected(state); return itemGridSelectors.hasAnySelected(state);
}, [state]); }, [state]);
const isDragging = useCallback(
(itemId: string) => {
return itemGridSelectors.isDragging(state, itemId);
},
[state],
);
const getData = useCallback(() => { const getData = useCallback(() => {
return getDataFn ? getDataFn() : []; return getDataFn ? getDataFn() : [];
}, [getDataFn]); }, [getDataFn]);
@@ -260,19 +326,25 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi
return useMemo( return useMemo(
() => ({ () => ({
clearAll, clearAll,
clearDragging,
clearExpanded, clearExpanded,
clearSelected, clearSelected,
findItemIndex, findItemIndex,
getData, getData,
getDragging,
getDraggingIds,
getExpanded, getExpanded,
getExpandedIds, getExpandedIds,
getSelected, getSelected,
getSelectedIds, getSelectedIds,
getVersion, getVersion,
hasDragging,
hasExpanded, hasExpanded,
hasSelected, hasSelected,
isDragging,
isExpanded, isExpanded,
isSelected, isSelected,
setDragging,
setExpanded, setExpanded,
setSelected, setSelected,
toggleExpanded, toggleExpanded,
@@ -280,19 +352,25 @@ export const useItemListState = (getDataFn?: () => unknown[]): ItemListStateActi
}), }),
[ [
clearAll, clearAll,
clearDragging,
clearExpanded, clearExpanded,
clearSelected, clearSelected,
findItemIndex, findItemIndex,
getData, getData,
getDragging,
getDraggingIds,
getExpanded, getExpanded,
getExpandedIds, getExpandedIds,
getSelected, getSelected,
getSelectedIds, getSelectedIds,
getVersion, getVersion,
hasDragging,
hasExpanded, hasExpanded,
hasSelected, hasSelected,
isDragging,
isExpanded, isExpanded,
isSelected, isSelected,
setDragging,
setExpanded, setExpanded,
setSelected, setSelected,
toggleExpanded, toggleExpanded,
@@ -36,8 +36,8 @@ import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { import {
ItemListItem,
ItemListStateActions, ItemListStateActions,
ItemListStateItem,
useItemListState, useItemListState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
@@ -46,6 +46,7 @@ import { LibraryItem } from '/@/shared/types/domain-types';
interface VirtualizedGridListProps { interface VirtualizedGridListProps {
controls: ItemControls; controls: ItemControls;
data: unknown[]; data: unknown[];
enableDrag?: boolean;
enableExpansion: boolean; enableExpansion: boolean;
enableSelection: boolean; enableSelection: boolean;
gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
@@ -68,6 +69,7 @@ const VirtualizedGridList = React.memo(
({ ({
controls, controls,
data, data,
enableDrag,
enableExpansion, enableExpansion,
enableSelection, enableSelection,
gap, gap,
@@ -86,6 +88,7 @@ const VirtualizedGridList = React.memo(
columns: tableMeta?.columnCount || 0, columns: tableMeta?.columnCount || 0,
controls, controls,
data, data,
enableDrag,
enableExpansion, enableExpansion,
enableSelection, enableSelection,
gap, gap,
@@ -97,6 +100,7 @@ const VirtualizedGridList = React.memo(
tableMeta, tableMeta,
controls, controls,
data, data,
enableDrag,
enableExpansion, enableExpansion,
enableSelection, enableSelection,
gap, gap,
@@ -229,6 +233,7 @@ export interface GridItemProps {
columns: number; columns: number;
controls: ItemCardProps['controls']; controls: ItemCardProps['controls'];
data: any[]; data: any[];
enableDrag?: boolean;
enableExpansion?: boolean; enableExpansion?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; gap: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
@@ -244,6 +249,7 @@ export interface GridItemProps {
export interface ItemGridListProps { export interface ItemGridListProps {
currentPage?: number; currentPage?: number;
data: unknown[]; data: unknown[];
enableDrag?: boolean;
enableExpansion?: boolean; enableExpansion?: boolean;
enableSelection?: boolean; enableSelection?: boolean;
gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs'; gap?: 'lg' | 'md' | 'sm' | 'xl' | 'xs';
@@ -258,6 +264,7 @@ export interface ItemGridListProps {
export const ItemGridList = ({ export const ItemGridList = ({
data, data,
enableDrag = true,
enableExpansion = true, enableExpansion = true,
enableSelection = true, enableSelection = true,
gap = 'sm', gap = 'sm',
@@ -336,7 +343,6 @@ export const ItemGridList = ({
const controls = useDefaultItemListControls(); const controls = useDefaultItemListControls();
// Scroll to a specific index
const scrollToIndex = useCallback( const scrollToIndex = useCallback(
(index: number) => { (index: number) => {
if (!listRef.current || !tableMeta) return; if (!listRef.current || !tableMeta) return;
@@ -346,7 +352,6 @@ export const ItemGridList = ({
[tableMeta], [tableMeta],
); );
// Scroll to a specific offset
const scrollToOffset = useCallback((offset: number) => { const scrollToOffset = useCallback((offset: number) => {
if (!listRef.current) return; if (!listRef.current) return;
listRef.current.scrollTo(offset); listRef.current.scrollTo(offset);
@@ -355,7 +360,6 @@ export const ItemGridList = ({
// Handle keyboard navigation // Handle keyboard navigation
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => { (e: React.KeyboardEvent<HTMLDivElement>) => {
console.log('handleKeyDown', e.key);
if (!enableSelection || !tableMeta) return; if (!enableSelection || !tableMeta) return;
if ( if (
e.key !== 'ArrowDown' && e.key !== 'ArrowDown' &&
@@ -480,7 +484,7 @@ export const ItemGridList = ({
const startIndex = Math.min(lastIndex, newIndex); const startIndex = Math.min(lastIndex, newIndex);
const stopIndex = Math.max(lastIndex, newIndex); const stopIndex = Math.max(lastIndex, newIndex);
const rangeItems: ItemListItem[] = []; const rangeItems: ItemListStateItem[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = data[i]; const rangeItem = data[i];
if ( if (
@@ -507,7 +511,7 @@ export const ItemGridList = ({
}); });
// Ensure the last item in selection is the item at newIndex for incremental extension // Ensure the last item in selection is the item at newIndex for incremental extension
const newItemListItem: ItemListItem = { const newItemListItem: ItemListStateItem = {
_serverId: newItem.serverId, _serverId: newItem.serverId,
id: newItem.id, id: newItem.id,
itemType, itemType,
@@ -546,7 +550,6 @@ export const ItemGridList = ({
[data, enableSelection, internalState, itemType, tableMeta, scrollToIndex], [data, enableSelection, internalState, itemType, tableMeta, scrollToIndex],
); );
// Create imperative handle
const imperativeHandle: ItemListHandle = useMemo(() => { const imperativeHandle: ItemListHandle = useMemo(() => {
return { return {
clearExpanded: () => { clearExpanded: () => {
@@ -568,12 +571,10 @@ export const ItemGridList = ({
}; };
}, [data, internalState, scrollToIndex, scrollToOffset]); }, [data, internalState, scrollToIndex, scrollToOffset]);
// Expose handle via ref
useEffect(() => { useEffect(() => {
handleRef.current = imperativeHandle; handleRef.current = imperativeHandle;
}, [imperativeHandle]); }, [imperativeHandle]);
// Expose handle via forwardRef
useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]); useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]);
return ( return (
@@ -588,6 +589,7 @@ export const ItemGridList = ({
<VirtualizedGridList <VirtualizedGridList
controls={controls} controls={controls}
data={data} data={data}
enableDrag={enableDrag}
enableExpansion={enableExpansion} enableExpansion={enableExpansion}
enableSelection={enableSelection} enableSelection={enableSelection}
gap={gap} gap={gap}
@@ -614,7 +616,7 @@ export const ItemGridList = ({
const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => { const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
const { index, style } = props; 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 items: ReactNode[] = [];
const itemCount = data.length; const itemCount = data.length;
@@ -640,6 +642,7 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
<ItemCard <ItemCard
controls={controls} controls={controls}
data={data[i]} data={data[i]}
enableDrag={enableDrag}
internalState={props.data.internalState} internalState={props.data.internalState}
itemType={itemType} itemType={itemType}
withControls withControls
@@ -23,7 +23,7 @@ import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item'; import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
import { import {
ItemListItem, ItemListStateItem,
ItemListStateActions, ItemListStateActions,
useItemListState, useItemListState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
@@ -976,7 +976,7 @@ export const ItemTableList = ({
return; return;
} }
const itemListItem: ItemListItem = { const itemListItem: ItemListStateItem = {
_serverId: item.serverId, _serverId: item.serverId,
id: item.id, id: item.id,
itemType, itemType,
@@ -1020,7 +1020,7 @@ export const ItemTableList = ({
const startIndex = Math.min(lastIndex, currentIndex); const startIndex = Math.min(lastIndex, currentIndex);
const stopIndex = Math.max(lastIndex, currentIndex); const stopIndex = Math.max(lastIndex, currentIndex);
const rangeItems: ItemListItem[] = []; const rangeItems: ItemListStateItem[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = data[i]; const rangeItem = data[i];
if ( if (
@@ -1128,7 +1128,7 @@ export const ItemTableList = ({
const startIndex = Math.min(lastIndex, newIndex); const startIndex = Math.min(lastIndex, newIndex);
const stopIndex = Math.max(lastIndex, newIndex); const stopIndex = Math.max(lastIndex, newIndex);
const rangeItems: ItemListItem[] = []; const rangeItems: ItemListStateItem[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = data[i]; const rangeItem = data[i];
if ( if (
@@ -1155,7 +1155,7 @@ export const ItemTableList = ({
}); });
// Ensure the last item in selection is the item at newIndex for incremental extension // Ensure the last item in selection is the item at newIndex for incremental extension
const newItemListItem: ItemListItem = { const newItemListItem: ItemListStateItem = {
_serverId: newItem.serverId, _serverId: newItem.serverId,
id: newItem.id, id: newItem.id,
itemType, itemType,
@@ -6,7 +6,7 @@ import { Fragment, Suspense } from 'react';
import styles from './expanded-album-list-item.module.css'; import styles from './expanded-album-list-item.module.css';
import { ItemListItem } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state';
import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { albumQueries } from '/@/renderer/features/albums/api/album-api';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@@ -18,7 +18,7 @@ import { TextTitle } from '/@/shared/components/text-title/text-title';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
interface ExpandedAlbumListItemProps { interface ExpandedAlbumListItemProps {
item: ItemListItem; item: ItemListStateItem;
} }
export const ExpandedAlbumListItem = ({ item }: ExpandedAlbumListItemProps) => { export const ExpandedAlbumListItem = ({ item }: ExpandedAlbumListItemProps) => {
@@ -7,7 +7,6 @@ import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate, useParams } from 'react-router'; 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 { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header'; import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder'; import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
@@ -32,7 +31,6 @@ const PlaylistDetailSongListRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer(); const server = useCurrentServer();
const handlePlayQueueAdd = useHandlePlayQueueAdd();
const detailQuery = useQuery( const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }), playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
@@ -159,13 +157,13 @@ const PlaylistDetailSongListRoute = () => {
); );
const filterSortedSongs = useMemo(() => { const filterSortedSongs = useMemo(() => {
let items = playlistSongs.data?.items; const items = playlistSongs.data?.items;
if (items) { if (items) {
const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm; const searchTerm = page?.table.id[playlistId]?.filter?.searchTerm;
if (searchTerm) { if (searchTerm) {
items = searchSongs(items, searchTerm); // items = searchSongs(items, searchTerm);
} }
const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID; const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID;
@@ -182,10 +180,10 @@ const PlaylistDetailSongListRoute = () => {
: undefined; : undefined;
const handlePlay = (play: Play) => { const handlePlay = (play: Play) => {
handlePlayQueueAdd?.({ // handlePlayQueueAdd?.({
byData: filterSortedSongs, // byData: filterSortedSongs,
playType: play, // playType: play,
}); // });
}; };
return ( return (
+155
View File
@@ -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<ElementDragType>) => 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 = <TElement extends HTMLElement>({
drag,
drop,
isEnabled,
}: UseDraggableProps) => {
const ref = useRef<null | TElement>(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(<DragPreview itemCount={1} />);
// // },
// });
},
}),
);
}
// 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,
};
};
+1 -1
View File
@@ -53,7 +53,7 @@ export const dndUtils = {
id: string[]; id: string[];
item?: TDataType[]; item?: TDataType[];
operation?: DragOperation[]; operation?: DragOperation[];
type: DragTarget; type: DragTarget | string;
}, },
metadata?: T, metadata?: T,
) => { ) => {