mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
parent
959ccdfd98
commit
745399ecab
|
@ -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) {
|
||||
|
|
|
@ -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!');
|
||||
|
|
@ -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}
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
124
interface/app/$libraryId/Explorer/View/useActiveItem.tsx
Normal file
124
interface/app/$libraryId/Explorer/View/useActiveItem.tsx
Normal 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
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue