This commit is contained in:
nikec 2024-06-26 10:29:10 +02:00
parent 20b8a2b93b
commit f97303ff54
19 changed files with 324 additions and 74 deletions

View file

@ -93,6 +93,7 @@ pub enum ExplorerLayout {
Grid, Grid,
List, List,
Media, Media,
Columns,
} }
#[derive(Clone, Serialize, Deserialize, Type, Debug)] #[derive(Clone, Serialize, Deserialize, Type, Debug)]

View file

@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { Slider } from '@sd/ui'; import { Slider } from '@sd/ui';
import { useLocale } from '~/hooks'; import { useLocale } from '~/hooks';
@ -15,21 +14,18 @@ export const IconSize = () => {
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const settings = explorer.useSettingsSnapshot(); const settings = explorer.useSettingsSnapshot();
const defaultValue = useMemo( const defaultValue = sizes.indexMap.get(settings.listViewIconSize)!;
() => sizes.findIndex((size) => size[0] === settings.listViewIconSize),
[settings.listViewIconSize]
);
return ( return (
<div> <div>
<Subheading>{t('icon_size')}</Subheading> <Subheading>{t('icon_size')}</Subheading>
<Slider <Slider
step={1} step={1}
max={sizes.length - 1} max={sizes.indexMap.size - 1}
defaultValue={[defaultValue]} defaultValue={[defaultValue]}
onValueChange={([value]) => { onValueChange={([value]) => {
const size = value !== undefined && sizes[value]; const size = value !== undefined && sizes.sizeMap.get(value);
if (size) explorer.settingsStore.listViewIconSize = size[0]; if (size) explorer.settingsStore.listViewIconSize = size;
}} }}
/> />
</div> </div>

View file

@ -1,4 +1,3 @@
import { useMemo } from 'react';
import { Slider } from '@sd/ui'; import { Slider } from '@sd/ui';
import { useLocale } from '~/hooks'; import { useLocale } from '~/hooks';
@ -15,21 +14,18 @@ export const TextSize = () => {
const explorer = useExplorerContext(); const explorer = useExplorerContext();
const settings = explorer.useSettingsSnapshot(); const settings = explorer.useSettingsSnapshot();
const defaultValue = useMemo( const defaultValue = sizes.indexMap.get(settings.listViewTextSize)!;
() => sizes.findIndex((size) => size[0] === settings.listViewTextSize),
[settings.listViewTextSize]
);
return ( return (
<div> <div>
<Subheading>{t('text_size')}</Subheading> <Subheading>{t('text_size')}</Subheading>
<Slider <Slider
step={1} step={1}
max={sizes.length - 1} max={sizes.indexMap.size - 1}
defaultValue={[defaultValue]} defaultValue={[defaultValue]}
onValueChange={([value]) => { onValueChange={([value]) => {
const size = value !== undefined && sizes[value]; const size = value !== undefined && sizes.sizeMap.get(value);
if (size) explorer.settingsStore.listViewTextSize = size[0]; if (size) explorer.settingsStore.listViewTextSize = size;
}} }}
/> />
</div> </div>

View file

@ -1,3 +1,20 @@
export function getSizes<T extends { [key: string]: number }>(sizes: T) { export function getSizes<T extends { [key: string]: number }>(sizes: T) {
return (Object.entries(sizes) as [keyof T, T[keyof T]][]).sort((a, b) => a[1] - b[1]); console.log('gen sizes');
const sizesArr = (Object.entries(sizes) as [keyof T, T[keyof T]][]).sort((a, b) => a[1] - b[1]);
// Map fo size to index
const indexMap = new Map<keyof T, number>();
// Map of index to size
const sizeMap = new Map<number, keyof T>();
for (let i = 0; i < sizesArr.length; i++) {
const size = sizesArr[i];
if (!size) continue;
indexMap.set(size[0], i);
sizeMap.set(i, size[0]);
}
return { indexMap, sizeMap };
} }

View file

@ -6,6 +6,7 @@ import {
SidebarSimple, SidebarSimple,
SlidersHorizontal, SlidersHorizontal,
SquaresFour, SquaresFour,
SquareSplitHorizontal,
Tag Tag
} from '@phosphor-icons/react'; } from '@phosphor-icons/react';
import clsx from 'clsx'; import clsx from 'clsx';
@ -25,7 +26,8 @@ import { explorerStore } from './store';
const layoutIcons: Record<ExplorerLayout, Icon> = { const layoutIcons: Record<ExplorerLayout, Icon> = {
grid: SquaresFour, grid: SquaresFour,
list: Rows, list: Rows,
media: MonitorPlay media: MonitorPlay,
columns: SquareSplitHorizontal
}; };
export const useExplorerTopBarOptions = () => { export const useExplorerTopBarOptions = () => {

View file

@ -0,0 +1,53 @@
import { CaretRight } from '@phosphor-icons/react';
import clsx from 'clsx';
import { memo } from 'react';
import { getItemFilePath, type ExplorerItem } from '@sd/client';
import { FileThumb } from '../../FilePath/Thumb';
import { RenamableItemText } from '../RenamableItemText';
export interface ColumnsViewItemProps {
data: ExplorerItem;
selected?: boolean;
cut?: boolean;
}
export const ColumnsViewItem = memo((props: ColumnsViewItemProps) => {
const filePath = getItemFilePath(props.data);
const isHidden = filePath?.hidden;
const isFolder = filePath?.is_dir;
const isLocation = props.data.type === 'Location';
return (
<div
className={clsx(
'flex items-center rounded px-4 py-1 pr-3',
props.selected && 'bg-accent'
)}
>
<FileThumb
data={props.data}
frame
frameClassName={clsx('!border', props.data.type === 'Label' && '!rounded-lg')}
blackBars
size={24}
className={clsx('mr-0.5 transition-[height_width]', props.cut && 'opacity-60')}
/>
<div className="relative flex-1">
<RenamableItemText
item={props.data}
selected={props.selected}
allowHighlight={false}
style={{ fontSize: 13 }}
className="absolute top-1/2 z-10 max-w-full -translate-y-1/2"
idleClassName="!w-full"
editLines={3}
/>
</div>
{isFolder && <CaretRight weight="bold" size={10} opacity={0.5} />}
</div>
);
});

View file

@ -0,0 +1,67 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { useCallback } from 'react';
import { getExplorerItemData } from '@sd/client';
import { useExplorerContext } from '../../Context';
import { useExplorerViewContext } from '../Context';
import { ColumnsViewItem } from './Item';
const ROW_HEIGHT = 20;
export function ColumnsView() {
const explorer = useExplorerContext();
const explorerView = useExplorerViewContext();
const rowVirtualizer = useVirtualizer({
count: Math.max(explorerView.items?.length ?? 0, explorerView.count ?? 0),
getScrollElement: useCallback(() => explorerView.ref.current, [explorerView.ref]),
estimateSize: useCallback(() => ROW_HEIGHT, []),
paddingStart: 8,
paddingEnd: 8 + (explorerView.scrollPadding?.bottom ?? 0),
// scrollMargin: listOffset,
overscan: explorer.overscan ?? 10,
scrollPaddingStart: explorerView.scrollPadding?.top,
scrollPaddingEnd: explorerView.scrollPadding?.bottom
});
const virtualRows = rowVirtualizer.getVirtualItems();
return (
<div>
<div className="relative" style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
<div
className="absolute left-0 top-0 min-w-full"
style={{
transform: `translateY(${
(virtualRows[0]?.start ?? 0) - rowVirtualizer.options.scrollMargin
}px)`
}}
>
{virtualRows.map((virtualRow) => {
const row = explorerView.items?.[virtualRow.index];
if (!row) return null;
const itemData = getExplorerItemData(row);
const previousRow = explorerView.items?.[virtualRow.index - 1];
const nextRow = explorerView.items?.[virtualRow.index + 1];
return (
<div
key={virtualRow.key}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
className="relative"
>
<ColumnsViewItem
data={row}
// selected={itemData.name === 'spacedrive'}
/>
</div>
);
})}
</div>
</div>
</div>
);
}

View file

@ -1,6 +1,11 @@
import { createContext, useContext, type ReactNode, type RefObject } from 'react'; import { createContext, useContext, type ReactNode, type RefObject } from 'react';
export interface ExplorerViewContext { import { useSelectedItems } from '../useExplorer';
import { useExplorerWindow } from '../useExplorerWindow';
export interface ExplorerViewContext
extends ReturnType<typeof useExplorerWindow>,
ReturnType<typeof useSelectedItems> {
ref: RefObject<HTMLDivElement>; ref: RefObject<HTMLDivElement>;
/** /**
* Padding to apply when scrolling to an item. * Padding to apply when scrolling to an item.

View file

@ -130,11 +130,11 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
}; };
if (!continueSelection) { if (!continueSelection) {
if (explorer.selectedItems.has(item.data)) { if (explorerView.selectedItems.has(item.data)) {
// Keep previous selection as selecto will reset it otherwise // Keep previous selection as selecto will reset it otherwise
selecto.current?.setSelectedTargets(e.beforeSelected); selecto.current?.setSelectedTargets(e.beforeSelected);
} else { } else {
explorer.resetSelectedItems([item.data]); explorerView.resetSelectedItems([item.data]);
selectedTargets.resetSelectedTargets([ selectedTargets.resetSelectedTargets([
{ id: String(item.id), node: element as HTMLElement } { id: String(item.id), node: element as HTMLElement }
]); ]);
@ -143,8 +143,8 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
return; return;
} }
if (e.added[0]) explorer.addSelectedItem(item.data); if (e.added[0]) explorerView.addSelectedItem(item.data);
else explorer.removeSelectedItem(item.data); else explorerView.removeSelectedItem(item.data);
// Update active item for further keyboard selection. // Update active item for further keyboard selection.
onActiveItemChange(item.data, { updateFirstItem: true, setFirstItemAsChanged: true }); onActiveItemChange(item.data, { updateFirstItem: true, setFirstItemAsChanged: true });

View file

@ -33,8 +33,8 @@ export const GridItem = ({ children, item, index, ...props }: Props) => {
const selected = useMemo( const selected = useMemo(
// Even though this checks object equality, it should still be safe since `selectedItems` // Even though this checks object equality, it should still be safe since `selectedItems`
// will be re-calculated before this memo runs. // will be re-calculated before this memo runs.
() => explorer.selectedItems.has(item), () => explorerView.selectedItems.has(item),
[explorer.selectedItems, item] [explorerView.selectedItems, item]
); );
const canGoBack = currentIndex !== 0; const canGoBack = currentIndex !== 0;
@ -61,8 +61,8 @@ export const GridItem = ({ children, item, index, ...props }: Props) => {
} }
}} }}
onContextMenu={(e) => { onContextMenu={(e) => {
if (!explorerView.selectable || explorer.selectedItems.has(item)) return; if (!explorerView.selectable || explorerView.selectedItems.has(item)) return;
explorer.resetSelectedItems([item]); explorerView.resetSelectedItems([item]);
dragSelect.resetSelectedTargets([{ id: uniqueId(item), node: e.currentTarget }]); dragSelect.resetSelectedTargets([{ id: uniqueId(item), node: e.currentTarget }]);
}} }}
> >

View file

@ -20,9 +20,9 @@ export const GridView = () => {
const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight; const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
const grid = useGrid({ const grid = useGrid({
scrollRef: explorer.scrollRef, scrollRef: explorerView.ref,
count: explorer.items?.length ?? 0, count: explorerView.items?.length ?? 0,
totalCount: explorer.count, totalCount: explorerView.count,
columns: 'auto', columns: 'auto',
size: { width: explorerSettings.gridItemSize, height: itemHeight }, size: { width: explorerSettings.gridItemSize, height: itemHeight },
padding: { padding: {
@ -32,14 +32,14 @@ export const GridView = () => {
}, },
gap: explorerSettings.gridGap, gap: explorerSettings.gridGap,
overscan: explorer.overscan ?? 5, overscan: explorer.overscan ?? 5,
onLoadMore: explorer.loadMore, onLoadMore: explorerView.loadMore,
getItemId: useCallback( getItemId: useCallback(
(index: number) => getItemId(index, explorer.items ?? []), (index: number) => getItemId(index, explorerView.items ?? []),
[explorer.items] [explorerView.items]
), ),
getItemData: useCallback( getItemData: useCallback(
(index: number) => getItemData(index, explorer.items ?? []), (index: number) => getItemData(index, explorerView.items ?? []),
[explorer.items] [explorerView.items]
) )
}); });
@ -49,7 +49,7 @@ export const GridView = () => {
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}> <DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
<Grid grid={grid}> <Grid grid={grid}>
{(index) => { {(index) => {
const item = explorer.items?.[index]; const item = explorerView.items?.[index];
if (!item) return null; if (!item) return null;
return ( return (

View file

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react'; import { CSSProperties, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useKeys } from 'rooks'; import { useKeys } from 'rooks';
import { import {
@ -21,9 +21,12 @@ import { QuickPreview } from '../QuickPreview';
import { useQuickPreviewContext } from '../QuickPreview/Context'; import { useQuickPreviewContext } from '../QuickPreview/Context';
import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store'; import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store';
import { explorerStore } from '../store'; import { explorerStore } from '../store';
import { useSelectedItems } from '../useExplorer';
import { useExplorerDroppable } from '../useExplorerDroppable'; import { useExplorerDroppable } from '../useExplorerDroppable';
import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem'; import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem';
import { useExplorerWindow } from '../useExplorerWindow';
import { useExplorerSearchParams } from '../util'; import { useExplorerSearchParams } from '../util';
import { ColumnsView } from './ColumnsView';
import { ViewContext, type ExplorerViewContext } from './Context'; import { ViewContext, type ExplorerViewContext } from './Context';
import { DragScrollable } from './DragScrollable'; import { DragScrollable } from './DragScrollable';
import { GridView } from './GridView'; import { GridView } from './GridView';
@ -34,9 +37,18 @@ import { useViewItemDoubleClick } from './ViewItem';
export interface ExplorerViewProps export interface ExplorerViewProps
extends Omit<ExplorerViewContext, 'selectable' | 'ref' | 'padding'> { extends Omit<ExplorerViewContext, 'selectable' | 'ref' | 'padding'> {
emptyNotice?: JSX.Element; emptyNotice?: JSX.Element;
path?: string;
style?: CSSProperties;
} }
export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => { export const View = ({
emptyNotice,
path: explorerPath,
style,
...contextProps
}: ExplorerViewProps) => {
const items = useExplorerWindow(explorerPath);
const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem(); const { explorerOperatingSystem, matchingOperatingSystem } = useExplorerOperatingSystem();
const explorer = useExplorerContext(); const explorer = useExplorerContext();
@ -86,6 +98,8 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
}) })
}); });
const selected = useSelectedItems(items.items);
useShortcuts(); useShortcuts();
useShortcut('explorerEscape', () => { useShortcut('explorerEscape', () => {
@ -142,10 +156,11 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
if (!explorer.layouts[layoutMode]) return null; if (!explorer.layouts[layoutMode]) return null;
return ( return (
<ViewContext.Provider value={{ ref, ...contextProps, selectable }}> <ViewContext.Provider value={{ ref, ...contextProps, selectable, ...items, ...selected }}>
<div <div
ref={ref} ref={ref}
className="flex flex-1" className="custom-scroll explorer-scroll flex size-full overflow-y-auto overflow-x-hidden border-r border-app-line"
style={{ width: layoutMode === 'columns' ? 400 : undefined, ...style }}
onMouseDown={(e) => { onMouseDown={(e) => {
if (e.button !== 0) return; if (e.button !== 0) return;
@ -165,6 +180,7 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
{layoutMode === 'grid' && <GridView />} {layoutMode === 'grid' && <GridView />}
{layoutMode === 'list' && <ListView />} {layoutMode === 'list' && <ListView />}
{layoutMode === 'media' && <MediaView />} {layoutMode === 'media' && <MediaView />}
{layoutMode === 'columns' && <ColumnsView />}
{showLoading && ( {showLoading && (
<Loader className="fixed bottom-10 left-0 w-[calc(100%+180px)]" /> <Loader className="fixed bottom-10 left-0 w-[calc(100%+180px)]" />
)} )}

View file

@ -1,5 +1,4 @@
import { FolderNotchOpen } from '@phosphor-icons/react'; import { CSSProperties, Suspense, type PropsWithChildren, type ReactNode } from 'react';
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
import { import {
explorerLayout, explorerLayout,
useExplorerLayoutStore, useExplorerLayoutStore,
@ -10,7 +9,6 @@ import { useShortcut } from '~/hooks';
import { useTopBarContext } from '../TopBar/Context'; import { useTopBarContext } from '../TopBar/Context';
import { useExplorerContext } from './Context'; import { useExplorerContext } from './Context';
import ContextMenu from './ContextMenu';
import DismissibleNotice from './DismissibleNotice'; import DismissibleNotice from './DismissibleNotice';
import { ExplorerPath, PATH_BAR_HEIGHT } from './ExplorerPath'; import { ExplorerPath, PATH_BAR_HEIGHT } from './ExplorerPath';
import { Inspector, INSPECTOR_WIDTH } from './Inspector'; import { Inspector, INSPECTOR_WIDTH } from './Inspector';
@ -19,11 +17,15 @@ import { getQuickPreviewStore } from './QuickPreview/store';
import { explorerStore } from './store'; import { explorerStore } from './store';
import { useKeyRevealFinder } from './useKeyRevealFinder'; import { useKeyRevealFinder } from './useKeyRevealFinder';
import { ExplorerViewProps, View } from './View'; import { ExplorerViewProps, View } from './View';
import { EmptyNotice } from './View/EmptyNotice';
import 'react-slidedown/lib/slidedown.css'; import 'react-slidedown/lib/slidedown.css';
import { FolderNotchOpen } from '@phosphor-icons/react';
import ContextMenu from './ContextMenu';
import { useExplorerDnd } from './useExplorerDnd'; import { useExplorerDnd } from './useExplorerDnd';
import { useExplorerSearchParams } from './util';
import { EmptyNotice } from './View/EmptyNotice';
interface Props { interface Props {
emptyNotice?: ExplorerViewProps['emptyNotice']; emptyNotice?: ExplorerViewProps['emptyNotice'];
@ -39,6 +41,8 @@ export default function Explorer(props: PropsWithChildren<Props>) {
const layoutStore = useExplorerLayoutStore(); const layoutStore = useExplorerLayoutStore();
const showInspector = useSelector(explorerStore, (s) => s.showInspector); const showInspector = useSelector(explorerStore, (s) => s.showInspector);
const [{ path }] = useExplorerSearchParams();
const showPathBar = explorer.showPathBar && layoutStore.showPathBar; const showPathBar = explorer.showPathBar && layoutStore.showPathBar;
// Can we put this somewhere else -_- // Can we put this somewhere else -_-
@ -76,39 +80,53 @@ export default function Explorer(props: PropsWithChildren<Props>) {
const topBar = useTopBarContext(); const topBar = useTopBarContext();
const paths = [undefined, ...(path?.split('/').filter(Boolean) ?? [])];
return ( return (
<> <>
<ExplorerContextMenu> <ExplorerContextMenu>
<div <div
ref={explorer.scrollRef} ref={explorer.scrollRef}
className="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden" className="custom-scroll explorer-scroll flex flex-1 flex-col overflow-x-hidden"
style={
{
'--scrollbar-margin-top': `${topBar.topBarHeight}px`,
'--scrollbar-margin-bottom': `${showPathBar ? PATH_BAR_HEIGHT : 0}px`,
'paddingTop': topBar.topBarHeight,
'paddingRight': showInspector ? INSPECTOR_WIDTH : 0
} as CSSProperties
}
> >
{explorer.items && explorer.items.length > 0 && <DismissibleNotice />} {explorer.items && explorer.items.length > 0 && <DismissibleNotice />}
<View <div className="flex flex-1 overflow-hidden">
contextMenu={props.contextMenu ? props.contextMenu() : <ContextMenu />} <Suspense fallback={<SuspanceFb />}>
emptyNotice={ {paths.map((path, i) => {
props.emptyNotice ?? ( const p = !path ? undefined : paths.slice(0, i + 1).join('/') + '/';
<EmptyNotice console.log('path', path, p);
icon={FolderNotchOpen} return (
message="This folder is empty" <View
/> key={path}
) style={
} {
listViewOptions={{ hideHeaderBorder: true }} '--scrollbar-margin-top': `${topBar.topBarHeight}px`,
scrollPadding={{ '--scrollbar-margin-bottom': `${showPathBar ? PATH_BAR_HEIGHT : 0}px`,
top: topBar.topBarHeight, 'paddingTop': topBar.topBarHeight,
bottom: showPathBar ? PATH_BAR_HEIGHT : undefined 'paddingRight': showInspector ? INSPECTOR_WIDTH : 0
}} } as CSSProperties
/> }
path={p}
contextMenu={props.contextMenu?.() ?? <ContextMenu />}
emptyNotice={
props.emptyNotice ?? (
<EmptyNotice
icon={FolderNotchOpen}
message="This folder is empty"
/>
)
}
listViewOptions={{ hideHeaderBorder: true }}
scrollPadding={{
top: topBar.topBarHeight,
bottom: showPathBar ? PATH_BAR_HEIGHT : undefined
}}
/>
);
})}
</Suspense>
</div>
</div> </div>
</ExplorerContextMenu> </ExplorerContextMenu>
@ -126,3 +144,9 @@ export default function Explorer(props: PropsWithChildren<Props>) {
</> </>
); );
} }
const SuspanceFb = () => {
console.log('Loading...');
return <div className="flex size-full items-center justify-center">Loading...</div>;
};

View file

@ -77,6 +77,7 @@ export function useExplorer<TOrder extends Ordering>({
grid: true, grid: true,
list: true, list: true,
media: true, media: true,
columns: true,
...layouts ...layouts
}, },
...settings, ...settings,
@ -140,7 +141,7 @@ export type UseExplorerSettings<TOrder extends Ordering, T> = ReturnType<
typeof useExplorerSettings<TOrder, T> typeof useExplorerSettings<TOrder, T>
>; >;
function useSelectedItems(items: ExplorerItem[] | null) { export function useSelectedItems(items: ExplorerItem[] | null) {
// Doing pointer lookups for hashes is a bit faster than assembling a bunch of strings // Doing pointer lookups for hashes is a bit faster than assembling a bunch of strings
// WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache // WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache
const itemHashesWeakMap = useRef(new WeakMap<ExplorerItem, string>()); const itemHashesWeakMap = useRef(new WeakMap<ExplorerItem, string>());

View file

@ -0,0 +1,67 @@
import { useMemo } from 'react';
import { useLocationExplorerSettings } from '../location/$id';
import { useSearchFromSearchParams } from '../search';
import { useSearchExplorerQuery } from '../search/useSearchExplorerQuery';
import { useExplorerContext } from './Context';
import { explorerStore } from './store';
import { useExplorerSearchParams } from './util';
export function useExplorerWindow(path?: string) {
const explorer = useExplorerContext();
if (explorer.parent?.type !== 'Location') {
throw new Error('useExplorerWindow must be used within a LocationExplorer');
}
const location = explorer.parent.location;
const [{ take }] = useExplorerSearchParams();
const { explorerSettings, preferences } = useLocationExplorerSettings(location);
const { layoutMode, mediaViewWithDescendants, showHiddenFiles } =
explorerSettings.useSettingsSnapshot();
const defaultFilters = useMemo(
() => [{ filePath: { locations: { in: [location.id] } } }],
[location.id]
);
const search = useSearchFromSearchParams();
const searchFiltersAreDefault = useMemo(
() => JSON.stringify(defaultFilters) !== JSON.stringify(search.filters),
[defaultFilters, search.filters]
);
//
const items = useSearchExplorerQuery({
search,
explorerSettings,
filters: [
...(search.allFilters.length > 0 ? search.allFilters : defaultFilters),
{
filePath: {
path: {
location_id: location.id,
path: path ?? '',
include_descendants:
search.search !== '' ||
(search.filters &&
search.filters.length > 0 &&
searchFiltersAreDefault) ||
(layoutMode === 'media' && mediaViewWithDescendants)
}
}
},
...(!showHiddenFiles ? [{ filePath: { hidden: false } }] : [])
],
take,
paths: { order: explorerSettings.useSettingsSnapshot().order },
onSuccess: () => explorerStore.resetNewThumbnails()
});
return items;
}

View file

@ -81,6 +81,8 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
[defaultFilters, search.filters] [defaultFilters, search.filters]
); );
//
const items = useSearchExplorerQuery({ const items = useSearchExplorerQuery({
search, search,
explorerSettings, explorerSettings,
@ -130,6 +132,8 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
useKeyDeleteFile(explorer.selectedItems, location.id); useKeyDeleteFile(explorer.selectedItems, location.id);
//
useShortcut('rescan', () => rescan(location.id)); useShortcut('rescan', () => rescan(location.id));
const title = useRouteTitle( const title = useRouteTitle(
@ -175,6 +179,7 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
)} )}
</TopBarPortal> </TopBarPortal>
</SearchContextProvider> </SearchContextProvider>
{isLocationIndexing ? ( {isLocationIndexing ? (
<div className="flex size-full items-center justify-center"> <div className="flex size-full items-center justify-center">
<Loader /> <Loader />
@ -223,7 +228,7 @@ function getLastSectionOfPath(path: string): string | undefined {
return lastSection; return lastSection;
} }
function useLocationExplorerSettings(location: Location) { export function useLocationExplorerSettings(location: Location) {
const rspc = useRspcLibraryContext(); const rspc = useRspcLibraryContext();
const preferences = useLibraryQuery(['preferences.get']); const preferences = useLibraryQuery(['preferences.get']);

View file

@ -67,5 +67,4 @@
"eslintConfig": { "eslintConfig": {
"root": true "root": true
}, },
"packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f" "packageManager": "pnpm@9.0.4"}
}

View file

@ -250,7 +250,7 @@ export type EphemeralRenameOne = { from_path: string; to: string }
export type ExplorerItem = { type: "Path"; thumbnail: string[] | null; item: FilePathWithObject } | { type: "Object"; thumbnail: string[] | null; item: ObjectWithFilePaths } | { type: "Location"; item: Location } | { type: "NonIndexedPath"; thumbnail: string[] | null; item: NonIndexedPathItem } | { type: "SpacedropPeer"; item: PeerMetadata } | { type: "Label"; thumbnails: string[][]; item: LabelWithObjects } export type ExplorerItem = { type: "Path"; thumbnail: string[] | null; item: FilePathWithObject } | { type: "Object"; thumbnail: string[] | null; item: ObjectWithFilePaths } | { type: "Location"; item: Location } | { type: "NonIndexedPath"; thumbnail: string[] | null; item: NonIndexedPathItem } | { type: "SpacedropPeer"; item: PeerMetadata } | { type: "Label"; thumbnails: string[][]; item: LabelWithObjects }
export type ExplorerLayout = "grid" | "list" | "media" export type ExplorerLayout = "grid" | "list" | "media" | "columns"
export type ExplorerSettings<TOrder> = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; gridGap: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; mediaViewWithDescendants: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colVisibility: { [key in string]: boolean } | null; colSizes: { [key in string]: number } | null; listViewIconSize: string | null; listViewTextSize: string | null; order?: TOrder | null; showHiddenFiles?: boolean } export type ExplorerSettings<TOrder> = { layoutMode: ExplorerLayout | null; gridItemSize: number | null; gridGap: number | null; mediaColumns: number | null; mediaAspectSquare: boolean | null; mediaViewWithDescendants: boolean | null; openOnDoubleClick: DoubleClickAction | null; showBytesInGridView: boolean | null; colVisibility: { [key in string]: boolean } | null; colSizes: { [key in string]: number } | null; listViewIconSize: string | null; listViewTextSize: string | null; order?: TOrder | null; showHiddenFiles?: boolean }

View file

@ -57,7 +57,8 @@ export function usePathsOffsetInfiniteQuery({
if (nodes.length >= arg.take) return (offset ?? 0) + 1; if (nodes.length >= arg.take) return (offset ?? 0) + 1;
}, },
onSuccess, onSuccess,
...args ...args,
suspense: true
}); });
const nodes = useMemo( const nodes = useMemo(