mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-05 13:52:56 -04:00
replace ListView with VirtualizedList
This commit is contained in:
@@ -18,11 +18,11 @@ import {
|
||||
} from 'phosphor-react';
|
||||
import React, { memo, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { FileList } from '../explorer/FileList';
|
||||
import { Inspector } from '../explorer/Inspector';
|
||||
import { WithContextMenu } from '../layout/MenuOverlay';
|
||||
import { TopBar } from '../layout/TopBar';
|
||||
import ExplorerContextMenu from './ExplorerContextMenu';
|
||||
import { VirtualizedList } from './VirtualizedList';
|
||||
|
||||
interface Props {
|
||||
data: ExplorerData;
|
||||
@@ -39,37 +39,31 @@ export default function Explorer(props: Props) {
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<ExplorerContextMenu>
|
||||
<ExplorerContent data={props.data} />
|
||||
</ExplorerContextMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ExplorerContent = (props: Props) => {
|
||||
const { selectedRowIndex, showInspector } = useExplorerStore((store) => ({
|
||||
selectedRowIndex: store.selectedRowIndex,
|
||||
showInspector: store.showInspector
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full bg-gray-650">
|
||||
<TopBar />
|
||||
<div className="relative flex flex-row w-full max-h-full">
|
||||
<FileList data={props.data?.items || []} context={props.data.context} />
|
||||
{showInspector && (
|
||||
<div className="min-w-[260px] max-w-[260px]">
|
||||
{props.data.items[selectedRowIndex]?.id && (
|
||||
<Inspector
|
||||
key={props.data.items[selectedRowIndex].id}
|
||||
data={props.data.items[selectedRowIndex]}
|
||||
/>
|
||||
<div className="relative">
|
||||
<ExplorerContextMenu>
|
||||
<div className="relative flex flex-col w-full bg-gray-650">
|
||||
<TopBar />
|
||||
<div className="relative flex flex-row w-full max-h-full">
|
||||
<VirtualizedList data={props.data?.items || []} context={props.data.context} />
|
||||
{showInspector && (
|
||||
<div className="min-w-[260px] max-w-[260px]">
|
||||
{props.data.items[selectedRowIndex]?.id && (
|
||||
<Inspector
|
||||
key={props.data.items[selectedRowIndex].id}
|
||||
data={props.data.items[selectedRowIndex]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ExplorerContextMenu>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExplorerLayoutMode, useExplorerStore } from '@sd/client';
|
||||
import { ExplorerContext, ExplorerItem, FilePath } from '@sd/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import React, { memo, useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useKey, useWindowSize } from 'rooks';
|
||||
|
||||
@@ -16,17 +16,47 @@ interface Props {
|
||||
data: ExplorerItem[];
|
||||
}
|
||||
|
||||
export const FileList: React.FC<Props> = (props) => {
|
||||
// const size = useWindowSize();
|
||||
const [goingUp, setGoingUp] = useState(false);
|
||||
export const VirtualizedList: React.FC<Props> = ({ data, context }) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { selectedRowIndex, layoutMode } = useExplorerStore((state) => ({
|
||||
selectedRowIndex: state.selectedRowIndex,
|
||||
layoutMode: state.layoutMode
|
||||
}));
|
||||
const [goingUp, setGoingUp] = useState(false);
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
const { gridItemSize, layoutMode, listItemSize, selectedRowIndex } = useExplorerStore(
|
||||
(state) => ({
|
||||
selectedRowIndex: state.selectedRowIndex,
|
||||
gridItemSize: state.gridItemSize,
|
||||
layoutMode: state.layoutMode,
|
||||
listItemSize: state.listItemSize
|
||||
})
|
||||
);
|
||||
|
||||
const set = useExplorerStore.getState().set;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(innerRef.current?.offsetWidth || 0);
|
||||
}, []);
|
||||
|
||||
const amountOfColumns = Math.floor(width / gridItemSize) || 8,
|
||||
amountOfRows = layoutMode === 'grid' ? Math.ceil(data.length / amountOfColumns) : data.length,
|
||||
itemSize = layoutMode === 'grid' ? gridItemSize + 25 : listItemSize;
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: amountOfRows,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
overscan: 500,
|
||||
estimateSize: () => itemSize,
|
||||
measureElement: (index) => itemSize
|
||||
});
|
||||
|
||||
// useEffect(() => {
|
||||
// if (selectedRowIndex === 0 && goingUp) rowVirtualizer.scrollToIndex(0);
|
||||
|
||||
// if (selectedRowIndex !== -1)
|
||||
// rowVirtualizer.scrollToIndex(goingUp ? selectedRowIndex - 1 : selectedRowIndex);
|
||||
// }, [goingUp, selectedRowIndex, rowVirtualizer]);
|
||||
|
||||
useKey('ArrowUp', (e) => {
|
||||
e.preventDefault();
|
||||
setGoingUp(true);
|
||||
@@ -37,7 +67,7 @@ export const FileList: React.FC<Props> = (props) => {
|
||||
useKey('ArrowDown', (e) => {
|
||||
e.preventDefault();
|
||||
setGoingUp(false);
|
||||
if (selectedRowIndex !== -1 && selectedRowIndex !== (props.data.length ?? 1) - 1)
|
||||
if (selectedRowIndex !== -1 && selectedRowIndex !== (data.length ?? 1) - 1)
|
||||
set({ selectedRowIndex: selectedRowIndex + 1 });
|
||||
});
|
||||
|
||||
@@ -65,94 +95,58 @@ export const FileList: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: -TOP_BAR_HEIGHT }} className="w-full pl-2 cursor-default">
|
||||
<Virtualizer items={props.data} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function Virtualizer({ items }: { items: ExplorerItem[] }) {
|
||||
const parentRef = useRef<HTMLDivElement>(null);
|
||||
const innerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { gridItemSize, layoutMode, listItemSize, selectedRowIndex } = useExplorerStore(
|
||||
(state) => ({
|
||||
selectedRowIndex: state.selectedRowIndex,
|
||||
gridItemSize: state.gridItemSize,
|
||||
layoutMode: state.layoutMode,
|
||||
listItemSize: state.listItemSize
|
||||
})
|
||||
);
|
||||
|
||||
const [width, setWidth] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setWidth(innerRef.current?.offsetWidth || 0);
|
||||
}, []);
|
||||
|
||||
const amountOfColumns = Math.floor(width / gridItemSize) || 8,
|
||||
amountOfRows = layoutMode === 'grid' ? Math.ceil(items.length / amountOfColumns) : items.length,
|
||||
itemSize = layoutMode === 'grid' ? gridItemSize + 25 : listItemSize;
|
||||
|
||||
const rowVirtualizer = useVirtualizer({
|
||||
count: amountOfRows,
|
||||
getScrollElement: () => parentRef.current,
|
||||
overscan: 500,
|
||||
estimateSize: () => itemSize,
|
||||
measureElement: (index) => itemSize
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="h-screen custom-scroll explorer-scroll">
|
||||
<div
|
||||
ref={innerRef}
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
marginTop: `${TOP_BAR_HEIGHT}px`
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
className="absolute top-0 left-0 flex w-full"
|
||||
key={virtualRow.key}
|
||||
>
|
||||
{layoutMode === 'list' ? (
|
||||
<WrappedItem
|
||||
kind="list"
|
||||
isSelected={selectedRowIndex === virtualRow.index}
|
||||
index={virtualRow.index}
|
||||
item={items[virtualRow.index]}
|
||||
/>
|
||||
) : (
|
||||
[...Array(amountOfColumns)].map((_, i) => {
|
||||
const index = virtualRow.index * amountOfColumns + i;
|
||||
const item = items[index];
|
||||
return (
|
||||
<div key={index} className="w-32 h-32">
|
||||
<div className="flex">
|
||||
{item && (
|
||||
<WrappedItem
|
||||
kind="grid"
|
||||
isSelected={selectedRowIndex === index}
|
||||
index={index}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
<div ref={scrollRef} className="h-screen custom-scroll explorer-scroll">
|
||||
<div
|
||||
ref={innerRef}
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
marginTop: `${TOP_BAR_HEIGHT}px`
|
||||
}}
|
||||
className="relative w-full"
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
|
||||
<div
|
||||
style={{
|
||||
height: `${virtualRow.size}px`,
|
||||
transform: `translateY(${virtualRow.start}px)`
|
||||
}}
|
||||
className="absolute top-0 left-0 flex w-full"
|
||||
key={virtualRow.key}
|
||||
>
|
||||
{layoutMode === 'list' ? (
|
||||
<WrappedItem
|
||||
kind="list"
|
||||
isSelected={selectedRowIndex === virtualRow.index}
|
||||
index={virtualRow.index}
|
||||
item={data[virtualRow.index]}
|
||||
/>
|
||||
) : (
|
||||
[...Array(amountOfColumns)].map((_, i) => {
|
||||
const index = virtualRow.index * amountOfColumns + i;
|
||||
const item = data[index];
|
||||
return (
|
||||
<div key={index} className="w-32 h-32">
|
||||
<div className="flex">
|
||||
{item && (
|
||||
<WrappedItem
|
||||
kind="grid"
|
||||
isSelected={selectedRowIndex === index}
|
||||
index={index}
|
||||
item={item}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
interface WrappedItemProps {
|
||||
item: ExplorerItem;
|
||||
@@ -176,23 +170,22 @@ const WrappedItem: React.FC<WrappedItemProps> = memo(({ item, index, isSelected,
|
||||
if (kind === 'list') {
|
||||
return (
|
||||
<FileRow
|
||||
selected={isSelected}
|
||||
data={item}
|
||||
index={index}
|
||||
onClick={onClick}
|
||||
onDoubleClick={onDoubleClick}
|
||||
index={index}
|
||||
selected={isSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileItem
|
||||
onDoubleClick={onDoubleClick}
|
||||
index={index}
|
||||
data={item}
|
||||
selected={isSelected}
|
||||
index={index}
|
||||
onClick={onClick}
|
||||
// size={100}
|
||||
onDoubleClick={onDoubleClick}
|
||||
selected={isSelected}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LockClosedIcon, PhotoIcon } from '@heroicons/react/24/outline';
|
||||
import { CogIcon, PlusIcon } from '@heroicons/react/24/solid';
|
||||
import { CogIcon, LockClosedIcon, PhotoIcon } from '@heroicons/react/24/outline';
|
||||
import { PlusIcon } from '@heroicons/react/24/solid';
|
||||
import {
|
||||
AppPropsContext,
|
||||
useCurrentLibrary,
|
||||
|
||||
Reference in New Issue
Block a user