mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 10:03:28 +00:00
c
This commit is contained in:
parent
20b8a2b93b
commit
f97303ff54
|
@ -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)]
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = () => {
|
||||||
|
|
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';
|
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.
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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 }]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)]" />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>;
|
||||||
|
};
|
||||||
|
|
|
@ -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>());
|
||||||
|
|
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]
|
[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']);
|
||||||
|
|
|
@ -67,5 +67,4 @@
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true
|
"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 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 }
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue