diff --git a/src/filehandle/WebdavFile.ts b/src/filehandle/WebdavFile.ts index af7be81..72cd16b 100644 --- a/src/filehandle/WebdavFile.ts +++ b/src/filehandle/WebdavFile.ts @@ -84,6 +84,10 @@ class WebdavFileSystemFileHandle implements FileSystemFileHandle { this.fullPath = fullPath; } + get name() { + return this._name; + } + createSyncAccessHandle(): Promise { throw new Error('Method not implemented.'); } @@ -106,14 +110,10 @@ class WebdavFileSystemFileHandle implements FileSystemFileHandle { throw new Error('Method not implemented.'); } - get name() { - return this._name; - } - async getFile() { const data = (await this.webdav.getFileContents(this.fullPath, { format: 'binary' - })) as string; + })) as ArrayBuffer; return new File([data], this._name); } @@ -198,28 +198,22 @@ class WebdavFileSystemDirectoryHandle implements FileSystemDirectoryHandle { [Symbol.asyncIterator]() { return this; }, - next() { - return (async function next() { - const values = await p; - - const curr = values[i++]; - - if (!curr) { - return { value: undefined, done: true }; - } - - return { - value: [ - curr.basename, - createWebdavFileSystemHandle( - curr, - webdav, - fullPath + curr.basename - ) - ], - done: false - }; - })(); + async next() { + const values = await p; + + const curr = values[i++]; + + if (!curr) { + return { value: undefined, done: true }; + } + + return { + value: [ + curr.basename, + createWebdavFileSystemHandle(curr, webdav, fullPath + curr.basename) + ], + done: false + }; } }; } @@ -280,12 +274,7 @@ class WebdavFileSystemDirectoryHandle implements FileSystemDirectoryHandle { return new WebdavFileSystemFileHandle(this.webdav, subFullPath, name); } - async removeEntry( - name: string, - options?: FileSystemHandleRecursiveOptions - ): Promise { - options; - + async removeEntry(name: string): Promise { const subFullPath = this.fullPath + name; await this.webdav.deleteFile(subFullPath); diff --git a/src/filehandle/components/FileContent.tsx b/src/filehandle/components/FileContent.tsx index 2bfa1ec..19a10dc 100644 --- a/src/filehandle/components/FileContent.tsx +++ b/src/filehandle/components/FileContent.tsx @@ -2,7 +2,7 @@ import { useContext, useMemo, useRef, useState } from 'react'; import type { InputProps } from 'antd'; import type { InputRef } from 'antd'; -import { Form, Input, Modal, Typography } from 'antd'; +import { Form, Input, Modal, Spin, Typography } from 'antd'; import { ExclamationCircleFilled } from '@ant-design/icons'; import type { WebdavInfo } from '@/store'; @@ -14,27 +14,23 @@ import { error, sleep, success, - validateFileName, - warning + validateFileName } from '@/utils'; import { ContextMenu, Icon } from '@/components'; import { FileSystemContext } from './FilePanel'; import styles from './styles/FileContent.module.less'; -import { isAlwaysExist } from '../utils'; import type { DH, FileInfo } from '../utils/fileManager'; -import { FileType, importDirectory, importFile } from '../utils/fileManager'; +import { FileType } from '../utils/fileManager'; function AddFileModal({ open, - current, type, onOk, onCancel }: { open: boolean; - current: DH; type: FileType; onOk: (name: string, type: FileType) => any; onCancel: () => any; @@ -49,6 +45,8 @@ function AddFileModal({ message: '' }); + const { children } = useContext(FileSystemContext); + const inputRef = useRef(null); useMemo(() => { @@ -59,31 +57,22 @@ function AddFileModal({ }, [open]); const handleInputChange = async (e: React.ChangeEvent) => { - const { value } = e.target; + const value = e.target.value.trim(); - try { - if (value !== '' && !validateFileName(value)) { - setInputStatus({ - name: value, - status: 'error', - message: '无效的文件名' - }); - } else if ( - /** TODO: When we use webdav this will always initiate the request */ await isAlwaysExist( - current, - value - ) - ) { - setInputStatus({ - name: value, - status: 'error', - message: '当前目录已包含同名的文件' - }); - } else { - setInputStatus({ name: value, status: '', message: '' }); - } - } catch (err) { - error((err as Error).message); + if (value !== '' && !validateFileName(value)) { + setInputStatus({ + name: value, + status: 'error', + message: '无效的文件名' + }); + } else if (children.some((c) => c.name === value)) { + setInputStatus({ + name: value, + status: 'error', + message: '当前目录已包含同名的文件' + }); + } else { + setInputStatus({ name: value, status: '', message: '' }); } }; @@ -96,10 +85,13 @@ function AddFileModal({ }; const handleOk = () => { - if (inputStatus.name.trim() === '' || inputStatus.status === 'error') { + if (inputStatus.name === '' || inputStatus.status === 'error') { return false; } - onOk(inputStatus.name.trim(), type); + + onOk(inputStatus.name, type); + + setInputStatus({ name: '', status: '', message: '' }); }; return ( @@ -137,7 +129,9 @@ function MountWebdavModal(props: { open: boolean; close(): void }) { const [form] = Form.useForm(); - const { webdavs, addWebdav } = useUserStore(); + const { children } = useContext(FileSystemContext); + + const { addWebdav } = useUserStore(); const handleOk = () => { form @@ -145,10 +139,10 @@ function MountWebdavModal(props: { open: boolean; close(): void }) { .then(() => { const webdav: WebdavInfo = form.getFieldsValue(); - if (webdavs.some((w) => w.name === webdav.name.trim())) { + if (children.some((w) => w.name === webdav.name.trim())) { alert({ title: '提示', - content: `已包含名称为 "${webdav.name.trim()}" 的挂载目录!` + content: `已包含名称为 "${webdav.name.trim()}" 的目录!` }); return void 0; @@ -281,13 +275,16 @@ const FileContent: React.FC = (props) => { const { current, children, + isBusy, fileHandles, fileLinked, backgroundManager, + enterDirectory, create, remove, - forceUpdate + importFile, + importDirectory } = useContext(FileSystemContext); const root = fileLinked?.root?.value; @@ -333,33 +330,6 @@ const FileContent: React.FC = (props) => { setMountModalOpen(true); }; - const handleImportFile = () => { - importFile(current) - .then((value) => { - if (!value) { - return warning('暂无法导入文件'); - } - - forceUpdate(); - }) - .catch((err) => { - error((err as Error).message); - }); - }; - - const handleImportDirectory = () => { - importDirectory(current) - .then((value) => { - if (!value) { - return warning('暂无法导入文件夹'); - } - forceUpdate(); - }) - .catch((err) => { - error((err as Error).message); - }); - }; - const open = async (file: FileInfo) => { try { const handle = fileHandles.find((handle) => @@ -420,20 +390,22 @@ const FileContent: React.FC = (props) => { return ( <>
- {children.map((child) => ( - { - setSelection([child]); - }} - onDoubleClick={ - child.type === FileType.DIRECTORY - ? () => enterDirectory(child) - : () => open(child) - } - /> - ))} + + {children.map((child) => ( + { + setSelection([child]); + }} + onDoubleClick={ + child.type === FileType.DIRECTORY + ? () => enterDirectory(child) + : () => open(child) + } + /> + ))} + sectionRef.current!} @@ -469,14 +441,14 @@ const FileContent: React.FC = (props) => { icon: ( ), - onClick: handleImportFile + onClick: importFile }, { name: '导入文件夹', icon: ( ), - onClick: handleImportDirectory + onClick: importDirectory }, { name: '删除', @@ -498,7 +470,6 @@ const FileContent: React.FC = (props) => { diff --git a/src/filehandle/hooks/useFileSystem.ts b/src/filehandle/hooks/useFileSystem.ts index 19a09da..79bc5a5 100644 --- a/src/filehandle/hooks/useFileSystem.ts +++ b/src/filehandle/hooks/useFileSystem.ts @@ -4,7 +4,7 @@ import { cloneDeep } from 'lodash-es'; import { useUserStore } from '@/store'; -import { error } from '@/utils'; +import { error, warning } from '@/utils'; import BackgroundManager from '../BackgroundManager'; import FileLinkedList from '../FileLinkedList'; @@ -14,6 +14,8 @@ import { createFile, FileType, getChildren, + importDirectory as importDH, + importFile as importFH, remove as removeFile } from '../utils/fileManager'; @@ -30,14 +32,23 @@ export interface FileHandle { } export interface FileSystem { + /** current folder */ current: DH; + /** current folder index */ children: FileInfo[]; + /** history */ fileLinked: FileLinkedList; + /** file processing program list */ fileHandles: FileHandle[]; + /** Background manager */ backgroundManager: BackgroundManager; + /** During the operation, blocking user operation is generally used for remote files */ + isBusy: boolean; create(name: string, type: FileType, data?: FileDataType): Promise; remove(name: string): any; move(): void; + importFile(): Promise; + importDirectory(): Promise; register(handler: FileHandle): void; returnToRoot(): void; enterDirectory(file: FileInfo): any; @@ -49,8 +60,10 @@ export function useFileSystem(): FileSystem { const root = useRef() as React.MutableRefObject; + const fileLinked = useRef() as React.MutableRefObject; + const fileHandlesRef = useRef([]); const backgroundManager = useRef(new BackgroundManager()); @@ -58,40 +71,54 @@ export function useFileSystem(): FileSystem { DH, React.Dispatch> ]; + const [children, setChildren] = useState([]); - const update = () => { - if (!current) return; + const [isBusy, setBusy] = useState(false); - getChildren(current, root.current === current ? webdavs : void 0) - .then((_children) => { - setChildren([..._children]); - }) - .catch((err) => { - error((err as Error).message); - }); - }; + const update = async (handle?: DH, cb?: () => void) => { + const target = handle || current; - useMemo(() => { - if (!current) return void 0; + if (target) { + try { + setBusy(true); - update(); - }, [current, webdavs]); + const children = await getChildren( + target, + root.current === target ? webdavs : void 0 + ); - useMemo(() => { - if (!fileLinked.current) return; + setCurrent(target); + setChildren(children); - return fileLinked.current.listener((directory) => { - setCurrent(directory); - }); - }, [fileLinked.current]); + cb && cb(); + } catch (err) { + error((err as Error).message); + } finally { + setBusy(false); + } + } + }; + + useMemo(() => update(), [webdavs]); + + useMemo( + () => fileLinked.current?.listener((directory) => update(directory)), + [fileLinked.current] + ); useEffect(() => { - window.navigator.storage.getDirectory().then((res) => { - setCurrent(res); - root.current = res; - fileLinked.current = new FileLinkedList(res); - }); + window.navigator.storage + .getDirectory() + .then((_root) => { + root.current = _root; + fileLinked.current = new FileLinkedList(_root); + + update(_root); + }) + .catch((err) => { + error((err as Error).message); + }); }, []); const fileSystem = useMemo(() => { @@ -109,27 +136,53 @@ export function useFileSystem(): FileSystem { ]; }); - update(); + setChildren([...children]); } - function move() {} + async function move() {} + + async function remove(name: string) { + try { + await removeFile(current, name); - function remove(name: string) { - removeFile(current, name) - .then(() => { - update(); - }) - .catch((err) => { - error((err as Error).message); - }); + setChildren(children.filter((c) => c.name !== name)); + } catch (err) { + error((err as Error).message); + } } async function create(name: string, type: FileType, data?: FileDataType) { try { - if (type === FileType.FILE) { - await createFile(current, name, data); - } else { - await createDirectory(current, name); + await (type === FileType.FILE + ? createFile(current, name, data) + : createDirectory(current, name)); + + update(); + } catch (err) { + error((err as Error).message); + } + } + + async function importFile() { + try { + const value = await importFH(current); + + if (!value) { + return warning('暂无法导入文件'); + } + + update(); + } catch (err) { + error((err as Error).message); + } + } + + async function importDirectory() { + try { + const value = await importDH(current); + + if (!value) { + return warning('暂无法导入文件夹'); } update(); @@ -140,19 +193,21 @@ export function useFileSystem(): FileSystem { function enterDirectory(file: FileInfo) { if (file.handle.kind === 'directory') { - setCurrent(file.handle); - fileLinked.current.inset(file.handle); + update(file.handle, () => fileLinked.current.inset(file.handle as DH)); } } function returnToRoot() { - setCurrent(root.current); - fileLinked.current = new FileLinkedList(root.current); + update( + root.current, + () => (fileLinked.current = new FileLinkedList(root.current)) + ); } return { current, children, + isBusy, fileLinked: fileLinked.current, fileHandles: fileHandlesRef.current, backgroundManager: backgroundManager.current, @@ -162,9 +217,17 @@ export function useFileSystem(): FileSystem { move, remove, create, + importFile, + importDirectory, forceUpdate: update }; - }, [current, children, fileLinked.current, backgroundManager.current]); + }, [ + current, + children, + fileLinked.current, + backgroundManager.current, + isBusy + ]); return fileSystem; } diff --git a/src/filehandle/utils/fileManager.ts b/src/filehandle/utils/fileManager.ts index ec9d47d..abc9cf1 100644 --- a/src/filehandle/utils/fileManager.ts +++ b/src/filehandle/utils/fileManager.ts @@ -18,13 +18,14 @@ export interface FileInfo { type: FileType; icon: React.ReactNode; handle: DH | FH; - /** this is webdav file? */ + /** this is remote file? */ remote?: boolean; ext?: string; } export async function getChildren(directory: DH, webdavs: WebdavInfo[] = []) { const children: FileInfo[] = []; + for await (const [key, handle] of directory.entries()) { const type = await getHandleType(handle); diff --git a/src/viewer/AboutSite.tsx b/src/viewer/AboutSite.tsx index 00913e2..3bc9688 100644 --- a/src/viewer/AboutSite.tsx +++ b/src/viewer/AboutSite.tsx @@ -38,7 +38,7 @@ const AboutSite = () => { 博客站点 diff --git a/src/viewer/Settings.tsx b/src/viewer/Settings.tsx index 772922d..23c7147 100644 --- a/src/viewer/Settings.tsx +++ b/src/viewer/Settings.tsx @@ -12,7 +12,7 @@ import { success } from '@/utils'; -import { getStorageUsage } from '@/filehandle'; +import { getStorageUsage } from '@/filehandle/utils/index'; import type { CanvasInstance } from '@/components'; import { Canvas } from '@/components';