add drag/drop from lists into queue

This commit is contained in:
jeffvli
2025-11-09 20:44:03 -08:00
parent 230f4f0792
commit 2f434c9d00
16 changed files with 718 additions and 355 deletions
@@ -23,7 +23,7 @@ import {
Playlist, Playlist,
Song, Song,
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { DragTarget } from '/@/shared/types/drag-and-drop'; import { DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
export interface ItemCardProps { export interface ItemCardProps {
controls?: ItemControls; controls?: ItemControls;
@@ -341,7 +341,7 @@ const PosterItemCard = ({
return []; return [];
} }
const draggedItems = getDraggedItems(data, itemType, internalState); const draggedItems = getDraggedItems(data, internalState);
return draggedItems.map((item) => item.id); return draggedItems.map((item) => item.id);
}, },
getItem: () => { getItem: () => {
@@ -349,14 +349,16 @@ const PosterItemCard = ({
return []; return [];
} }
return [data]; const draggedItems = getDraggedItems(data, internalState);
return draggedItems;
}, },
itemType,
onDragStart: () => { onDragStart: () => {
if (!data || !internalState) { if (!data || !internalState) {
return; return;
} }
const draggedItems = getDraggedItems(data, itemType, internalState); const draggedItems = getDraggedItems(data, internalState);
internalState.setDragging(draggedItems); internalState.setDragging(draggedItems);
}, },
onDrop: () => { onDrop: () => {
@@ -364,6 +366,10 @@ const PosterItemCard = ({
internalState.setDragging([]); internalState.setDragging([]);
} }
}, },
operation:
itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
target: DragTarget.ALBUM, target: DragTarget.ALBUM,
}, },
isEnabled: !!enableDrag && !!data, isEnabled: !!enableDrag && !!data,
@@ -1,28 +1,25 @@
import { import {
ItemListStateActions, ItemListStateActions,
ItemListStateItem, ItemListStateItemWithRequiredProperties,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/shared/types/domain-types';
Album,
AlbumArtist,
Artist,
LibraryItem,
Playlist,
Song,
} from '/@/shared/types/domain-types';
/** /**
* Converts domain data to ItemListStateItem format * Type guard to assert that an item has the required properties for dragging
*/ */
const convertToItemListItem = ( const hasRequiredDragProperties = (
data: Album | AlbumArtist | Artist | Playlist | Song, item: unknown,
itemType: LibraryItem, ): item is ItemListStateItemWithRequiredProperties => {
): ItemListStateItem => { return (
return { typeof item === 'object' &&
_serverId: data._serverId, item !== null &&
id: data.id, 'id' in item &&
itemType, typeof (item as any).id === 'string' &&
}; 'itemType' in item &&
typeof (item as any).itemType === 'string' &&
'_serverId' in item &&
typeof (item as any)._serverId === 'string'
);
}; };
/** /**
@@ -34,31 +31,40 @@ const convertToItemListItem = (
* @param itemType - The type of library item * @param itemType - The type of library item
* @param internalState - The item list state actions (optional) * @param internalState - The item list state actions (optional)
* @param updateSelection - Whether to update the selection state (default: true) * @param updateSelection - Whether to update the selection state (default: true)
* @returns Array of ItemListItem objects that should be dragged * @returns Array of items that should be dragged (with original values, asserting id, itemType, and _serverId)
*/ */
export const getDraggedItems = ( export const getDraggedItems = (
data: Album | AlbumArtist | Artist | Playlist | Song | undefined, data: Album | AlbumArtist | Artist | Playlist | Song | undefined,
itemType: LibraryItem,
internalState?: ItemListStateActions, internalState?: ItemListStateActions,
updateSelection: boolean = true, updateSelection: boolean = true,
): ItemListStateItem[] => { ): ItemListStateItemWithRequiredProperties[] => {
if (!data || !internalState) { if (!data || !internalState) {
return []; return [];
} }
// Convert data to ItemListStateItem format if (!hasRequiredDragProperties(data)) {
const draggedItem = convertToItemListItem(data, itemType); return [];
}
const draggedItem = data as ItemListStateItemWithRequiredProperties;
const previouslySelected = internalState.getSelected(); const previouslySelected = internalState.getSelected();
const isDraggingSelectedItem = previouslySelected.some((selected) => selected.id === data.id); const isDraggingSelectedItem = previouslySelected.some((selected) => {
if (hasRequiredDragProperties(selected)) {
return selected.id === data.id;
}
return false;
});
const draggedItems: ItemListStateItem[] = []; const draggedItems: ItemListStateItemWithRequiredProperties[] = [];
if (isDraggingSelectedItem) { if (isDraggingSelectedItem) {
// If dragging a selected item, drag all selected items const selectedItems = previouslySelected.filter(
draggedItems.push(...previouslySelected); (item): item is ItemListStateItemWithRequiredProperties =>
hasRequiredDragProperties(item),
);
draggedItems.push(...selectedItems);
} else { } else {
// If dragging an unselected item, select it and drag only it
if (updateSelection) { if (updateSelection) {
internalState.setSelected([draggedItem]); internalState.setSelected([draggedItem]);
} }
@@ -1,6 +1,6 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state'; import { ItemListStateItemWithRequiredProperties } 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';
@@ -10,16 +10,13 @@ export const useDefaultItemListControls = () => {
const controls: ItemControls = useMemo(() => { const controls: ItemControls = useMemo(() => {
return { return {
onClick: ({ event, internalState, item, itemType }: DefaultItemControlProps) => { onClick: ({ event, internalState, item }: DefaultItemControlProps) => {
if (!item || !internalState || !event) { if (!item || !internalState || !event) {
return; return;
} }
const itemListItem: ItemListStateItem = { // Use the full item instead of converting to minimal
_serverId: item._serverId, const itemListItem = item as ItemListStateItemWithRequiredProperties;
id: item.id,
itemType,
};
// Check if ctrl/cmd key is held for multi-selection // Check if ctrl/cmd key is held for multi-selection
if (event.ctrlKey || event.metaKey) { if (event.ctrlKey || event.metaKey) {
@@ -29,13 +26,27 @@ export const useDefaultItemListControls = () => {
// Remove this item from selection // Remove this item from selection
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const filteredSelected = currentSelected.filter( const filteredSelected = currentSelected.filter(
(selectedItem) => selectedItem.id !== item.id, (
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' &&
selectedItem !== null &&
'id' in selectedItem &&
(selectedItem as any).id !== item.id,
); );
internalState.setSelected(filteredSelected); internalState.setSelected(filteredSelected);
} else { } else {
// Add this item to selection // Add this item to selection
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const newSelected = [...currentSelected, itemListItem]; const newSelected = [
...currentSelected.filter(
(
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' && selectedItem !== null,
),
itemListItem,
];
internalState.setSelected(newSelected); internalState.setSelected(newSelected);
} }
} }
@@ -44,7 +55,12 @@ export const useDefaultItemListControls = () => {
const selectedItems = internalState.getSelected(); const selectedItems = internalState.getSelected();
const lastSelectedItem = selectedItems[selectedItems.length - 1]; const lastSelectedItem = selectedItems[selectedItems.length - 1];
if (lastSelectedItem) { if (
lastSelectedItem &&
typeof lastSelectedItem === 'object' &&
lastSelectedItem !== null &&
'id' in lastSelectedItem
) {
// Get the data array from internalState // Get the data array from internalState
const data = internalState.getData(); const data = internalState.getData();
// Filter out null/undefined values (e.g., header row) // Filter out null/undefined values (e.g., header row)
@@ -53,7 +69,7 @@ export const useDefaultItemListControls = () => {
); );
// Find the indices of the last selected item and current item // Find the indices of the last selected item and current item
const lastIndex = internalState.findItemIndex(lastSelectedItem.id); const lastIndex = internalState.findItemIndex((lastSelectedItem as any).id);
const currentIndex = internalState.findItemIndex(item.id); const currentIndex = internalState.findItemIndex(item.id);
if (lastIndex !== -1 && currentIndex !== -1) { if (lastIndex !== -1 && currentIndex !== -1) {
@@ -61,28 +77,38 @@ 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: ItemListStateItem[] = []; const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = validData[i]; const rangeItem = validData[i];
if ( if (
rangeItem && rangeItem &&
typeof rangeItem === 'object' && typeof rangeItem === 'object' &&
'id' in rangeItem && 'id' in rangeItem &&
'_serverId' in rangeItem '_serverId' in rangeItem &&
'itemType' in rangeItem
) { ) {
rangeItems.push({ rangeItems.push(
_serverId: (rangeItem as any)._serverId, rangeItem as ItemListStateItemWithRequiredProperties,
id: (rangeItem as any).id, );
itemType,
});
} }
} }
// Merge with existing selection, avoiding duplicates // Merge with existing selection, avoiding duplicates
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const newSelected = [...currentSelected]; const newSelected = [
...currentSelected.filter(
(
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' && selectedItem !== null,
),
];
rangeItems.forEach((rangeItem) => { rangeItems.forEach((rangeItem) => {
if (!newSelected.some((selected) => selected.id === rangeItem.id)) { if (
!newSelected.some(
(selected) => (selected as any).id === rangeItem.id,
)
) {
newSelected.push(rangeItem); newSelected.push(rangeItem);
} }
}); });
@@ -97,7 +123,11 @@ export const useDefaultItemListControls = () => {
// If this item is already the only selected item, deselect it // If this item is already the only selected item, deselect it
const selectedItems = internalState.getSelected(); const selectedItems = internalState.getSelected();
const isOnlySelected = const isOnlySelected =
selectedItems.length === 1 && selectedItems[0].id === item.id; selectedItems.length === 1 &&
typeof selectedItems[0] === 'object' &&
selectedItems[0] !== null &&
'id' in selectedItems[0] &&
(selectedItems[0] as any).id === item.id;
if (isOnlySelected) { if (isOnlySelected) {
internalState.clearSelected(); internalState.clearSelected();
@@ -111,16 +141,14 @@ export const useDefaultItemListControls = () => {
console.log('onDoubleClick', item, itemType, internalState); console.log('onDoubleClick', item, itemType, internalState);
}, },
onExpand: ({ internalState, item, itemType }: DefaultItemControlProps) => { onExpand: ({ internalState, item }: DefaultItemControlProps) => {
if (!item || !internalState) { if (!item || !internalState) {
return; return;
} }
return internalState?.toggleExpanded({ return internalState?.toggleExpanded(
_serverId: item._serverId, item as ItemListStateItemWithRequiredProperties,
id: item.id, );
itemType,
});
}, },
onFavorite: ({ onFavorite: ({
@@ -1,4 +1,4 @@
import { ItemListAction, ItemListStateItem, ItemListState } from './item-list-state'; import { ItemListAction, ItemListState, ItemListStateItemWithRequiredProperties } from './item-list-state';
/** /**
* Action creators for item grid state management * Action creators for item grid state management
@@ -17,27 +17,27 @@ export const itemGridActions = {
type: 'CLEAR_SELECTED', type: 'CLEAR_SELECTED',
}), }),
setDragging: (items: ItemListStateItem[]): ItemListAction => ({ setDragging: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({
payload: items, payload: items,
type: 'SET_DRAGGING', type: 'SET_DRAGGING',
}), }),
setExpanded: (items: ItemListStateItem[]): ItemListAction => ({ setExpanded: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({
payload: items, payload: items,
type: 'SET_EXPANDED', type: 'SET_EXPANDED',
}), }),
setSelected: (items: ItemListStateItem[]): ItemListAction => ({ setSelected: (items: ItemListStateItemWithRequiredProperties[]): ItemListAction => ({
payload: items, payload: items,
type: 'SET_SELECTED', type: 'SET_SELECTED',
}), }),
toggleExpanded: (item: ItemListStateItem): ItemListAction => ({ toggleExpanded: (item: ItemListStateItemWithRequiredProperties): ItemListAction => ({
payload: item, payload: item,
type: 'TOGGLE_EXPANDED', type: 'TOGGLE_EXPANDED',
}), }),
toggleSelected: (item: ItemListStateItem): ItemListAction => ({ toggleSelected: (item: ItemListStateItemWithRequiredProperties): ItemListAction => ({
payload: item, payload: item,
type: 'TOGGLE_SELECTED', type: 'TOGGLE_SELECTED',
}), }),
@@ -48,7 +48,7 @@ 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 = {
getDragging: (state: ItemListState): ItemListStateItem[] => { getDragging: (state: ItemListState): unknown[] => {
return Array.from(state.draggingItems.values()); return Array.from(state.draggingItems.values());
}, },
@@ -60,7 +60,7 @@ export const itemGridSelectors = {
return Array.from(state.dragging); return Array.from(state.dragging);
}, },
getExpanded: (state: ItemListState): ItemListStateItem[] => { getExpanded: (state: ItemListState): unknown[] => {
return Array.from(state.expandedItems.values()); return Array.from(state.expandedItems.values());
}, },
@@ -72,7 +72,7 @@ export const itemGridSelectors = {
return Array.from(state.expanded); return Array.from(state.expanded);
}, },
getSelected: (state: ItemListState): ItemListStateItem[] => { getSelected: (state: ItemListState): unknown[] => {
return Array.from(state.selectedItems.values()); return Array.from(state.selectedItems.values());
}, },
@@ -146,7 +146,7 @@ export const itemListUtils = {
* Toggle expansion of all items in a list * Toggle expansion of all items in a list
*/ */
toggleAllExpanded: ( toggleAllExpanded: (
items: ItemListStateItem[], items: ItemListStateItemWithRequiredProperties[],
currentState: ItemListState, currentState: ItemListState,
): ItemListAction => { ): ItemListAction => {
const allExpanded = items.every((item) => currentState.expanded.has(item.id)); const allExpanded = items.every((item) => currentState.expanded.has(item.id));
@@ -157,7 +157,7 @@ export const itemListUtils = {
* Toggle selection of all items in a list * Toggle selection of all items in a list
*/ */
toggleAllSelected: ( toggleAllSelected: (
items: ItemListStateItem[], items: ItemListStateItemWithRequiredProperties[],
currentState: ItemListState, currentState: ItemListState,
): ItemListAction => { ): ItemListAction => {
const allSelected = items.every((item) => currentState.selected.has(item.id)); const allSelected = items.every((item) => currentState.selected.has(item.id));
@@ -4,11 +4,11 @@ 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: ItemListStateItem; type: 'TOGGLE_EXPANDED' } | { payload: ItemListStateItemWithRequiredProperties; type: 'TOGGLE_EXPANDED' }
| { payload: ItemListStateItem; type: 'TOGGLE_SELECTED' } | { payload: ItemListStateItemWithRequiredProperties; type: 'TOGGLE_SELECTED' }
| { payload: ItemListStateItem[]; type: 'SET_DRAGGING' } | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_DRAGGING' }
| { payload: ItemListStateItem[]; type: 'SET_EXPANDED' } | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_EXPANDED' }
| { payload: ItemListStateItem[]; type: 'SET_SELECTED' } | { payload: ItemListStateItemWithRequiredProperties[]; type: 'SET_SELECTED' }
| { type: 'CLEAR_ALL' } | { type: 'CLEAR_ALL' }
| { type: 'CLEAR_DRAGGING' } | { type: 'CLEAR_DRAGGING' }
| { type: 'CLEAR_EXPANDED' } | { type: 'CLEAR_EXPANDED' }
@@ -16,11 +16,11 @@ export type ItemListAction =
export interface ItemListState { export interface ItemListState {
dragging: Set<string>; dragging: Set<string>;
draggingItems: Map<string, ItemListStateItem>; draggingItems: Map<string, unknown>;
expanded: Set<string>; expanded: Set<string>;
expandedItems: Map<string, ItemListStateItem>; expandedItems: Map<string, unknown>;
selected: Set<string>; selected: Set<string>;
selectedItems: Map<string, ItemListStateItem>; selectedItems: Map<string, unknown>;
version: number; version: number;
} }
@@ -31,11 +31,11 @@ export interface ItemListStateActions {
clearSelected: () => void; clearSelected: () => void;
findItemIndex: (itemId: string) => number; findItemIndex: (itemId: string) => number;
getData: () => unknown[]; getData: () => unknown[];
getDragging: () => ItemListStateItem[]; getDragging: () => unknown[];
getDraggingIds: () => string[]; getDraggingIds: () => string[];
getExpanded: () => ItemListStateItem[]; getExpanded: () => unknown[];
getExpandedIds: () => string[]; getExpandedIds: () => string[];
getSelected: () => ItemListStateItem[]; getSelected: () => unknown[];
getSelectedIds: () => string[]; getSelectedIds: () => string[];
getVersion: () => number; getVersion: () => number;
hasDragging: () => boolean; hasDragging: () => boolean;
@@ -44,11 +44,11 @@ export interface ItemListStateActions {
isDragging: (itemId: string) => boolean; isDragging: (itemId: string) => boolean;
isExpanded: (itemId: string) => boolean; isExpanded: (itemId: string) => boolean;
isSelected: (itemId: string) => boolean; isSelected: (itemId: string) => boolean;
setDragging: (items: ItemListStateItem[]) => void; setDragging: (items: ItemListStateItemWithRequiredProperties[]) => void;
setExpanded: (items: ItemListStateItem[]) => void; setExpanded: (items: ItemListStateItemWithRequiredProperties[]) => void;
setSelected: (items: ItemListStateItem[]) => void; setSelected: (items: ItemListStateItemWithRequiredProperties[]) => void;
toggleExpanded: (item: ItemListStateItem) => void; toggleExpanded: (item: ItemListStateItemWithRequiredProperties) => void;
toggleSelected: (item: ItemListStateItem) => void; toggleSelected: (item: ItemListStateItemWithRequiredProperties) => void;
} }
export interface ItemListStateItem { export interface ItemListStateItem {
@@ -57,6 +57,12 @@ export interface ItemListStateItem {
itemType: LibraryItem; itemType: LibraryItem;
} }
export type ItemListStateItemWithRequiredProperties = Record<string, unknown> & {
_serverId: string;
id: string;
itemType: LibraryItem;
};
/** /**
* Reusable reducer for item grid state management * Reusable reducer for item grid state management
* Can be used in different components or contexts * Can be used in different components or contexts
@@ -101,7 +107,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
case 'SET_DRAGGING': { case 'SET_DRAGGING': {
const newDragging = new Set<string>(); const newDragging = new Set<string>();
const newDraggingItems = new Map<string, ItemListStateItem>(); const newDraggingItems = new Map<string, unknown>();
action.payload.forEach((item) => { action.payload.forEach((item) => {
newDragging.add(item.id); newDragging.add(item.id);
@@ -118,9 +124,7 @@ export const itemListReducer = (state: ItemListState, action: ItemListAction): I
case 'SET_EXPANDED': { case 'SET_EXPANDED': {
const newExpanded = new Set<string>(); const newExpanded = new Set<string>();
const newExpandedItems = new Map<string, ItemListStateItem>(); const newExpandedItems = new Map<string, unknown>();
console.log('SET_EXPANDED', action.payload);
if (action.payload.length > 0) { if (action.payload.length > 0) {
const firstItem = action.payload[0]; const firstItem = action.payload[0];
@@ -138,7 +142,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, ItemListStateItem>(); const newSelectedItems = new Map<string, unknown>();
action.payload.forEach((item) => { action.payload.forEach((item) => {
newSelected.add(item.id); newSelected.add(item.id);
@@ -155,7 +159,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, ItemListStateItem>(); const newExpandedItems = new Map<string, unknown>();
// 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)) {
@@ -212,23 +216,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: ItemListStateItem[]) => { const setExpanded = useCallback((items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({ payload: items, type: 'SET_EXPANDED' }); dispatch({ payload: items, type: 'SET_EXPANDED' });
}, []); }, []);
const setDragging = useCallback((items: ItemListStateItem[]) => { const setDragging = useCallback((items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({ payload: items, type: 'SET_DRAGGING' }); dispatch({ payload: items, type: 'SET_DRAGGING' });
}, []); }, []);
const setSelected = useCallback((items: ItemListStateItem[]) => { const setSelected = useCallback((items: ItemListStateItemWithRequiredProperties[]) => {
dispatch({ payload: items, type: 'SET_SELECTED' }); dispatch({ payload: items, type: 'SET_SELECTED' });
}, []); }, []);
const toggleExpanded = useCallback((item: ItemListStateItem) => { const toggleExpanded = useCallback((item: ItemListStateItemWithRequiredProperties) => {
dispatch({ payload: item, type: 'TOGGLE_EXPANDED' }); dispatch({ payload: item, type: 'TOGGLE_EXPANDED' });
}, []); }, []);
const toggleSelected = useCallback((item: ItemListStateItem) => { const toggleSelected = useCallback((item: ItemListStateItemWithRequiredProperties) => {
dispatch({ payload: item, type: 'TOGGLE_SELECTED' }); dispatch({ payload: item, type: 'TOGGLE_SELECTED' });
}, []); }, []);
@@ -119,6 +119,44 @@
opacity: 0.5; opacity: 0.5;
} }
.container.data-row.dragged-over-top::before {
position: absolute;
top: -2px;
right: 0;
left: 0;
z-index: 3;
height: 2px;
pointer-events: none;
content: '';
background-color: var(--theme-colors-primary);
}
.container.data-row.dragged-over-top.dragged-over-first-cell::before {
right: -9999px;
left: -9999px;
margin-right: 9999px;
margin-left: 9999px;
}
.container.data-row.dragged-over-bottom::after {
position: absolute;
right: 0;
bottom: -2px;
left: 0;
z-index: 3;
height: 2px;
pointer-events: none;
content: '';
background-color: var(--theme-colors-primary);
}
.container.data-row.dragged-over-bottom.dragged-over-first-cell::after {
right: -9999px;
left: -9999px;
margin-right: 9999px;
margin-left: 9999px;
}
.container.data-row > * { .container.data-row > * {
position: relative; position: relative;
z-index: 2; z-index: 2;
@@ -126,6 +164,8 @@
.container.data-row { .container.data-row {
cursor: pointer; cursor: pointer;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
} }
.header-container { .header-container {
@@ -35,13 +35,17 @@ import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
import { Icon } from '/@/shared/components/icon/icon'; import { Icon } from '/@/shared/components/icon/icon';
import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop'; import { LibraryItem } from '/@/shared/types/domain-types';
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
import { TableColumn } from '/@/shared/types/types'; import { TableColumn } from '/@/shared/types/types';
export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {} export interface ItemTableListColumn extends CellComponentProps<TableItemProps> {}
export interface ItemTableListInnerColumn extends ItemTableListColumn { export interface ItemTableListInnerColumn extends ItemTableListColumn {
controls: ItemControls; controls: ItemControls;
dragRef?: null | React.Ref<HTMLDivElement>;
isDraggedOver?: 'bottom' | 'top' | null;
isDragging?: boolean;
type: TableColumn; type: TableColumn;
} }
@@ -49,9 +53,149 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
const type = props.columns[props.columnIndex].id as TableColumn; const type = props.columns[props.columnIndex].id as TableColumn;
const isHeaderEnabled = !!props.enableHeader; const isHeaderEnabled = !!props.enableHeader;
const isDataRow = isHeaderEnabled ? props.rowIndex > 0 : true;
const item = isDataRow ? props.data[props.rowIndex] : null;
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item;
const {
isDraggedOver,
isDragging: isDraggingLocal,
ref: dragRef,
} = useDragDrop<HTMLDivElement>({
drag: {
getId: () => {
if (!item || !isDataRow) {
return [];
}
const draggedItems = getDraggedItems(item as any, props.internalState);
console.log('getId', draggedItems);
return draggedItems.map((draggedItem) => draggedItem.id);
},
getItem: () => {
if (!item || !isDataRow) {
return [];
}
const draggedItems = getDraggedItems(item as any, props.internalState);
console.log('getItem', draggedItems);
return draggedItems;
},
itemType: props.itemType,
onDragStart: () => {
if (!item || !isDataRow || !props.internalState) {
return;
}
const draggedItems = getDraggedItems(item as any, props.internalState);
props.internalState.setDragging(draggedItems);
},
onDrop: () => {
if (props.internalState) {
props.internalState.setDragging([]);
}
},
operation:
props.itemType === LibraryItem.QUEUE_SONG
? [DragOperation.REORDER, DragOperation.ADD]
: [DragOperation.ADD],
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
},
drop: {
canDrop: () => {
if (props.itemType === LibraryItem.QUEUE_SONG) {
return true;
}
return false;
},
getData: () => {
return {
id: [(item as unknown as { id: string }).id],
item: [item as unknown as unknown[]],
itemType: props.itemType,
type: DragTargetMap[props.itemType] || DragTarget.GENERIC,
};
},
onDrag: () => {
return;
},
onDragLeave: () => {
return;
},
onDrop: (args) => {
if (args.self.type === DragTarget.QUEUE_SONG) {
const sourceServerId = (
args.source.item?.[0] as unknown as { _serverId: string }
)._serverId;
const sourceItemType = args.source.itemType as LibraryItem;
const droppedOnUniqueId = (
args.self.item?.[0] as unknown as { _uniqueId: string }
)._uniqueId;
switch (args.source.type) {
case DragTarget.ALBUM: {
props.playerContext.addToQueueByFetch(
sourceServerId,
args.source.id,
sourceItemType,
{ edge: args.edge, uniqueId: droppedOnUniqueId },
);
break;
}
case DragTarget.ALBUM_ARTIST: {
break;
}
case DragTarget.ARTIST: {
break;
}
case DragTarget.GENRE: {
break;
}
case DragTarget.PLAYLIST: {
break;
}
case DragTarget.QUEUE_SONG: {
break;
}
case DragTarget.SONG: {
break;
}
case DragTarget.TABLE_COLUMN: {
break;
}
default: {
break;
}
}
}
return;
},
},
isEnabled: shouldEnableDrag,
});
const isDragging =
item && typeof item === 'object' && 'id' in item && props.internalState
? props.internalState.isDragging((item as any).id)
: isDraggingLocal;
const controls = props.controls; const controls = props.controls;
const dragProps = {
dragRef: shouldEnableDrag ? dragRef : null,
isDraggedOver: isDraggedOver === 'top' || isDraggedOver === 'bottom' ? isDraggedOver : null,
isDragging,
};
if (isHeaderEnabled && props.rowIndex === 0) { if (isHeaderEnabled && props.rowIndex === 0) {
return <TableColumnHeaderContainer {...props} controls={controls} type={type} />; return <TableColumnHeaderContainer {...props} controls={controls} type={type} />;
} }
@@ -59,22 +203,22 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
switch (type) { switch (type) {
case TableColumn.ACTIONS: case TableColumn.ACTIONS:
case TableColumn.SKIP: case TableColumn.SKIP:
return <ActionsColumn {...props} controls={controls} type={type} />; return <ActionsColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.ALBUM_ARTIST: case TableColumn.ALBUM_ARTIST:
return <AlbumArtistsColumn {...props} controls={controls} type={type} />; return <AlbumArtistsColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.ALBUM_COUNT: case TableColumn.ALBUM_COUNT:
case TableColumn.PLAY_COUNT: case TableColumn.PLAY_COUNT:
case TableColumn.SONG_COUNT: case TableColumn.SONG_COUNT:
return <CountColumn {...props} controls={controls} type={type} />; return <CountColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.ARTIST: case TableColumn.ARTIST:
return <ArtistsColumn {...props} controls={controls} type={type} />; return <ArtistsColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.BIOGRAPHY: case TableColumn.BIOGRAPHY:
case TableColumn.COMMENT: case TableColumn.COMMENT:
return <TextColumn {...props} controls={controls} type={type} />; return <TextColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.BIT_RATE: case TableColumn.BIT_RATE:
case TableColumn.BPM: case TableColumn.BPM:
@@ -82,50 +226,52 @@ export const ItemTableListColumn = (props: ItemTableListColumn) => {
case TableColumn.DISC_NUMBER: case TableColumn.DISC_NUMBER:
case TableColumn.TRACK_NUMBER: case TableColumn.TRACK_NUMBER:
case TableColumn.YEAR: case TableColumn.YEAR:
return <NumericColumn {...props} controls={controls} type={type} />; return <NumericColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.DATE_ADDED: case TableColumn.DATE_ADDED:
case TableColumn.RELEASE_DATE: case TableColumn.RELEASE_DATE:
return <DateColumn {...props} controls={controls} type={type} />; return <DateColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.DURATION: case TableColumn.DURATION:
return <DurationColumn {...props} controls={controls} type={type} />; return <DurationColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.GENRE: case TableColumn.GENRE:
return <GenreColumn {...props} controls={controls} type={type} />; return <GenreColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.GENRE_BADGE: case TableColumn.GENRE_BADGE:
return <GenreBadgeColumn {...props} controls={controls} type={type} />; return <GenreBadgeColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.IMAGE: case TableColumn.IMAGE:
return <ImageColumn {...props} controls={controls} type={type} />; return <ImageColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.LAST_PLAYED: case TableColumn.LAST_PLAYED:
return <RelativeDateColumn {...props} controls={controls} type={type} />; return <RelativeDateColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.PATH: case TableColumn.PATH:
return <PathColumn {...props} controls={controls} type={type} />; return <PathColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.ROW_INDEX: case TableColumn.ROW_INDEX:
return <RowIndexColumn {...props} controls={controls} type={type} />; return <RowIndexColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.SIZE: case TableColumn.SIZE:
return <SizeColumn {...props} controls={controls} type={type} />; return <SizeColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.TITLE: case TableColumn.TITLE:
return <TitleColumn {...props} controls={controls} type={type} />; return <TitleColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.TITLE_COMBINED: case TableColumn.TITLE_COMBINED:
return <TitleCombinedColumn {...props} controls={controls} type={type} />; return (
<TitleCombinedColumn {...props} {...dragProps} controls={controls} type={type} />
);
case TableColumn.USER_FAVORITE: case TableColumn.USER_FAVORITE:
return <FavoriteColumn {...props} controls={controls} type={type} />; return <FavoriteColumn {...props} {...dragProps} controls={controls} type={type} />;
case TableColumn.USER_RATING: case TableColumn.USER_RATING:
return <RatingColumn {...props} controls={controls} type={type} />; return <RatingColumn {...props} {...dragProps} controls={controls} type={type} />;
default: default:
return <DefaultColumn {...props} controls={controls} type={type} />; return <DefaultColumn {...props} {...dragProps} controls={controls} type={type} />;
} }
}; };
@@ -137,6 +283,9 @@ export const TableColumnTextContainer = (
className?: string; className?: string;
containerClassName?: string; containerClassName?: string;
controls: ItemControls; controls: ItemControls;
dragRef?: null | React.Ref<HTMLDivElement>;
isDraggedOver?: 'bottom' | 'top' | null;
isDragging?: boolean;
type: TableColumn; type: TableColumn;
}, },
) => { ) => {
@@ -149,57 +298,8 @@ export const TableColumnTextContainer = (
? props.internalState.isSelected((item as any).id) ? props.internalState.isSelected((item as any).id)
: false; : false;
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item; const isDragging = props.isDragging ?? false;
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
drag: {
getId: () => {
if (!item || !isDataRow) {
return [];
}
const draggedItems = getDraggedItems(
item as any,
props.itemType,
props.internalState,
);
return draggedItems.map((draggedItem) => draggedItem.id);
},
getItem: () => {
if (!item || !isDataRow) {
return [];
}
return [item];
},
onDragStart: () => {
if (!item || !isDataRow || !props.internalState) {
return;
}
const draggedItems = getDraggedItems(
item as any,
props.itemType,
props.internalState,
);
props.internalState.setDragging(draggedItems);
},
onDrop: () => {
if (props.internalState) {
props.internalState.setDragging([]);
}
},
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
},
isEnabled: shouldEnableDrag,
});
const isDragging =
item && typeof item === 'object' && 'id' in item && props.internalState
? props.internalState.isDragging((item as any).id)
: isDraggingLocal;
const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current) return; if (!isDataRow || !containerRef.current) return;
@@ -209,13 +309,17 @@ export const TableColumnTextContainer = (
const handleMouseEnter = () => { const handleMouseEnter = () => {
// Find all cells in the same row and add hover class // Find all cells in the same row and add hover class
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`); const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => cell.classList.add(styles.rowHovered)); allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
// Remove hover class from all cells in the same row // Remove hover class from all cells in the same row
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`); const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered)); allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
}; };
@@ -226,7 +330,53 @@ export const TableColumnTextContainer = (
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
// Apply dragged over state to all cells in the row so border can span entire row
useEffect(() => {
if (!isDataRow || !containerRef.current) return;
const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver;
if (draggedOverState) {
// Find all cells in the same row and add dragged over class
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell, index) => {
if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
} else if (draggedOverState === 'bottom') {
cell.classList.add(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverTop);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell);
});
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => { const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements // Don't trigger row selection if clicking on interactive elements
@@ -259,6 +409,8 @@ export const TableColumnTextContainer = (
[styles.center]: props.columns[props.columnIndex].align === 'center', [styles.center]: props.columns[props.columnIndex].align === 'center',
[styles.compact]: props.size === 'compact', [styles.compact]: props.size === 'compact',
[styles.dataRow]: isDataRow, [styles.dataRow]: isDataRow,
[styles.draggedOverBottom]: isDataRow && props.isDraggedOver === 'bottom',
[styles.draggedOverTop]: isDataRow && props.isDraggedOver === 'top',
[styles.dragging]: isDataRow && isDragging, [styles.dragging]: isDataRow && isDragging,
[styles.large]: props.size === 'large', [styles.large]: props.size === 'large',
[styles.left]: props.columns[props.columnIndex].align === 'start', [styles.left]: props.columns[props.columnIndex].align === 'start',
@@ -274,7 +426,7 @@ export const TableColumnTextContainer = (
props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0, props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0,
[styles.withVerticalBorder]: props.enableVerticalBorders, [styles.withVerticalBorder]: props.enableVerticalBorders,
})} })}
data-row-index={isDataRow ? props.rowIndex : undefined} data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
onClick={handleCellClick} onClick={handleCellClick}
ref={mergedRef} ref={mergedRef}
style={props.style} style={props.style}
@@ -299,6 +451,9 @@ export const TableColumnContainer = (
className?: string; className?: string;
containerStyle?: CSSProperties; containerStyle?: CSSProperties;
controls: ItemControls; controls: ItemControls;
dragRef?: null | React.Ref<HTMLDivElement>;
isDraggedOver?: 'bottom' | 'top' | null;
isDragging?: boolean;
type: TableColumn; type: TableColumn;
}, },
) => { ) => {
@@ -311,57 +466,8 @@ export const TableColumnContainer = (
? props.internalState.isSelected((item as any).id) ? props.internalState.isSelected((item as any).id)
: false; : false;
const shouldEnableDrag = !!props.enableDrag && isDataRow && !!item; const isDragging = props.isDragging ?? false;
const mergedRef = useMergedRef(containerRef, props.dragRef ?? null);
const { isDragging: isDraggingLocal, ref: dragRef } = useDragDrop<HTMLDivElement>({
drag: {
getId: () => {
if (!item || !isDataRow) {
return [];
}
const draggedItems = getDraggedItems(
item as any,
props.itemType,
props.internalState,
);
return draggedItems.map((draggedItem) => draggedItem.id);
},
getItem: () => {
if (!item || !isDataRow) {
return [];
}
return [item];
},
onDragStart: () => {
if (!item || !isDataRow || !props.internalState) {
return;
}
const draggedItems = getDraggedItems(
item as any,
props.itemType,
props.internalState,
);
props.internalState.setDragging(draggedItems);
},
onDrop: () => {
if (props.internalState) {
props.internalState.setDragging([]);
}
},
target: DragTargetMap[props.itemType] || DragTarget.GENERIC,
},
isEnabled: shouldEnableDrag,
});
const isDragging =
item && typeof item === 'object' && 'id' in item && props.internalState
? props.internalState.isDragging((item as any).id)
: isDraggingLocal;
const mergedRef = useMergedRef(containerRef, shouldEnableDrag ? dragRef : null);
useEffect(() => { useEffect(() => {
if (!isDataRow || !containerRef.current) return; if (!isDataRow || !containerRef.current) return;
@@ -371,13 +477,17 @@ export const TableColumnContainer = (
const handleMouseEnter = () => { const handleMouseEnter = () => {
// Find all cells in the same row and add hover class // Find all cells in the same row and add hover class
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`); const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => cell.classList.add(styles.rowHovered)); allCells.forEach((cell) => cell.classList.add(styles.rowHovered));
}; };
const handleMouseLeave = () => { const handleMouseLeave = () => {
// Remove hover class from all cells in the same row // Remove hover class from all cells in the same row
const allCells = document.querySelectorAll(`[data-row-index="${rowIndex}"]`); const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => cell.classList.remove(styles.rowHovered)); allCells.forEach((cell) => cell.classList.remove(styles.rowHovered));
}; };
@@ -388,7 +498,53 @@ export const TableColumnContainer = (
container.removeEventListener('mouseenter', handleMouseEnter); container.removeEventListener('mouseenter', handleMouseEnter);
container.removeEventListener('mouseleave', handleMouseLeave); container.removeEventListener('mouseleave', handleMouseLeave);
}; };
}, [isDataRow, props.rowIndex, props.enableRowHoverHighlight]); }, [isDataRow, props.rowIndex, props.enableRowHoverHighlight, props.tableId]);
// Apply dragged over state to all cells in the row so border can span entire row
useEffect(() => {
if (!isDataRow || !containerRef.current) return;
const rowIndex = props.rowIndex;
const draggedOverState = props.isDraggedOver;
if (draggedOverState) {
// Find all cells in the same row and add dragged over class
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell, index) => {
if (draggedOverState === 'top') {
cell.classList.add(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
} else if (draggedOverState === 'bottom') {
cell.classList.add(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverTop);
// Mark first cell so border can span full width
if (index === 0) {
cell.classList.add(styles.draggedOverFirstCell);
} else {
cell.classList.remove(styles.draggedOverFirstCell);
}
}
});
} else {
// Remove dragged over classes from all cells in the same row
const allCells = document.querySelectorAll(
`[data-row-index="${props.tableId}-${rowIndex}"]`,
);
allCells.forEach((cell) => {
cell.classList.remove(styles.draggedOverTop);
cell.classList.remove(styles.draggedOverBottom);
cell.classList.remove(styles.draggedOverFirstCell);
});
}
}, [isDataRow, props.rowIndex, props.isDraggedOver, props.tableId]);
const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => { const handleCellClick = (event: React.MouseEvent<HTMLDivElement>) => {
// Don't trigger row selection if clicking on interactive elements // Don't trigger row selection if clicking on interactive elements
@@ -421,6 +577,8 @@ export const TableColumnContainer = (
[styles.center]: props.columns[props.columnIndex].align === 'center', [styles.center]: props.columns[props.columnIndex].align === 'center',
[styles.compact]: props.size === 'compact', [styles.compact]: props.size === 'compact',
[styles.dataRow]: isDataRow, [styles.dataRow]: isDataRow,
[styles.draggedOverBottom]: isDataRow && props.isDraggedOver === 'bottom',
[styles.draggedOverTop]: isDataRow && props.isDraggedOver === 'top',
[styles.dragging]: isDataRow && isDragging, [styles.dragging]: isDataRow && isDragging,
[styles.large]: props.size === 'large', [styles.large]: props.size === 'large',
[styles.left]: props.columns[props.columnIndex].align === 'start', [styles.left]: props.columns[props.columnIndex].align === 'start',
@@ -436,7 +594,7 @@ export const TableColumnContainer = (
props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0, props.enableHorizontalBorders && props.enableHeader && props.rowIndex > 0,
[styles.withVerticalBorder]: props.enableVerticalBorders, [styles.withVerticalBorder]: props.enableVerticalBorders,
})} })}
data-row-index={isDataRow ? props.rowIndex : undefined} data-row-index={isDataRow ? `${props.tableId}-${props.rowIndex}` : undefined}
onClick={handleCellClick} onClick={handleCellClick}
ref={mergedRef} ref={mergedRef}
style={{ ...props.containerStyle, ...props.style }} style={{ ...props.containerStyle, ...props.style }}
@@ -13,14 +13,17 @@
min-width: 0; min-width: 0;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.item-table-grid-container { .item-table-grid-container {
position: relative; position: relative;
flex: 1 1 auto; flex: 1 1 auto;
width: 100%; width: 100%;
min-width: 0;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
overflow: hidden;
} }
.item-table-pinned-rows-container { .item-table-pinned-rows-container {
@@ -11,6 +11,7 @@ import React, {
Ref, Ref,
useCallback, useCallback,
useEffect, useEffect,
useId,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useRef, useRef,
@@ -26,6 +27,7 @@ import { useDefaultItemListControls } from '/@/renderer/components/item-list/hel
import { import {
ItemListStateActions, ItemListStateActions,
ItemListStateItem, ItemListStateItem,
ItemListStateItemWithRequiredProperties,
useItemListState, useItemListState,
} from '/@/renderer/components/item-list/helpers/item-list-state'; } from '/@/renderer/components/item-list/helpers/item-list-state';
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns'; import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
@@ -34,8 +36,45 @@ import {
ItemListHandle, ItemListHandle,
ItemTableListColumnConfig, ItemTableListColumnConfig,
} from '/@/renderer/components/item-list/types'; } from '/@/renderer/components/item-list/types';
import {
PlayerContext,
usePlayerContext,
} from '/@/renderer/features/player/context/player-context';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
/**
* Type guard to check if an item has the required properties (id and serverId)
* Similar to the type guard used in ItemCard
*/
const hasRequiredItemProperties = (item: unknown): item is { id: string; serverId: string } => {
return (
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof (item as any).id === 'string' &&
'serverId' in item &&
typeof (item as any).serverId === 'string'
);
};
/**
* Type guard to check if an item has the required properties for ItemListStateItemWithRequiredProperties
*/
const hasRequiredStateItemProperties = (
item: unknown,
): item is ItemListStateItemWithRequiredProperties => {
return (
typeof item === 'object' &&
item !== null &&
'id' in item &&
typeof (item as any).id === 'string' &&
'_serverId' in item &&
typeof (item as any)._serverId === 'string' &&
'itemType' in item &&
typeof (item as any).itemType === 'string'
);
};
interface VirtualizedTableGridProps { interface VirtualizedTableGridProps {
calculatedColumnWidths: number[]; calculatedColumnWidths: number[];
CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>; CellComponent: JSXElementConstructor<CellComponentProps<TableItemProps>>;
@@ -64,9 +103,11 @@ interface VirtualizedTableGridProps {
pinnedRightColumnRef: React.RefObject<HTMLDivElement>; pinnedRightColumnRef: React.RefObject<HTMLDivElement>;
pinnedRowCount: number; pinnedRowCount: number;
pinnedRowRef: React.RefObject<HTMLDivElement>; pinnedRowRef: React.RefObject<HTMLDivElement>;
playerContext: PlayerContext;
showLeftShadow: boolean; showLeftShadow: boolean;
showRightShadow: boolean; showRightShadow: boolean;
size: 'compact' | 'default' | 'large'; size: 'compact' | 'default' | 'large';
tableId: string;
totalColumnCount: number; totalColumnCount: number;
totalRowCount: number; totalRowCount: number;
} }
@@ -100,9 +141,11 @@ const VirtualizedTableGrid = React.memo(
pinnedRightColumnRef, pinnedRightColumnRef,
pinnedRowCount, pinnedRowCount,
pinnedRowRef, pinnedRowRef,
playerContext,
showLeftShadow, showLeftShadow,
showRightShadow, showRightShadow,
size, size,
tableId,
totalColumnCount, totalColumnCount,
totalRowCount, totalRowCount,
}: VirtualizedTableGridProps) => { }: VirtualizedTableGridProps) => {
@@ -129,7 +172,9 @@ const VirtualizedTableGrid = React.memo(
internalState, internalState,
itemType, itemType,
onRowClick, onRowClick,
playerContext,
size, size,
tableId,
}), }),
[ [
cellPadding, cellPadding,
@@ -146,9 +191,11 @@ const VirtualizedTableGrid = React.memo(
enableVerticalBorders, enableVerticalBorders,
getRowHeight, getRowHeight,
internalState, internalState,
playerContext,
itemType, itemType,
onRowClick, onRowClick,
size, size,
tableId,
], ],
); );
@@ -430,7 +477,9 @@ export interface TableItemProps {
internalState: ItemListStateActions; internalState: ItemListStateActions;
itemType: ItemTableListProps['itemType']; itemType: ItemTableListProps['itemType'];
onRowClick?: (item: any, event: React.MouseEvent<HTMLDivElement>) => void; onRowClick?: (item: any, event: React.MouseEvent<HTMLDivElement>) => void;
playerContext: PlayerContext;
size?: ItemTableListProps['size']; size?: ItemTableListProps['size'];
tableId: string;
} }
interface ItemTableListProps { interface ItemTableListProps {
@@ -484,10 +533,11 @@ export const ItemTableList = ({
rowHeight, rowHeight,
size = 'default', size = 'default',
}: ItemTableListProps) => { }: ItemTableListProps) => {
const tableId = useId();
const totalItemCount = enableHeader ? data.length + 1 : data.length; const totalItemCount = enableHeader ? data.length + 1 : data.length;
const parsedColumns = useMemo(() => parseTableColumns(columns), [columns]); const parsedColumns = useMemo(() => parseTableColumns(columns), [columns]);
const columnCount = parsedColumns.length; const columnCount = parsedColumns.length;
const playerContext = usePlayerContext();
const [centerContainerWidth, setCenterContainerWidth] = useState(0); const [centerContainerWidth, setCenterContainerWidth] = useState(0);
useEffect(() => { useEffect(() => {
@@ -614,7 +664,9 @@ export const ItemTableList = ({
getRowHeight: () => DEFAULT_ROW_HEIGHT, getRowHeight: () => DEFAULT_ROW_HEIGHT,
internalState: {} as ItemListStateActions, internalState: {} as ItemListStateActions,
itemType, itemType,
playerContext,
size, size,
tableId,
}; };
for (let i = 0; i < adjustedIndex; i++) { for (let i = 0; i < adjustedIndex; i++) {
@@ -632,11 +684,9 @@ export const ItemTableList = ({
}, },
[ [
enableHeader, enableHeader,
rowHeight,
size,
DEFAULT_ROW_HEIGHT,
cellPadding, cellPadding,
parsedColumns, parsedColumns,
data,
enableAlternateRowColors, enableAlternateRowColors,
enableExpansion, enableExpansion,
enableHorizontalBorders, enableHorizontalBorders,
@@ -644,7 +694,11 @@ export const ItemTableList = ({
enableSelection, enableSelection,
enableVerticalBorders, enableVerticalBorders,
itemType, itemType,
data, playerContext,
size,
tableId,
DEFAULT_ROW_HEIGHT,
rowHeight,
], ],
); );
@@ -989,7 +1043,7 @@ export const ItemTableList = ({
const handleRowClick = useCallback( const handleRowClick = useCallback(
(item: any, event: React.MouseEvent<HTMLDivElement>) => { (item: any, event: React.MouseEvent<HTMLDivElement>) => {
if (!enableSelection || !item) { if (!enableSelection || !item || !hasRequiredItemProperties(item)) {
return; return;
} }
@@ -1007,29 +1061,35 @@ export const ItemTableList = ({
// Remove this item from selection // Remove this item from selection
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const filteredSelected = currentSelected.filter( const filteredSelected = currentSelected.filter(
(selectedItem) => selectedItem.id !== item.id, (selectedItem): selectedItem is ItemListStateItemWithRequiredProperties =>
hasRequiredStateItemProperties(selectedItem) &&
selectedItem.id !== item.id,
); );
internalState.setSelected(filteredSelected); internalState.setSelected(filteredSelected);
} else { } else {
// Add this item to selection // Add this item to selection
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const newSelected = [...currentSelected, itemListItem]; const validSelected = currentSelected.filter(hasRequiredStateItemProperties);
const newSelected: ItemListStateItemWithRequiredProperties[] = [
...validSelected,
itemListItem as ItemListStateItemWithRequiredProperties,
];
internalState.setSelected(newSelected); internalState.setSelected(newSelected);
} }
} }
// Check if shift key is held for range selection // Check if shift key is held for range selection
else if (event.shiftKey) { else if (event.shiftKey) {
const selectedItems = internalState.getSelected(); const selectedItems = internalState.getSelected();
const lastSelectedItem = selectedItems[selectedItems.length - 1]; const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
if (lastSelectedItem) { if (lastSelectedItem) {
// Find the indices of the last selected item and current item // Find the indices of the last selected item and current item
const lastIndex = data.findIndex( const lastIndex = data.findIndex(
(d) => (d) => hasRequiredItemProperties(d) && d.id === lastSelectedItem.id,
d && typeof d === 'object' && 'id' in d && d.id === lastSelectedItem.id,
); );
const currentIndex = data.findIndex( const currentIndex = data.findIndex(
(d) => d && typeof d === 'object' && 'id' in d && d.id === item.id, (d) => hasRequiredItemProperties(d) && d.id === item.id,
); );
if (lastIndex !== -1 && currentIndex !== -1) { if (lastIndex !== -1 && currentIndex !== -1) {
@@ -1037,20 +1097,15 @@ 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: ItemListStateItem[] = []; const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = data[i]; const rangeItem = data[i];
if ( if (hasRequiredItemProperties(rangeItem)) {
rangeItem &&
typeof rangeItem === 'object' &&
'id' in rangeItem &&
'serverId' in rangeItem
) {
rangeItems.push({ rangeItems.push({
_serverId: (rangeItem as any).serverId, _serverId: rangeItem.serverId,
id: (rangeItem as any).id, id: rangeItem.id,
itemType, itemType,
}); } as ItemListStateItemWithRequiredProperties);
} }
} }
@@ -1061,7 +1116,10 @@ export const ItemTableList = ({
// Deselect the range // Deselect the range
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const filteredSelected = currentSelected.filter( const filteredSelected = currentSelected.filter(
(selectedItem) => (
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
hasRequiredStateItemProperties(selectedItem) &&
!rangeItems.some( !rangeItems.some(
(rangeItem) => rangeItem.id === selectedItem.id, (rangeItem) => rangeItem.id === selectedItem.id,
), ),
@@ -1070,7 +1128,12 @@ export const ItemTableList = ({
} else { } else {
// Select the range // Select the range
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const newSelected = [...currentSelected]; const validSelected = currentSelected.filter(
hasRequiredStateItemProperties,
);
const newSelected: ItemListStateItemWithRequiredProperties[] = [
...validSelected,
];
rangeItems.forEach((rangeItem) => { rangeItems.forEach((rangeItem) => {
if (!newSelected.some((selected) => selected.id === rangeItem.id)) { if (!newSelected.some((selected) => selected.id === rangeItem.id)) {
newSelected.push(rangeItem); newSelected.push(rangeItem);
@@ -1081,19 +1144,24 @@ export const ItemTableList = ({
} }
} else { } else {
// No previous selection, just toggle this item // No previous selection, just toggle this item
internalState.toggleSelected(itemListItem); internalState.toggleSelected(
itemListItem as ItemListStateItemWithRequiredProperties,
);
} }
} else { } else {
// Regular click - deselect all others and select only this item // Regular click - deselect all others and select only this item
// If this item is already the only selected item, deselect it // If this item is already the only selected item, deselect it
const selectedItems = internalState.getSelected(); const selectedItems = internalState.getSelected();
const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
const isOnlySelected = const isOnlySelected =
selectedItems.length === 1 && selectedItems[0].id === item.id; validSelectedItems.length === 1 && validSelectedItems[0].id === item.id;
if (isOnlySelected) { if (isOnlySelected) {
internalState.clearSelected(); internalState.clearSelected();
} else { } else {
internalState.setSelected([itemListItem]); internalState.setSelected([
itemListItem as ItemListStateItemWithRequiredProperties,
]);
} }
} }
}, },
@@ -1108,12 +1176,13 @@ export const ItemTableList = ({
e.stopPropagation(); e.stopPropagation();
const selected = internalState.getSelected(); const selected = internalState.getSelected();
const validSelected = selected.filter(hasRequiredStateItemProperties);
let currentIndex = -1; let currentIndex = -1;
if (selected.length > 0) { if (validSelected.length > 0) {
const lastSelected = selected[selected.length - 1]; const lastSelected = validSelected[validSelected.length - 1];
currentIndex = data.findIndex( currentIndex = data.findIndex(
(d: any) => d && typeof d === 'object' && 'id' in d && d.id === lastSelected.id, (d) => hasRequiredItemProperties(d) && d.id === lastSelected.id,
); );
} }
@@ -1131,13 +1200,13 @@ export const ItemTableList = ({
// Handle Shift + Arrow for incremental range selection (matches shift+click behavior) // Handle Shift + Arrow for incremental range selection (matches shift+click behavior)
if (e.shiftKey) { if (e.shiftKey) {
const selectedItems = internalState.getSelected(); const selectedItems = internalState.getSelected();
const lastSelectedItem = selectedItems[selectedItems.length - 1]; const validSelectedItems = selectedItems.filter(hasRequiredStateItemProperties);
const lastSelectedItem = validSelectedItems[validSelectedItems.length - 1];
if (lastSelectedItem) { if (lastSelectedItem) {
// Find the indices of the last selected item and new item // Find the indices of the last selected item and new item
const lastIndex = data.findIndex( const lastIndex = data.findIndex(
(d: any) => (d) => hasRequiredItemProperties(d) && d.id === lastSelectedItem.id,
d && typeof d === 'object' && 'id' in d && d.id === lastSelectedItem.id,
); );
if (lastIndex !== -1 && newIndex !== -1) { if (lastIndex !== -1 && newIndex !== -1) {
@@ -1145,26 +1214,26 @@ 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: ItemListStateItem[] = []; const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
for (let i = startIndex; i <= stopIndex; i++) { for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = data[i]; const rangeItem = data[i];
if ( if (hasRequiredItemProperties(rangeItem)) {
rangeItem &&
typeof rangeItem === 'object' &&
'id' in rangeItem &&
'serverId' in rangeItem
) {
rangeItems.push({ rangeItems.push({
_serverId: (rangeItem as any).serverId, _serverId: rangeItem.serverId,
id: (rangeItem as any).id, id: rangeItem.id,
itemType, itemType,
}); } as ItemListStateItemWithRequiredProperties);
} }
} }
// Add range items to selection (matching shift+click behavior) // Add range items to selection (matching shift+click behavior)
const currentSelected = internalState.getSelected(); const currentSelected = internalState.getSelected();
const newSelected = [...currentSelected]; const validSelected = currentSelected.filter(
hasRequiredStateItemProperties,
);
const newSelected: ItemListStateItemWithRequiredProperties[] = [
...validSelected,
];
rangeItems.forEach((rangeItem) => { rangeItems.forEach((rangeItem) => {
if (!newSelected.some((selected) => selected.id === rangeItem.id)) { if (!newSelected.some((selected) => selected.id === rangeItem.id)) {
newSelected.push(rangeItem); newSelected.push(rangeItem);
@@ -1172,11 +1241,12 @@ 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: ItemListStateItem = { if (hasRequiredItemProperties(newItem)) {
const newItemListItem: ItemListStateItemWithRequiredProperties = {
_serverId: newItem.serverId, _serverId: newItem.serverId,
id: newItem.id, id: newItem.id,
itemType, itemType,
}; } as ItemListStateItemWithRequiredProperties;
// Remove the new item from its current position if it exists // Remove the new item from its current position if it exists
const filteredSelected = newSelected.filter( const filteredSelected = newSelected.filter(
(item) => item.id !== newItemListItem.id, (item) => item.id !== newItemListItem.id,
@@ -1185,26 +1255,31 @@ export const ItemTableList = ({
filteredSelected.push(newItemListItem); filteredSelected.push(newItemListItem);
internalState.setSelected(filteredSelected); internalState.setSelected(filteredSelected);
} }
}
} else { } else {
// No previous selection, just select the new item // No previous selection, just select the new item
if (hasRequiredItemProperties(newItem)) {
internalState.setSelected([ internalState.setSelected([
{ {
_serverId: newItem.serverId, _serverId: newItem.serverId,
id: newItem.id, id: newItem.id,
itemType, itemType,
}, } as ItemListStateItemWithRequiredProperties,
]); ]);
} }
}
} else { } else {
// Without Shift: select only the new item // Without Shift: select only the new item
if (hasRequiredItemProperties(newItem)) {
internalState.setSelected([ internalState.setSelected([
{ {
_serverId: newItem.serverId, _serverId: newItem.serverId,
id: newItem.id, id: newItem.id,
itemType, itemType,
}, } as ItemListStateItemWithRequiredProperties,
]); ]);
} }
}
const offset = calculateScrollTopForIndex(newIndex); const offset = calculateScrollTopForIndex(newIndex);
scrollToTableOffset(offset); scrollToTableOffset(offset);
@@ -1304,9 +1379,11 @@ export const ItemTableList = ({
pinnedRightColumnRef={pinnedRightColumnRef} pinnedRightColumnRef={pinnedRightColumnRef}
pinnedRowCount={pinnedRowCount} pinnedRowCount={pinnedRowCount}
pinnedRowRef={pinnedRowRef} pinnedRowRef={pinnedRowRef}
playerContext={playerContext}
showLeftShadow={showLeftShadow} showLeftShadow={showLeftShadow}
showRightShadow={showRightShadow} showRightShadow={showRightShadow}
size={size} size={size}
tableId={tableId}
totalColumnCount={totalColumnCount} totalColumnCount={totalColumnCount}
totalRowCount={totalRowCount} totalRowCount={totalRowCount}
/> />
@@ -101,7 +101,7 @@ export const PlayQueueListControls = ({
// mpvPlayer!.pause(); // mpvPlayer!.pause();
// } // }
updateSong(undefined); player.clearQueue();
// setCurrentTime(0); // setCurrentTime(0);
// pause(); // pause();
@@ -1,6 +1,7 @@
import type { Ref } from 'react'; import type { Ref } from 'react';
import { forwardRef, useMemo } from 'react'; import { nanoid } from 'nanoid/non-secure';
import { forwardRef, useEffect, useMemo, useRef } from 'react';
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
@@ -27,19 +28,54 @@ export const PlayQueue = forwardRef(({ listKey, searchTerm }: QueueProps, ref: R
return queue; return queue;
}, [queue, searchTerm]); }, [queue, searchTerm]);
const playQueueKeyRef = useRef({
alreadyRendered: false,
key: nanoid(),
prevLength: 0,
});
useEffect(() => {
if (playQueueKeyRef.current.alreadyRendered && playQueueKeyRef.current.prevLength === 0) {
return;
}
if (data.length === 0) {
playQueueKeyRef.current = {
alreadyRendered: false,
key: nanoid(),
prevLength: data.length,
};
return;
}
if (data.length > 0 && !playQueueKeyRef.current.alreadyRendered) {
playQueueKeyRef.current = {
alreadyRendered: true,
key: nanoid(),
prevLength: data.length,
};
}
}, [data.length, playQueueKeyRef]);
return ( return (
<ItemTableList <ItemTableList
CellComponent={ItemTableListColumn} CellComponent={ItemTableListColumn}
columns={table.columns} columns={table.columns}
data={data || []} data={data || []}
enableAlternateRowColors={table.enableAlternateRowColors} enableAlternateRowColors={table.enableAlternateRowColors}
enableDrag={true}
enableExpansion={false} enableExpansion={false}
enableHeader={true} enableHeader={true}
enableHorizontalBorders={table.enableHorizontalBorders} enableHorizontalBorders={table.enableHorizontalBorders}
enableRowHoverHighlight={table.enableRowHoverHighlight} enableRowHoverHighlight={table.enableRowHoverHighlight}
enableSelection={true} enableSelection={true}
enableVerticalBorders={table.enableVerticalBorders} enableVerticalBorders={table.enableVerticalBorders}
itemType={LibraryItem.ALBUM} initialTop={{
to: 0,
type: 'offset',
}}
itemType={LibraryItem.QUEUE_SONG}
key={playQueueKeyRef.current.key}
ref={ref} ref={ref}
size={table.size} size={table.size}
/> />
@@ -23,7 +23,7 @@ import {
} from '/@/shared/types/domain-types'; } from '/@/shared/types/domain-types';
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types'; import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types';
interface PlayerContext { export interface PlayerContext {
addToQueueByData: (data: Song[], type: AddToQueueType) => void; addToQueueByData: (data: Song[], type: AddToQueueType) => void;
addToQueueByFetch: ( addToQueueByFetch: (
serverId: string, serverId: string,
+50 -54
View File
@@ -8,35 +8,34 @@ import {
BaseEventPayload, BaseEventPayload,
CleanupFn, CleanupFn,
ElementDragType, ElementDragType,
Input,
} from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types'; } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types';
import { import {
draggable, draggable,
dropTargetForElements, dropTargetForElements,
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview'; 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 { useEffect, useRef, useState } from 'react';
import { createRoot } from 'react-dom/client';
import { LibraryItem } from '/@/shared/types/domain-types';
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop'; import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
interface UseDraggableProps { interface UseDraggableProps {
drag?: { drag?: {
getId: () => string[]; getId: () => string[];
getItem: () => unknown[]; getItem: () => unknown[];
itemType?: LibraryItem;
onDragStart?: () => void; onDragStart?: () => void;
onDrop?: () => void; onDrop?: () => void;
onGenerateDragPreview?: (data: BaseEventPayload<ElementDragType>) => void; onGenerateDragPreview?: (data: BaseEventPayload<ElementDragType>) => void;
operation: DragOperation[];
target: DragTarget | string; target: DragTarget | string;
}; };
drop?: { drop?: {
canDrop: (args: { source: DragData }) => boolean; canDrop: (args: { source: DragData }) => boolean;
getData: (args: { element: HTMLElement; input: Input }) => DragData; getData: () => DragData;
onDrag: (args: { self: DragData }) => void; onDrag: (args: { edge: Edge | null }) => void;
onDragLeave: () => void; onDragLeave: () => void;
onDrop: (args: { self: DragData }) => void; onDrop: (args: { edge: Edge | null; self: DragData; source: DragData }) => void;
}; };
isEnabled: boolean; isEnabled: boolean;
} }
@@ -49,6 +48,7 @@ export const useDragDrop = <TElement extends HTMLElement>({
const ref = useRef<null | TElement>(null); const ref = useRef<null | TElement>(null);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
useEffect(() => { useEffect(() => {
if (!ref.current || !isEnabled) return; if (!ref.current || !isEnabled) return;
@@ -66,6 +66,8 @@ export const useDragDrop = <TElement extends HTMLElement>({
const data = dndUtils.generateDragData({ const data = dndUtils.generateDragData({
id, id,
item, item,
itemType: drag.itemType,
operation: drag.operation,
type: drag.target, type: drag.target,
}); });
return data; return data;
@@ -96,59 +98,53 @@ export const useDragDrop = <TElement extends HTMLElement>({
); );
} }
// if (drop) { if (drop) {
// functions.push( functions.push(
// dropTargetForElements({ dropTargetForElements({
// canDrop: (args) => { canDrop: (args) => {
// const data = args.source.data as unknown as DragData; return (
// const isSelf = (args.source.data.id as string[])[0] === option.value; drop.canDrop?.({ source: args.source.data as unknown as DragData }) ||
// return dndUtils.isDropTarget(data.type, [DragTarget.GENERIC]) && !isSelf; false
// }, );
// element: ref.current, },
// getData: ({ element, input }) => { element: ref.current,
// const data = dndUtils.generateDragData({ getData: (args) => {
// id: [option.value], const dropData = drop.getData();
// operation: [DragOperation.REORDER],
// type: DragTarget.GENERIC,
// });
// return attachClosestEdge(data, { const data = dndUtils.generateDragData(dropData);
// 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[]; return attachClosestEdge(data, {
// const to = args.self.data.id as string[]; allowedEdges: ['top', 'bottom'],
element: args.element,
// const newOrder = dndUtils.reorderById({ input: args.input,
// edge: closestEdgeOfTarget, });
// idFrom: from[0], },
// idTo: to[0], onDrag: (args) => {
// list: values, const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
// }); drop.onDrag?.({ edge: closestEdgeOfTarget });
setIsDraggedOver(closestEdgeOfTarget);
// onChange(newOrder); },
// setIsDraggedOver(null); onDragLeave: () => {
// }, setIsDraggedOver(null);
// }), },
// ); onDrop: (args) => {
// } const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
drop.onDrop?.({
edge: closestEdgeOfTarget,
self: args.self.data as unknown as DragData,
source: args.source.data as unknown as DragData,
});
setIsDraggedOver(null);
},
}),
);
}
return combine(...functions); return combine(...functions);
}, [drag, drop, isDragging]); }, [drag, drop, isDragging, isDraggedOver, isEnabled]);
return { return {
isDraggedOver,
isDragging, isDragging,
ref, ref,
}; };
@@ -1,6 +1,9 @@
.right-sidebar-container { .right-sidebar-container {
position: relative; position: relative;
display: flex;
flex-direction: column;
grid-area: right-sidebar; grid-area: right-sidebar;
height: 100%;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
border-left: 1px solid alpha(var(--theme-colors-border), 0.3); border-left: 1px solid alpha(var(--theme-colors-border), 0.3);
+1
View File
@@ -2,6 +2,7 @@
* { * {
box-sizing: border-box; box-sizing: border-box;
outline: none;
} }
*, *,
+7 -2
View File
@@ -9,8 +9,9 @@ export enum DragTarget {
GENERIC = 'generic', GENERIC = 'generic',
GENRE = LibraryItem.GENRE, GENRE = LibraryItem.GENRE,
PLAYLIST = LibraryItem.PLAYLIST, PLAYLIST = LibraryItem.PLAYLIST,
QUEUE_SONG = LibraryItem.QUEUE_SONG,
SONG = LibraryItem.SONG,
TABLE_COLUMN = 'tableColumn', TABLE_COLUMN = 'tableColumn',
TRACK = LibraryItem.SONG,
} }
export const DragTargetMap = { export const DragTargetMap = {
@@ -19,7 +20,8 @@ export const DragTargetMap = {
[LibraryItem.ARTIST]: DragTarget.ARTIST, [LibraryItem.ARTIST]: DragTarget.ARTIST,
[LibraryItem.GENRE]: DragTarget.GENRE, [LibraryItem.GENRE]: DragTarget.GENRE,
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST, [LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
[LibraryItem.SONG]: DragTarget.TRACK, [LibraryItem.QUEUE_SONG]: DragTarget.QUEUE_SONG,
[LibraryItem.SONG]: DragTarget.SONG,
}; };
export enum DragOperation { export enum DragOperation {
@@ -38,6 +40,7 @@ export interface DragData<
> { > {
id: string[]; id: string[];
item?: TDataType[]; item?: TDataType[];
itemType?: LibraryItem;
metadata?: T; metadata?: T;
operation?: DragOperation[]; operation?: DragOperation[];
type: DragTarget; type: DragTarget;
@@ -52,6 +55,7 @@ export const dndUtils = {
args: { args: {
id: string[]; id: string[];
item?: TDataType[]; item?: TDataType[];
itemType?: LibraryItem;
operation?: DragOperation[]; operation?: DragOperation[];
type: DragTarget | string; type: DragTarget | string;
}, },
@@ -60,6 +64,7 @@ export const dndUtils = {
return { return {
id: args.id, id: args.id,
item: args.item, item: args.item,
itemType: args.itemType,
metadata, metadata,
operation: args.operation, operation: args.operation,
type: args.type, type: args.type,