refactor(view):调整无限列表功能所使用的库。

This commit is contained in:
徐涛 2023-03-09 11:11:12 +08:00
parent db70eb25d9
commit b17b3d308e
3 changed files with 71 additions and 65 deletions

View File

@ -21,7 +21,6 @@
"@mantine/notifications": "^6.0.0", "@mantine/notifications": "^6.0.0",
"@mantine/nprogress": "^6.0.0", "@mantine/nprogress": "^6.0.0",
"@tabler/icons-react": "^2.8.0", "@tabler/icons-react": "^2.8.0",
"@tanstack/react-virtual": "3.0.0-beta.35",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"events": "^3.3.0", "events": "^3.3.0",
"framer-motion": "^8.1.1", "framer-motion": "^8.1.1",
@ -30,6 +29,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-use": "^17.4.0", "react-use": "^17.4.0",
"react-window": "^1.8.8",
"use-immer": "^0.8.1", "use-immer": "^0.8.1",
"zustand": "^4.2.0" "zustand": "^4.2.0"
}, },
@ -41,6 +41,7 @@
"@types/ramda": "^0.28.20", "@types/ramda": "^0.28.20",
"@types/react": "^18.0.15", "@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/react-window": "^1.8.5",
"@vitejs/plugin-react": "^3.0.0", "@vitejs/plugin-react": "^3.0.0",
"typescript": "^4.6.4", "typescript": "^4.6.4",
"vite": "^4.0.0" "vite": "^4.0.0"

42
pnpm-lock.yaml generated
View File

