From 745399ecab3fdb6f6f8a8dc2855d68a184cc6573 Mon Sep 17 00:00:00 2001 From: nikec <43032218+niikeec@users.noreply.github.com> Date: Mon, 22 Apr 2024 20:54:42 +0200 Subject: [PATCH] [ENG-1751] Improve active item handling (#2367) base --- .../Explorer/QuickPreview/index.tsx | 10 + .../Explorer/View/{Context.ts => Context.tsx} | 8 +- .../Explorer/View/Grid/DragSelect/index.tsx | 62 +++-- .../Explorer/View/Grid/useKeySelection.tsx | 256 ++---------------- .../Explorer/View/GridView/index.tsx | 4 +- .../Explorer/View/ListView/index.tsx | 25 ++ .../Explorer/View/MediaView/index.tsx | 4 +- .../app/$libraryId/Explorer/View/index.tsx | 11 +- .../Explorer/View/useActiveItem.tsx | 124 +++++++++ .../app/$libraryId/Explorer/useExplorer.ts | 9 +- .../Explorer/useExplorerOperatingSystem.tsx | 5 +- 11 files changed, 249 insertions(+), 269 deletions(-) rename interface/app/$libraryId/Explorer/View/{Context.ts => Context.tsx} (63%) create mode 100644 interface/app/$libraryId/Explorer/View/useActiveItem.tsx diff --git a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx index 10e3b11df829..d75bdeb4e336 100644 --- a/interface/app/$libraryId/Explorer/QuickPreview/index.tsx +++ b/interface/app/$libraryId/Explorer/QuickPreview/index.tsx @@ -48,6 +48,7 @@ import { Conditional } from '../ContextMenu/ConditionalItem'; import { FileThumb } from '../FilePath/Thumb'; import { SingleItemMetadata } from '../Inspector'; import { explorerStore } from '../store'; +import { useExplorerViewContext } from '../View/Context'; import { ImageSlider } from './ImageSlider'; import { getQuickPreviewStore, useQuickPreviewStore } from './store'; @@ -74,6 +75,7 @@ export const QuickPreview = () => { const { openFilePaths, openEphemeralFiles } = usePlatform(); const explorerLayoutStore = useExplorerLayoutStore(); const explorer = useExplorerContext(); + const explorerView = useExplorerViewContext(); const { open, itemIndex } = useQuickPreviewStore(); const thumb = createRef(); @@ -155,6 +157,14 @@ export const QuickPreview = () => { setShowMetadata(false); }, [item, open]); + useEffect(() => { + if (open) explorerView.updateActiveItem(null, { updateFirstItem: true }); + + // "open" is excluded, as we only want this to trigger when hashes change, + // that way we don't have to manually update the active item. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [explorer.selectedItemHashes, explorerView.updateActiveItem]); + const handleMoveBetweenItems = (step: number) => { const nextPreviewItem = items[itemIndex + step]; if (nextPreviewItem) { diff --git a/interface/app/$libraryId/Explorer/View/Context.ts b/interface/app/$libraryId/Explorer/View/Context.tsx similarity index 63% rename from interface/app/$libraryId/Explorer/View/Context.ts rename to interface/app/$libraryId/Explorer/View/Context.tsx index 1c67dd3d80f8..38ce6c2ba491 100644 --- a/interface/app/$libraryId/Explorer/View/Context.ts +++ b/interface/app/$libraryId/Explorer/View/Context.tsx @@ -1,6 +1,8 @@ import { createContext, useContext, type ReactNode, type RefObject } from 'react'; -export interface ExplorerViewContext { +import { useActiveItem } from './useActiveItem'; + +export interface ExplorerViewContextProps extends ReturnType { ref: RefObject; /** * Padding to apply when scrolling to an item. @@ -13,10 +15,10 @@ export interface ExplorerViewContext { }; } -export const ViewContext = createContext(null); +export const ExplorerViewContext = createContext(null); export const useExplorerViewContext = () => { - const ctx = useContext(ViewContext); + const ctx = useContext(ExplorerViewContext); if (ctx === null) throw new Error('ViewContext.Provider not found!'); diff --git a/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx b/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx index c98d9c6cb74f..e58a5678719e 100644 --- a/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx +++ b/interface/app/$libraryId/Explorer/View/Grid/DragSelect/index.tsx @@ -7,7 +7,6 @@ import { useExplorerContext } from '../../../Context'; import { explorerStore } from '../../../store'; import { useExplorerOperatingSystem } from '../../../useExplorerOperatingSystem'; import { useExplorerViewContext } from '../../Context'; -import { useKeySelection } from '../useKeySelection'; import { DragSelectContext } from './context'; import { useSelectedTargets } from './useSelectedTargets'; import { getElementIndex, SELECTABLE_DATA_ATTRIBUTE } from './util'; @@ -16,7 +15,6 @@ const CHROME_REGEX = /Chrome/; interface Props extends PropsWithChildren { grid: ReturnType>; - onActiveItemChange: ReturnType['updateActiveItem']; } export interface Drag { @@ -26,11 +24,13 @@ export interface Drag { endRow: number; } -export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => { +export const DragSelect = ({ grid, children }: Props) => { const isChrome = CHROME_REGEX.test(navigator.userAgent); const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem(); + const isWindows = explorerOperatingSystem === 'windows' && matchingOperatingSystem; + const explorer = useExplorerContext(); const explorerView = useExplorerViewContext(); @@ -99,20 +99,20 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => { // Update active item to the first selected target(first grid item in DOM). const target = selecto.current?.getSelectedTargets()?.[0]; - const item = target && getGridItem(target)?.data; - if (item) onActiveItemChange(item, { updateFirstItem: true, setFirstItemAsChanged: true }); + + const item = target && getGridItem(target); + if (!item) return; + + explorerView.updateActiveItem(item.id as string, { + updateFirstItem: true + }); } function handleSelect(e: SelectoEvents['select']) { const inputEvent = e.inputEvent as MouseEvent; - let continueSelection = false; - - if (explorerOperatingSystem === 'windows') { - continueSelection = matchingOperatingSystem ? inputEvent.ctrlKey : inputEvent.metaKey; - } else { - continueSelection = inputEvent.shiftKey || inputEvent.metaKey; - } + const continueSelection = + inputEvent.shiftKey || (isWindows ? inputEvent.ctrlKey : inputEvent.metaKey); // Handle select on mouse down if (inputEvent.type === 'mousedown') { @@ -130,7 +130,10 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => { }; if (!continueSelection) { - if (explorer.selectedItems.has(item.data)) { + if ( + explorerOperatingSystem !== 'windows' && + explorer.selectedItems.has(item.data) + ) { // Keep previous selection as selecto will reset it otherwise selecto.current?.setSelectedTargets(e.beforeSelected); } else { @@ -140,14 +143,31 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => { ]); } + explorerView.updateActiveItem(item.id as string, { updateFirstItem: true }); return; } - if (e.added[0]) explorer.addSelectedItem(item.data); - else explorer.removeSelectedItem(item.data); + if (explorerOperatingSystem === 'windows' && inputEvent.shiftKey) { + explorerView.handleWindowsGridShiftSelection(item.index); + return; + } - // Update active item for further keyboard selection. - onActiveItemChange(item.data, { updateFirstItem: true, setFirstItemAsChanged: true }); + if (e.added[0]) { + explorer.addSelectedItem(item.data); + explorerView.updateActiveItem(item.id as string, { updateFirstItem: true }); + return; + } + + explorer.removeSelectedItem(item.data); + + explorerView.updateActiveItem( + explorerOperatingSystem === 'windows' ? (item.id as string) : null, + { + updateFirstItem: true + } + ); + + return; } // Handle select by drag @@ -557,13 +577,7 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => { throttleTime: isChrome ? 30 : 10000 }} selectableTargets={[`[${SELECTABLE_DATA_ATTRIBUTE}]`]} - toggleContinueSelect={ - explorerOperatingSystem === 'windows' - ? matchingOperatingSystem - ? 'ctrl' - : 'meta' - : [['shift'], ['meta']] - } + toggleContinueSelect={[['shift'], [isWindows ? 'ctrl' : 'meta']]} hitRate={0} onDrag={handleDrag} onDragStart={handleDragStart} diff --git a/interface/app/$libraryId/Explorer/View/Grid/useKeySelection.tsx b/interface/app/$libraryId/Explorer/View/Grid/useKeySelection.tsx index cac96fb2de66..f89e95825a32 100644 --- a/interface/app/$libraryId/Explorer/View/Grid/useKeySelection.tsx +++ b/interface/app/$libraryId/Explorer/View/Grid/useKeySelection.tsx @@ -1,5 +1,4 @@ import { useGrid } from '@virtual-grid/react'; -import { useCallback, useEffect, useRef } from 'react'; import { useDebouncedCallback } from 'use-debounce'; import { ExplorerItem } from '@sd/client'; import { useShortcut } from '~/hooks'; @@ -18,92 +17,18 @@ interface Options { scrollToEnd?: boolean; } -interface UpdateActiveItemOptions { - /** - * The index of the item to update. If not provided, the index will be reset. - * @default null - */ - itemIndex?: number | null; - /** - * Whether to update the first active item. - * @default false - */ - updateFirstItem?: boolean; - /** - * Whether to set the first item as changed. This is used to reset the selection. - * @default false - */ - setFirstItemAsChanged?: boolean; -} - export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: false }) => { - const { explorerOperatingSystem } = useExplorerOperatingSystem(); - const explorer = useExplorerContext(); const explorerView = useExplorerViewContext(); - // The item that further selection will move from (shift + arrow for example). - const activeItem = useRef(null); - - // The index of the active item. This is stored so we don't have to look - // for the index every time we want to move to the next item. - const activeItemIndex = useRef(null); - - // The first active item that acts as a head. - // Only used for windows OS to keep track of the first selected item. - const firstActiveItem = useRef(null); - - // The index of the first active item. - // Only used for windows OS to keep track of the first selected item index. - const firstActiveItemIndex = useRef(null); - - // Whether the first active item has been changed. - // Only used for windows OS to keep track whether selection should be reset. - const hasFirstActiveItemChanged = useRef(true); - - // Reset active item when selection changes, as the active item - // might not be in the selection anymore (further lookups are handled in handleNavigation). - useEffect(() => { - activeItem.current = null; - }, [explorer.selectedItems]); - - // Reset active item index when items change, - // as we can't guarantee the item is still in the same position - useEffect(() => { - activeItemIndex.current = null; - firstActiveItemIndex.current = null; - }, [explorer.items]); - - const updateFirstActiveItem = useCallback( - ( - item: ExplorerItem | null, - options: Omit = {} - ) => { - if (explorerOperatingSystem !== 'windows') return; - - firstActiveItem.current = item; - firstActiveItemIndex.current = options.itemIndex ?? null; - if (options.setFirstItemAsChanged) hasFirstActiveItemChanged.current = true; - }, - [explorerOperatingSystem] - ); - - const updateActiveItem = useCallback( - (item: ExplorerItem | null, options: UpdateActiveItemOptions = {}) => { - // Timeout so the useEffect doesn't override it - setTimeout(() => { - activeItem.current = item; - activeItemIndex.current = options.itemIndex ?? null; - }); - - if (options.updateFirstItem) updateFirstActiveItem(item, options); - }, - [updateFirstActiveItem] - ); + const { explorerOperatingSystem } = useExplorerOperatingSystem(); - const scrollToItem = (item: NonNullable>) => { + const scrollToItem = (index: number) => { if (!explorer.scrollRef.current || !explorerView.ref.current) return; + const item = grid.getItem(index); + if (!item) return; + const { top: viewTop } = explorerView.ref.current.getBoundingClientRect(); const { height: scrollHeight } = explorer.scrollRef.current.getBoundingClientRect(); @@ -143,56 +68,25 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa // Select first item in grid if no items are selected, on down/right keybind // TODO: Handle when no items are selected and up/left keybind is executed (should select last item in grid) - if ((direction === 'down' || direction === 'right') && explorer.selectedItems.size === 0) { - const item = grid.getItem(0); - if (!item?.data) return; - - explorer.resetSelectedItems([item.data]); - scrollToItem(item); - - updateActiveItem(item.data, { itemIndex: 0, updateFirstItem: true }); - - return; - } - - let currentItemIndex = activeItemIndex.current; - - // Check for any mismatches between the stored index and the current item - if (currentItemIndex !== null) { - if (activeItem.current) { - const itemAtActiveIndex = explorer.items[currentItemIndex]; - const uniqueId = itemAtActiveIndex && explorer.getItemUniqueId(itemAtActiveIndex); - if (uniqueId !== explorer.getItemUniqueId(activeItem.current)) { - currentItemIndex = null; - } - } else { - currentItemIndex = null; - } - } - - // Find index of current active item - if (currentItemIndex === null) { - let currentItem = activeItem.current; + if (explorer.selectedItems.size === 0) { + if (direction !== 'down' && direction !== 'right') return; - if (!currentItem) { - const [item] = explorer.selectedItems; - if (!item) return; + const item = explorer.items[0]; + if (!item) return; - currentItem = item; - } + scrollToItem(0); - const currentItemId = explorer.getItemUniqueId(currentItem); + explorer.resetSelectedItems([item]); - const index = explorer.items.findIndex((item) => { - return explorer.getItemUniqueId(item) === currentItemId; + explorerView.updateActiveItem(explorer.getItemUniqueId(item), { + updateFirstItem: true }); - if (index === -1) return; - - currentItemIndex = index; + return; } - if (currentItemIndex === null) return; + const currentItemIndex = explorerView.getActiveItemIndex(); + if (currentItemIndex === undefined) return; let newIndex = currentItemIndex; @@ -225,118 +119,26 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa } } - const newSelectedItem = grid.getItem(newIndex); - if (!newSelectedItem?.data) return; + const newSelectedItem = explorer.items[newIndex]; + if (!newSelectedItem) return; + + scrollToItem(newIndex); if (!e.shiftKey) { - explorer.resetSelectedItems([newSelectedItem.data]); + explorer.resetSelectedItems([newSelectedItem]); } else if ( explorerOperatingSystem !== 'windows' && - !explorer.isItemSelected(newSelectedItem.data) + !explorer.isItemSelected(newSelectedItem) ) { - explorer.addSelectedItem(newSelectedItem.data); + explorer.addSelectedItem(newSelectedItem); } else if (explorerOperatingSystem === 'windows') { - let firstItemId = firstActiveItem.current - ? explorer.getItemUniqueId(firstActiveItem.current) - : undefined; - - let firstItemIndex = firstActiveItemIndex.current; - - // Check if the firstActiveItem is still in the selection. If not, - // update the firstActiveItem to the current active item. - if (firstActiveItem.current && explorer.selectedItems.has(firstActiveItem.current)) { - let searchIndex = firstItemIndex === null; - - if (firstItemIndex !== null) { - const itemAtIndex = explorer.items[firstItemIndex]; - const uniqueId = itemAtIndex && explorer.getItemUniqueId(itemAtIndex); - if (uniqueId !== firstItemId) searchIndex = true; - } - - // Search for the firstActiveItem index if we're missing the index or the ExplorerItem - // at the stored index position doesn't match with the firstActiveItem - if (searchIndex) { - const item = explorer.items[currentItemIndex]; - if (!item) return; - - if (explorer.getItemUniqueId(item) === firstItemId) { - firstItemIndex = currentItemIndex; - } else { - const index = explorer.items.findIndex((item) => { - return explorer.getItemUniqueId(item) === firstItemId; - }); - - if (index === -1) return; - - firstItemIndex = index; - } - - updateFirstActiveItem(firstActiveItem.current, { itemIndex: firstItemIndex }); - } - } else { - const item = explorer.items[currentItemIndex]; - if (!item) return; - - firstItemId = explorer.getItemUniqueId(item); - firstItemIndex = currentItemIndex; - - updateFirstActiveItem(item, { itemIndex: firstItemIndex }); - } - - if (firstItemIndex === null) return; - - const addItems: ExplorerItem[] = []; - const removeItems: ExplorerItem[] = []; - - // Determine if we moved further away from the first selected item. - // This is used to determine if we should add or remove items from the selection. - let movedAwayFromFirstItem = false; - - if (firstItemIndex === currentItemIndex) { - movedAwayFromFirstItem = newIndex !== currentItemIndex; - } else if (firstItemIndex < currentItemIndex) { - movedAwayFromFirstItem = newIndex > currentItemIndex; - } else { - movedAwayFromFirstItem = newIndex < currentItemIndex; - } - - // Determine if the new index is on the other side - // of the firstActiveItem(head) based on the current index. - const isIndexOverHead = (index: number) => - (currentItemIndex < firstItemIndex && index > firstItemIndex) || - (currentItemIndex > firstItemIndex && index < firstItemIndex); - - const itemsCount = - Math.abs(currentItemIndex - newIndex) + (isIndexOverHead(newIndex) ? 1 : 0); - - for (let i = 0; i < itemsCount; i++) { - const _i = i + (movedAwayFromFirstItem ? 1 : 0); - const index = currentItemIndex + (currentItemIndex < newIndex ? _i : -_i); - - const item = explorer.items[index]; - if (!item || explorer.getItemUniqueId(item) === firstItemId) continue; - - const addItem = isIndexOverHead(index) || movedAwayFromFirstItem; - (addItem ? addItems : removeItems).push(item); - } - - if (hasFirstActiveItemChanged.current) { - if (firstActiveItem.current) addItems.push(firstActiveItem.current); - explorer.resetSelectedItems(addItems); - hasFirstActiveItemChanged.current = false; - } else { - if (addItems.length > 0) explorer.addSelectedItem(addItems); - if (removeItems.length > 0) explorer.removeSelectedItem(removeItems); - } + explorerView.handleWindowsGridShiftSelection(newIndex); + return; } - updateActiveItem(newSelectedItem.data, { itemIndex: newIndex }); - updateFirstActiveItem( - e.shiftKey ? firstActiveItem.current ?? newSelectedItem.data : newSelectedItem.data, - { itemIndex: e.shiftKey ? firstActiveItemIndex.current ?? currentItemIndex : newIndex } - ); - - scrollToItem(newSelectedItem); + explorerView.updateActiveItem(explorer.getItemUniqueId(newSelectedItem), { + updateFirstItem: true + }); }; // Debounce keybinds to prevent weird execution order @@ -346,6 +148,4 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa useShortcut('explorerDown', (e) => debounce(() => handleNavigation(e, 'down'))); useShortcut('explorerLeft', (e) => debounce(() => handleNavigation(e, 'left'))); useShortcut('explorerRight', (e) => debounce(() => handleNavigation(e, 'right'))); - - return { updateActiveItem, updateFirstActiveItem }; }; diff --git a/interface/app/$libraryId/Explorer/View/GridView/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/index.tsx index ea1bd6974312..daf89cae90ab 100644 --- a/interface/app/$libraryId/Explorer/View/GridView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView/index.tsx @@ -43,10 +43,10 @@ export const GridView = () => { ) }); - const { updateActiveItem } = useKeySelection(grid, { scrollToEnd: true }); + useKeySelection(grid, { scrollToEnd: true }); return ( - + {(index) => { const item = explorer.items?.[index]; diff --git a/interface/app/$libraryId/Explorer/View/ListView/index.tsx b/interface/app/$libraryId/Explorer/View/ListView/index.tsx index 76a385669362..0249cfd88614 100644 --- a/interface/app/$libraryId/Explorer/View/ListView/index.tsx +++ b/interface/app/$libraryId/Explorer/View/ListView/index.tsx @@ -732,6 +732,31 @@ export const ListView = memo(() => { // Set list offset useLayoutEffect(() => setListOffset(tableRef.current?.offsetTop ?? 0), []); + // Handle active item selection + // TODO: This is a temporary solution + useEffect(() => { + return () => { + const firstRange = getRangeByIndex(0); + if (!firstRange) return; + + const lastRange = getRangeByIndex(ranges.length - 1); + if (!lastRange) return; + + const firstItem = firstRange.start.original; + const lastItem = lastRange.end.original; + + explorerView.updateFirstActiveItem(explorer.getItemUniqueId(firstItem)); + explorerView.updateActiveItem(explorer.getItemUniqueId(lastItem)); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + ranges, + getRangeByIndex, + explorerView.updateFirstActiveItem, + explorerView.updateActiveItem, + explorer.getItemUniqueId + ]); + return (
{ orderDirection ]); - const { updateActiveItem } = useKeySelection(grid); + useKeySelection(grid); return (
{ > {isSortingByDate && } - + {virtualRows.map((virtualRow) => ( {columnVirtualizer.getVirtualItems().map((virtualColumn) => { diff --git a/interface/app/$libraryId/Explorer/View/index.tsx b/interface/app/$libraryId/Explorer/View/index.tsx index b3abcaff43a6..6a780790951b 100644 --- a/interface/app/$libraryId/Explorer/View/index.tsx +++ b/interface/app/$libraryId/Explorer/View/index.tsx @@ -24,15 +24,16 @@ import { explorerStore } from '../store'; import { useExplorerDroppable } from '../useExplorerDroppable'; import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem'; import { useExplorerSearchParams } from '../util'; -import { ViewContext, type ExplorerViewContext } from './Context'; +import { ExplorerViewContext, ExplorerViewContextProps } from './Context'; import { DragScrollable } from './DragScrollable'; import { GridView } from './GridView'; import { ListView } from './ListView'; import { MediaView } from './MediaView'; +import { useActiveItem } from './useActiveItem'; import { useViewItemDoubleClick } from './ViewItem'; export interface ExplorerViewProps - extends Omit { + extends Pick { emptyNotice?: JSX.Element; } @@ -91,6 +92,8 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => { }) }); + const activeItem = useActiveItem(); + useShortcuts(); useShortcut('explorerEscape', () => explorer.resetSelectedItems([]), { @@ -148,7 +151,7 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => { if (!explorer.layouts[layoutMode]) return null; return ( - +
{ {quickPreview.ref && createPortal(, quickPreview.ref)} - + ); }; diff --git a/interface/app/$libraryId/Explorer/View/useActiveItem.tsx b/interface/app/$libraryId/Explorer/View/useActiveItem.tsx new file mode 100644 index 000000000000..5a1915fa311f --- /dev/null +++ b/interface/app/$libraryId/Explorer/View/useActiveItem.tsx @@ -0,0 +1,124 @@ +import { MutableRefObject, useCallback, useRef } from 'react'; + +import { useExplorerContext } from '../Context'; +import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem'; + +type ActiveItem = string | null; + +type UpdateActiveItem = ActiveItem | ((current: ActiveItem) => ActiveItem); + +interface UpdateActiveItemOptions { + /** + * Whether to update the first active item. + * @default false + */ + updateFirstItem?: boolean; +} + +export function useActiveItem() { + const explorer = useExplorerContext(); + + const { explorerOperatingSystem } = useExplorerOperatingSystem(); + + // The item that further selection will move from (shift + arrow for example). + const activeItem = useRef(null); + + // The first active item that acts as a head. + // Only used for windows OS to keep track of the first selected item. + const firstActiveItem = useRef(null); + + const updateItem = useCallback((item: MutableRefObject, data: UpdateActiveItem) => { + item.current = typeof data === 'function' ? data(firstActiveItem.current) : data; + }, []); + + const updateFirstActiveItem = useCallback( + (item: UpdateActiveItem) => { + if (explorerOperatingSystem !== 'windows') return; + updateItem(firstActiveItem, item); + }, + [explorerOperatingSystem, updateItem] + ); + + const updateActiveItem = useCallback( + (item: UpdateActiveItem, options: UpdateActiveItemOptions = {}) => { + updateItem(activeItem, item); + if (options.updateFirstItem) updateFirstActiveItem(item); + }, + [updateFirstActiveItem, updateItem] + ); + + const getNewActiveItemIndex = useCallback(() => { + const [item] = explorer.selectedItems; + + const uniqueId = item && explorer.getItemUniqueId(item); + if (!uniqueId) return; + + return explorer.itemsMap.get(uniqueId)?.index; + + // No need to include the whole explorer object here + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [explorer.selectedItems, explorer.itemsMap, explorer.getItemUniqueId]); + + const getItemIndex = useCallback( + (activeItem: MutableRefObject) => { + if (!activeItem.current) return; + return explorer.itemsMap.get(activeItem.current)?.index; + }, + [explorer.itemsMap] + ); + + const getActiveItemIndex = useCallback( + () => getItemIndex(activeItem) ?? getNewActiveItemIndex(), + [getItemIndex, getNewActiveItemIndex] + ); + + const getFirstActiveItemIndex = useCallback( + () => getItemIndex(firstActiveItem), + [getItemIndex] + ); + + const handleWindowsGridShiftSelection = useCallback( + (newIndex: number) => { + if (!explorer.items) return; + + const newItem = explorer.items[newIndex]; + if (!newItem) return; + + const activeItemIndex = getActiveItemIndex() ?? 0; + const firstActiveItemIndex = getFirstActiveItemIndex() ?? activeItemIndex; + + const item = explorer.items[firstActiveItemIndex]; + if (!item) return; + + const items = explorer.items.slice( + Math.min(firstActiveItemIndex, newIndex), + Math.max(firstActiveItemIndex, newIndex) + 1 + ); + + explorer.resetSelectedItems(items); + + updateActiveItem(explorer.getItemUniqueId(newItem)); + updateFirstActiveItem(explorer.getItemUniqueId(item)); + }, + + // No need to include the whole explorer object here + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + explorer.items, + explorer.getItemUniqueId, + explorer.resetSelectedItems, + getActiveItemIndex, + getFirstActiveItemIndex, + updateActiveItem, + updateFirstActiveItem + ] + ); + + return { + getActiveItemIndex, + getFirstActiveItemIndex, + updateActiveItem, + updateFirstActiveItem, + handleWindowsGridShiftSelection + }; +} diff --git a/interface/app/$libraryId/Explorer/useExplorer.ts b/interface/app/$libraryId/Explorer/useExplorer.ts index 99986a894806..36c798e6238e 100644 --- a/interface/app/$libraryId/Explorer/useExplorer.ts +++ b/interface/app/$libraryId/Explorer/useExplorer.ts @@ -158,12 +158,12 @@ function useSelectedItems(items: ExplorerItem[] | null) { const itemsMap = useMemo( () => - (items ?? []).reduce((items, item) => { + (items ?? []).reduce((items, item, i) => { const hash = itemHashesWeakMap.current.get(item) ?? uniqueId(item); itemHashesWeakMap.current.set(item, hash); - items.set(hash, item); + items.set(hash, { index: i, data: item }); return items; - }, new Map()), + }, new Map()), [items] ); @@ -171,7 +171,7 @@ function useSelectedItems(items: ExplorerItem[] | null) { () => [...selectedItemHashes.value].reduce((items, hash) => { const item = itemsMap.get(hash); - if (item) items.add(item); + if (item) items.add(item.data); return items; }, new Set()), [itemsMap, selectedItemHashes] @@ -183,6 +183,7 @@ function useSelectedItems(items: ExplorerItem[] | null) { ); return { + itemsMap, selectedItems, selectedItemHashes, getItemUniqueId, diff --git a/interface/app/$libraryId/Explorer/useExplorerOperatingSystem.tsx b/interface/app/$libraryId/Explorer/useExplorerOperatingSystem.tsx index b7d5a9735992..05080a6114f5 100644 --- a/interface/app/$libraryId/Explorer/useExplorerOperatingSystem.tsx +++ b/interface/app/$libraryId/Explorer/useExplorerOperatingSystem.tsx @@ -1,9 +1,10 @@ import { useEffect } from 'react'; -import { proxy, useSnapshot } from 'valtio'; +import { useSnapshot } from 'valtio'; +import { valtioPersist } from '@sd/client'; import { useOperatingSystem } from '~/hooks'; import { OperatingSystem } from '~/util/Platform'; -export const explorerOperatingSystemStore = proxy({ +export const explorerOperatingSystemStore = valtioPersist('sd-explorer-behavior', { os: undefined as Extract | undefined });