mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-06-30 11:23:33 +00:00
c
This commit is contained in:
parent
20b8a2b93b
commit
f97303ff54
|
@ -93,6 +93,7 @@ pub enum ExplorerLayout {
|
|||
Grid,
|
||||
List,
|
||||
Media,
|
||||
Columns,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Type, Debug)]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Slider } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
|
@ -15,21 +14,18 @@ export const IconSize = () => {
|
|||
const explorer = useExplorerContext();
|
||||
const settings = explorer.useSettingsSnapshot();
|
||||
|
||||
const defaultValue = useMemo(
|
||||
() => sizes.findIndex((size) => size[0] === settings.listViewIconSize),
|
||||
[settings.listViewIconSize]
|
||||
);
|
||||
const defaultValue = sizes.indexMap.get(settings.listViewIconSize)!;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subheading>{t('icon_size')}</Subheading>
|
||||
<Slider
|
||||
step={1}
|
||||
max={sizes.length - 1}
|
||||
max={sizes.indexMap.size - 1}
|
||||
defaultValue={[defaultValue]}
|
||||
onValueChange={([value]) => {
|
||||
const size = value !== undefined && sizes[value];
|
||||
if (size) explorer.settingsStore.listViewIconSize = size[0];
|
||||
const size = value !== undefined && sizes.sizeMap.get(value);
|
||||
if (size) explorer.settingsStore.listViewIconSize = size;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Slider } from '@sd/ui';
|
||||
import { useLocale } from '~/hooks';
|
||||
|
||||
|
@ -15,21 +14,18 @@ export const TextSize = () => {
|
|||
const explorer = useExplorerContext();
|
||||
const settings = explorer.useSettingsSnapshot();
|
||||
|
||||
const defaultValue = useMemo(
|
||||
() => sizes.findIndex((size) => size[0] === settings.listViewTextSize),
|
||||
[settings.listViewTextSize]
|
||||
);
|
||||
const defaultValue = sizes.indexMap.get(settings.listViewTextSize)!;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subheading>{t('text_size')}</Subheading>
|
||||
<Slider
|
||||
step={1}
|
||||
max={sizes.length - 1}
|
||||
max={sizes.indexMap.size - 1}
|
||||
defaultValue={[defaultValue]}
|
||||
onValueChange={([value]) => {
|
||||
const size = value !== undefined && sizes[value];
|
||||
if (size) explorer.settingsStore.listViewTextSize = size[0];
|
||||
const size = value !== undefined && sizes.sizeMap.get(value);
|
||||
if (size) explorer.settingsStore.listViewTextSize = size;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,3 +1,20 @@
|
|||
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 };
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
SidebarSimple,
|
||||
SlidersHorizontal,
|
||||
SquaresFour,
|
||||
SquareSplitHorizontal,
|
||||
Tag
|
||||
} from '@phosphor-icons/react';
|
||||
import clsx from 'clsx';
|
||||
|
@ -25,7 +26,8 @@ import { explorerStore } from './store';
|
|||
const layoutIcons: Record<ExplorerLayout, Icon> = {
|
||||
grid: SquaresFour,
|
||||
list: Rows,
|
||||
media: MonitorPlay
|
||||
media: MonitorPlay,
|
||||
columns: SquareSplitHorizontal
|
||||
};
|
||||
|
||||
export const useExplorerTopBarOptions = () => {
|
||||
|
|
53
interface/app/$libraryId/Explorer/View/ColumnsView/Item.tsx
Normal file
53
interface/app/$libraryId/Explorer/View/ColumnsView/Item.tsx
Normal 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>
|
||||
);
|
||||
});
|
67
interface/app/$libraryId/Explorer/View/ColumnsView/index.tsx
Normal file
67
interface/app/$libraryId/Explorer/View/ColumnsView/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,11 @@
|
|||
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>;
|
||||
/**
|
||||
* Padding to apply when scrolling to an item.
|
||||
|
|
|
@ -130,11 +130,11 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
};
|
||||
|
||||
if (!continueSelection) {
|
||||
if (explorer.selectedItems.has(item.data)) {
|
||||
if (explorerView.selectedItems.has(item.data)) {
|
||||
// Keep previous selection as selecto will reset it otherwise
|
||||
selecto.current?.setSelectedTargets(e.beforeSelected);
|
||||
} else {
|
||||
explorer.resetSelectedItems([item.data]);
|
||||
explorerView.resetSelectedItems([item.data]);
|
||||
selectedTargets.resetSelectedTargets([
|
||||
{ id: String(item.id), node: element as HTMLElement }
|
||||
]);
|
||||
|
@ -143,8 +143,8 @@ export const DragSelect = ({ grid, children, onActiveItemChange }: Props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (e.added[0]) explorer.addSelectedItem(item.data);
|
||||
else explorer.removeSelectedItem(item.data);
|
||||
if (e.added[0]) explorerView.addSelectedItem(item.data);
|
||||
else explorerView.removeSelectedItem(item.data);
|
||||
|
||||
// Update active item for further keyboard selection.
|
||||
onActiveItemChange(item.data, { updateFirstItem: true, setFirstItemAsChanged: true });
|
||||
|
|
|
@ -33,8 +33,8 @@ export const GridItem = ({ children, item, index, ...props }: Props) => {
|
|||
const selected = useMemo(
|
||||
// Even though this checks object equality, it should still be safe since `selectedItems`
|
||||
// will be re-calculated before this memo runs.
|
||||
() => explorer.selectedItems.has(item),
|
||||
[explorer.selectedItems, item]
|
||||
() => explorerView.selectedItems.has(item),
|
||||
[explorerView.selectedItems, item]
|
||||
);
|
||||
|
||||
const canGoBack = currentIndex !== 0;
|
||||
|
@ -61,8 +61,8 @@ export const GridItem = ({ children, item, index, ...props }: Props) => {
|
|||
}
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
if (!explorerView.selectable || explorer.selectedItems.has(item)) return;
|
||||
explorer.resetSelectedItems([item]);
|
||||
if (!explorerView.selectable || explorerView.selectedItems.has(item)) return;
|
||||
explorerView.resetSelectedItems([item]);
|
||||
dragSelect.resetSelectedTargets([{ id: uniqueId(item), node: e.currentTarget }]);
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -20,9 +20,9 @@ export const GridView = () => {
|
|||
const itemHeight = explorerSettings.gridItemSize + itemDetailsHeight;
|
||||
|
||||
const grid = useGrid({
|
||||
scrollRef: explorer.scrollRef,
|
||||
count: explorer.items?.length ?? 0,
|
||||
totalCount: explorer.count,
|
||||
scrollRef: explorerView.ref,
|
||||
count: explorerView.items?.length ?? 0,
|
||||
totalCount: explorerView.count,
|
||||
columns: 'auto',
|
||||
size: { width: explorerSettings.gridItemSize, height: itemHeight },
|
||||
padding: {
|
||||
|
@ -32,14 +32,14 @@ export const GridView = () => {
|
|||
},
|
||||
gap: explorerSettings.gridGap,
|
||||
overscan: explorer.overscan ?? 5,
|
||||
onLoadMore: explorer.loadMore,
|
||||
onLoadMore: explorerView.loadMore,
|
||||
getItemId: useCallback(
|
||||
(index: number) => getItemId(index, explorer.items ?? []),
|
||||
[explorer.items]
|
||||
(index: number) => getItemId(index, explorerView.items ?? []),
|
||||
[explorerView.items]
|
||||
),
|
||||
getItemData: useCallback(
|
||||
(index: number) => getItemData(index, explorer.items ?? []),
|
||||
[explorer.items]
|
||||
(index: number) => getItemData(index, explorerView.items ?? []),
|
||||
[explorerView.items]
|
||||
)
|
||||
});
|
||||
|
||||
|
@ -49,7 +49,7 @@ export const GridView = () => {
|
|||
<DragSelect grid={grid} onActiveItemChange={updateActiveItem}>
|
||||
<Grid grid={grid}>
|
||||
{(index) => {
|
||||
const item = explorer.items?.[index];
|
||||
const item = explorerView.items?.[index];
|
||||
if (!item) return null;
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import { CSSProperties, useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useKeys } from 'rooks';
|
||||
import {
|
||||
|
@ -21,9 +21,12 @@ import { QuickPreview } from '../QuickPreview';
|
|||
import { useQuickPreviewContext } from '../QuickPreview/Context';
|
||||
import { getQuickPreviewStore, useQuickPreviewStore } from '../QuickPreview/store';
|
||||
import { explorerStore } from '../store';
|
||||
import { useSelectedItems } from '../useExplorer';
|
||||
import { useExplorerDroppable } from '../useExplorerDroppable';
|
||||
import { useExplorerOperatingSystem } from '../useExplorerOperatingSystem';
|
||||
import { useExplorerWindow } from '../useExplorerWindow';
|
||||
import { useExplorerSearchParams } from '../util';
|
||||
import { ColumnsView } from './ColumnsView';
|
||||
import { ViewContext, type ExplorerViewContext } from './Context';
|
||||
import { DragScrollable } from './DragScrollable';
|
||||
import { GridView } from './GridView';
|
||||
|
@ -34,9 +37,18 @@ import { useViewItemDoubleClick } from './ViewItem';
|
|||
export interface ExplorerViewProps
|
||||
extends Omit<ExplorerViewContext, 'selectable' | 'ref' | 'padding'> {
|
||||
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 explorer = useExplorerContext();
|
||||
|
@ -86,6 +98,8 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
|||
})
|
||||
});
|
||||
|
||||
const selected = useSelectedItems(items.items);
|
||||
|
||||
useShortcuts();
|
||||
|
||||
useShortcut('explorerEscape', () => {
|
||||
|
@ -142,10 +156,11 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
|||
if (!explorer.layouts[layoutMode]) return null;
|
||||
|
||||
return (
|
||||
<ViewContext.Provider value={{ ref, ...contextProps, selectable }}>
|
||||
<ViewContext.Provider value={{ ref, ...contextProps, selectable, ...items, ...selected }}>
|
||||
<div
|
||||
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) => {
|
||||
if (e.button !== 0) return;
|
||||
|
||||
|
@ -165,6 +180,7 @@ export const View = ({ emptyNotice, ...contextProps }: ExplorerViewProps) => {
|
|||
{layoutMode === 'grid' && <GridView />}
|
||||
{layoutMode === 'list' && <ListView />}
|
||||
{layoutMode === 'media' && <MediaView />}
|
||||
{layoutMode === 'columns' && <ColumnsView />}
|
||||
{showLoading && (
|
||||
<Loader className="fixed bottom-10 left-0 w-[calc(100%+180px)]" />
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
import { CSSProperties, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import { CSSProperties, Suspense, type PropsWithChildren, type ReactNode } from 'react';
|
||||
import {
|
||||
explorerLayout,
|
||||
useExplorerLayoutStore,
|
||||
|
@ -10,7 +9,6 @@ import { useShortcut } from '~/hooks';
|
|||
|
||||
import { useTopBarContext } from '../TopBar/Context';
|
||||
import { useExplorerContext } from './Context';
|
||||
import ContextMenu from './ContextMenu';
|
||||
import DismissibleNotice from './DismissibleNotice';
|
||||
import { ExplorerPath, PATH_BAR_HEIGHT } from './ExplorerPath';
|
||||
import { Inspector, INSPECTOR_WIDTH } from './Inspector';
|
||||
|
@ -19,11 +17,15 @@ import { getQuickPreviewStore } from './QuickPreview/store';
|
|||
import { explorerStore } from './store';
|
||||
import { useKeyRevealFinder } from './useKeyRevealFinder';
|
||||
import { ExplorerViewProps, View } from './View';
|
||||
import { EmptyNotice } from './View/EmptyNotice';
|
||||
|
||||
import 'react-slidedown/lib/slidedown.css';
|
||||
|
||||
import { FolderNotchOpen } from '@phosphor-icons/react';
|
||||
|
||||
import ContextMenu from './ContextMenu';
|
||||
import { useExplorerDnd } from './useExplorerDnd';
|
||||
import { useExplorerSearchParams } from './util';
|
||||
import { EmptyNotice } from './View/EmptyNotice';
|
||||
|
||||
interface Props {
|
||||
emptyNotice?: ExplorerViewProps['emptyNotice'];
|
||||
|
@ -39,6 +41,8 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
|||
const layoutStore = useExplorerLayoutStore();
|
||||
const showInspector = useSelector(explorerStore, (s) => s.showInspector);
|
||||
|
||||
const [{ path }] = useExplorerSearchParams();
|
||||
|
||||
const showPathBar = explorer.showPathBar && layoutStore.showPathBar;
|
||||
|
||||
// Can we put this somewhere else -_-
|
||||
|
@ -76,39 +80,53 @@ export default function Explorer(props: PropsWithChildren<Props>) {
|
|||
|
||||
const topBar = useTopBarContext();
|
||||
|
||||
const paths = [undefined, ...(path?.split('/').filter(Boolean) ?? [])];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerContextMenu>
|
||||
<div
|
||||
ref={explorer.scrollRef}
|
||||
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 />}
|
||||
|
||||
<View
|
||||
contextMenu={props.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
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Suspense fallback={<SuspanceFb />}>
|
||||
{paths.map((path, i) => {
|
||||
const p = !path ? undefined : paths.slice(0, i + 1).join('/') + '/';
|
||||
console.log('path', path, p);
|
||||
return (
|
||||
<View
|
||||
key={path}
|
||||
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
|
||||
}
|
||||
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>
|
||||
</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>;
|
||||
};
|
||||
|
|
|
@ -77,6 +77,7 @@ export function useExplorer<TOrder extends Ordering>({
|
|||
grid: true,
|
||||
list: true,
|
||||
media: true,
|
||||
columns: true,
|
||||
...layouts
|
||||
},
|
||||
...settings,
|
||||
|
@ -140,7 +141,7 @@ export type UseExplorerSettings<TOrder extends Ordering, T> = ReturnType<
|
|||
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
|
||||
// WeakMap ensures that ExplorerItems aren't held onto after they're evicted from cache
|
||||
const itemHashesWeakMap = useRef(new WeakMap<ExplorerItem, string>());
|
||||
|
|
67
interface/app/$libraryId/Explorer/useExplorerWindow.tsx
Normal file
67
interface/app/$libraryId/Explorer/useExplorerWindow.tsx
Normal 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;
|
||||
}
|
|
@ -81,6 +81,8 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
|||
[defaultFilters, search.filters]
|
||||
);
|
||||
|
||||
//
|
||||
|
||||
const items = useSearchExplorerQuery({
|
||||
search,
|
||||
explorerSettings,
|
||||
|
@ -130,6 +132,8 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
|||
|
||||
useKeyDeleteFile(explorer.selectedItems, location.id);
|
||||
|
||||
//
|
||||
|
||||
useShortcut('rescan', () => rescan(location.id));
|
||||
|
||||
const title = useRouteTitle(
|
||||
|
@ -175,6 +179,7 @@ const LocationExplorer = ({ location }: { location: Location; path?: string }) =
|
|||
)}
|
||||
</TopBarPortal>
|
||||
</SearchContextProvider>
|
||||
|
||||
{isLocationIndexing ? (
|
||||
<div className="flex size-full items-center justify-center">
|
||||
<Loader />
|
||||
|
@ -223,7 +228,7 @@ function getLastSectionOfPath(path: string): string | undefined {
|
|||
return lastSection;
|
||||
}
|
||||
|
||||
function useLocationExplorerSettings(location: Location) {
|
||||
export function useLocationExplorerSettings(location: Location) {
|
||||
const rspc = useRspcLibraryContext();
|
||||
|
||||
const preferences = useLibraryQuery(['preferences.get']);
|
||||
|
|
|
@ -67,5 +67,4 @@
|
|||
"eslintConfig": {
|
||||
"root": true
|
||||
},
|
||||
"packageManager": "pnpm@8.15.6+sha256.01c01eeb990e379b31ef19c03e9d06a14afa5250b82e81303f88721c99ff2e6f"
|
||||
}
|
||||
"packageManager": "pnpm@9.0.4"}
|
||||
|
|
|
@ -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 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 }
|
||||
|
||||
|
|
|
@ -57,7 +57,8 @@ export function usePathsOffsetInfiniteQuery({
|
|||
if (nodes.length >= arg.take) return (offset ?? 0) + 1;
|
||||
},
|
||||
onSuccess,
|
||||
...args
|
||||
...args,
|
||||
suspense: true
|
||||
});
|
||||
|
||||
const nodes = useMemo(
|
||||
|
|
Loading…
Reference in a new issue