@ -12,7 +12,6 @@ specifiers:
'@mantine/notifications': ^6.0.0 '@mantine/notifications': ^6.0.0
'@mantine/nprogress': ^6.0.0 '@mantine/nprogress': ^6.0.0
'@tabler/icons-react': ^2.8.0 '@tabler/icons-react': ^2.8.0
'@tanstack/react-virtual': 3.0.0-beta.35
'@tauri-apps/api': ^1.2.0 '@tauri-apps/api': ^1.2.0
'@tauri-apps/cli': ^1.2.2 '@tauri-apps/cli': ^1.2.2
'@types/events': ^3.0.0 '@types/events': ^3.0.0
@ -20,6 +19,7 @@ specifiers:
'@types/ramda': ^0.28.20 '@types/ramda': ^0.28.20
'@types/react': ^18.0.15 '@types/react': ^18.0.15
'@types/react-dom': ^18.0.6 '@types/react-dom': ^18.0.6
'@types/react-window': ^1.8.5
'@vitejs/plugin-react': ^3.0.0 '@vitejs/plugin-react': ^3.0.0
events: ^3.3.0 events: ^3.3.0
framer-motion: ^8.1.1 framer-motion: ^8.1.1
@ -28,6 +28,7 @@ specifiers:
react: ^18.2.0 react: ^18.2.0
react-dom: ^18.2.0 react-dom: ^18.2.0
react-use: ^17.4.0 react-use: ^17.4.0
react-window: ^1.8.8
typescript: ^4.6.4 typescript: ^4.6.4
use-immer: ^0.8.1 use-immer: ^0.8.1
vite: ^4.0.0 vite: ^4.0.0
@ -44,7 +45,6 @@ dependencies:
'@mantine/notifications': 6.0.0_uziugpv5zwkk3pqsn64qbjkxrm '@mantine/notifications': 6.0.0_uziugpv5zwkk3pqsn64qbjkxrm
'@mantine/nprogress': 6.0.0_uziugpv5zwkk3pqsn64qbjkxrm '@mantine/nprogress': 6.0.0_uziugpv5zwkk3pqsn64qbjkxrm
'@tabler/icons-react': 2.8.0_react@18.2.0 '@tabler/icons-react': 2.8.0_react@18.2.0
'@tanstack/react-virtual': 3.0.0-beta.35_react@18.2.0
'@tauri-apps/api': 1.2.0 '@tauri-apps/api': 1.2.0
events: 3.3.0 events: 3.3.0
framer-motion: 8.5.5_biqbaboplfbrettd7655fr4n2y framer-motion: 8.5.5_biqbaboplfbrettd7655fr4n2y
@ -53,6 +53,7 @@ dependencies:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y react-use: 17.4.0_biqbaboplfbrettd7655fr4n2y
react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y
use-immer: 0.8.1_immer@9.0.19+react@18.2.0 use-immer: 0.8.1_immer@9.0.19+react@18.2.0
zustand: 4.3.6_immer@9.0.19+react@18.2.0 zustand: 4.3.6_immer@9.0.19+react@18.2.0
@ -64,6 +65,7 @@ devDependencies:
'@types/ramda': 0.28.23 '@types/ramda': 0.28.23
'@types/react': 18.0.28 '@types/react': 18.0.28
'@types/react-dom': 18.0.11 '@types/react-dom': 18.0.11
'@types/react-window': 1.8.5
'@vitejs/plugin-react': 3.1.0_vite@4.1.4 '@vitejs/plugin-react': 3.1.0_vite@4.1.4
typescript: 4.9.5 typescript: 4.9.5
vite: 4.1.4_@types+node@18.14.6 vite: 4.1.4_@types+node@18.14.6
@ -988,19 +990,6 @@ packages:
resolution: {integrity: sha512-8diABuB3J+NEUtdwIXJF0bJSE5VpnnyzWeiZGFq/XlcTYJNEF+36ijiPNUGpsV/QXY6syesJKmmPkUUwD+LxdA==} resolution: {integrity: sha512-8diABuB3J+NEUtdwIXJF0bJSE5VpnnyzWeiZGFq/XlcTYJNEF+36ijiPNUGpsV/QXY6syesJKmmPkUUwD+LxdA==}
dev: false dev: false
/@tanstack/react-virtual/3.0.0-beta.35_react@18.2.0:
resolution: {integrity: sha512-x4dicQTGao3p7wA1bGJyKsbtabO6UJKNUJdMWbxzrdc9CvpFl+ASOBtQHReDRjjAy7Z5/4loTLle8RtZu+2c2A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@tanstack/virtual-core': 3.0.0-beta.35
react: 18.2.0
dev: false
/@tanstack/virtual-core/3.0.0-beta.35:
resolution: {integrity: sha512-p+dNBkN70nz3RzsfJImpmg1eoEHoX3X49lOk1N4I6jtUttKzZ/0U2+LqDmErNa5q/ksWD45zegOU4MnHwKRp2w==}
dev: false
/@tauri-apps/api/1.2.0: /@tauri-apps/api/1.2.0:
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==} resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
@ -1134,6 +1123,12 @@ packages:
'@types/react': 18.0.28 '@types/react': 18.0.28
dev: true dev: true
/@types/react-window/1.8.5:
resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==}
dependencies:
'@types/react': 18.0.28
dev: true
/@types/react/18.0.28: /@types/react/18.0.28:
resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==} resolution: {integrity: sha512-RD0ivG1kEztNBdoAK7lekI9M+azSnitIn85h4iOiaLjaTrMjzslhaqCGaI4IyCJ1RljWiLCEu4jyrLLgqxBTew==}
dependencies: dependencies:
@ -1531,6 +1526,10 @@ packages:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
dev: false dev: false
/memoize-one/5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
/ms/2.1.2: /ms/2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
dev: true dev: true
@ -1751,6 +1750,19 @@ packages:
tslib: 2.5.0 tslib: 2.5.0
dev: false dev: false
/react-window/1.8.8_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==}
engines: {node: '>8.0.0'}
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.21.0
memoize-one: 5.2.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0: /react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}

View File

