Compare commits

..

No commits in common. "3e6b89ab394456105be38ec2498dab8f06db629f" and "39accb3cb701babe864837bec6f42fe52df425e3" have entirely different histories.

8 changed files with 64 additions and 74 deletions

View File

@ -29,7 +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-virtuoso": "^4.1.0", "react-window": "^1.8.8",
"use-immer": "^0.8.1", "use-immer": "^0.8.1",
"zustand": "^4.2.0" "zustand": "^4.2.0"
}, },

20
pnpm-lock.yaml generated
View File

@ -28,7 +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-virtuoso: ^4.1.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
@ -53,7 +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-virtuoso: 4.1.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
@ -1526,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
@ -1746,13 +1750,15 @@ packages:
tslib: 2.5.0 tslib: 2.5.0
dev: false dev: false
/react-virtuoso/4.1.0_biqbaboplfbrettd7655fr4n2y: /react-window/1.8.8_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-Vcq5WXn18PvPT55kdeGQ8BN3K95XyPe7hum8zG6Tx7g1CtUYVsQKN7fouMxBSy+XymEDB5ynGy8JWhuqyLLtPw==} resolution: {integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==}
engines: {node: '>=10'} engines: {node: '>8.0.0'}
peerDependencies: peerDependencies:
react: '>=16 || >=17 || >= 18' react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: '>=16 || >=17 || >= 18' react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
'@babel/runtime': 7.21.0
memoize-one: 5.2.1
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
dev: false dev: false

View File

