feat(view):基本实现连续视图的功能。
This commit is contained in:
parent
848c8c01e7
commit
9437e45b8d
|
@ -14,7 +14,7 @@ tauri-build = { version = "1.2", features = [] }
|
|||
|
||||
[dependencies]
|
||||
once_cell = "1.17.0"
|
||||
tauri = { version = "1.2", features = ["dialog-open", "shell-open"] }
|
||||
tauri = { version = "1.2", features = ["dialog-open", "fs-read-file", "protocol-all", "shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
|
|
|
@ -7,6 +7,7 @@ pub mod prelude {
|
|||
}
|
||||
|
||||
/// 用于持有应用实例,可存放不同的应用实例。
|
||||
#[allow(dead_code)]
|
||||
pub enum AppHold<'a, R: Runtime> {
|
||||
Instance(&'a App<R>),
|
||||
Handle(&'a AppHandle<R>),
|
||||
|
|
|
@ -13,6 +13,22 @@
|
|||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"fs": {
|
||||
"all": false,
|
||||
"copyFile": false,
|
||||
"createDir": false,
|
||||
"exists": false,
|
||||
"readDir": false,
|
||||
"readFile": true,
|
||||
"removeDir": false,
|
||||
"removeFile": false,
|
||||
"renameFile": false,
|
||||
"scope": [],
|
||||
"writeFile": false
|
||||
},
|
||||
"protocol": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
|
@ -39,7 +55,7 @@
|
|||
"targets": "all"
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
|
|
|
@ -1,6 +1,26 @@
|
|||
import { Box } from '@mantine/core';
|
||||
import { FC } from 'react';
|
||||
import { equals } from 'ramda';
|
||||
import { FC, useLayoutEffect } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
import { useZoomState } from '../states/zoom';
|
||||
import { ContinuationView } from './ContinuationView';
|
||||
import { DoubleView } from './DoubleView';
|
||||
import { SingleView } from './SingleView';
|
||||
|
||||
export const ComicView: FC = () => {
|
||||
return <Box w="100%" h="100%" sx={{ overflow: 'hidden' }}></Box>;
|
||||
const viewMode = useZoomState.use.viewMode();
|
||||
const updateViewHeight = useZoomState.use.updateViewHeight();
|
||||
const [containerRef, { height }] = useMeasure();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
updateViewHeight(height);
|
||||
}, [height]);
|
||||
|
||||
return (
|
||||
<Box w="100%" h="100%" sx={{ overflow: 'hidden' }} ref={containerRef}>
|
||||
{equals(viewMode, 'single') && <SingleView />}
|
||||
{equals(viewMode, 'double') && <DoubleView />}
|
||||
{equals(viewMode, 'continuation') && <ContinuationView />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
64
src/components/ContinuationView.tsx
Normal file
64
src/components/ContinuationView.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { Box, Stack } from '@mantine/core';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
||||
import { filter, isEmpty, length, map, pluck } from 'ramda';
|
||||
import { FC, useLayoutEffect, useMemo, useRef } from 'react';
|
||||
import { useFileListStore } from '../states/files';
|
||||
import { useZoomState } from '../states/zoom';
|
||||
import { withinRange } from '../utils/offset_func';
|
||||
|
||||
export const ContinuationView: FC = () => {
|
||||
const files = useFileListStore.use.files();
|
||||
const zoom = useZoomState.use.currentZoom();
|
||||
const viewHeight = useZoomState.use.viewHeight();
|
||||
const updateActives = useFileListStore.use.updateActiveFiles();
|
||||
const fileCount = useMemo(() => length(files), [files]);
|
||||
const parentRef = useRef();
|
||||
const virtualizer = useVirtualizer({
|
||||
count: fileCount,
|
||||
getScrollElement: () => parentRef.current,
|
||||
estimateSize: () => zoom * 10
|
||||
});
|
||||
const items = virtualizer.getVirtualItems();
|
||||
|
||||
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 (
|
||||
<div style={{ overflow: 'auto', contain: 'strict', height: '100%' }} ref={parentRef}>
|
||||
{!isEmpty(files) && (
|
||||
<Box pos="relative" w="100%" h={virtualizer.getTotalSize()}>
|
||||
<Stack
|
||||
pos="absolute"
|
||||
top={0}
|
||||
left={0}
|
||||
w="100%"
|
||||
justify="start"
|
||||
align="center"
|
||||
spacing={0}
|
||||
style={{
|
||||
transform: `translateY(${items[0].start}px)`
|
||||
}}
|
||||
>
|
||||
{items.map(row => (
|
||||
<img
|
||||
key={files[row.index].filename}
|
||||
src={convertFileSrc(files[row.index].path)}
|
||||
ref={virtualizer.measureElement}
|
||||
data-index={row.index}
|
||||
style={{ width: `${zoom}%` }}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
</Box>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
5
src/components/DoubleView.tsx
Normal file
5
src/components/DoubleView.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
export const DoubleView: FC = () => {
|
||||
return <div></div>;
|
||||
};
|
|
@ -1,17 +1,26 @@
|
|||
import { Box, Center, Text } from '@mantine/core';
|
||||
import { isEmpty, map, pipe, sort } from 'ramda';
|
||||
import { includes, isEmpty, map, pipe, sort } from 'ramda';
|
||||
import { FC } from 'react';
|
||||
import { useFileListStore } from '../states/files';
|
||||
|
||||
export const FileList: FC = () => {
|
||||
const files = useFileListStore.use.files();
|
||||
console.log('[debug]files from store: ', files);
|
||||
const activeFiles = useFileListStore.use.actives();
|
||||
|
||||
return (
|
||||
<Box w="100%" h="100%" pl={4} sx={{ flexGrow: 1, overflowY: 'auto', overflowX: 'hidden' }}>
|
||||
{pipe(
|
||||
sort((fa, fb) => fa.sort - fb.sort),
|
||||
map(item => <div key={item.filename}>{item.filename}</div>)
|
||||
map(item => (
|
||||
<Box
|
||||
bg={includes(item.filename, activeFiles) && 'grape'}
|
||||
key={item.filename}
|
||||
px={4}
|
||||
py={2}
|
||||
>
|
||||
{item.filename}
|
||||
</Box>
|
||||
))
|
||||
)(files)}
|
||||
{isEmpty(files) && (
|
||||
<Center h="100%">
|
||||
|
|
|
@ -16,7 +16,6 @@ export const FileToolbar: FC = () => {
|
|||
multiple: false
|
||||
});
|
||||
const files = await invoke('scan_directory', { target: directory });
|
||||
console.log('[debug]file list: ', files);
|
||||
storeFiles(files);
|
||||
} catch (e) {
|
||||
console.error('[error]打开文件夹', e);
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
import { ActionIcon, Group, NumberInput, rem, SegmentedControl, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
ActionIcon,
|
||||
Group,
|
||||
NumberInput,
|
||||
NumberInputHandlers,
|
||||
rem,
|
||||
SegmentedControl,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconArrowAutofitWidth,
|
||||
IconLock,
|
||||
|
@ -6,11 +14,12 @@ import {
|
|||
IconZoomIn,
|
||||
IconZoomOut
|
||||
} from '@tabler/icons-react';
|
||||
import { FC } from 'react';
|
||||
import { FC, useRef } from 'react';
|
||||
import { useZoomState } from '../states/zoom';
|
||||
|
||||
export const PicToolbar: FC = () => {
|
||||
const { lock, autoFit, currentZoom, viewMode } = useZoomState();
|
||||
const { lock, autoFit, currentZoom, viewMode, zoom } = useZoomState();
|
||||
const zoomHandlers = useRef<NumberInputHandlers>();
|
||||
|
||||
return (
|
||||
<Group w="100%" position="right" spacing={8} px={4} py={4}>
|
||||
|
@ -25,7 +34,11 @@ export const PicToolbar: FC = () => {
|
|||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="缩小">
|
||||
<ActionIcon variant="subtle" color="grape">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="grape"
|
||||
onClick={() => zoomHandlers.current?.decrement()}
|
||||
>
|
||||
<IconZoomOut stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
@ -36,11 +49,17 @@ export const PicToolbar: FC = () => {
|
|||
max={100}
|
||||
step={5}
|
||||
value={currentZoom}
|
||||
onChange={value => zoom(value)}
|
||||
handlersRef={zoomHandlers}
|
||||
styles={{ input: { width: rem(58), textAlign: 'center' } }}
|
||||
rightSection={<IconPercentage stroke={1.5} size={16} />}
|
||||
/>
|
||||
<Tooltip label="放大">
|
||||
<ActionIcon variant="subtle" color="grape">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="grape"
|
||||
onClick={() => zoomHandlers.current?.increment()}
|
||||
>
|
||||
<IconZoomIn stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
|
5
src/components/SingleView.tsx
Normal file
5
src/components/SingleView.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
export const SingleView: FC = () => {
|
||||
return <div></div>;
|
||||
};
|
|
@ -5,14 +5,17 @@ import { createStoreHook } from '../utils/store_creator';
|
|||
|
||||
interface FileListState {
|
||||
files: FileItem[];
|
||||
actives: string[];
|
||||
}
|
||||
|
||||
type FileListActions = {
|
||||
updateFiles: SyncObjectCallback<Omit<FileItem, 'sort'>[]>;
|
||||
updateActiveFiles: SyncObjectCallback<string[]>;
|
||||
};
|
||||
|
||||
const initialState: FileListState = {
|
||||
files: []
|
||||
files: [],
|
||||
actives: []
|
||||
};
|
||||
|
||||
export const useFileListStore = createStoreHook<FileListState & FileListActions>(set => ({
|
||||
|
@ -24,5 +27,10 @@ export const useFileListStore = createStoreHook<FileListState & FileListActions>
|
|||
files
|
||||
);
|
||||
});
|
||||
},
|
||||
updateActiveFiles(filenames) {
|
||||
set(df => {
|
||||
df.actives = filenames;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SyncObjectCallback } from '../types';
|
||||
import { createStoreHook } from '../utils/store_creator';
|
||||
|
||||
interface ZoomState {
|
||||
|
@ -5,15 +6,32 @@ interface ZoomState {
|
|||
autoFit: boolean;
|
||||
currentZoom: number;
|
||||
viewMode: 'single' | 'double' | 'continuation';
|
||||
viewHeight: number;
|
||||
}
|
||||
|
||||
type ZoomActions = {
|
||||
zoom: SyncObjectCallback<number>;
|
||||
updateViewHeight: SyncObjectCallback<number>;
|
||||
};
|
||||
|
||||
const initialState: ZoomState = {
|
||||
lock: true,
|
||||
autoFit: false,
|
||||
currentZoom: 100,
|
||||
viewMode: 'continuation'
|
||||
viewMode: 'continuation',
|
||||
viewHeight: 0
|
||||
};
|
||||
|
||||
export const useZoomState = createStoreHook<ZoomState>(set => ({
|
||||
...initialState
|
||||
export const useZoomState = createStoreHook<ZoomState & ZoomActions>(set => ({
|
||||
...initialState,
|
||||
zoom(ratio) {
|
||||
set(df => {
|
||||
df.currentZoom = ratio;
|
||||
});
|
||||
},
|
||||
updateViewHeight(height) {
|
||||
set(df => {
|
||||
df.viewHeight = height;
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
|
10
src/utils/offset_func.ts
Normal file
10
src/utils/offset_func.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { and, gt, lt } from 'ramda';
|
||||
|
||||
export function withinRange(
|
||||
itemStart: number,
|
||||
itemEnd: number,
|
||||
offsetStart: number,
|
||||
offsetEnd: number
|
||||
): boolean {
|
||||
return and(lt(itemStart, offsetEnd), gt(itemEnd, offsetStart));
|
||||
}
|
Loading…
Reference in New Issue
Block a user