@ -1,13 +1,11 @@
import { Box, Stack } from '@mantine/core';
import { useVirtualizer } from '@tanstack/react-virtual';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { filter, indexOf, isEmpty, length, map, pluck } from 'ramda'; import { indexOf, isEmpty, length, map, mergeLeft, pluck, range } from 'ramda';
import { FC, useContext, useLayoutEffect, useMemo, useRef } from 'react'; import { FC, useCallback, useContext, useMemo, useRef } from 'react';
import { useLifecycles } from 'react-use'; import { useLifecycles } from 'react-use';
import { VariableSizeList } from 'react-window';
import { EventBusContext } from '../EventBus'; import { EventBusContext } from '../EventBus';
import { useFileListStore } from '../states/files'; import { useFileListStore } from '../states/files';
import { useZoomState } from '../states/zoom'; import { useZoomState } from '../states/zoom';
import { withinRange } from '../utils/offset_func';
export const ContinuationView: FC = () => { export const ContinuationView: FC = () => {
const { files } = useFileListStore(); const { files } = useFileListStore();
@ -15,23 +13,24 @@ export const ContinuationView: FC = () => {
const viewHeight = useZoomState.use.viewHeight(); const viewHeight = useZoomState.use.viewHeight();
const updateActives = useFileListStore.use.updateActiveFiles(); const updateActives = useFileListStore.use.updateActiveFiles();
const fileCount = useMemo(() => length(files), [files]); const fileCount = useMemo(() => length(files), [files]);
const parentRef = useRef();
const ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
const virtualizer = useVirtualizer({ const virtualListRef = useRef<VariableSizeList | null>();
count: fileCount, const handleOnRenderAction = useCallback(
getScrollElement: () => parentRef.current, ({ visibleStartIndex, visibleStopIndex }) => {
estimateSize: () => 100 console.log('[debug]on render:', visibleStartIndex, visibleStopIndex);
}); updateActives(map(i => files[i].filename, range(visibleStartIndex, visibleStopIndex + 1)));
const items = virtualizer.getVirtualItems(); },
[files]
);
useLifecycles( useLifecycles(
() => { () => {
ebus?.addListener('navigate_offset', ({ filename }) => { ebus?.addListener('navigate_offset', ({ filename }) => {
let index = indexOf(filename, pluck('filename', files)); let index = indexOf(filename, pluck('filename', files));
virtualizer.scrollToIndex(index); virtualListRef.current?.scrollToItem(index);
}); });
ebus?.addListener('reset_views', () => { ebus?.addListener('reset_views', () => {
virtualizer.scrollToOffset(0); virtualListRef.current?.scrollTo(0);
}); });
}, },
() => { () => {
@ -40,43 +39,37 @@ export const ContinuationView: FC = () => {
} }
); );
useLayoutEffect(() => {
let rangeStart = virtualizer.scrollOffset;
let rangeEnd = virtualizer.scrollOffset + viewHeight;
let onShowItems = pluck(
'index',
filter(item => withinRange(item.start, item.end, rangeStart, rangeEnd), items)
);
updateActives(map(i => files[i].filename, onShowItems));
}, [virtualizer.scrollOffset, viewHeight, items]);
return ( return (
<div style={{ overflow: 'auto', contain: 'strict', height: '100%' }} ref={parentRef}> <div
style={{
overflow: 'auto',
contain: 'strict',
height: '100%'
}}
>
{!isEmpty(files) && ( {!isEmpty(files) && (
<Box pos="relative" w="100%" h={virtualizer.getTotalSize()}> <VariableSizeList
<Stack itemData={files}
pos="absolute" itemCount={fileCount}
top={0} itemSize={index => files[index].height * (zoom / 100)}
left={0} height={viewHeight}
w="100%" width="100%"
justify="start" ref={virtualListRef}
align="center" onItemsRendered={handleOnRenderAction}
spacing={0} >
style={{ {({ index, style, data }) => (
transform: `translateY(${items[0].start}px)` <div
}} style={mergeLeft(style, {
> display: 'flex',
{items.map(row => ( flexDirection: 'row',
<img justifyContent: 'center',
key={files[row.index].filename} alignItems: 'flex-start'
src={files[row.index].path} })}
ref={virtualizer.measureElement} >
data-index={row.index} <img src={data[index].path} style={{ width: data[index].width * (zoom / 100) }} />
style={{ width: `${zoom}%` }} </div>
/> )}
))} </VariableSizeList>
</Stack>
</Box>
)} )}
</div> </div>
); );