feat(layout):完成基本界面布局以及文件夹扫描功能。
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
import { Box, Group } from "@mantine/core";
|
||||
import { FC } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { NavMenu } from "./NavMenu";
|
||||
import { Group, Stack } from '@mantine/core';
|
||||
import { FC } from 'react';
|
||||
import { ComicView } from './components/ComicView';
|
||||
import { PicToolbar } from './components/PicToolbar';
|
||||
import { NavMenu } from './NavMenu';
|
||||
|
||||
export const MainLayout: FC = () => {
|
||||
return (
|
||||
<Group grow noWrap spacing={0} h="100%" w="100%">
|
||||
<NavMenu />
|
||||
<Box h="inherit" w="inherit" maw="100%" sx={{ flexGrow: 5 }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
<Stack h="inherit" w="inherit" maw="100%" sx={{ flexGrow: 5 }} spacing={0}>
|
||||
<PicToolbar />
|
||||
<ComicView />
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
@@ -1,32 +1,38 @@
|
||||
import { Stack, useMantineTheme } from "@mantine/core";
|
||||
import { ifElse, path, propEq } from "ramda";
|
||||
import { FC, useMemo } from "react";
|
||||
import { Stack, useMantineTheme } from '@mantine/core';
|
||||
import { ifElse, path, propEq } from 'ramda';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { FileList } from './components/FileList';
|
||||
import { FileToolbar } from './components/FileTools';
|
||||
|
||||
const bgSelectFn = ifElse(
|
||||
propEq("colorScheme", "dark"),
|
||||
path(["colors", "cbg", 2]),
|
||||
path(["colors", "cbg", 7])
|
||||
propEq('colorScheme', 'dark'),
|
||||
path(['colors', 'cbg', 2]),
|
||||
path(['colors', 'cbg', 7])
|
||||
);
|
||||
|
||||
export const NavMenu: FC = () => {
|
||||
const theme = useMantineTheme();
|
||||
const normalColor = useMemo(() => path(["violet", 7])(theme.colors), [theme.colors]);
|
||||
const activatedColor = useMemo(() => path<string>(["violet", 3])(theme.colors), [theme.colors]);
|
||||
const disabledColor = useMemo(() => path<string>(["gray", 7])(theme.colors), [theme.colors]);
|
||||
const normalColor = useMemo(() => path(['violet', 7])(theme.colors), [theme.colors]);
|
||||
const activatedColor = useMemo(() => path<string>(['violet', 3])(theme.colors), [theme.colors]);
|
||||
const disabledColor = useMemo(() => path<string>(['gray', 7])(theme.colors), [theme.colors]);
|
||||
const navMenuBg = useMemo(() => bgSelectFn(theme), [theme, theme]);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
spacing={24}
|
||||
maw={64}
|
||||
spacing={8}
|
||||
miw={200}
|
||||
h="inherit"
|
||||
sx={(theme) => ({
|
||||
sx={theme => ({
|
||||
flexGrow: 1,
|
||||
backgroundColor: navMenuBg,
|
||||
overflow: 'hidden'
|
||||
})}
|
||||
px={4}
|
||||
py={4}
|
||||
align="center"
|
||||
px={16}
|
||||
py={16}
|
||||
></Stack>
|
||||
>
|
||||
<FileToolbar />
|
||||
<FileList />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
6
src/components/ComicView.tsx
Normal file
6
src/components/ComicView.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Box } from '@mantine/core';
|
||||
import { FC } from 'react';
|
||||
|
||||
export const ComicView: FC = () => {
|
||||
return <Box w="100%" h="100%" sx={{ overflow: 'hidden' }}></Box>;
|
||||
};
|
23
src/components/FileList.tsx
Normal file
23
src/components/FileList.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Box, Center, Text } from '@mantine/core';
|
||||
import { 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);
|
||||
|
||||
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>)
|
||||
)(files)}
|
||||
{isEmpty(files) && (
|
||||
<Center h="100%">
|
||||
<Text size="xs">请先打开一个文件夹。</Text>
|
||||
</Center>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
42
src/components/FileTools.tsx
Normal file
42
src/components/FileTools.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Button, Group, Tooltip } from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconFolder } from '@tabler/icons-react';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import { FC, useCallback } from 'react';
|
||||
import { useFileListStore } from '../states/files';
|
||||
|
||||
export const FileToolbar: FC = () => {
|
||||
const storeFiles = useFileListStore.use.updateFiles();
|
||||
const handleOpenAction = useCallback(async () => {
|
||||
try {
|
||||
const directory = await open({
|
||||
title: '打开要浏览的漫画所在的文件夹',
|
||||
directory: true,
|
||||
multiple: false
|
||||
});
|
||||
const files = await invoke('scan_directory', { target: directory });
|
||||
console.log('[debug]file list: ', files);
|
||||
storeFiles(files);
|
||||
} catch (e) {
|
||||
console.error('[error]打开文件夹', e);
|
||||
notifications.show({ title: '未能成功打开指定文件夹,请重试。', color: 'red' });
|
||||
}
|
||||
}, [storeFiles]);
|
||||
|
||||
return (
|
||||
<Group align="start" w="100%" spacing={4}>
|
||||
<Tooltip label="打开漫画所在文件夹">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="subtle"
|
||||
fullWidth
|
||||
leftIcon={<IconFolder stroke={1.5} size={14} />}
|
||||
onClick={handleOpenAction}
|
||||
>
|
||||
打开文件夹
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
};
|
61
src/components/PicToolbar.tsx
Normal file
61
src/components/PicToolbar.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { ActionIcon, Group, NumberInput, rem, SegmentedControl, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
IconArrowAutofitWidth,
|
||||
IconLock,
|
||||
IconPercentage,
|
||||
IconZoomIn,
|
||||
IconZoomOut
|
||||
} from '@tabler/icons-react';
|
||||
import { FC } from 'react';
|
||||
import { useZoomState } from '../states/zoom';
|
||||
|
||||
export const PicToolbar: FC = () => {
|
||||
const { lock, autoFit, currentZoom, viewMode } = useZoomState();
|
||||
|
||||
return (
|
||||
<Group w="100%" position="right" spacing={8} px={4} py={4}>
|
||||
<Tooltip label="锁定缩放">
|
||||
<ActionIcon variant={lock ? 'filled' : 'subtle'} color="grape">
|
||||
<IconLock stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="适应窗口宽度">
|
||||
<ActionIcon variant={autoFit ? 'filled' : 'subtle'} color="grape">
|
||||
<IconArrowAutofitWidth stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="缩小">
|
||||
<ActionIcon variant="subtle" color="grape">
|
||||
<IconZoomOut stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<NumberInput
|
||||
hideControls
|
||||
size="xs"
|
||||
min={20}
|
||||
max={100}
|
||||
step={5}
|
||||
value={currentZoom}
|
||||
styles={{ input: { width: rem(58), textAlign: 'center' } }}
|
||||
rightSection={<IconPercentage stroke={1.5} size={16} />}
|
||||
/>
|
||||
<Tooltip label="放大">
|
||||
<ActionIcon variant="subtle" color="grape">
|
||||
<IconZoomIn stroke={1.5} size={24} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="翻页模式">
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={viewMode}
|
||||
color="grape"
|
||||
data={[
|
||||
{ label: '单页', value: 'single' },
|
||||
{ label: '双页', value: 'double' },
|
||||
{ label: '连续', value: 'continuation' }
|
||||
]}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
);
|
||||
};
|
5
src/models.ts
Normal file
5
src/models.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type FileItem = {
|
||||
sort: number;
|
||||
filename: string;
|
||||
path: string;
|
||||
};
|
28
src/states/files.ts
Normal file
28
src/states/files.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { addIndex, map, mergeRight } from 'ramda';
|
||||
import { FileItem } from '../models';
|
||||
import { SyncObjectCallback } from '../types';
|
||||
import { createStoreHook } from '../utils/store_creator';
|
||||
|
||||
interface FileListState {
|
||||
files: FileItem[];
|
||||
}
|
||||
|
||||
type FileListActions = {
|
||||
updateFiles: SyncObjectCallback<Omit<FileItem, 'sort'>[]>;
|
||||
};
|
||||
|
||||
const initialState: FileListState = {
|
||||
files: []
|
||||
};
|
||||
|
||||
export const useFileListStore = createStoreHook<FileListState & FileListActions>(set => ({
|
||||
...initialState,
|
||||
updateFiles(files) {
|
||||
set(df => {
|
||||
df.files = addIndex<Omit<FileItem, 'sort'>, FileItem>(map)(
|
||||
(item, index) => mergeRight({ sort: index * 10 }, item),
|
||||
files
|
||||
);
|
||||
});
|
||||
}
|
||||
}));
|
19
src/states/zoom.ts
Normal file
19
src/states/zoom.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createStoreHook } from '../utils/store_creator';
|
||||
|
||||
interface ZoomState {
|
||||
lock: boolean;
|
||||
autoFit: boolean;
|
||||
currentZoom: number;
|
||||
viewMode: 'single' | 'double' | 'continuation';
|
||||
}
|
||||
|
||||
const initialState: ZoomState = {
|
||||
lock: true,
|
||||
autoFit: false,
|
||||
currentZoom: 100,
|
||||
viewMode: 'continuation'
|
||||
};
|
||||
|
||||
export const useZoomState = createStoreHook<ZoomState>(set => ({
|
||||
...initialState
|
||||
}));
|
Reference in New Issue
Block a user