Compare commits
4 Commits
39accb3cb7
...
3e6b89ab39
Author | SHA1 | Date | |
---|---|---|---|
|
3e6b89ab39 | ||
|
925b055318 | ||
|
0685e7f4e1 | ||
|
a80e8eb74f |
|
@ -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-window": "^1.8.8",
|
"react-virtuoso": "^4.1.0",
|
||||||
"use-immer": "^0.8.1",
|
"use-immer": "^0.8.1",
|
||||||
"zustand": "^4.2.0"
|
"zustand": "^4.2.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -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-window: ^1.8.8
|
react-virtuoso: ^4.1.0
|
||||||
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-window: 1.8.8_biqbaboplfbrettd7655fr4n2y
|
react-virtuoso: 4.1.0_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,10 +1526,6 @@ 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
|
||||||
|
@ -1750,15 +1746,13 @@ packages:
|
||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-window/1.8.8_biqbaboplfbrettd7655fr4n2y:
|
/react-virtuoso/4.1.0_biqbaboplfbrettd7655fr4n2y:
|
||||||
resolution: {integrity: sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ==}
|
resolution: {integrity: sha512-Vcq5WXn18PvPT55kdeGQ8BN3K95XyPe7hum8zG6Tx7g1CtUYVsQKN7fouMxBSy+XymEDB5ynGy8JWhuqyLLtPw==}
|
||||||
engines: {node: '>8.0.0'}
|
engines: {node: '>=10'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
react: '>=16 || >=17 || >= 18'
|
||||||
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0
|
react-dom: '>=16 || >=17 || >= 18'
|
||||||
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
|
||||||
|
|
|
@ -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) {
|
if is_hidden(&entry) || is_root(&entry) || entry.path().is_file() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let dir_hash_id = entry
|
let dir_hash_id = entry
|
||||||
|
|
|
@ -13,12 +13,16 @@ 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 [containerRef, { height }] = useMeasure();
|
const updateViewWidth = useZoomState.use.updateViewWidth();
|
||||||
|
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}>
|
||||||
|
|
|
@ -1,35 +1,34 @@
|
||||||
//@ts-nocheck
|
//@ts-nocheck
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { indexOf, isEmpty, length, map, mergeLeft, pluck, range } from 'ramda';
|
import { indexOf, isEmpty, length, map, pluck, range } from 'ramda';
|
||||||
import { FC, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
import { FC, useCallback, useContext, useEffect, useMemo, useRef } from 'react';
|
||||||
import { VariableSizeList } from 'react-window';
|
import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
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 zoom = useZoomState.use.currentZoom();
|
const { currentZoom: zoom, viewWidth, viewHeight } = useZoomState();
|
||||||
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<VariableSizeList | null>();
|
const virtualListRef = useRef<VirtuosoHandle | null>(null);
|
||||||
const handleOnRenderAction = useCallback(
|
const handleOnRenderAction = useCallback(
|
||||||
({ visibleStartIndex, visibleStopIndex }) => {
|
({ startIndex, endIndex }: ListRange) => {
|
||||||
updateActives(map(i => files[i].filename, range(visibleStartIndex, visibleStopIndex + 1)));
|
updateActives(map(i => files[i].filename, range(startIndex, endIndex + 1)));
|
||||||
},
|
},
|
||||||
[files]
|
[files]
|
||||||
);
|
);
|
||||||
const fileHeights = useMemo(() => map(item => item.height * (zoom / 100), files), [files, zoom]);
|
const maxImageWidth = useMemo(() => viewWidth * (zoom / 100), [viewWidth, 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?.scrollToItem(index);
|
virtualListRef.current?.scrollToIndex({ index, align: 'start', behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
ebus?.addListener('reset_views', () => {
|
ebus?.addListener('reset_views', () => {
|
||||||
virtualListRef.current?.scrollTo(0);
|
virtualListRef.current?.scrollTo({ top: 0 });
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
ebus?.removeAllListeners('navigate_offset');
|
ebus?.removeAllListeners('navigate_offset');
|
||||||
|
@ -46,29 +45,27 @@ export const ContinuationView: FC = () => {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isEmpty(files) && (
|
{!isEmpty(files) && (
|
||||||
<VariableSizeList
|
<Virtuoso
|
||||||
itemData={files}
|
style={{ height: viewHeight }}
|
||||||
itemCount={fileCount}
|
|
||||||
itemSize={index => fileHeights[index]}
|
|
||||||
itemKey={index => files[index].id}
|
|
||||||
height={viewHeight}
|
|
||||||
width="100%"
|
|
||||||
ref={virtualListRef}
|
ref={virtualListRef}
|
||||||
onItemsRendered={handleOnRenderAction}
|
totalCount={fileCount}
|
||||||
>
|
computeItemKey={index => files[index].id}
|
||||||
{({ index, style, data }) => (
|
rangeChanged={handleOnRenderAction}
|
||||||
|
itemContent={index => (
|
||||||
<div
|
<div
|
||||||
style={mergeLeft(style, {
|
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={data[index].path} style={{ width: data[index].width * (zoom / 100) }} />
|
<img src={files[index].path} style={{ width: maxImageWidth }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</VariableSizeList>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
//@ts-nocheck
|
//@ts-nocheck
|
||||||
import { Box, Center, Text } from '@mantine/core';
|
import { Box, Center, Text, Tooltip } from '@mantine/core';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { head, includes, indexOf, isEmpty, length, mergeLeft, not, pluck } from 'ramda';
|
import { head, includes, indexOf, isEmpty, length, 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 { FixedSizeList } from 'react-window';
|
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||||
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<FixedSizeList | null>();
|
const listRef = useRef<VirtuosoHandle | null>(null);
|
||||||
const handleFileSelectAction = useCallback(
|
const handleFileSelectAction = useCallback(
|
||||||
(filename: string) => {
|
(filename: string) => {
|
||||||
ebus.emit('navigate_offset', { filename });
|
ebus.emit('navigate_offset', { filename });
|
||||||
|
@ -25,7 +25,11 @@ 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?.scrollToItem(firstActivedIndex, 'smart');
|
listRef.current?.scrollToIndex({
|
||||||
|
index: firstActivedIndex,
|
||||||
|
align: 'center',
|
||||||
|
behavior: 'auto'
|
||||||
|
});
|
||||||
}, [activeFiles]);
|
}, [activeFiles]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,27 +41,30 @@ export const FileList: FC = () => {
|
||||||
ref={parentRef}
|
ref={parentRef}
|
||||||
>
|
>
|
||||||
{!isEmpty(files) && (
|
{!isEmpty(files) && (
|
||||||
<FixedSizeList itemCount={filesCount} itemSize={35} height={parentHeight} ref={listRef}>
|
<Virtuoso
|
||||||
{({ index, style }) => (
|
style={{ height: parentHeight }}
|
||||||
|
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 => ({
|
||||||
mergeLeft(style, {
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
color: not(includes(files[index].filename, activeFiles)) && theme.colors.red
|
color: not(includes(files[index].filename, activeFiles)) && theme.colors.red
|
||||||
}
|
}
|
||||||
})
|
})}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{files[index].filename}
|
<Tooltip label={files[index].filename} zIndex={999}>
|
||||||
|
<Text truncate>{files[index].filename}</Text>
|
||||||
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</FixedSizeList>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEmpty(files) && (
|
{isEmpty(files) && (
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
|
|
|
@ -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 zoom = useZoomState.use.currentZoom();
|
const { currentZoom: zoom, viewHeight, viewWidth } = useZoomState();
|
||||||
const viewHeight = useZoomState.use.viewHeight();
|
const maxImageWidth = useMemo(() => viewWidth * (zoom / 100), [viewWidth, zoom]);
|
||||||
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 * (zoom / 100);
|
let imageHeightAfterZoom = activeFile?.height * (maxImageWidth / activeFile?.width);
|
||||||
return gt(imageHeightAfterZoom, viewHeight);
|
return gt(imageHeightAfterZoom, viewHeight);
|
||||||
}, [activeFile, viewHeight, zoom]);
|
}, [activeFile, viewHeight, maxImageWidth]);
|
||||||
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: `${zoom}%` }} />
|
<img src={activeFile.path} style={{ width: maxImageWidth }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,11 +6,13 @@ 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'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,7 +20,8 @@ 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 => ({
|
||||||
|
@ -33,6 +36,11 @@ 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;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user