feat(browse):基本完成文件夹浏览器的功能。
This commit is contained in:
parent
55de6f7993
commit
733dd48663
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "comic_viewer",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
8
src-tauri/.cargo/config
Normal file
8
src-tauri/.cargo/config
Normal file
@ -0,0 +1,8 @@
|
||||
[target.'cfg(target_os = "linux")']
|
||||
rustflags = ["-C", "link-arg=-nostartfiles"]
|
||||
|
||||
[target.'cfg(target_os = "windows")']
|
||||
rustflags = ["-C", "link-args=/ENTRY:_start /SUBSYSTEM:console"]
|
||||
|
||||
[target.'cfg(target_os = "macos")']
|
||||
rustflags = ["-C", "link-args=-e __start -static -nostartfiles"]
|
23
src-tauri/Cargo.lock
generated
23
src-tauri/Cargo.lock
generated
@ -326,11 +326,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "comic_viewer"
|
||||
version = "0.0.0"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"image",
|
||||
"md-5",
|
||||
"mountpoints",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -1590,6 +1592,15 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
@ -1626,6 +1637,16 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mountpoints"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3090e3ea5226b07cedd89a57c735d3ed0374a84236a85529a1d8bfec98eb8ec5"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nanorand"
|
||||
version = "0.7.0"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "comic_viewer"
|
||||
version = "0.0.0"
|
||||
version = "0.2.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
@ -14,7 +14,7 @@ tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
once_cell = "1.17.0"
|
||||
tauri = { version = "1.2", features = ["dialog-open", "fs-read-file", "protocol-all", "shell-open"] }
|
||||
tauri = { version = "1.2", features = ["dialog-open", "fs-exists", "fs-read-dir", "fs-read-file", "protocol-asset", "shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
@ -25,6 +25,8 @@ serde_repr = "0.1.10"
|
||||
tokio = { version = "1.23.1", features = ["full"] }
|
||||
image = "0.24.5"
|
||||
uuid = "1.3.0"
|
||||
mountpoints = "0.2.1"
|
||||
md-5 = "0.10.5"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
|
@ -1,6 +1,12 @@
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use mountpoints::mountinfos;
|
||||
use serde::Serialize;
|
||||
use walkdir::WalkDir;
|
||||
use tauri::Runtime;
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
use crate::utils;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FileItem {
|
||||
@ -11,16 +17,54 @@ pub struct FileItem {
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DirItem {
|
||||
pub id: String,
|
||||
pub dirname: String,
|
||||
pub path: String,
|
||||
pub root: bool,
|
||||
}
|
||||
|
||||
fn is_hidden(entry: &DirEntry) -> bool {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map(|s| s.starts_with(".") || s.starts_with("$"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_self<P: AsRef<Path>>(entry: &DirEntry, target: P) -> bool {
|
||||
entry.path().eq(target.as_ref())
|
||||
}
|
||||
|
||||
fn is_root(entry: &DirEntry) -> bool {
|
||||
entry
|
||||
.path()
|
||||
.to_str()
|
||||
.map(|s| s.eq_ignore_ascii_case("/"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
||||
pub async fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
||||
let mut file_items = WalkDir::new(target)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| !is_hidden(entry))
|
||||
.filter_map(|f| f.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.map(|f| {
|
||||
let (width, height) = image::image_dimensions(f.path())?;
|
||||
let file_hash_id = f
|
||||
.path()
|
||||
.to_str()
|
||||
.map(crate::utils::md5_hash)
|
||||
.map(|hash| utils::uuid_from(hash.as_slice()))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
Ok(FileItem {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
id: file_hash_id,
|
||||
filename: f
|
||||
.path()
|
||||
.file_name()
|
||||
@ -44,3 +88,101 @@ pub fn scan_directory(target: String) -> Result<Vec<FileItem>, String> {
|
||||
|
||||
Ok(file_items)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn show_drives<R: Runtime>(
|
||||
_app: tauri::AppHandle<R>,
|
||||
_window: tauri::Window<R>,
|
||||
) -> Result<Vec<DirItem>, String> {
|
||||
let mut drives = vec![];
|
||||
match mountinfos() {
|
||||
Ok(mounts) => {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
let mounts = mounts
|
||||
.iter()
|
||||
.filter(|m| !m.path.starts_with("/System") && !m.path.starts_with("/dev"));
|
||||
for mount in mounts {
|
||||
let dirname = mount
|
||||
.path
|
||||
.as_path()
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_os_string()
|
||||
.into_string()
|
||||
.unwrap();
|
||||
let dirname = if dirname.len() == 0 {
|
||||
String::from("/")
|
||||
} else {
|
||||
dirname
|
||||
};
|
||||
let dir_hash_id = mount
|
||||
.path
|
||||
.to_str()
|
||||
.map(crate::utils::md5_hash)
|
||||
.map(|hash| utils::uuid_from(hash.as_slice()))
|
||||
.transpose()?
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
drives.push(DirItem {
|
||||
id: dir_hash_id,
|
||||
dirname,
|
||||
path: String::from(mount.path.to_str().unwrap_or_default()),
|
||||
root: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(format!("不能列出系统中已挂载的磁盘:{}", err)),
|
||||
}
|
||||
Ok(drives)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn scan_for_child_dirs<R: Runtime>(
|
||||
_app: tauri::AppHandle<R>,
|
||||
_window: tauri::Window<R>,
|
||||
target: String,
|
||||
) -> Result<Vec<DirItem>, String> {
|
||||
println!("请求扫描文件夹:{}", target);
|
||||
let target = if target.eq_ignore_ascii_case("/") {
|
||||
Path::new(r"/")
|
||||
} else {
|
||||
Path::new(&target)
|
||||
};
|
||||
let mut child_dirs = WalkDir::new(target)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_entry(|entry| !is_hidden(entry) && !is_root(entry))
|
||||
.filter_map(|d| d.ok())
|
||||
.filter(|d| d.path().is_dir() && !is_self(d, target))
|
||||
.map(|d| {
|
||||
println!("扫描到的文件夹:{}", d.path().display());
|
||||
let dir_hash_id = d
|
||||
.path()
|
||||
.to_str()
|
||||
.map(crate::utils::md5_hash)
|
||||
.map(|hash| utils::uuid_from(hash.as_slice()))
|
||||
.transpose()
|
||||
.map_err(|e| anyhow!(e))?
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
Ok(DirItem {
|
||||
id: dir_hash_id,
|
||||
dirname: d
|
||||
.path()
|
||||
.file_name()
|
||||
.ok_or(anyhow!("不能获取到文件夹的名称。"))?
|
||||
.to_owned()
|
||||
.into_string()
|
||||
.unwrap(),
|
||||
path: d
|
||||
.path()
|
||||
.clone()
|
||||
.to_str()
|
||||
.ok_or(anyhow!("不能获取到文件夹路径。"))?
|
||||
.to_string(),
|
||||
root: false,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<DirItem>, anyhow::Error>>()
|
||||
.map_err(|e| e.to_string())?;
|
||||
child_dirs.sort_by(|a, b| a.dirname.partial_cmp(&b.dirname).unwrap());
|
||||
Ok(child_dirs)
|
||||
}
|
||||
|
@ -2,13 +2,18 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod commands;
|
||||
mod utils;
|
||||
|
||||
use commands::AppHold;
|
||||
use tauri::Manager;
|
||||
|
||||
fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![commands::prelude::scan_directory])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
commands::prelude::scan_directory,
|
||||
commands::prelude::show_drives,
|
||||
commands::prelude::scan_for_child_dirs
|
||||
])
|
||||
.setup(|app| {
|
||||
let main_window = app.get_window("main").unwrap();
|
||||
#[cfg(debug_assertions)]
|
||||
|
19
src-tauri/src/utils.rs
Normal file
19
src-tauri/src/utils.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use md5::{Digest, Md5};
|
||||
use uuid::Builder;
|
||||
|
||||
pub fn md5_hash(source: &str) -> Vec<u8> {
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(source);
|
||||
let hash = hasher.finalize();
|
||||
hash.to_vec()
|
||||
}
|
||||
|
||||
pub fn uuid_from(source: &[u8]) -> Result<String, String> {
|
||||
let builder = Builder::from_md5_bytes(
|
||||
source
|
||||
.try_into()
|
||||
.map_err(|_| String::from("源内容长度不足。"))?,
|
||||
);
|
||||
|
||||
Ok(builder.into_uuid().to_string())
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "漫画阅读器",
|
||||
"version": "0.1.0"
|
||||
"version": "0.2.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@ -17,17 +17,23 @@
|
||||
"all": false,
|
||||
"copyFile": false,
|
||||
"createDir": false,
|
||||
"exists": false,
|
||||
"readDir": false,
|
||||
"exists": true,
|
||||
"readDir": true,
|
||||
"readFile": true,
|
||||
"removeDir": false,
|
||||
"removeFile": false,
|
||||
"renameFile": false,
|
||||
"scope": [],
|
||||
"scope": [
|
||||
"*"
|
||||
],
|
||||
"writeFile": false
|
||||
},
|
||||
"protocol": {
|
||||
"all": true
|
||||
"all": false,
|
||||
"asset": true,
|
||||
"assetScope": [
|
||||
"*"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
@ -60,8 +66,7 @@
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"devCsp": "default-src 'self'; img-src 'self' asset: https://asset.localhost",
|
||||
"csp": null
|
||||
"csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost"
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
|
@ -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"
|
||||
>
|
||||
<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
130
src/components/DirTree.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
};
|
||||
|
24
src/queries/directories.ts
Normal file
24
src/queries/directories.ts
Normal 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
141
src/states/dirs.ts
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user