feat(view):基本实现连续视图的功能。

This commit is contained in:
徐涛 2023-03-08 16:28:24 +08:00
parent 848c8c01e7
commit 9437e45b8d
13 changed files with 191 additions and 17 deletions

View File

@ -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"] }

View File

@ -7,6 +7,7 @@ pub mod prelude {
}
/// 用于持有应用实例,可存放不同的应用实例。
#[allow(dead_code)]
pub enum AppHold<'a, R: Runtime> {
Instance(&'a App<R>),
Handle(&'a AppHandle<R>),

View File

@ -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

View File

@ -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>
);
};

View 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>
);
};

View File

@ -0,0 +1,5 @@
import { FC } from 'react';
export const DoubleView: FC = () => {
return <div></div>;
};

View File

@ -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%">

View File

@ -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);

View File

@ -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>

View File

@ -0,0 +1,5 @@
import { FC } from 'react';
export const SingleView: FC = () => {
return <div></div>;
};

View File

@ -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;
});
}
}));

View File

@ -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
View 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));
}