[ENG-1751] Improve active item handling (#2367)

base
This commit is contained in:
nikec 2024-04-22 20:54:42 +02:00 committed by GitHub
parent 959ccdfd98
commit 745399ecab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 251 additions and 271 deletions

View file

@ -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<HTMLDivElement>();
@ -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) {

View file

@ -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<typeof useActiveItem> {
ref: RefObject<HTMLDivElement>;
/**
* Padding to apply when scrolling to an item.
@ -13,10 +15,10 @@ export interface ExplorerViewContext {
};
}
export const ViewContext = createContext<ExplorerViewContext | null>(null);
export const ExplorerViewContext = createContext<ExplorerViewContextProps | null>(null);
export const useExplorerViewContext = () => {
const ctx = useContext(ViewContext);
const ctx = useContext(ExplorerViewContext);
if (ctx === null) throw new Error('ViewContext.Provider not found!');

View file

@ -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<typeof useGrid<string, ExplorerItem | undefined>>;
onActiveItemChange: ReturnType<typeof useKeySelection>['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}

View file

@ -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<ExplorerItem | null>(null);
const { explorerOperatingSystem } = useExplorerOperatingSystem();
// 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<number | null>(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<ExplorerItem | null>(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<number | null>(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<UpdateActiveItemOptions, 'updateFirstItem'> = {}
) => {
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 scrollToItem = (item: NonNullable<ReturnType<Grid['getItem']>>) => {
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;
if (explorer.selectedItems.size === 0) {
if (direction !== 'down' && direction !== 'right') return;
explorer.resetSelectedItems([item.data]);
scrollToItem(item);
const item = explorer.items[0];
if (!item) return;
updateActiveItem(item.data, { itemIndex: 0, updateFirstItem: true });
scrollToItem(0);
explorer.resetSelectedItems([item]);
explorerView.updateActiveItem(explorer.getItemUniqueId(item), {
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 (!currentItem) {
const [item] = explorer.selectedItems;
if (!item) return;
currentItem = item;
}
const currentItemId = explorer.getItemUniqueId(currentItem);
const index = explorer.items.findIndex((item) => {
return explorer.getItemUniqueId(item) === currentItemId;
});
if (index === -1) return;
currentItemIndex = index;
}
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 };
};

View file

@ -43,10 +43,10 @@ export const GridView = () => {
)
});
const { updateActiveItem } = useKeySelection(grid, { scrollToEnd: true });
useKeySelection(grid, { scrollToEnd: true });
return (
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
<DragSelect grid={grid}>
<Grid grid={grid}>
{(index) => {
const item = explorer.items?.[index];

View file

@ -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 (
<TableContext.Provider value={{ columnSizing }}>
<div

View file

@ -168,7 +168,7 @@ export const MediaView = () => {
orderDirection
]);
const { updateActiveItem } = useKeySelection(grid);
useKeySelection(grid);
return (
<div
@ -181,7 +181,7 @@ export const MediaView = () => {
>
{isSortingByDate && <DateHeader date={date} />}
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
<DragSelect grid={grid}>
{virtualRows.map((virtualRow) => (
<React.Fragment key={virtualRow.key}>
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {

View file

@ -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<ExplorerViewContext, 'selectable' | 'ref' | 'padding'> {
extends Pick<ExplorerViewContextProps, 'contextMenu' | 'scrollPadding' | 'listViewOptions'> {
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 (
<ViewContext.Provider value={{ ref, ...contextProps, selectable }}>
<ExplorerViewContext.Provider value={{ ref, selectable, ...contextProps, ...activeItem }}>
<div
ref={ref}
className="flex flex-1"
@ -185,7 +188,7 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
<DragScrollable />
{quickPreview.ref && createPortal(<QuickPreview />, quickPreview.ref)}
</ViewContext.Provider>
</ExplorerViewContext.Provider>
);
};

View file

@ -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<ActiveItem>(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<ActiveItem>(null);
const updateItem = useCallback((item: MutableRefObject<ActiveItem>, 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<ActiveItem>) => {
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
};
}

View file

@ -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<string, ExplorerItem>()),
}, new Map<string, { index: number; data: ExplorerItem }>()),
[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<ExplorerItem>()),
[itemsMap, selectedItemHashes]
@ -183,6 +183,7 @@ function useSelectedItems(items: ExplorerItem[] | null) {
);
return {
itemsMap,
selectedItems,
selectedItemHashes,
getItemUniqueId,

View file

@ -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<OperatingSystem, 'windows' | 'macOS'> | undefined
});