feat(browse):基本完成文件夹浏览器的功能。

This commit is contained in:
徐涛
2023-03-20 16:42:18 +08:00
parent 55de6f7993
commit 733dd48663
13 changed files with 557 additions and 20 deletions

View File

@@ -1,9 +1,14 @@
//@ts-nocheck
import { Stack, useMantineTheme } from '@mantine/core';
import { Stack, Tabs, useMantineTheme } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconFiles, IconFolders } from '@tabler/icons-react';
import { ifElse, path, propEq } from 'ramda';
import { FC, useMemo } from 'react';
import { useMount } from 'react-use';
import { DirTree } from './components/DirTree';
import { FileList } from './components/FileList';
import { FileToolbar } from './components/FileTools';
import { loadDrives } from './queries/directories';
const bgSelectFn = ifElse(
propEq('colorScheme', 'dark'),
@@ -18,10 +23,18 @@ export const NavMenu: FC = () => {
const disabledColor = useMemo(() => path<string>(['gray', 7])(theme.colors), [theme.colors]);
const navMenuBg = useMemo(() => bgSelectFn(theme), [theme, theme]);
useMount(() => {
try {
loadDrives();
} catch (e) {
notifications.show({ message: `未能成功加载全部磁盘列表,${e.message}`, color: 'red' });
}
});
return (
<Stack
spacing={8}
miw={200}
miw={220}
h="inherit"
sx={theme => ({
flexGrow: 1,
@@ -32,8 +45,25 @@ export const NavMenu: FC = () => {
py={4}
align="center"
>
<FileToolbar />
<FileList />
<Tabs defaultValue="folder" w="100%" h="100%">
<Tabs.List>
<Tabs.Tab value="folder" icon={<IconFolders stroke={1.5} size={16} />}>
</Tabs.Tab>
<Tabs.Tab value="files" icon={<IconFiles stroke={1.5} size={16} />}>
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="folder" h="100%">
<DirTree />
</Tabs.Panel>
<Tabs.Panel value="files" h="100%">
<Stack spacing={8} py={4} w="100%" h="100%" align="center">
<FileToolbar />
<FileList />
</Stack>
</Tabs.Panel>
</Tabs>
</Stack>
);
};

130
src/components/DirTree.tsx Normal file
View File

@@ -0,0 +1,130 @@
import styled from '@emotion/styled';
import { ActionIcon, Flex, Stack, Text, Tooltip } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconSquareMinus, IconSquarePlus } from '@tabler/icons-react';
import { invoke } from '@tauri-apps/api';
import EventEmitter from 'events';
import { equals, isEmpty, length, map, not } from 'ramda';
import { FC, PropsWithChildren, useCallback, useContext, useState } from 'react';
import { EventBusContext } from '../EventBus';
import { DirItem } from '../models';
import { loadSubDirectories } from '../queries/directories';
import {
currentRootsSelector,
isExpandedSelector,
selectDirectories,
useDirTreeStore
} from '../states/dirs';
import { useFileListStore } from '../states/files';
const Tree = styled.ul`
--spacing: 0.5rem;
list-style-type: none;
list-style-position: outside;
padding: 0;
margin: 0;
width: max-content;
li ul {
padding-left: calc(2 * var(--spacing));
}
`;
const Branch: FC<PropsWithChildren<{ current: DirItem; expanded: boolean }>> = ({
children,
current
}) => {
const { directories: allSubDirs } = useDirTreeStore();
const [subDirs, setSubDirs] = useState<DirItem[]>([]);
const isCurrentExpanded = useDirTreeStore(isExpandedSelector(current.id));
const expend = useDirTreeStore.use.expandDir();
const fold = useDirTreeStore.use.foldDir();
const selectDir = useDirTreeStore.use.selectDirectory();
const selectedDirectory = useDirTreeStore.use.selected();
const storeFiles = useFileListStore.use.updateFiles();
const ebus = useContext<EventEmitter>(EventBusContext);
const handleExpandAction = useCallback(async () => {
try {
if (isCurrentExpanded) {
fold(current.id);
} else {
await loadSubDirectories(current);
setSubDirs(selectDirectories(current.id)(useDirTreeStore.getState().directories));
expend(current.id);
}
} catch (e) {
notifications.show({
message: `未能成功加载指定文件夹下的子文件夹,${e}`,
color: 'red'
});
}
}, [current, allSubDirs, isCurrentExpanded]);
const handleSelectAction = useCallback(async () => {
try {
selectDir(current.id);
const files = await invoke('scan_directory', { target: current.path });
console.log('[debug]获取到文件个数:', length(files));
storeFiles(files);
ebus.emit('reset_views');
} catch (e) {
console.error('[error]打开文件夹', e);
notifications.show({ message: `未能成功打开指定文件夹,请重试。${e}`, color: 'red' });
}
}, [current]);
return (
<li>
<Flex direction="row" justify="flex-start" align="center" spacing={8} maw={250}>
<ActionIcon onClick={handleExpandAction}>
{isCurrentExpanded ? (
<IconSquareMinus stroke={1.5} size={16} />
) : (
<IconSquarePlus stroke={1.5} size={16} />
)}
</ActionIcon>
<Tooltip label={children}>
<Text
size="sm"
truncate
onClick={handleSelectAction}
sx={{ cursor: 'pointer' }}
bg={equals(current.id, selectedDirectory) && 'blue'}
>
{children}
</Text>
</Tooltip>
</Flex>
{not(isEmpty(subDirs)) && isCurrentExpanded && (
<Tree>
{map(
item => (
<Branch key={item.id} current={item}>
{item.dirname}
</Branch>
),
subDirs
)}
</Tree>
)}
</li>
);
};
export const DirTree: FC = () => {
const roots = useDirTreeStore(currentRootsSelector());
return (
<Stack w="auto" h="100%" spacing={8} px={4} py={4} sx={{ overflow: 'auto' }}>
<Tree>
{map(
item => (
<Branch key={item.id} current={item}>
{item.dirname}
</Branch>
),
roots
)}
</Tree>
</Stack>
);
};

View File

@@ -6,3 +6,13 @@ export type FileItem = {
width: number;
height: number;
};
export type DirItem = {
sort: number;
parent?: string;
id: string;
dirname: string;
path: string;
root: boolean;
expanded: boolean;
};

View File

@@ -0,0 +1,24 @@
import { invoke } from '@tauri-apps/api';
import { useDirTreeStore } from '../states/dirs';
export async function loadDrives() {
try {
const drives = await invoke('show_drives');
const { getState } = useDirTreeStore;
getState().updateDrives(drives);
} catch (e) {
console.error('[error]fetch drives', e);
throw e;
}
}
export async function loadSubDirectories(target: DirItem) {
try {
const directories = await invoke('scan_for_child_dirs', { target: target.path });
const { getState } = useDirTreeStore;
getState().saveDirectories(directories, target.id);
} catch (e) {
console.error('[error]fetch subdirs', e);
throw e;
}
}

141
src/states/dirs.ts Normal file
View File

@@ -0,0 +1,141 @@
import {
addIndex,
append,
compose,
equals,
filter,
find,
includes,
isNil,
map,
mergeLeft,
not,
propEq,
reduce,
uniq
} from 'ramda';
import { DirItem } from '../models';
import { SyncAction, SyncObjectCallback, SyncParamAction } from '../types';
import { createStoreHook } from '../utils/store_creator';
interface DirsStates {
drives: DirItem[];
directories: DirItem[];
focused?: DirItem;
selected?: DirItem;
expanded: string[];
}
type DirsActions = {
updateDrives: (dirs: Omit<DirItem, 'sort' | 'parent'>[]) => void;
saveDirectories: (dirs: Omit<DirItem, 'sort' | 'parent'>[], parent: string) => void;
focus: SyncParamAction<string>;
unfocus: SyncAction;
selectDirectory: SyncParamAction<string>;
unselectDirectory: SyncAction;
expandDir: SyncParamAction<string>;
foldDir: SyncParamAction<string>;
};
const initialState: DirsStates = {
drives: [],
directories: [],
focused: undefined,
selected: undefined,
expanded: []
};
export const useDirTreeStore = createStoreHook<DirsStates & DirsActions>((set, get) => ({
...initialState,
updateDrives(dirs) {
set(df => {
df.drives = addIndex<Omit<DirItem, 'sort' | 'paraent'>, DirItem>(map)(
(item, index) => mergeLeft({ sort: index * 10, path: item.path, parent: undefined }, item),
dirs
);
});
},
saveDirectories(dirs, parent) {
const convertedDirs = addIndex<Omit<DirItem, 'sort' | 'parent'>, DirItem>(map)(
(item, index) => mergeLeft({ sort: index * 10, path: item.path, parent }, item),
dirs
);
const premerged = reduce(
(acc, elem) => {
const dir = find(propEq('id', elem.id), convertedDirs);
if (not(isNil(dir))) {
acc = append(mergeLeft(dir, elem), acc);
} else {
acc = append(elem, acc);
}
return acc;
},
[],
get().directories
);
const afterMerged = reduce(
(acc, elem) => {
const dir = find(propEq('id', elem.id), acc);
if (isNil(dir)) {
return append(elem, acc);
} else {
return acc;
}
},
premerged,
convertedDirs
);
set(df => {
df.directories = afterMerged;
});
},
focus(specifiedDirId) {
const requestedDir = find(propEq('id', specifiedDirId), get().directories);
if (not(isNil(requestedDir))) {
set(df => {
df.focused = requestedDir;
});
}
},
unfocus() {
set(df => {
df.focus = undefined;
});
},
selectDirectory(dirId) {
set(df => {
df.selected = dirId;
});
},
unselectDirectory() {
set(df => {
df.selected = undefined;
});
},
expandDir(dirId) {
set(df => {
df.expanded = uniq(append(dirId, df.expanded));
});
},
foldDir(dirId) {
set(df => {
df.expanded = filter(compose(not, equals(dirId)), df.expanded);
});
}
}));
export function currentRootsSelector(): SyncObjectCallback<DirsStates, DirItem[]> {
return state => (isNil(state.focused) ? state.drives : [state.focused]);
}
export function selectDirectories(parent: string): SyncObjectCallback<DirItem[], DirItem[]> {
return dirs => filter(propEq('parent', parent), dirs) ?? [];
}
export function subDirectoriesSelector(parent: string): SyncObjectCallback<DirsStates, DirItem[]> {
return state => filter(propEq('parent', parent), state.directories) ?? [];
}
export function isExpandedSelector(dirId: string): SyncObjectCallback<DirsStates, bool> {
return state => includes(dirId, state.expanded);
}