mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[ENG-1731] Explorer grid selection behaviour for windows (#2307)
* Add windows explorer selection behavior * update media view
This commit is contained in:
parent
56cee3c64d
commit
b71c046aa9
|
@ -5,7 +5,9 @@ import { ExplorerItem } from '@sd/client';
|
|||
|
||||
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';
|
||||
|
@ -14,7 +16,7 @@ const CHROME_REGEX = /Chrome/;
|
|||
|
||||
interface Props extends PropsWithChildren {
|
||||
grid: ReturnType<typeof useGrid<string, ExplorerItem | undefined>>;
|
||||
onActiveItemChange: (item: ExplorerItem | null) => void;
|
||||
onActiveItemChange: ReturnType<typeof useKeySelection>['updateActiveItem'];
|
||||
}
|
||||
|
||||
export interface Drag {
|
||||
|
@ -27,6 +29,8 @@ export interface Drag {
|
|||
export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
||||
const isChrome = CHROME_REGEX.test(navigator.userAgent);
|
||||
|
||||
const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem();
|
||||
|
||||
const explorer = useExplorerContext();
|
||||
const explorerView = useExplorerViewContext();
|
||||
|
||||
|
@ -81,21 +85,38 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
|
||||
function handleDragEnd() {
|
||||
explorerStore.isDragSelecting = false;
|
||||
|
||||
const dragState = drag.current;
|
||||
drag.current = null;
|
||||
|
||||
// Set active item to the first selected target
|
||||
// Targets are already sorted
|
||||
// Determine if the drag event was a click
|
||||
if (
|
||||
dragState?.startColumn === dragState?.endColumn &&
|
||||
dragState?.startRow === dragState?.endRow
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (item) onActiveItemChange(item, { updateFirstItem: true, setFirstItemAsChanged: 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;
|
||||
}
|
||||
|
||||
// Handle select on mouse down
|
||||
if (inputEvent.type === 'mousedown') {
|
||||
const element = inputEvent.shiftKey ? e.added[0] || e.removed[0] : e.selected[0];
|
||||
const element = continueSelection ? e.added[0] || e.removed[0] : e.selected[0];
|
||||
if (!element) return;
|
||||
|
||||
const item = getGridItem(element);
|
||||
|
@ -108,7 +129,7 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
endRow: item.row
|
||||
};
|
||||
|
||||
if (!inputEvent.shiftKey) {
|
||||
if (!continueSelection) {
|
||||
if (explorer.selectedItems.has(item.data)) {
|
||||
// Keep previous selection as selecto will reset it otherwise
|
||||
selecto.current?.setSelectedTargets(e.beforeSelected);
|
||||
|
@ -124,6 +145,9 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
|
||||
if (e.added[0]) explorer.addSelectedItem(item.data);
|
||||
else explorer.removeSelectedItem(item.data);
|
||||
|
||||
// Update active item for further keyboard selection.
|
||||
onActiveItemChange(item.data, { updateFirstItem: true, setFirstItemAsChanged: true });
|
||||
}
|
||||
|
||||
// Handle select by drag
|
||||
|
@ -387,7 +411,7 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
addedRows.add(item.row);
|
||||
}
|
||||
|
||||
if (inputEvent.shiftKey) {
|
||||
if (continueSelection) {
|
||||
if (explorer.selectedItems.has(item.data)) {
|
||||
explorer.removeSelectedItem(item.data);
|
||||
} else {
|
||||
|
@ -533,7 +557,13 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
throttleTime: isChrome ? 30 : 10000
|
||||
}}
|
||||
selectableTargets={[`[${SELECTABLE_DATA_ATTRIBUTE}]`]}
|
||||
toggleContinueSelect="shift"
|
||||
toggleContinueSelect={
|
||||
explorerOperatingSystem === 'windows'
|
||||
? matchingOperatingSystem
|
||||
? 'ctrl'
|
||||
: 'meta'
|
||||
: [['shift'], ['meta']]
|
||||
}
|
||||
hitRate={0}
|
||||
onDrag={handleDrag}
|
||||
onDragStart={handleDragStart}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { useGrid } from '@virtual-grid/react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { ExplorerItem } from '@sd/client';
|
||||
import { useShortcut } from '~/hooks';
|
||||
|
||||
import { useExplorerContext } from '../../Context';
|
||||
import { useQuickPreviewStore } from '../../QuickPreview/store';
|
||||
import { uniqueId } from '../../util';
|
||||
import { useExplorerOperatingSystem } from '../../useExplorerOperatingSystem';
|
||||
import { useExplorerViewContext } from '../Context';
|
||||
|
||||
type Grid = ReturnType<typeof useGrid<string, ExplorerItem | undefined>>;
|
||||
|
@ -18,10 +18,29 @@ 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();
|
||||
const quickPreview = useQuickPreviewStore();
|
||||
|
||||
// The item that further selection will move from (shift + arrow for example).
|
||||
const activeItem = useRef<ExplorerItem | null>(null);
|
||||
|
@ -30,14 +49,54 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa
|
|||
// for the index every time we want to move to the next item.
|
||||
const activeItemIndex = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (quickPreview.open) return;
|
||||
activeItem.current = [...explorer.selectedItems][0] ?? null;
|
||||
}, [explorer.selectedItems, quickPreview.open]);
|
||||
// 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;
|
||||
}, [explorer.items, explorer.selectedItems]);
|
||||
firstActiveItemIndex.current = null;
|
||||
}, [explorer.items]);
|
||||
|
||||
const updateFirstActiveItem = useCallback(
|
||||
(item: ExplorerItem | null, options: UpdateActiveItemOptions = {}) => {
|
||||
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']>>) => {
|
||||
if (!explorer.scrollRef.current || !explorerView.ref.current) return;
|
||||
|
@ -74,7 +133,7 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa
|
|||
};
|
||||
|
||||
const handleNavigation = (e: KeyboardEvent, direction: 'up' | 'down' | 'left' | 'right') => {
|
||||
if (!explorerView.selectable) return;
|
||||
if (!explorerView.selectable || !explorer.items) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
@ -88,21 +147,44 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa
|
|||
explorer.resetSelectedItems([item.data]);
|
||||
scrollToItem(item);
|
||||
|
||||
updateActiveItem(item.data, { itemIndex: 0, updateFirstItem: true });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let currentItemIndex = activeItemIndex.current;
|
||||
|
||||
// Find current index if we don't have the index stored
|
||||
// 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) {
|
||||
const currentItem = activeItem.current;
|
||||
if (!currentItem) return;
|
||||
let currentItem = activeItem.current;
|
||||
|
||||
const index = explorer.items?.findIndex(
|
||||
(item) => uniqueId(item) === uniqueId(currentItem)
|
||||
);
|
||||
if (!currentItem) {
|
||||
const [item] = explorer.selectedItems;
|
||||
if (!item) return;
|
||||
|
||||
if (index === undefined || index === -1) 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;
|
||||
}
|
||||
|
@ -125,28 +207,142 @@ export const useKeySelection = (grid: Grid, options: Options = { scrollToEnd: fa
|
|||
newIndex += 1;
|
||||
}
|
||||
|
||||
// Adjust index if it's out of bounds
|
||||
if (direction === 'down' && newIndex > explorer.items.length - 1) {
|
||||
// Check if we're at the last row
|
||||
if (grid.getItem(currentItemIndex)?.row === grid.rowCount - 1) return;
|
||||
|
||||
// By default select the last index in the grid if running on windows,
|
||||
// otherwise only if we're out of bounds by one item
|
||||
if (
|
||||
explorerOperatingSystem === 'windows' ||
|
||||
newIndex - (explorer.items.length - 1) === 1
|
||||
) {
|
||||
newIndex = explorer.items.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
const newSelectedItem = grid.getItem(newIndex);
|
||||
if (!newSelectedItem?.data) return;
|
||||
|
||||
if (!e.shiftKey) {
|
||||
explorer.resetSelectedItems([newSelectedItem.data]);
|
||||
} else if (!explorer.isItemSelected(newSelectedItem.data)) {
|
||||
} else if (
|
||||
explorerOperatingSystem !== 'windows' &&
|
||||
!explorer.isItemSelected(newSelectedItem.data)
|
||||
) {
|
||||
explorer.addSelectedItem(newSelectedItem.data);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout so useEffects don't override it
|
||||
setTimeout(() => {
|
||||
activeItem.current = newSelectedItem.data!;
|
||||
activeItemIndex.current = newIndex;
|
||||
});
|
||||
updateActiveItem(newSelectedItem.data, { itemIndex: newIndex });
|
||||
updateFirstActiveItem(
|
||||
e.shiftKey ? firstActiveItem.current ?? newSelectedItem.data : newSelectedItem.data,
|
||||
{ itemIndex: e.shiftKey ? firstActiveItemIndex.current ?? currentItemIndex : newIndex }
|
||||
);
|
||||
|
||||
scrollToItem(newSelectedItem);
|
||||
};
|
||||
|
||||
useShortcut('explorerUp', (e) => handleNavigation(e, 'up'));
|
||||
useShortcut('explorerDown', (e) => handleNavigation(e, 'down'));
|
||||
useShortcut('explorerLeft', (e) => handleNavigation(e, 'left'));
|
||||
useShortcut('explorerRight', (e) => handleNavigation(e, 'right'));
|
||||
// Debounce keybinds to prevent weird execution order
|
||||
const debounce = useDebouncedCallback((fn: () => void) => fn(), 10);
|
||||
|
||||
return { activeItem };
|
||||
useShortcut('explorerUp', (e) => debounce(() => handleNavigation(e, 'up')));
|
||||
useShortcut('explorerDown', (e) => debounce(() => handleNavigation(e, 'down')));
|
||||
useShortcut('explorerLeft', (e) => debounce(() => handleNavigation(e, 'left')));
|
||||
useShortcut('explorerRight', (e) => debounce(() => handleNavigation(e, 'right')));
|
||||
|
||||
return { updateActiveItem, updateFirstActiveItem };
|
||||
};
|
||||
|
|
|
@ -43,10 +43,10 @@ export const GridView = () => {
|
|||
)
|
||||
});
|
||||
|
||||
const { activeItem } = useKeySelection(grid, { scrollToEnd: true });
|
||||
const { updateActiveItem } = useKeySelection(grid, { scrollToEnd: true });
|
||||
|
||||
return (
|
||||
<DragSelect grid={grid} onActiveItemChange={(item) => (activeItem.current = item)}>
|
||||
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
|
||||
<Grid grid={grid}>
|
||||
{(index) => {
|
||||
const item = explorer.items?.[index];
|
||||
|
|
|
@ -175,7 +175,7 @@ export const MediaView = () => {
|
|||
orderDirection
|
||||
]);
|
||||
|
||||
const { activeItem } = useKeySelection(grid);
|
||||
const { updateActiveItem } = useKeySelection(grid);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -188,7 +188,7 @@ export const MediaView = () => {
|
|||
>
|
||||
{isSortingByDate && <DateHeader date={date ?? ''} />}
|
||||
|
||||
<DragSelect grid={grid} onActiveItemChange={(item) => (activeItem.current = item)}>
|
||||
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
|
||||
{virtualRows.map((virtualRow) => (
|
||||
<React.Fragment key={virtualRow.key}>
|
||||
{columnVirtualizer.getVirtualItems().map((virtualColumn) => {
|
||||
|
|
|
@ -22,6 +22,7 @@ import { useQuickPreviewContext } from '../QuickPreview/Context';
|
|||
import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store';
|
||||
import { explorerStore } from '../store';
|
||||
import { useExplorerDroppable } from '../useExplorerDroppable';
|
||||
import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem';
|
||||
import { useExplorerSearchParams } from '../util';
|
||||
import { ViewContext, type ExplorerViewContext } from './Context';
|
||||
import { DragScrollable } from './DragScrollable';
|
||||
|
@ -36,6 +37,8 @@ export interface ExplorerViewProps
|
|||
}
|
||||
|
||||
export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
||||
const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem();
|
||||
|
||||
const explorer = useExplorerContext();
|
||||
const [isContextMenuOpen, isRenaming, drag] = useSelector(explorerStore, (s) => [
|
||||
s.isContextMenuOpen,
|
||||
|
@ -144,7 +147,15 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
|||
ref={ref}
|
||||
className="flex flex-1"
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 2 || (e.button === 0 && e.shiftKey)) return;
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const isWindowsExplorer =
|
||||
explorerOperatingSystem === 'windows' && matchingOperatingSystem;
|
||||
|
||||
// Prevent selection reset when holding shift or ctrl/cmd
|
||||
// This is to allow drag multi-selection
|
||||
if (e.shiftKey || (isWindowsExplorer ? e.ctrlKey : e.metaKey)) return;
|
||||
|
||||
explorer.selectedItems.size !== 0 && explorer.resetSelectedItems();
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -180,30 +180,46 @@ function useSelectedItems(items: ExplorerItem[] | null) {
|
|||
[itemsMap, selectedItemHashes]
|
||||
);
|
||||
|
||||
const getItemUniqueId = useCallback(
|
||||
(item: ExplorerItem) => itemHashesWeakMap.current.get(item) ?? uniqueId(item),
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
selectedItems,
|
||||
selectedItemHashes,
|
||||
getItemUniqueId,
|
||||
addSelectedItem: useCallback(
|
||||
(item: ExplorerItem) => {
|
||||
selectedItemHashes.value.add(uniqueId(item));
|
||||
(item: ExplorerItem | ExplorerItem[]) => {
|
||||
const items = Array.isArray(item) ? item : [item];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
selectedItemHashes.value.add(getItemUniqueId(items[i]!));
|
||||
}
|
||||
|
||||
updateHashes();
|
||||
},
|
||||
[selectedItemHashes.value, updateHashes]
|
||||
[getItemUniqueId, selectedItemHashes.value, updateHashes]
|
||||
),
|
||||
removeSelectedItem: useCallback(
|
||||
(item: ExplorerItem) => {
|
||||
selectedItemHashes.value.delete(uniqueId(item));
|
||||
(item: ExplorerItem | ExplorerItem[]) => {
|
||||
const items = Array.isArray(item) ? item : [item];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
selectedItemHashes.value.delete(getItemUniqueId(items[i]!));
|
||||
}
|
||||
|
||||
updateHashes();
|
||||
},
|
||||
[selectedItemHashes.value, updateHashes]
|
||||
[getItemUniqueId, selectedItemHashes.value, updateHashes]
|
||||
),
|
||||
resetSelectedItems: useCallback(
|
||||
(items?: ExplorerItem[]) => {
|
||||
selectedItemHashes.value.clear();
|
||||
items?.forEach((item) => selectedItemHashes.value.add(uniqueId(item)));
|
||||
items?.forEach((item) => selectedItemHashes.value.add(getItemUniqueId(item)));
|
||||
updateHashes();
|
||||
},
|
||||
[selectedItemHashes.value, updateHashes]
|
||||
[getItemUniqueId, selectedItemHashes.value, updateHashes]
|
||||
),
|
||||
isItemSelected: useCallback(
|
||||
(item: ExplorerItem) => selectedItems.has(item),
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { useEffect } from 'react';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
import { OperatingSystem } from '~/util/Platform';
|
||||
|
||||
export const explorerOperatingSystemStore = proxy({
|
||||
os: undefined as Extract<OperatingSystem, 'windows' | 'macOS'> | undefined
|
||||
});
|
||||
|
||||
// This hook is used to determine the operating system behavior of the explorer.
|
||||
export const useExplorerOperatingSystem = () => {
|
||||
const operatingSystem = useOperatingSystem(true);
|
||||
const store = useSnapshot(explorerOperatingSystemStore);
|
||||
|
||||
useEffect(() => {
|
||||
if (store.os) return;
|
||||
explorerOperatingSystemStore.os = operatingSystem === 'windows' ? 'windows' : 'macOS';
|
||||
}, [operatingSystem, store.os]);
|
||||
|
||||
const explorerOperatingSystem =
|
||||
store.os ?? (operatingSystem === 'windows' ? 'windows' : 'macOS');
|
||||
|
||||
return {
|
||||
operatingSystem,
|
||||
explorerOperatingSystem,
|
||||
matchingOperatingSystem: operatingSystem === explorerOperatingSystem
|
||||
};
|
||||
};
|
|
@ -25,6 +25,10 @@ import {
|
|||
import { toggleRenderRects } from '~/hooks';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import {
|
||||
explorerOperatingSystemStore,
|
||||
useExplorerOperatingSystem
|
||||
} from '../../Explorer/useExplorerOperatingSystem';
|
||||
import Setting from '../../settings/Setting';
|
||||
|
||||
export default () => {
|
||||
|
@ -148,6 +152,13 @@ export default () => {
|
|||
<SelectOption value="enabled">Enabled</SelectOption>
|
||||
</Select>
|
||||
</Setting>
|
||||
<Setting
|
||||
mini
|
||||
title="Explorer behavior"
|
||||
description="Change the explorer selection behavior"
|
||||
>
|
||||
<ExplorerBehaviorSelect />
|
||||
</Setting>
|
||||
<FeatureFlagSelector />
|
||||
<InvalidateDebugPanel />
|
||||
{/* <TestNotifications /> */}
|
||||
|
@ -267,3 +278,17 @@ function CloudOriginSelect() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ExplorerBehaviorSelect() {
|
||||
const { explorerOperatingSystem } = useExplorerOperatingSystem();
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={explorerOperatingSystem}
|
||||
onChange={(v) => (explorerOperatingSystemStore.os = v)}
|
||||
>
|
||||
<SelectOption value="macOS">macOS</SelectOption>
|
||||
<SelectOption value="windows">windows</SelectOption>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue