Files
feishin/src/renderer/components/item-list/helpers/item-list-controls.ts
T
2025-11-29 19:32:19 -08:00

227 lines
9.9 KiB
TypeScript

import { useMemo } from 'react';
import { ItemListStateItemWithRequiredProperties } from '/@/renderer/components/item-list/helpers/item-list-state';
import { DefaultItemControlProps, ItemControls } from '/@/renderer/components/item-list/types';
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { Play } from '/@/shared/types/types';
export const useDefaultItemListControls = () => {
const player = usePlayerContext();
const controls: ItemControls = useMemo(() => {
return {
onClick: ({ event, internalState, item }: DefaultItemControlProps) => {
if (!item || !internalState || !event) {
return;
}
// Extract rowId from the item
const rowId = internalState.extractRowId(item);
if (!rowId) return;
// Use the item directly (rowId is separate, used only as key in state)
const itemListItem = item as ItemListStateItemWithRequiredProperties;
// Check if ctrl/cmd key is held for multi-selection
if (event.ctrlKey || event.metaKey) {
const isCurrentlySelected = internalState.isSelected(rowId);
if (isCurrentlySelected) {
// Remove this item from selection
const currentSelected = internalState.getSelected();
const filteredSelected = currentSelected.filter(
(
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' &&
selectedItem !== null &&
internalState.extractRowId(selectedItem) !== rowId,
);
internalState.setSelected(filteredSelected);
} else {
// Add this item to selection
const currentSelected = internalState.getSelected();
const newSelected = [
...currentSelected.filter(
(
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' && selectedItem !== null,
),
itemListItem,
];
internalState.setSelected(newSelected);
}
}
// Check if shift key is held for range selection
else if (event.shiftKey) {
const selectedItems = internalState.getSelected();
const lastSelectedItem = selectedItems[selectedItems.length - 1];
if (
lastSelectedItem &&
typeof lastSelectedItem === 'object' &&
lastSelectedItem !== null
) {
// Get the data array from internalState
const data = internalState.getData();
// Filter out null/undefined values (e.g., header row)
const validData = data.filter((d) => d && typeof d === 'object');
// Find the indices of the last selected item and current item
const lastRowId = internalState.extractRowId(lastSelectedItem);
if (!lastRowId) return;
const lastIndex = internalState.findItemIndex(lastRowId);
const currentIndex = internalState.findItemIndex(rowId);
if (lastIndex !== -1 && currentIndex !== -1) {
// Create range selection - select ALL items in the range
const startIndex = Math.min(lastIndex, currentIndex);
const stopIndex = Math.max(lastIndex, currentIndex);
const rangeItems: ItemListStateItemWithRequiredProperties[] = [];
for (let i = startIndex; i <= stopIndex; i++) {
const rangeItem = validData[i];
if (
rangeItem &&
typeof rangeItem === 'object' &&
'_serverId' in rangeItem &&
'itemType' in rangeItem
) {
const rangeRowId = internalState.extractRowId(rangeItem);
if (rangeRowId) {
rangeItems.push(
rangeItem as ItemListStateItemWithRequiredProperties,
);
}
}
}
// Merge with existing selection, avoiding duplicates
const currentSelected = internalState.getSelected();
const newSelected = [
...currentSelected.filter(
(
selectedItem,
): selectedItem is ItemListStateItemWithRequiredProperties =>
typeof selectedItem === 'object' && selectedItem !== null,
),
];
rangeItems.forEach((rangeItem) => {
const rangeRowId = internalState.extractRowId(rangeItem);
if (
rangeRowId &&
!newSelected.some(
(selected) =>
internalState.extractRowId(selected) === rangeRowId,
)
) {
newSelected.push(rangeItem);
}
});
internalState.setSelected(newSelected);
}
} else {
// No previous selection, just select this item
internalState.setSelected([itemListItem]);
}
} else {
// Regular click - deselect all others and select only this item
// If this item is already the only selected item, deselect it
const selectedItems = internalState.getSelected();
const isOnlySelected =
selectedItems.length === 1 &&
typeof selectedItems[0] === 'object' &&
selectedItems[0] !== null &&
internalState.extractRowId(selectedItems[0]) === rowId;
if (isOnlySelected) {
internalState.clearSelected();
} else {
internalState.setSelected([itemListItem]);
}
}
},
onDoubleClick: ({ item, itemType }: DefaultItemControlProps) => {
if (!item) {
return;
}
if (itemType === LibraryItem.QUEUE_SONG) {
const queueSong = item as QueueSong;
if (queueSong._uniqueId) {
player.mediaPlay(queueSong._uniqueId);
}
}
},
onExpand: ({ internalState, item }: DefaultItemControlProps) => {
if (!item || !internalState) {
return;
}
// Extract rowId from the item
const rowId = internalState.extractRowId(item);
if (!rowId) return;
// Use the item directly (rowId is separate, used only as key in state)
const itemListItem = item as ItemListStateItemWithRequiredProperties;
return internalState?.toggleExpanded(itemListItem);
},
onFavorite: ({
favorite,
item,
itemType,
}: DefaultItemControlProps & { favorite: boolean }) => {
if (!item) {
return;
}
player.setFavorite(item._serverId, [item.id], itemType, favorite);
},
onMore: ({ internalState, item, itemType }: DefaultItemControlProps) => {
console.log('handleItemMore', item, itemType, internalState);
},
onPlay: ({
item,
itemType,
playType,
}: DefaultItemControlProps & { playType: Play }) => {
if (!item) {
return;
}
player.addToQueueByFetch(item._serverId, [item.id], itemType, playType);
},
onRating: ({
item,
itemType,
rating,
}: DefaultItemControlProps & { rating: number }) => {
if (!item) {
return;
}
const previousRating = (item as { userRating: number }).userRating || 0;
let newRating = rating;
if (previousRating === rating) {
newRating = 0;
}
player.setRating(item._serverId, [item.id], itemType, newRating);
},
};
}, [player]);
return controls;
};