diff --git a/src/renderer/components/item-list/helpers/item-list-state.ts b/src/renderer/components/item-list/helpers/item-list-state.ts index f9afc2214..e005d7ea9 100644 --- a/src/renderer/components/item-list/helpers/item-list-state.ts +++ b/src/renderer/components/item-list/helpers/item-list-state.ts @@ -77,6 +77,7 @@ export interface ItemListStateActions { clearDragging: () => void; clearExpanded: () => void; clearSelected: () => void; + deselectAll: () => void; extractRowId: (item: unknown) => string | undefined; findItemIndex: (rowId: string) => number; getData: () => unknown[]; @@ -91,9 +92,12 @@ export interface ItemListStateActions { hasDragging: () => boolean; hasExpanded: () => boolean; hasSelected: () => boolean; + isAllSelected: () => boolean; isDragging: (rowId: string) => boolean; isExpanded: (rowId: string) => boolean; isSelected: (rowId: string) => boolean; + isSomeSelected: () => boolean; + selectAll: () => void; setDragging: (items: ItemListStateItemWithRequiredProperties[]) => void; setExpanded: (items: ItemListStateItemWithRequiredProperties[]) => void; setSelected: (items: ItemListStateItemWithRequiredProperties[]) => void; @@ -587,6 +591,29 @@ export const useItemListState = ( [getDataFn, extractRowId], ); + const selectAll = useCallback(() => { + const data = getDataFn ? getDataFn() : []; + const items = data + .filter((d) => d && typeof d === 'object') + .map((d) => d as ItemListStateItemWithRequiredProperties); + store.dispatch({ extractRowId: extractRowIdFn, payload: items, type: 'SET_SELECTED' }); + }, [extractRowIdFn, getDataFn, store]); + + const deselectAll = useCallback(() => { + store.dispatch({ type: 'CLEAR_SELECTED' }); + }, [store]); + + const isAllSelected = useCallback(() => { + const state = getCurrentState(); + const data = getDataFn ? getDataFn() : []; + return state.selected.size === data.filter((d) => d && typeof d === 'object').length; + }, [getCurrentState, getDataFn]); + + const isSomeSelected = useCallback(() => { + const state = getCurrentState(); + return state.selected.size > 0; + }, [getCurrentState]); + // Expose the store so components can subscribe if needed // Store it in the actions object for access const actions = useMemo(() => { @@ -597,6 +624,7 @@ export const useItemListState = ( clearDragging, clearExpanded, clearSelected, + deselectAll, extractRowId: extractRowIdFn, findItemIndex, getData, @@ -611,9 +639,12 @@ export const useItemListState = ( hasDragging, hasExpanded, hasSelected, + isAllSelected, isDragging, isExpanded, isSelected, + isSomeSelected, + selectAll, setDragging, setExpanded, setSelected, @@ -625,10 +656,15 @@ export const useItemListState = ( }; return actionsObj; }, [ + getCurrentState, + store, clearAll, clearDragging, clearExpanded, clearSelected, + isAllSelected, + isSomeSelected, + deselectAll, extractRowIdFn, findItemIndex, getData, @@ -646,13 +682,12 @@ export const useItemListState = ( isDragging, isExpanded, isSelected, + selectAll, setDragging, setExpanded, setSelected, toggleExpanded, toggleSelected, - store, - getCurrentState, ]); return actions; diff --git a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx index a662d649e..6ee51f77c 100644 --- a/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx +++ b/src/renderer/components/item-list/item-grid-list/item-grid-list.tsx @@ -44,6 +44,8 @@ import { import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types'; import { animationProps } from '/@/shared/components/animations/animation-props'; import { useElementSize } from '/@/shared/hooks/use-element-size'; +import { useFocusWithin } from '/@/shared/hooks/use-focus-within'; +import { useHotkeys } from '/@/shared/hooks/use-hotkeys'; import { useMergedRef } from '/@/shared/hooks/use-merged-ref'; import { LibraryItem } from '/@/shared/types/domain-types'; @@ -288,7 +290,7 @@ const BaseItemGridList = ({ const outerRef = useRef(null); const listRef = useRef>(null); const { ref: containerRef, width: containerWidth } = useElementSize(); - const containerFocusRef = useRef(null); + const { focused, ref: containerFocusRef } = useFocusWithin(); const handleRef = useRef(null); const mergedContainerRef = useMergedRef(containerRef, rootRef, containerFocusRef); @@ -654,6 +656,21 @@ const BaseItemGridList = ({ useImperativeHandle(ref, () => imperativeHandle, [imperativeHandle]); + useHotkeys([ + [ + 'mod+a', + () => { + if (focused) { + if (internalState.isAllSelected()) { + internalState.deselectAll(); + } else { + internalState.selectAll(); + } + } + }, + ], + ]); + return ( (null); - const containerFocusRef = useRef(null); + const { focused, ref: focusRef } = useFocusWithin(); + const containerRef = useRef(null); + const mergedContainerRef = useMergedRef(containerRef, focusRef); const stickyHeaderRef = useRef(null); const stickyGroupRowRef = useRef(null); @@ -856,7 +860,7 @@ const BaseItemTableList = ({ const stickyHeaderRightRef = useRef(null); const { shouldShowStickyHeader, stickyTop } = useStickyTableHeader({ - containerRef: containerFocusRef, + containerRef: containerRef, enabled: enableHeader && enableStickyHeader, headerRef: pinnedRowRef, mainGridRef: rowRef, @@ -867,12 +871,12 @@ const BaseItemTableList = ({ // Update position and width of sticky header (scroll sync is handled in the hook) useEffect(() => { - if (!shouldShowStickyHeader || !stickyHeaderRef.current || !containerFocusRef.current) { + if (!shouldShowStickyHeader || !stickyHeaderRef.current || !containerRef.current) { return; } const stickyHeader = stickyHeaderRef.current; - const container = containerFocusRef.current; + const container = containerRef.current; const updatePosition = () => { const containerRect = container.getBoundingClientRect(); @@ -914,7 +918,7 @@ const BaseItemTableList = ({ // Track total container width for autoSizeColumns useEffect(() => { - const el = containerFocusRef.current; + const el = containerRef.current; if (!el || !autoFitColumns) return; const updateWidth = () => { @@ -1495,7 +1499,7 @@ const BaseItemTableList = ({ stickyGroupIndex, stickyTop: stickyGroupTop, } = useStickyTableGroupRows({ - containerRef: containerFocusRef, + containerRef: containerRef, enabled: enableStickyGroupRows && !!groups && groups.length > 0, getRowHeight: getRowHeightWrapper, groups, @@ -1510,16 +1514,12 @@ const BaseItemTableList = ({ // Update position and width of sticky group row useEffect(() => { - if ( - !shouldRenderStickyGroupRow || - !stickyGroupRowRef.current || - !containerFocusRef.current - ) { + if (!shouldRenderStickyGroupRow || !stickyGroupRowRef.current || !containerRef.current) { return; } const stickyGroupRow = stickyGroupRowRef.current; - const container = containerFocusRef.current; + const container = containerRef.current; const updatePosition = () => { const containerRect = container.getBoundingClientRect(); @@ -2109,6 +2109,21 @@ const BaseItemTableList = ({ stickyGroupTop, ]); + useHotkeys([ + [ + 'mod+a', + () => { + if (focused) { + if (internalState.isAllSelected()) { + internalState.deselectAll(); + } else { + internalState.selectAll(); + } + } + }, + ], + ]); + return (