mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-26 13:57:36 +02:00
add drag/drop from lists into queue
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user