@ -146,7 +146,7 @@ pub async fn scan_for_child_dirs<R: Runtime>(
for entry in std::fs::read_dir(target).map_err(|e| format!("无法读取指定文件夹,{}", e))? for entry in std::fs::read_dir(target).map_err(|e| format!("无法读取指定文件夹,{}", e))?
{ {
let entry = entry.map_err(|e| format!("无法获取指定文件夹信息,{}", e))?; let entry = entry.map_err(|e| format!("无法获取指定文件夹信息,{}", e))?;
if is_hidden(&entry) || is_root(&entry) || entry.path().is_file() { if is_hidden(&entry) || is_root(&entry) {
continue; continue;
} }
let dir_hash_id = entry let dir_hash_id = entry

View File

@ -13,16 +13,12 @@ export const ComicView: FC = () => {
const files = useFileListStore(sortedFilesSelector()); const files = useFileListStore(sortedFilesSelector());
const viewMode = useZoomState.use.viewMode(); const viewMode = useZoomState.use.viewMode();
const updateViewHeight = useZoomState.use.updateViewHeight(); const updateViewHeight = useZoomState.use.updateViewHeight();
const updateViewWidth = useZoomState.use.updateViewWidth(); const [containerRef, { height }] = useMeasure();
const [containerRef, { height, width }] = useMeasure();
const firstFileId = useMemo(() => head(files)?.id ?? '', [files, files.length]); const firstFileId = useMemo(() => head(files)?.id ?? '', [files, files.length]);
useLayoutEffect(() => { useLayoutEffect(() => {
updateViewHeight(height); updateViewHeight(height);
}, [height]); }, [height]);
useLayoutEffect(() => {
updateViewWidth(width);
}, [width]);
return ( return (
<Box w="100%" h="100%" sx={{ overflow: 'hidden' }} ref={containerRef}> <Box w="100%" h="100%" sx={{ overflow: 'hidden' }} ref={containerRef}>

View File

@ -1,34 +1,35 @@
//@ts-nocheck //@ts-nocheck
import EventEmitter from 'events'; import EventEmitter from 'events';
import { indexOf, isEmpty, length, map, pluck, range } from 'ramda'; import { indexOf, isEmpty, length, map, mergeLeft, pluck, range } from 'ramda';
import { FC, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { FC, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { VariableSizeList } from 'react-window';
import { EventBusContext } from '../EventBus'; import { EventBusContext } from '../EventBus';
import { sortedFilesSelector, useFileListStore } from '../states/files'; import { sortedFilesSelector, useFileListStore } from '../states/files';
import { useZoomState } from '../states/zoom'; import { useZoomState } from '../states/zoom';
export const ContinuationView: FC = () => { export const ContinuationView: FC = () => {
const files = useFileListStore(sortedFilesSelector()); const files = useFileListStore(sortedFilesSelector());
const { currentZoom: zoom, viewWidth, viewHeight } = useZoomState(); const zoom = useZoomState.use.currentZoom();
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 ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
const virtualListRef = useRef<VirtuosoHandle | null>(null); const virtualListRef = useRef<VariableSizeList | null>();
const handleOnRenderAction = useCallback( const handleOnRenderAction = useCallback(
({ startIndex, endIndex }: ListRange) => { ({ visibleStartIndex, visibleStopIndex }) => {
updateActives(map(i => files[i].filename, range(startIndex, endIndex + 1))); updateActives(map(i => files[i].filename, range(visibleStartIndex, visibleStopIndex + 1)));
}, },
[files] [files]
); );
const maxImageWidth = useMemo(() => viewWidth * (zoom / 100), [viewWidth, zoom]); const fileHeights = useMemo(() => map(item => item.height * (zoom / 100), files), [files, zoom]);
useEffect(() => { useEffect(() => {
ebus?.addListener('navigate_offset', ({ filename }) => { ebus?.addListener('navigate_offset', ({ filename }) => {
let index = indexOf(filename, pluck('filename', files)); let index = indexOf(filename, pluck('filename', files));
virtualListRef.current?.scrollToIndex({ index, align: 'start', behavior: 'smooth' }); virtualListRef.current?.scrollToItem(index);
}); });
ebus?.addListener('reset_views', () => { ebus?.addListener('reset_views', () => {
virtualListRef.current?.scrollTo({ top: 0 }); virtualListRef.current?.scrollTo(0);
}); });
return () => { return () => {
ebus?.removeAllListeners('navigate_offset'); ebus?.removeAllListeners('navigate_offset');
@ -45,27 +46,29 @@ export const ContinuationView: FC = () => {
}} }}
> >
{!isEmpty(files) && ( {!isEmpty(files) && (
<Virtuoso <VariableSizeList
style={{ height: viewHeight }} itemData={files}
itemCount={fileCount}
itemSize={index => fileHeights[index]}
itemKey={index => files[index].id}
height={viewHeight}
width="100%"
ref={virtualListRef} ref={virtualListRef}
totalCount={fileCount} onItemsRendered={handleOnRenderAction}
computeItemKey={index => files[index].id} >
rangeChanged={handleOnRenderAction} {({ index, style, data }) => (
itemContent={index => (
<div <div
style={{ style={mergeLeft(style, {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'flex-start', alignItems: 'flex-start'
width: '100%', })}
height: files[index].height * (maxImageWidth / files[index].width)
}}
> >
<img src={files[index].path} style={{ width: maxImageWidth }} /> <img src={data[index].path} style={{ width: data[index].width * (zoom / 100) }} />
</div> </div>
)} )}
/> </VariableSizeList>
)} )}
</div> </div>
); );

View File

@ -1,10 +1,10 @@
//@ts-nocheck //@ts-nocheck
import { Box, Center, Text, Tooltip } from '@mantine/core'; import { Box, Center, Text } from '@mantine/core';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { head, includes, indexOf, isEmpty, length, not, pluck } from 'ramda'; import { head, includes, indexOf, isEmpty, length, mergeLeft, not, pluck } from 'ramda';
import { FC, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react'; import { FC, useCallback, useContext, useLayoutEffect, useMemo, useRef } from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; import { FixedSizeList } from 'react-window';
import { EventBusContext } from '../EventBus'; import { EventBusContext } from '../EventBus';
import { sortedFilesSelector, useFileListStore } from '../states/files'; import { sortedFilesSelector, useFileListStore } from '../states/files';
@ -14,7 +14,7 @@ export const FileList: FC = () => {
const ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
const filesCount = useMemo(() => length(files), [files]); const filesCount = useMemo(() => length(files), [files]);
const [parentRef, { height: parentHeight }] = useMeasure(); const [parentRef, { height: parentHeight }] = useMeasure();
const listRef = useRef<VirtuosoHandle | null>(null); const listRef = useRef<FixedSizeList | null>();
const handleFileSelectAction = useCallback( const handleFileSelectAction = useCallback(
(filename: string) => { (filename: string) => {
ebus.emit('navigate_offset', { filename }); ebus.emit('navigate_offset', { filename });
@ -25,11 +25,7 @@ export const FileList: FC = () => {
useLayoutEffect(() => { useLayoutEffect(() => {
let firstActived = head(activeFiles); let firstActived = head(activeFiles);
let firstActivedIndex = indexOf(firstActived, pluck('filename', files)); let firstActivedIndex = indexOf(firstActived, pluck('filename', files));
listRef.current?.scrollToIndex({ listRef.current?.scrollToItem(firstActivedIndex, 'smart');
index: firstActivedIndex,
align: 'center',
behavior: 'auto'
});
}, [activeFiles]); }, [activeFiles]);
return ( return (
@ -41,30 +37,27 @@ export const FileList: FC = () => {
ref={parentRef} ref={parentRef}
> >
{!isEmpty(files) && ( {!isEmpty(files) && (
<Virtuoso <FixedSizeList itemCount={filesCount} itemSize={35} height={parentHeight} ref={listRef}>
style={{ height: parentHeight }} {({ index, style }) => (
totalCount={filesCount}
ref={listRef}
itemContent={index => (
<Box <Box
bg={includes(files[index].filename, activeFiles) && 'grape'} bg={includes(files[index].filename, activeFiles) && 'grape'}
key={index} key={index}
px={4} px={4}
py={2} py={2}
onClick={() => handleFileSelectAction(files[index].filename)} onClick={() => handleFileSelectAction(files[index].filename)}
sx={theme => ({ sx={theme =>
cursor: 'pointer', mergeLeft(style, {
'&:hover': { cursor: 'pointer',
color: not(includes(files[index].filename, activeFiles)) && theme.colors.red '&:hover': {
} color: not(includes(files[index].filename, activeFiles)) && theme.colors.red
})} }
})
}
> >
<Tooltip label={files[index].filename} zIndex={999}> {files[index].filename}
<Text truncate>{files[index].filename}</Text>
</Tooltip>
</Box> </Box>
)} )}
/> </FixedSizeList>
)} )}
{isEmpty(files) && ( {isEmpty(files) && (
<Center h="100%"> <Center h="100%">

View File

@ -10,8 +10,8 @@ import { useZoomState } from '../states/zoom';
export const SingleView: FC = () => { export const SingleView: FC = () => {
const files = useFileListStore.use.files(); const files = useFileListStore.use.files();
const actives = useFileListStore.use.actives(); const actives = useFileListStore.use.actives();
const { currentZoom: zoom, viewHeight, viewWidth } = useZoomState(); const zoom = useZoomState.use.currentZoom();
const maxImageWidth = useMemo(() => viewWidth * (zoom / 100), [viewWidth, zoom]); const viewHeight = useZoomState.use.viewHeight();
const updateActives = useFileListStore.use.updateActiveFiles(); const updateActives = useFileListStore.use.updateActiveFiles();
const ebus = useContext<EventEmitter>(EventBusContext); const ebus = useContext<EventEmitter>(EventBusContext);
const [pageConRef, { width: pageConWidth }] = useMeasure(); const [pageConRef, { width: pageConWidth }] = useMeasure();
@ -21,9 +21,9 @@ export const SingleView: FC = () => {
}, [files, actives]); }, [files, actives]);
const largerThanView = useMemo(() => { const largerThanView = useMemo(() => {
if (isNil(activeFile)) return false; if (isNil(activeFile)) return false;
let imageHeightAfterZoom = activeFile?.height * (maxImageWidth / activeFile?.width); let imageHeightAfterZoom = activeFile?.height * (zoom / 100);
return gt(imageHeightAfterZoom, viewHeight); return gt(imageHeightAfterZoom, viewHeight);
}, [activeFile, viewHeight, maxImageWidth]); }, [activeFile, viewHeight, zoom]);
const handlePaginationAction = useCallback( const handlePaginationAction = useCallback(
(event: BaseSyntheticEvent) => { (event: BaseSyntheticEvent) => {
let middle = pageConWidth / 2; let middle = pageConWidth / 2;
@ -62,7 +62,7 @@ export const SingleView: FC = () => {
height: largerThanView ? '100%' : viewHeight height: largerThanView ? '100%' : viewHeight
}} }}
> >
<img src={activeFile.path} style={{ width: maxImageWidth }} /> <img src={activeFile.path} style={{ width: `${zoom}%` }} />
</div> </div>
)} )}
</div> </div>

View File

@ -6,13 +6,11 @@ interface ZoomState {
currentZoom: number; currentZoom: number;
viewMode: 'single' | 'continuation'; viewMode: 'single' | 'continuation';
viewHeight: number; viewHeight: number;
viewWidth: number;
} }
type ZoomActions = { type ZoomActions = {
zoom: SyncObjectCallback<number>; zoom: SyncObjectCallback<number>;
updateViewHeight: SyncObjectCallback<number>; updateViewHeight: SyncObjectCallback<number>;
updateViewWidth: SyncObjectCallback<number>;
switchViewMode: SyncObjectCallback<'single' | 'continuation'>; switchViewMode: SyncObjectCallback<'single' | 'continuation'>;
}; };
@ -20,8 +18,7 @@ const initialState: ZoomState = {
autoFit: false, autoFit: false,
currentZoom: 80, currentZoom: 80,
viewMode: 'continuation', viewMode: 'continuation',
viewHeight: 0, viewHeight: 0
viewWidth: 0
}; };
export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({ export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({
@ -36,11 +33,6 @@ export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({
df.viewHeight = height; df.viewHeight = height;
}); });
}, },
updateViewWidth(width) {
set(df => {
df.viewWidth = width;
});
},
switchViewMode(mode) { switchViewMode(mode) {
set(df => { set(df => {
df.viewMode = mode; df.viewMode = mode;