diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index cbdd53a..eb082a3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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"] } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index e554353..0cd11dc 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -7,6 +7,7 @@ pub mod prelude { } /// 用于持有应用实例,可存放不同的应用实例。 +#[allow(dead_code)] pub enum AppHold<'a, R: Runtime> { Instance(&'a App), Handle(&'a AppHandle), diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0d94b9a..0776e03 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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 diff --git a/src/components/ComicView.tsx b/src/components/ComicView.tsx index c1e9efb..75a4dd5 100644 --- a/src/components/ComicView.tsx +++ b/src/components/ComicView.tsx @@ -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 ; + const viewMode = useZoomState.use.viewMode(); + const updateViewHeight = useZoomState.use.updateViewHeight(); + const [containerRef, { height }] = useMeasure(); + + useLayoutEffect(() => { + updateViewHeight(height); + }, [height]); + + return ( + + {equals(viewMode, 'single') && } + {equals(viewMode, 'double') && } + {equals(viewMode, 'continuation') && } + + ); }; diff --git a/src/components/ContinuationView.tsx b/src/components/ContinuationView.tsx new file mode 100644 index 0000000..1cef3ef --- /dev/null +++ b/src/components/ContinuationView.tsx @@ -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 ( +
+ {!isEmpty(files) && ( + + + {items.map(row => ( + + ))} + + + )} +
+ ); +}; diff --git a/src/components/DoubleView.tsx b/src/components/DoubleView.tsx new file mode 100644 index 0000000..2f238c7 --- /dev/null +++ b/src/components/DoubleView.tsx @@ -0,0 +1,5 @@ +import { FC } from 'react'; + +export const DoubleView: FC = () => { + return
; +}; diff --git a/src/components/FileList.tsx b/src/components/FileList.tsx index 4561e89..45dd1fa 100644 --- a/src/components/FileList.tsx +++ b/src/components/FileList.tsx @@ -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 ( {pipe( sort((fa, fb) => fa.sort - fb.sort), - map(item =>
{item.filename}
) + map(item => ( + + {item.filename} + + )) )(files)} {isEmpty(files) && (
diff --git a/src/components/FileTools.tsx b/src/components/FileTools.tsx index c91222d..4879415 100644 --- a/src/components/FileTools.tsx +++ b/src/components/FileTools.tsx @@ -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); diff --git a/src/components/PicToolbar.tsx b/src/components/PicToolbar.tsx index aa3a8ee..a62bc1d 100644 --- a/src/components/PicToolbar.tsx +++ b/src/components/PicToolbar.tsx @@ -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(); return ( @@ -25,7 +34,11 @@ export const PicToolbar: FC = () => { - + zoomHandlers.current?.decrement()} + > @@ -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={} /> - + zoomHandlers.current?.increment()} + > diff --git a/src/components/SingleView.tsx b/src/components/SingleView.tsx new file mode 100644 index 0000000..c38daa1 --- /dev/null +++ b/src/components/SingleView.tsx @@ -0,0 +1,5 @@ +import { FC } from 'react'; + +export const SingleView: FC = () => { + return
; +}; diff --git a/src/states/files.ts b/src/states/files.ts index 23c830d..a355caa 100644 --- a/src/states/files.ts +++ b/src/states/files.ts @@ -5,14 +5,17 @@ import { createStoreHook } from '../utils/store_creator'; interface FileListState { files: FileItem[]; + actives: string[]; } type FileListActions = { updateFiles: SyncObjectCallback[]>; + updateActiveFiles: SyncObjectCallback; }; const initialState: FileListState = { - files: [] + files: [], + actives: [] }; export const useFileListStore = createStoreHook(set => ({ @@ -24,5 +27,10 @@ export const useFileListStore = createStoreHook files ); }); + }, + updateActiveFiles(filenames) { + set(df => { + df.actives = filenames; + }); } })); diff --git a/src/states/zoom.ts b/src/states/zoom.ts index aec10d2..4add559 100644 --- a/src/states/zoom.ts +++ b/src/states/zoom.ts @@ -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; + updateViewHeight: SyncObjectCallback; +}; + const initialState: ZoomState = { lock: true, autoFit: false, currentZoom: 100, - viewMode: 'continuation' + viewMode: 'continuation', + viewHeight: 0 }; -export const useZoomState = createStoreHook(set => ({ - ...initialState +export const useZoomState = createStoreHook(set => ({ + ...initialState, + zoom(ratio) { + set(df => { + df.currentZoom = ratio; + }); + }, + updateViewHeight(height) { + set(df => { + df.viewHeight = height; + }); + } })); diff --git a/src/utils/offset_func.ts b/src/utils/offset_func.ts new file mode 100644 index 0000000..e2a241a --- /dev/null +++ b/src/utils/offset_func.ts @@ -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)); +}