add drag state to item grid

This commit is contained in:
jeffvli
2025-11-09 00:36:35 -08:00
parent 7d4a17e89c
commit ad409fecfa
13 changed files with 451 additions and 71 deletions
@@ -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 { 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 (
@@ -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);
},
@@ -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<string>;
draggingItems: Map<string, ItemListStateItem>;
expanded: Set<string>;
expandedItems: Map<string, ItemListItem>;
expandedItems: Map<string, ItemListStateItem>;
selected: Set<string>;
selectedItems: Map<string, ItemListItem>;
selectedItems: Map<string, ItemListStateItem>;
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<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': {
const newExpanded = new Set<string>();
const newExpandedItems = new Map<string, ItemListItem>();
const newExpandedItems = new Map<string, ItemListStateItem>();
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<string>();
const newSelectedItems = new Map<string, ItemListItem>();
const newSelectedItems = new Map<string, ItemListStateItem>();
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<string>();
const newExpandedItems = new Map<string, ItemListItem>();
const newExpandedItems = new Map<string, ItemListStateItem>();
// 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,