From 3621d1952794478e4ac55f2385f2e63519d936ee Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Thu, 23 Jan 2025 22:44:18 +0800 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=9F=BA=E7=A1=80=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/storage/FileSystemFactory.ts | 15 ++++++ src/core/storage/IFileSystem.ts | 24 +++++++++ src/core/storage/TauriFileSystem.ts.tmp | 66 +++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/core/storage/FileSystemFactory.ts create mode 100644 src/core/storage/IFileSystem.ts create mode 100644 src/core/storage/TauriFileSystem.ts.tmp diff --git a/src/core/storage/FileSystemFactory.ts b/src/core/storage/FileSystemFactory.ts new file mode 100644 index 00000000..75bac99b --- /dev/null +++ b/src/core/storage/FileSystemFactory.ts @@ -0,0 +1,15 @@ +import { IFileSystem } from "./IFileSystem"; +import { TauriFileSystem } from "./TauriFileSystem"; + +export type StorageType = "tauri" | "memory"; + +export class FileSystemFactory { + static create(storageType: StorageType = "tauri"): IFileSystem { + switch (storageType) { + case "tauri": + return new TauriFileSystem(); + default: + throw new Error(`Unsupported storage type: ${storageType}`); + } + } +} diff --git a/src/core/storage/IFileSystem.ts b/src/core/storage/IFileSystem.ts new file mode 100644 index 00000000..b137d773 --- /dev/null +++ b/src/core/storage/IFileSystem.ts @@ -0,0 +1,24 @@ +export interface FileStats { + isFile: boolean; + isDirectory: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isFile: boolean; + isDirectory: boolean; +} + +export interface IFileSystem { + readFile(path: string): Promise; + writeFile(path: string, content: Uint8Array | string): Promise; + readDir(path: string): Promise; + mkdir(path: string, recursive?: boolean): Promise; + stat(path: string): Promise; + rename(oldPath: string, newPath: string): Promise; + deleteFile(path: string): Promise; + deleteDirectory(path: string): Promise; + exists(path: string): Promise; +} diff --git a/src/core/storage/TauriFileSystem.ts.tmp b/src/core/storage/TauriFileSystem.ts.tmp new file mode 100644 index 00000000..10a93815 --- /dev/null +++ b/src/core/storage/TauriFileSystem.ts.tmp @@ -0,0 +1,66 @@ +import { + readBinaryFile, + writeFile, + readDir, + createDir, + renameFile, + removeFile, + removeDir, + exists as tauriExists, + type FileEntry, +} from "@tauri-apps/plugin-fs"; +import { metadata } from "@tauri-apps/api/fs"; +import { DirectoryEntry, FileStats } from "./IFileSystem"; + +export class TauriFileSystem implements IFileSystem { + async readFile(path: string): Promise { + return await readBinaryFile(path); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + const options = + content instanceof Uint8Array + ? { contents: content } + : { contents: content }; + await writeFile({ path, ...options }); + } + + async readDir(dirPath: string): Promise { + const entries = await readDir(dirPath); + return entries.map((entry) => ({ + name: entry.name || "", + isFile: entry.children === undefined, + isDirectory: entry.children !== undefined, + })); + } + + async mkdir(dirPath: string, recursive = false): Promise { + await createDir(dirPath, { recursive }); + } + + async stat(filePath: string): Promise { + const md = await metadata(filePath); + return { + isFile: md.isFile, + isDirectory: md.isDir, + size: md.size || 0, + modified: new Date(md.modifiedAt || 0), + }; + } + + async rename(oldPath: string, newPath: string): Promise { + await renameFile(oldPath, newPath); + } + + async deleteFile(filePath: string): Promise { + await removeFile(filePath); + } + + async deleteDirectory(dirPath: string): Promise { + await removeDir(dirPath, { recursive: true }); + } + + async exists(path: string): Promise { + return await tauriExists(path); + } +} From 507cc231021530016607dc9f26141b274b43ffb2 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 25 Jan 2025 22:09:42 +0800 Subject: [PATCH 02/22] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=86=85?= =?UTF-8?q?=E9=83=A8indexedDB=E5=AD=98=E5=82=A8=E4=BB=A5=E5=8F=8Atauri?= =?UTF-8?q?=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/lib.rs | 141 ++++--- src/core/storage/IFileSystem.ts | 24 -- src/core/storage/TauriFileSystem.ts.tmp | 66 ---- .../storage => utils/fs}/FileSystemFactory.ts | 0 src/utils/fs/IFileSystem.ts | 28 ++ src/utils/fs/IndexedDBFileSystem.ts | 351 ++++++++++++++++++ src/utils/fs/TauriFileSystem.ts | 111 ++++++ 7 files changed, 578 insertions(+), 143 deletions(-) delete mode 100644 src/core/storage/IFileSystem.ts delete mode 100644 src/core/storage/TauriFileSystem.ts.tmp rename src/{core/storage => utils/fs}/FileSystemFactory.ts (100%) create mode 100644 src/utils/fs/IFileSystem.ts create mode 100644 src/utils/fs/IndexedDBFileSystem.ts create mode 100644 src/utils/fs/TauriFileSystem.ts diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b6eba25b..3c4b1efe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,77 +1,111 @@ -use std::env; -use std::io::Read; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::Path; +use std::time::UNIX_EPOCH; +use tauri::Manager; -use base64::engine::general_purpose; -use base64::Engine; +#[derive(Debug, Serialize, Deserialize)] +struct FileStats { + name: String, + #[serde(rename = "isDir")] + is_directory: bool, + size: u64, + modified: i64, // milliseconds since epoch +} -use tauri::Manager; +#[derive(Debug, Serialize, Deserialize)] +struct DirectoryEntry { + name: String, + #[serde(rename = "isDir")] + is_directory: bool, +} -/// 判断文件是否存在 #[tauri::command] -fn exists(path: String) -> bool { - std::path::Path::new(&path).exists() +async fn read_file(path: String) -> Result, String> { + std::fs::read(&path).map_err(|e| e.to_string()) } -/// 读取文件,返回字符串 #[tauri::command] -fn read_text_file(path: String) -> String { - let mut file = std::fs::File::open(path).unwrap(); - let mut contents = String::new(); - file.read_to_string(&mut contents).unwrap(); - contents +async fn write_file(path: String, content: Vec) -> Result<(), String> { + fs::write(path, content).map_err(|e| e.to_string()) } -/// 读取文件,返回base64 #[tauri::command] -fn read_file_base64(path: String) -> Result { - Ok(general_purpose::STANDARD - .encode(&std::fs::read(path).map_err(|e| format!("无法读取文件: {}", e))?)) +async fn read_dir(path: String) -> Result, String> { + let entries = fs::read_dir(path).map_err(|e| e.to_string())?; + let mut result = Vec::new(); + + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + let metadata = entry.metadata().map_err(|e| e.to_string())?; + + result.push(DirectoryEntry { + name: entry.file_name().to_string_lossy().to_string(), + is_directory: metadata.is_dir(), + }); + } + + Ok(result) } -/// 写入文件 #[tauri::command] -fn write_text_file(path: String, content: String) -> Result<(), String> { - std::fs::write(path, content).map_err(|e| e.to_string())?; - Ok(()) +async fn mkdir(path: String, recursive: bool) -> Result<(), String> { + if recursive { + fs::create_dir_all(path).map_err(|e| e.to_string()) + } else { + fs::create_dir(path).map_err(|e| e.to_string()) + } +} + +#[tauri::command] +async fn stat(path: String) -> Result { + let metadata = fs::metadata(&path).map_err(|e| e.to_string())?; + + // 获取文件名 + let name = Path::new(&path) + .file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()) + .ok_or_else(|| "无法解析文件名".to_string())?; + + // 转换修改时间 + let modified = metadata + .modified() + .map_err(|e| e.to_string())? + .duration_since(UNIX_EPOCH) + .map_err(|e| e.to_string())? + .as_millis() as i64; + + Ok(FileStats { + name, // 新增的name字段 + is_directory: metadata.is_dir(), + size: metadata.len(), + modified, + }) } -/// 写入文件,base64字符串 #[tauri::command] -fn write_file_base64(content: String, path: String) -> Result<(), String> { - std::fs::write( - &path, - &general_purpose::STANDARD - .decode(content) - .map_err(|e| format!("解码失败: {}", e))?, - ) - .map_err(|e| { - eprintln!("写入文件失败: {}", e); - return e.to_string(); - })?; - Ok(()) +async fn rename(old_path: String, new_path: String) -> Result<(), String> { + fs::rename(old_path, new_path).map_err(|e| e.to_string()) } #[tauri::command] -fn write_stdout(content: String) { - println!("{}", content); +async fn delete_file(path: String) -> Result<(), String> { + fs::remove_file(path).map_err(|e| e.to_string()) } #[tauri::command] -fn write_stderr(content: String) { - eprintln!("{}", content); +async fn delete_directory(path: String) -> Result<(), String> { + fs::remove_dir_all(path).map_err(|e| e.to_string()) } #[tauri::command] -fn exit(code: i32) { - std::process::exit(code); +async fn exists(path: String) -> Result { + Ok(fs::metadata(path).is_ok()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - // 在 Linux 上禁用 DMA-BUF 渲染器 - // 否则无法在 Linux 上运行 - // 相同的bug: https://github.com/tauri-apps/tauri/issues/10702 - // 解决方案来源: https://github.com/clash-verge-rev/clash-verge-rev/blob/ae5b2cfb79423c7e76a281725209b812774367fa/src-tauri/src/lib.rs#L27-L28 #[cfg(target_os = "linux")] std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); @@ -98,14 +132,15 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ - read_text_file, - write_text_file, - exists, - read_file_base64, - write_file_base64, - write_stdout, - write_stderr, - exit + read_file, + write_file, + read_dir, + mkdir, + stat, + rename, + delete_file, + delete_directory, + exists ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/core/storage/IFileSystem.ts b/src/core/storage/IFileSystem.ts deleted file mode 100644 index b137d773..00000000 --- a/src/core/storage/IFileSystem.ts +++ /dev/null @@ -1,24 +0,0 @@ -export interface FileStats { - isFile: boolean; - isDirectory: boolean; - size: number; - modified: Date; -} - -export interface DirectoryEntry { - name: string; - isFile: boolean; - isDirectory: boolean; -} - -export interface IFileSystem { - readFile(path: string): Promise; - writeFile(path: string, content: Uint8Array | string): Promise; - readDir(path: string): Promise; - mkdir(path: string, recursive?: boolean): Promise; - stat(path: string): Promise; - rename(oldPath: string, newPath: string): Promise; - deleteFile(path: string): Promise; - deleteDirectory(path: string): Promise; - exists(path: string): Promise; -} diff --git a/src/core/storage/TauriFileSystem.ts.tmp b/src/core/storage/TauriFileSystem.ts.tmp deleted file mode 100644 index 10a93815..00000000 --- a/src/core/storage/TauriFileSystem.ts.tmp +++ /dev/null @@ -1,66 +0,0 @@ -import { - readBinaryFile, - writeFile, - readDir, - createDir, - renameFile, - removeFile, - removeDir, - exists as tauriExists, - type FileEntry, -} from "@tauri-apps/plugin-fs"; -import { metadata } from "@tauri-apps/api/fs"; -import { DirectoryEntry, FileStats } from "./IFileSystem"; - -export class TauriFileSystem implements IFileSystem { - async readFile(path: string): Promise { - return await readBinaryFile(path); - } - - async writeFile(path: string, content: Uint8Array | string): Promise { - const options = - content instanceof Uint8Array - ? { contents: content } - : { contents: content }; - await writeFile({ path, ...options }); - } - - async readDir(dirPath: string): Promise { - const entries = await readDir(dirPath); - return entries.map((entry) => ({ - name: entry.name || "", - isFile: entry.children === undefined, - isDirectory: entry.children !== undefined, - })); - } - - async mkdir(dirPath: string, recursive = false): Promise { - await createDir(dirPath, { recursive }); - } - - async stat(filePath: string): Promise { - const md = await metadata(filePath); - return { - isFile: md.isFile, - isDirectory: md.isDir, - size: md.size || 0, - modified: new Date(md.modifiedAt || 0), - }; - } - - async rename(oldPath: string, newPath: string): Promise { - await renameFile(oldPath, newPath); - } - - async deleteFile(filePath: string): Promise { - await removeFile(filePath); - } - - async deleteDirectory(dirPath: string): Promise { - await removeDir(dirPath, { recursive: true }); - } - - async exists(path: string): Promise { - return await tauriExists(path); - } -} diff --git a/src/core/storage/FileSystemFactory.ts b/src/utils/fs/FileSystemFactory.ts similarity index 100% rename from src/core/storage/FileSystemFactory.ts rename to src/utils/fs/FileSystemFactory.ts diff --git a/src/utils/fs/IFileSystem.ts b/src/utils/fs/IFileSystem.ts new file mode 100644 index 00000000..ad283757 --- /dev/null +++ b/src/utils/fs/IFileSystem.ts @@ -0,0 +1,28 @@ +export interface FileStats { + name: string; + isDir: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isDir: boolean; +} + +// 强制使用 `/` 作为分隔符 +export abstract class IFileSystem { + abstract readFile(path: string): Promise; + abstract writeFile(path: string, content: Uint8Array | string): Promise; + abstract readDir(path: string): Promise; + abstract mkdir(path: string, recursive?: boolean): Promise; + abstract stat(path: string): Promise; + abstract rename(oldPath: string, newPath: string): Promise; + abstract deleteFile(path: string): Promise; + abstract deleteDirectory(path: string): Promise; + abstract exists(path: string): Promise; + async readTextFile(path: string) { + const content = await this.readFile(path); + return new TextDecoder("utf-8").decode(content); + } +} diff --git a/src/utils/fs/IndexedDBFileSystem.ts b/src/utils/fs/IndexedDBFileSystem.ts new file mode 100644 index 00000000..c8fc4518 --- /dev/null +++ b/src/utils/fs/IndexedDBFileSystem.ts @@ -0,0 +1,351 @@ +import { + IFileSystem, + type FileStats, + type DirectoryEntry, +} from "./IFileSystem"; + +const DB_VERSION = 1; + +export class IndexedDBFileSystem extends IFileSystem { + private db: IDBDatabase | null = null; + private DIR_STORE_NAME: string; + constructor( + private DB_NAME: string, + private STORE_NAME: string, + ) { + super(); + this.DIR_STORE_NAME = STORE_NAME + "_DIR"; + this.initDB(); + } + + private async initDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(this.DB_NAME, DB_VERSION); + + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + console.log( + this, + db.objectStoreNames.contains(this.STORE_NAME), + db.objectStoreNames.contains(this.DIR_STORE_NAME), + ); + if (!db.objectStoreNames.contains(this.STORE_NAME)) { + db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); + } + if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { + db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); + } + }; + + request.onsuccess = (event) => { + this.db = (event.target as IDBOpenDBRequest).result; + resolve(); + }; + + request.onerror = (event) => { + reject((event.target as IDBOpenDBRequest).error); + }; + }); + } + + private async getTransaction( + storeNames: string | string[], + mode: IDBTransactionMode = "readonly", + ): Promise { + if (!this.db) { + await this.initDB(); + } + return this.db!.transaction(storeNames, mode); + } + + private async getStore( + storeName: string, + mode: IDBTransactionMode = "readonly", + ): Promise { + const transaction = await this.getTransaction(storeName, mode); + return transaction.objectStore(storeName); + } + + async exists(path: string): Promise { + if (!this.db) { + await this.initDB(); + } + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); + + return new Promise((resolve) => { + const request = filesStore.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(true); + } else { + const dirRequest = dirsStore.get(path); + dirRequest.onsuccess = () => resolve(!!dirRequest.result); + dirRequest.onerror = () => resolve(false); + } + }; + request.onerror = () => resolve(false); + }); + } + + async readFile(path: string): Promise { + const store = await this.getStore(this.STORE_NAME); + return new Promise((resolve, reject) => { + const request = store.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(request.result.content); + } else { + reject(new Error(`File not found: ${path}`)); + } + }; + request.onerror = () => reject(request.error); + }); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + const store = await this.getStore(this.STORE_NAME, "readwrite"); + const data = + typeof content === "string" ? new TextEncoder().encode(content) : content; + + return new Promise((resolve, reject) => { + const request = store.put({ path, content: data }); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async readDir(path: string): Promise { + const store = await this.getStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const request = store.get(path); + request.onsuccess = () => { + if (request.result) { + resolve(request.result.entries); + } else { + resolve([]); + } + }; + request.onerror = () => reject(request.error); + }); + } + + async mkdir(path: string, recursive = false): Promise { + if (!recursive) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.put({ path, entries: [] }); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + // 递归创建父目录 + const parts = path.split("/"); + let currentPath = ""; + const pathsToCreate: string[] = []; + + // 收集需要创建的路径 + for (const part of parts) { + if (!part) continue; + currentPath = currentPath ? `${currentPath}/${part}` : part; + if (!(await this.exists(currentPath))) { + pathsToCreate.push(currentPath); + } + } + + // 在单个事务中创建所有目录 + if (pathsToCreate.length > 0) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + let completed = 0; + let error: IDBRequest | null = null; + + for (const path of pathsToCreate) { + const request = store.put({ path, entries: [] }); + request.onsuccess = () => { + completed++; + if (completed === pathsToCreate.length && !error) { + resolve(); + } + }; + request.onerror = () => { + if (!error) { + error = request; + reject(request.error); + } + }; + } + }); + } + } + + async stat(path: string): Promise { + const filesStore = await this.getStore(this.STORE_NAME); + const dirsStore = await this.getStore(this.DIR_STORE_NAME); + + return new Promise((resolve, reject) => { + const fileRequest = filesStore.get(path); + fileRequest.onsuccess = () => { + if (fileRequest.result) { + resolve({ + name: path.split("/").pop() || "", + isDir: false, + size: fileRequest.result.content.byteLength, + modified: new Date(), + }); + } else { + const dirRequest = dirsStore.get(path); + dirRequest.onsuccess = () => { + if (dirRequest.result) { + resolve({ + name: path.split("/").pop() || "", + isDir: true, + size: 0, + modified: new Date(), + }); + } else { + reject(new Error(`Path not found: ${path}`)); + } + }; + } + }; + }); + } + + async rename(oldPath: string, newPath: string): Promise { + const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); + const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + + return new Promise((resolve, reject) => { + const moveFile = () => { + const request = filesStore.get(oldPath); + request.onsuccess = () => { + if (request.result) { + const content = request.result.content; + const deleteRequest = filesStore.delete(oldPath); + deleteRequest.onsuccess = () => { + const putRequest = filesStore.put({ path: newPath, content }); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }; + } else { + moveDir(); + } + }; + }; + + const moveDir = () => { + const request = dirsStore.get(oldPath); + request.onsuccess = () => { + if (request.result) { + const entries = request.result.entries; + const deleteRequest = dirsStore.delete(oldPath); + deleteRequest.onsuccess = () => { + const putRequest = dirsStore.put({ path: newPath, entries }); + putRequest.onsuccess = () => resolve(); + putRequest.onerror = () => reject(putRequest.error); + }; + } else { + reject(new Error(`Path not found: ${oldPath}`)); + } + }; + }; + + moveFile(); + }); + } + + async deleteFile(path: string): Promise { + const store = await this.getStore(this.STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.delete(path); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + async deleteDirectory(path: string): Promise { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + return new Promise((resolve, reject) => { + const request = store.delete(path); + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); + } + + /** + * 验证文件内容是否匹配 + * @param fs 文件系统实例 + * @param path 文件路径 + * @param expectedContent 预期内容 + */ + private static async verifyFileContent( + fs: IndexedDBFileSystem, + path: string, + expectedContent: string, + ): Promise { + const content = await fs.readFile(path); + const actualContent = new TextDecoder("utf-8").decode(content); + if (actualContent !== expectedContent) { + throw new Error( + `File content verification failed at ${path}\n` + + `Expected: ${expectedContent}\n` + + `Actual: ${actualContent}`, + ); + } + } + + /** + * 测试IndexedDB文件系统功能 + * @param dbName 测试数据库名称 + * @param storeName 测试存储名称 + */ + static async testFileSystem( + dbName: string, + storeName: string, + ): Promise { + const fs = new IndexedDBFileSystem(dbName, storeName); + + // 初始化数据库 + await fs.initDB(); + + // 测试目录操作 + const testDirPath = "/test-dir"; + await fs.mkdir(testDirPath, true); + console.log(`Created directory: ${testDirPath}`); + + // 测试文件操作 + const testFilePath = `${testDirPath}/test.txt`; + const testContent = "Hello IndexedDB File System!"; + + // 写入前验证文件不存在 + if (await fs.exists(testFilePath)) { + throw new Error(`File already exists: ${testFilePath}`); + } + + // 写入文件 + await fs.writeFile(testFilePath, testContent); + console.log(`Wrote file: ${testFilePath}`); + + // 写入后验证内容 + await this.verifyFileContent(fs, testFilePath, testContent); + console.log(`Verified file content: ${testFilePath}`); + + // 删除文件 + await fs.deleteFile(testFilePath); + console.log(`Deleted file: ${testFilePath}`); + + // 删除目录 + await fs.deleteDirectory(testDirPath); + console.log(`Deleted directory: ${testDirPath}`); + + // 清理测试数据库 + indexedDB.deleteDatabase(dbName); + console.log(`Cleaned up test database: ${dbName}`); + } +} diff --git a/src/utils/fs/TauriFileSystem.ts b/src/utils/fs/TauriFileSystem.ts new file mode 100644 index 00000000..a7435f31 --- /dev/null +++ b/src/utils/fs/TauriFileSystem.ts @@ -0,0 +1,111 @@ +import { invoke } from "@tauri-apps/api/core"; +import { IFileSystem, FileStats, DirectoryEntry } from "./IFileSystem"; + +/** + * Tauri 文件系统工具类 + */ +export class TauriFileSystem extends IFileSystem { + async exists(path: string): Promise { + return invoke("exists", { path }); + } + + async readFile(path: string): Promise { + return new Uint8Array(await invoke("read_file", { path })); + } + + async writeFile(path: string, content: Uint8Array | string): Promise { + let data: Uint8Array; + if (typeof content === "string") { + data = new TextEncoder().encode(content); + } else { + data = content; + } + return invoke("write_file", { + path, + content: Array.from(data), + }); + } + + async mkdir(path: string, recursive = false): Promise { + return invoke("mkdir", { path, recursive }); + } + + async stat(path: string): Promise { + return invoke("stat", { path }); + } + + async readDir(path: string): Promise { + return invoke("read_dir", { path }); + } + + async rename(oldPath: string, newPath: string): Promise { + return invoke("rename", { oldPath, newPath }); + } + + async deleteFile(path: string): Promise { + return invoke("delete_file", { path }); + } + + async deleteDirectory(path: string): Promise { + return invoke("delete_directory", { path }); + } + + /** + * 验证文件内容是否匹配 + * @param fs 文件系统实例 + * @param path 文件路径 + * @param expectedContent 预期内容 + */ + private static async verifyFileContent( + fs: TauriFileSystem, + path: string, + expectedContent: string, + ): Promise { + const content = await fs.readFile(path); + const actualContent = new TextDecoder("utf-8").decode(content); + if (actualContent !== expectedContent) { + throw new Error( + `File content verification failed at ${path}\n` + + `Expected: ${expectedContent}\n` + + `Actual: ${actualContent}`, + ); + } + } + + /** + * 测试Tauri文件系统功能 + * @param dirPath 要测试的目录路径 + */ + static async testFileSystem(dirPath: string): Promise { + const fs = new TauriFileSystem(); + + // 测试目录操作 + await fs.mkdir(dirPath, true); + console.log(`Created directory: ${dirPath}`); + + // 测试文件操作 + const testFilePath = `${dirPath}/test.txt`; + const testContent = "Hello Tauri File System!"; + + // 写入前验证文件不存在 + if (await fs.exists(testFilePath)) { + throw new Error(`File already exists: ${testFilePath}`); + } + + // 写入文件 + await fs.writeFile(testFilePath, testContent); + console.log(`Wrote file: ${testFilePath}`); + + // 写入后验证内容 + await this.verifyFileContent(fs, testFilePath, testContent); + console.log(`Verified file content: ${testFilePath}`); + + // 删除文件 + await fs.deleteFile(testFilePath); + console.log(`Deleted file: ${testFilePath}`); + + // 删除目录 + await fs.deleteDirectory(dirPath); + console.log(`Deleted directory: ${dirPath}`); + } +} From 8356e70b752978450e58453a5e92eb654391d2a1 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Wed, 29 Jan 2025 20:34:41 +0800 Subject: [PATCH 03/22] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E8=87=B3VFileS?= =?UTF-8?q?ystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/lib.rs | 27 ++- src/cli.tsx | 4 +- src/core/service/RecentFileManager.tsx | 2 +- src/core/service/SoundService.tsx | 2 +- src/core/service/VFileSystem.tsx | 97 +++++++++ .../controller/concrete/ControllerCopy.tsx | 37 ++-- .../concrete/ControllerDragFile.tsx | 8 +- src/core/stage/StageSaveManager.tsx | 20 +- .../stage/stageObject/entity/ImageNode.tsx | 8 +- src/main.tsx | 5 +- src/pages/_app_menu.tsx | 20 +- src/pages/_start_file_panel.tsx | 8 +- src/pages/_toolbar.tsx | 6 +- src/pages/index.tsx | 6 + ...SystemFactory.ts => FileSystemFactory.tsx} | 0 src/utils/fs/IFileSystem.ts | 28 --- src/utils/fs/IFileSystem.tsx | 117 ++++++++++ ...BFileSystem.ts => IndexedDBFileSystem.tsx} | 203 +++++++++++------- ...TauriFileSystem.ts => TauriFileSystem.tsx} | 46 ++-- src/utils/fs/WebFileApiSystem.tsx | 192 +++++++++++++++++ src/utils/{fs.tsx => fs/com.tsx} | 136 ++++++------ src/utils/platform.tsx | 4 + 22 files changed, 721 insertions(+), 255 deletions(-) create mode 100644 src/core/service/VFileSystem.tsx rename src/utils/fs/{FileSystemFactory.ts => FileSystemFactory.tsx} (100%) delete mode 100644 src/utils/fs/IFileSystem.ts create mode 100644 src/utils/fs/IFileSystem.tsx rename src/utils/fs/{IndexedDBFileSystem.ts => IndexedDBFileSystem.tsx} (63%) rename src/utils/fs/{TauriFileSystem.ts => TauriFileSystem.tsx} (62%) create mode 100644 src/utils/fs/WebFileApiSystem.tsx rename src/utils/{fs.tsx => fs/com.tsx} (52%) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c4b1efe..58006a36 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,16 +1,21 @@ use serde::{Deserialize, Serialize}; use std::fs; -use std::path::Path; +use std::path::{Path, MAIN_SEPARATOR}; use std::time::UNIX_EPOCH; use tauri::Manager; +// 新增路径规范化函数 +fn normalize_path(path: &str) -> String { + path.replace('/', &MAIN_SEPARATOR.to_string()) +} + #[derive(Debug, Serialize, Deserialize)] struct FileStats { name: String, #[serde(rename = "isDir")] is_directory: bool, size: u64, - modified: i64, // milliseconds since epoch + modified: i64, } #[derive(Debug, Serialize, Deserialize)] @@ -22,16 +27,19 @@ struct DirectoryEntry { #[tauri::command] async fn read_file(path: String) -> Result, String> { + let path = normalize_path(&path); std::fs::read(&path).map_err(|e| e.to_string()) } #[tauri::command] async fn write_file(path: String, content: Vec) -> Result<(), String> { + let path = normalize_path(&path); fs::write(path, content).map_err(|e| e.to_string()) } #[tauri::command] async fn read_dir(path: String) -> Result, String> { + let path = normalize_path(&path); let entries = fs::read_dir(path).map_err(|e| e.to_string())?; let mut result = Vec::new(); @@ -50,6 +58,7 @@ async fn read_dir(path: String) -> Result, String> { #[tauri::command] async fn mkdir(path: String, recursive: bool) -> Result<(), String> { + let path = normalize_path(&path); if recursive { fs::create_dir_all(path).map_err(|e| e.to_string()) } else { @@ -59,16 +68,15 @@ async fn mkdir(path: String, recursive: bool) -> Result<(), String> { #[tauri::command] async fn stat(path: String) -> Result { - let metadata = fs::metadata(&path).map_err(|e| e.to_string())?; + let normalized_path = normalize_path(&path); + let metadata = fs::metadata(&normalized_path).map_err(|e| e.to_string())?; - // 获取文件名 - let name = Path::new(&path) + let name = Path::new(&normalized_path) .file_name() .and_then(|n| n.to_str()) .map(|s| s.to_string()) .ok_or_else(|| "无法解析文件名".to_string())?; - // 转换修改时间 let modified = metadata .modified() .map_err(|e| e.to_string())? @@ -77,7 +85,7 @@ async fn stat(path: String) -> Result { .as_millis() as i64; Ok(FileStats { - name, // 新增的name字段 + name, is_directory: metadata.is_dir(), size: metadata.len(), modified, @@ -86,21 +94,26 @@ async fn stat(path: String) -> Result { #[tauri::command] async fn rename(old_path: String, new_path: String) -> Result<(), String> { + let old_path = normalize_path(&old_path); + let new_path = normalize_path(&new_path); fs::rename(old_path, new_path).map_err(|e| e.to_string()) } #[tauri::command] async fn delete_file(path: String) -> Result<(), String> { + let path = normalize_path(&path); fs::remove_file(path).map_err(|e| e.to_string()) } #[tauri::command] async fn delete_directory(path: String) -> Result<(), String> { + let path = normalize_path(&path); fs::remove_dir_all(path).map_err(|e| e.to_string()) } #[tauri::command] async fn exists(path: String) -> Result { + let path = normalize_path(&path); Ok(fs::metadata(path).is_ok()) } diff --git a/src/cli.tsx b/src/cli.tsx index efdd7019..511f560b 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,7 +1,7 @@ import { CliMatches } from "@tauri-apps/plugin-cli"; import { StageDumperSvg } from "./core/stage/StageDumperSvg"; -import { writeTextFile } from "./utils/fs"; import { writeStdout } from "./utils/otherApi"; +import { TauriBaseFS } from "./utils/fs/TauriFileSystem"; export async function runCli(matches: CliMatches) { if (matches.args.output?.occurrences > 0) { @@ -13,7 +13,7 @@ export async function runCli(matches: CliMatches) { if (outputPath === "-") { writeStdout(result); } else { - await writeTextFile(outputPath, result); + await TauriBaseFS.writeTextFile(outputPath, result); } } else { throw new Error("Invalid output format. Only SVG format is supported."); diff --git a/src/core/service/RecentFileManager.tsx b/src/core/service/RecentFileManager.tsx index a4f577b9..03dc4886 100644 --- a/src/core/service/RecentFileManager.tsx +++ b/src/core/service/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../types/node"; -import { exists, readTextFile } from "../../utils/fs"; +import { exists, readTextFile } from "../../utils/fs/com"; import { createStore } from "../../utils/store"; import { Camera } from "../stage/Camera"; import { Stage } from "../stage/Stage"; diff --git a/src/core/service/SoundService.tsx b/src/core/service/SoundService.tsx index 64bb9fc5..a397395b 100644 --- a/src/core/service/SoundService.tsx +++ b/src/core/service/SoundService.tsx @@ -2,7 +2,7 @@ // @tauri-apps/plugin-fs 只能读取文本文件,不能强行读取流文件并强转为ArrayBuffer // import { readTextFile } from "@tauri-apps/plugin-fs"; -import { readFile } from "../../utils/fs"; +import { readFile } from "../../utils/fs/com"; import { StringDict } from "../dataStruct/StringDict"; import { Settings } from "./Settings"; diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/VFileSystem.tsx new file mode 100644 index 00000000..f6751cd3 --- /dev/null +++ b/src/core/service/VFileSystem.tsx @@ -0,0 +1,97 @@ +import JSZip, * as jszip from "jszip"; +import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; +import { readFile } from "../../utils/fs/com"; + +export enum FSType { + Tauri = "Tauri", + WebFS = "WebFS", + IndexedDB = "IndexedDB", +} +export namespace VFileSystem { + const fs = new IndexedDBFileSystem("PG", "Project"); + export async function getMetaData() { + return fs.readTextFile("/meta.json"); + } + export async function setMetaData(content: string) { + return fs.writeTextFile("/meta.json", content); + } + export async function loadFromPath(path: string) { + const data = await readFile(path); + const zip = await jszip.loadAsync(data); + const entries = zip.files; + + const operations: Promise[] = []; + + for (const [rawPath, file] of Object.entries(entries)) { + // 标准化路径:替换多个斜杠为单个,并移除末尾斜杠 + const normalizedPath = rawPath.replace(/\/+/g, "/").replace(/\/$/, ""); + + if (file.dir) { + await fs.mkdir(normalizedPath, true); + } else { + // 处理文件 + operations.push( + (async () => { + try { + // 分离目录和文件名 + const lastSlashIndex = normalizedPath.lastIndexOf("/"); + const parentDir = + lastSlashIndex >= 0 + ? normalizedPath.slice(0, lastSlashIndex) + : ""; + + // 创建父目录(如果存在) + if (parentDir) { + await fs.mkdir(parentDir, true); + } + + // 写入文件内容 + const content = await file.async("uint8array"); + await fs.writeFile(normalizedPath, content); + } catch (error) { + console.error(`Process file failed: ${normalizedPath}`, error); + } + })(), + ); + } + } + + await Promise.all(operations); + } + export async function exportZipData(): Promise { + const zip = new JSZip(); + + // 递归添加目录和文件到zip + async function addToZip(zipParent: jszip, path: string) { + console.log(zipParent, path); + const entries = await fs.readDir(path); + + for (const entry of entries) { + const fullPath = path ? `${path}/${entry.name}` : entry.name; + + if (entry.isDir) { + // 创建目录节点并递归处理子项 + const dirZip = zipParent.folder(entry.name); + await addToZip(dirZip!, fullPath); + } else { + // 添加文件内容到zip + const content = await fs.readFile(fullPath); + zipParent.file(entry.name, content); + } + } + } + + // 从根目录开始处理 + await addToZip(zip, "/"); + + // 生成zip文件内容 + return zip.generateAsync({ + type: "uint8array", + compression: "DEFLATE", // 使用压缩 + compressionOptions: { level: 6 }, + }); + } + export function getFS() { + return fs; + } +} diff --git a/src/core/service/controller/concrete/ControllerCopy.tsx b/src/core/service/controller/concrete/ControllerCopy.tsx index 3ee499aa..3fc3554f 100644 --- a/src/core/service/controller/concrete/ControllerCopy.tsx +++ b/src/core/service/controller/concrete/ControllerCopy.tsx @@ -1,6 +1,5 @@ import { v4 as uuidv4 } from "uuid"; import { Dialog } from "../../../../utils/dialog"; -import { writeFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; @@ -15,6 +14,7 @@ import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { Entity } from "../../../stage/stageObject/StageObject"; import { Controller } from "../Controller"; import { ControllerClass } from "../ControllerClass"; +import { VFileSystem } from "../../VFileSystem"; /** * 关于复制相关的功能 @@ -124,13 +124,14 @@ async function readClipboardItems(mouseLocation: Vector) { } const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); - const folder = PathString.dirPath(Stage.Path.getFilePath()); - const imagePath = `${folder}${Stage.Path.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败 // writeFile(imagePath, new Uint8Array(await blob.arrayBuffer())); // 下面这样的写法是没有问题的 - writeFileBase64(imagePath, await convertBlobToBase64(blob)); + VFileSystem.getFS().writeFile( + `/picture/${imageUUID}.png`, + await blob.bytes(), + ); // 要延迟一下,等待保存完毕 setTimeout(() => { @@ -190,17 +191,17 @@ function blobToText(blob: Blob): Promise { }); } -async function convertBlobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === "string") { - resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 - } else { - reject(new Error("Invalid result type")); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); -} +// async function convertBlobToBase64(blob: Blob): Promise { +// return new Promise((resolve, reject) => { +// const reader = new FileReader(); +// reader.onloadend = () => { +// if (typeof reader.result === "string") { +// resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 +// } else { +// reject(new Error("Invalid result type")); +// } +// }; +// reader.onerror = reject; +// reader.readAsDataURL(blob); +// }); +// } diff --git a/src/core/service/controller/concrete/ControllerDragFile.tsx b/src/core/service/controller/concrete/ControllerDragFile.tsx index 517d9160..f538538a 100644 --- a/src/core/service/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controller/concrete/ControllerDragFile.tsx @@ -1,6 +1,4 @@ import { v4 as uuidv4 } from "uuid"; -import { writeFileBase64 } from "../../../../utils/fs"; -import { PathString } from "../../../../utils/pathString"; import { Color } from "../../../dataStruct/Color"; import { Vector } from "../../../dataStruct/Vector"; import { Renderer } from "../../../render/canvas2d/renderer"; @@ -12,6 +10,7 @@ import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; +import { VFileSystem } from "../../VFileSystem"; // import { listen } from "@tauri-apps/api/event"; // listen("tauri://file-drop", (event) => { @@ -195,9 +194,8 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - const folderPath = PathString.dirPath(Stage.Path.getFilePath()); - writeFileBase64( - `${folderPath}${Stage.Path.getSep()}${imageUUID}.png`, + VFileSystem.getFS().writeFileBase64( + `/picture/${imageUUID}.png`, fileContent.split(",")[1], ); const imageNode = new ImageNode({ diff --git a/src/core/stage/StageSaveManager.tsx b/src/core/stage/StageSaveManager.tsx index df0f6e99..c8129bc2 100644 --- a/src/core/stage/StageSaveManager.tsx +++ b/src/core/stage/StageSaveManager.tsx @@ -1,7 +1,8 @@ import { Serialized } from "../../types/node"; -import { exists, writeTextFile } from "../../utils/fs"; +import { exists, writeFile, writeTextFile } from "../../utils/fs/com"; import { PathString } from "../../utils/pathString"; import { ViewFlashEffect } from "../service/effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "../service/VFileSystem"; import { Stage } from "./Stage"; import { StageHistoryManager } from "./stageManager/StageHistoryManager"; import { StageManager } from "./stageManager/StageManager"; @@ -20,7 +21,8 @@ export namespace StageSaveManager { * @param errorCallback */ export async function saveHandle(path: string, data: Serialized.File) { - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(path, await VFileSystem.exportZipData()); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); StageHistoryManager.reset(data); // 重置历史 isCurrentSaved = true; @@ -41,7 +43,11 @@ export namespace StageSaveManager { if (Stage.Path.isDraft()) { throw new Error("当前文档的状态为草稿,请您先保存为文件"); } - await writeTextFile(Stage.Path.getFilePath(), JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile( + Stage.Path.getFilePath(), + await VFileSystem.exportZipData(), + ); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } @@ -65,8 +71,8 @@ export namespace StageSaveManager { if (!isExists) { throw new Error("备份文件路径错误:" + backupFolderPath); } - - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(path, await VFileSystem.exportZipData()); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } /** @@ -89,8 +95,8 @@ export namespace StageSaveManager { const dateTime = PathString.getTime(); const backupPath = `${Stage.Path.getFilePath()}.${dateTime}.backup`; - - await writeTextFile(backupPath, JSON.stringify(data)); + await VFileSystem.setMetaData(JSON.stringify(data)); + await writeFile(backupPath, await VFileSystem.exportZipData()); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } diff --git a/src/core/stage/stageObject/entity/ImageNode.tsx b/src/core/stage/stageObject/entity/ImageNode.tsx index 195f0e0e..89f974f6 100644 --- a/src/core/stage/stageObject/entity/ImageNode.tsx +++ b/src/core/stage/stageObject/entity/ImageNode.tsx @@ -1,12 +1,11 @@ -import { join } from "@tauri-apps/api/path"; import { Serialized } from "../../../../types/node"; -import { readFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; import { Stage } from "../../Stage"; import { CollisionBox } from "../collisionBox/collisionBox"; import { ConnectableEntity } from "../StageObject"; +import { VFileSystem } from "../../../service/VFileSystem"; /** * 一个图片节点 @@ -117,13 +116,14 @@ export class ImageNode extends ConnectableEntity { * @param folderPath 工程文件所在路径文件夹,不加尾部斜杠 * @returns */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars public updateBase64StringByPath(folderPath: string) { if (this.path === "") { return; } - join(folderPath, this.path) - .then((path) => readFileBase64(path)) + VFileSystem.getFS() + .readFileBase64(`/picture/${this.path}`) .then((res) => { // 获取base64String成功 diff --git a/src/main.tsx b/src/main.tsx index b8e640ce..580668da 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -36,11 +36,13 @@ import "./index.pcss"; import { ColorPanel } from "./pages/_toolbar"; import "./polyfills/roundRect"; import { Dialog } from "./utils/dialog"; -import { exists } from "./utils/fs"; +import { exists } from "./utils/fs/com"; import { exit, openDevtools, writeStderr, writeStdout } from "./utils/otherApi"; import { getCurrentWindow, isDesktop, isWeb } from "./utils/platform"; import { Popup } from "./utils/popup"; import { InputElement } from "./core/render/domElement/inputElement"; +// import { VFileSystem } from "./core/service/VFileSystem"; +import { IndexedDBFileSystem } from "./utils/fs/IndexedDBFileSystem"; const router = createMemoryRouter(routes); const Routes = () => ; @@ -52,6 +54,7 @@ const el = document.getElementById("root")!; (async () => { const matches = !isWeb && isDesktop ? await getMatches() : null; const isCliMode = isDesktop && matches?.args.output?.occurrences === 1; + IndexedDBFileSystem.testFileSystem("A", "B"); await Promise.all([ Settings.init(), RecentFileManager.init(), diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 4e20c640..d221fbb7 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -152,7 +152,7 @@ export default function AppMenu({ ? [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ] : [], @@ -160,9 +160,9 @@ export default function AppMenu({ if (!path) { return; } - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个gp文件", type: "error", }); return; @@ -173,7 +173,7 @@ export default function AppMenu({ setFile(path); } catch (e) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的gp文件", content: String(e), type: "error", }); @@ -194,7 +194,8 @@ export default function AppMenu({ // await writeTextFile(path, JSON.stringify(data, null, 2)); // 将数据写入文件 try { await StageSaveManager.saveHandle(path_, data); - } catch { + } catch (e) { + console.error(e); await Dialog.show({ title: "保存失败", content: "保存失败,请重试", @@ -204,14 +205,14 @@ export default function AppMenu({ const onSaveNew = async () => { const path = isWeb - ? "file.json" + ? "file.gp" : await saveFileDialog({ title: "另存为", - defaultPath: "新文件.json", // 提供一个默认的文件名 + defaultPath: "新文件.gp", // 提供一个默认的文件名 filters: [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ], }); @@ -224,7 +225,8 @@ export default function AppMenu({ try { await StageSaveManager.saveHandle(path, data); setFile(path); - } catch { + } catch (e) { + console.error(e); await Dialog.show({ title: "保存失败", content: "保存失败,请重试", diff --git a/src/pages/_start_file_panel.tsx b/src/pages/_start_file_panel.tsx index b1ea08d4..67642536 100644 --- a/src/pages/_start_file_panel.tsx +++ b/src/pages/_start_file_panel.tsx @@ -74,7 +74,7 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { ? [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ] : [], @@ -82,9 +82,9 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { if (!path) { return; } - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个gp文件", type: "error", }); return; @@ -101,7 +101,7 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { updateStartFiles(); } catch (e) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的gp文件", content: String(e), type: "error", }); diff --git a/src/pages/_toolbar.tsx b/src/pages/_toolbar.tsx index f2957176..8733b534 100644 --- a/src/pages/_toolbar.tsx +++ b/src/pages/_toolbar.tsx @@ -40,7 +40,7 @@ import { StageManager } from "../core/stage/stageManager/StageManager"; import { cn } from "../utils/cn"; // import { StageSaveManager } from "../core/stage/StageSaveManager"; import { Dialog } from "../utils/dialog"; -import { writeTextFile } from "../utils/fs"; +import { writeTextFile } from "../utils/fs/com"; import { Popup } from "../utils/popup"; // import { PathString } from "../utils/pathString"; import { ColorManager } from "../core/service/ColorManager"; @@ -500,11 +500,11 @@ export default function Toolbar({ className = "" }: { className?: string }) { const onSaveSelectedNew = async () => { const path = await saveFileDialog({ title: "另存为", - defaultPath: "新文件.json", // 提供一个默认的文件名 + defaultPath: "新文件.gp", // 提供一个默认的文件名 filters: [ { name: "Project Graph", - extensions: ["json"], + extensions: ["gp"], }, ], }); diff --git a/src/pages/index.tsx b/src/pages/index.tsx index d4704369..bf68bcc2 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -11,6 +11,7 @@ import DetailsEditSidePanel from "./_details_edit_side_panel"; import HintText from "./_hint_text"; import SearchingNodePanel from "./_searching_node_panel"; import Toolbar from "./_toolbar"; +// import { WebFileApiSystem } from "../utils/fs/WebFileApiSystem"; export default function Home() { const canvasRef: React.RefObject = useRef(null); @@ -51,6 +52,11 @@ export default function Home() { }); } + // window.addEventListener("click", () => { + // const p = window.showDirectoryPicker(); + // const sys = new WebFileApiSystem(p); + // sys.mkdir("/a/b/c/d/e/f", true); + // }); window.addEventListener("resize", handleResize); window.addEventListener("focus", handleFocus); window.addEventListener("blur", handleBlur); diff --git a/src/utils/fs/FileSystemFactory.ts b/src/utils/fs/FileSystemFactory.tsx similarity index 100% rename from src/utils/fs/FileSystemFactory.ts rename to src/utils/fs/FileSystemFactory.tsx diff --git a/src/utils/fs/IFileSystem.ts b/src/utils/fs/IFileSystem.ts deleted file mode 100644 index ad283757..00000000 --- a/src/utils/fs/IFileSystem.ts +++ /dev/null @@ -1,28 +0,0 @@ -export interface FileStats { - name: string; - isDir: boolean; - size: number; - modified: Date; -} - -export interface DirectoryEntry { - name: string; - isDir: boolean; -} - -// 强制使用 `/` 作为分隔符 -export abstract class IFileSystem { - abstract readFile(path: string): Promise; - abstract writeFile(path: string, content: Uint8Array | string): Promise; - abstract readDir(path: string): Promise; - abstract mkdir(path: string, recursive?: boolean): Promise; - abstract stat(path: string): Promise; - abstract rename(oldPath: string, newPath: string): Promise; - abstract deleteFile(path: string): Promise; - abstract deleteDirectory(path: string): Promise; - abstract exists(path: string): Promise; - async readTextFile(path: string) { - const content = await this.readFile(path); - return new TextDecoder("utf-8").decode(content); - } -} diff --git a/src/utils/fs/IFileSystem.tsx b/src/utils/fs/IFileSystem.tsx new file mode 100644 index 00000000..4f8ac636 --- /dev/null +++ b/src/utils/fs/IFileSystem.tsx @@ -0,0 +1,117 @@ +export interface FileStats { + name: string; + isDir: boolean; + size: number; + modified: Date; +} + +export interface DirectoryEntry { + name: string; + isDir: boolean; +} +export function base64ToUint8Array(base64String: string) { + const padding = "=".repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/"); + + const sliceSize = 572; + const bytes = new Uint8Array((base64.length / 4) * 3); + let byteIndex = 0; + + for (let offset = 0; offset < base64.length; offset += sliceSize) { + const slice = base64.slice(offset, offset + sliceSize); + + const byteChars = atob(slice); + + for (let i = 0; i < byteChars.length; i++) { + bytes[byteIndex++] = byteChars.charCodeAt(i); + } + } + + return bytes.subarray(0, byteIndex); +} + +export function uint8ArrayToBase64(u8Arr: Uint8Array): string { + let binaryStr = ""; + for (let i = 0; i < u8Arr.length; i++) { + binaryStr += String.fromCharCode(u8Arr[i]); + } + return btoa(binaryStr); +} + +// 强制使用 `/` 作为分隔符 +export abstract class IFileSystem { + // 私有方法:统一规范化路径格式 + static normalizePath(path: string): string { + return path.replace(/[\\/]+/g, "/"); // 替换所有分隔符为 / + } + + // 抽象原始方法(带下划线版本) + abstract _readFile(path: string): Promise; + abstract _writeFile( + path: string, + content: Uint8Array | string, + ): Promise; + abstract _readDir(path: string): Promise; + abstract _mkdir(path: string, recursive?: boolean): Promise; + abstract _stat(path: string): Promise; + abstract _rename(oldPath: string, newPath: string): Promise; + abstract _deleteFile(path: string): Promise; + abstract _deleteDirectory(path: string): Promise; + abstract _exists(path: string): Promise; + + // 公共方法(自动处理路径分隔符) + readFile(path: string) { + return this._readFile(IFileSystem.normalizePath(path)); + } + + writeFile(path: string, content: Uint8Array | string) { + return this._writeFile(IFileSystem.normalizePath(path), content); + } + + readDir(path: string) { + return this._readDir(IFileSystem.normalizePath(path)); + } + + mkdir(path: string, recursive?: boolean) { + return this._mkdir(IFileSystem.normalizePath(path), recursive); + } + + stat(path: string) { + return this._stat(IFileSystem.normalizePath(path)); + } + + rename(oldPath: string, newPath: string) { + return this._rename( + IFileSystem.normalizePath(oldPath), + IFileSystem.normalizePath(newPath), + ); + } + + deleteFile(path: string) { + return this._deleteFile(IFileSystem.normalizePath(path)); + } + + deleteDirectory(path: string) { + return this._deleteDirectory(IFileSystem.normalizePath(path)); + } + + exists(path: string) { + return this._exists(IFileSystem.normalizePath(path)); + } + + async readTextFile(path: string) { + const content = await this.readFile(path); // 注意这里调用的是处理后的公共方法 + return new TextDecoder("utf-8").decode(content); + } + async writeTextFile(path: string, content: string) { + const text = new TextEncoder().encode(content); + return this.writeFile(path, text); // 注意这里调用的是处理后的公共方法 + } + + async readFileBase64(path: string) { + return uint8ArrayToBase64(await this.readFile(path)); + } + async writeFileBase64(path: string, str: string) { + return this.writeFile(path, base64ToUint8Array(str)); + } +} diff --git a/src/utils/fs/IndexedDBFileSystem.ts b/src/utils/fs/IndexedDBFileSystem.tsx similarity index 63% rename from src/utils/fs/IndexedDBFileSystem.ts rename to src/utils/fs/IndexedDBFileSystem.tsx index c8fc4518..fc329743 100644 --- a/src/utils/fs/IndexedDBFileSystem.ts +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -65,8 +65,53 @@ export class IndexedDBFileSystem extends IFileSystem { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } + private normalizePath(path: string): string { + let normalized = path.replace(/\/+/g, "/").replace(/\/$/, ""); + if (normalized === "") return "/"; + if (!normalized.startsWith("/")) normalized = "/" + normalized; + return normalized; + } + + private getParentPath(path: string): string | null { + const normalized = this.normalizePath(path); + if (normalized === "/") return null; + const parts = normalized.split("/").filter((p) => p !== ""); + if (parts.length === 0) return null; + parts.pop(); + return parts.length === 0 ? "/" : `/${parts.join("/")}`; + } + + private async addEntryToParent( + childPath: string, + isDir: boolean, + ): Promise { + const parentPath = this.getParentPath(childPath); + if (!parentPath) return; + + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parentDir = await new Promise((resolve, reject) => { + const req = store.get(parentPath); + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + + if (!parentDir) throw new Error(`Parent directory ${parentPath} not found`); + + const childName = childPath.split("/").pop()!; + const exists = parentDir.entries.some( + (e: DirectoryEntry) => e.name === childName, + ); + if (!exists) { + parentDir.entries.push({ name: childName, isDir }); + await new Promise((resolve, reject) => { + const req = store.put(parentDir); + req.onsuccess = () => resolve(); + req.onerror = () => reject(req.error); + }); + } + } - async exists(path: string): Promise { + async _exists(path: string): Promise { if (!this.db) { await this.initDB(); } @@ -76,7 +121,6 @@ export class IndexedDBFileSystem extends IFileSystem { ); const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve) => { const request = filesStore.get(path); request.onsuccess = () => { @@ -92,7 +136,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async readFile(path: string): Promise { + async _readFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME); return new Promise((resolve, reject) => { const request = store.get(path); @@ -107,86 +151,77 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async writeFile(path: string, content: Uint8Array | string): Promise { - const store = await this.getStore(this.STORE_NAME, "readwrite"); - const data = - typeof content === "string" ? new TextEncoder().encode(content) : content; - - return new Promise((resolve, reject) => { - const request = store.put({ path, content: data }); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); - } - - async readDir(path: string): Promise { - const store = await this.getStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const request = store.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(request.result.entries); - } else { - resolve([]); - } - }; - request.onerror = () => reject(request.error); - }); - } - - async mkdir(path: string, recursive = false): Promise { + async _mkdir(path: string, recursive = false): Promise { + path = this.normalizePath(path); if (!recursive) { + const parentPath = this.getParentPath(path); + if (parentPath && !(await this._exists(parentPath))) { + throw new Error(`Parent directory does not exist: ${parentPath}`); + } const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.put({ path, entries: [] }); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); + await new Promise((resolve, reject) => { + const r = store.put({ path, entries: [] }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); }); + if (parentPath) await this.addEntryToParent(path, true); + return; } - // 递归创建父目录 - const parts = path.split("/"); + const parts = path.split("/").filter((p) => p !== ""); let currentPath = ""; const pathsToCreate: string[] = []; - - // 收集需要创建的路径 for (const part of parts) { - if (!part) continue; - currentPath = currentPath ? `${currentPath}/${part}` : part; - if (!(await this.exists(currentPath))) { - pathsToCreate.push(currentPath); - } + currentPath = currentPath ? `${currentPath}/${part}` : `/${part}`; + currentPath = this.normalizePath(currentPath); + if (!(await this._exists(currentPath))) pathsToCreate.push(currentPath); } - // 在单个事务中创建所有目录 if (pathsToCreate.length > 0) { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - let completed = 0; - let error: IDBRequest | null = null; - - for (const path of pathsToCreate) { - const request = store.put({ path, entries: [] }); - request.onsuccess = () => { - completed++; - if (completed === pathsToCreate.length && !error) { - resolve(); - } - }; - request.onerror = () => { - if (!error) { - error = request; - reject(request.error); - } - }; - } - }); + for (const p of pathsToCreate) { + await new Promise((resolve, reject) => { + const r = store.put({ path: p, entries: [] }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); + }); + const parentPath = this.getParentPath(p); + if (parentPath) await this.addEntryToParent(p, true); + } } } - async stat(path: string): Promise { - const filesStore = await this.getStore(this.STORE_NAME); - const dirsStore = await this.getStore(this.DIR_STORE_NAME); + async _writeFile(path: string, content: Uint8Array | string): Promise { + path = this.normalizePath(path); + const store = await this.getStore(this.STORE_NAME, "readwrite"); + const data = + typeof content === "string" ? new TextEncoder().encode(content) : content; + await new Promise((resolve, reject) => { + const r = store.put({ path, content: data }); + r.onsuccess = () => resolve(); + r.onerror = () => reject(); + }); + const parentPath = this.getParentPath(path); + if (parentPath) await this.addEntryToParent(path, false); + } + + async _readDir(path: string): Promise { + path = this.normalizePath(path); + const store = await this.getStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const req = store.get(path); + req.onsuccess = () => resolve(req.result?.entries || []); + req.onerror = () => reject(req.error); + }); + } + + async _stat(path: string): Promise { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); return new Promise((resolve, reject) => { const fileRequest = filesStore.get(path); @@ -217,9 +252,13 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async rename(oldPath: string, newPath: string): Promise { - const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); - const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + async _rename(oldPath: string, newPath: string): Promise { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); return new Promise((resolve, reject) => { const moveFile = () => { @@ -260,7 +299,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async deleteFile(path: string): Promise { + async _deleteFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME, "readwrite"); return new Promise((resolve, reject) => { const request = store.delete(path); @@ -269,7 +308,7 @@ export class IndexedDBFileSystem extends IFileSystem { }); } - async deleteDirectory(path: string): Promise { + async _deleteDirectory(path: string): Promise { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); return new Promise((resolve, reject) => { const request = store.delete(path); @@ -278,6 +317,24 @@ export class IndexedDBFileSystem extends IFileSystem { }); } + async clear() { + const transaction = await this.getTransaction( + [this.STORE_NAME, this.DIR_STORE_NAME], + "readwrite", + ); + const filesStore = transaction.objectStore(this.STORE_NAME); + const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); + return new Promise((resolve, reject) => { + const r0 = filesStore.clear(); + r0.onsuccess = () => { + const r2 = dirsStore.clear(); + r2.onsuccess = () => resolve(); + r2.onerror = () => reject(); + }; + r0.onerror = () => reject(); + }); + } + /** * 验证文件内容是否匹配 * @param fs 文件系统实例 diff --git a/src/utils/fs/TauriFileSystem.ts b/src/utils/fs/TauriFileSystem.tsx similarity index 62% rename from src/utils/fs/TauriFileSystem.ts rename to src/utils/fs/TauriFileSystem.tsx index a7435f31..b56a42b0 100644 --- a/src/utils/fs/TauriFileSystem.ts +++ b/src/utils/fs/TauriFileSystem.tsx @@ -5,15 +5,20 @@ import { IFileSystem, FileStats, DirectoryEntry } from "./IFileSystem"; * Tauri 文件系统工具类 */ export class TauriFileSystem extends IFileSystem { - async exists(path: string): Promise { - return invoke("exists", { path }); + constructor(private basePath: string = "") { + super(); + } + async _exists(path: string): Promise { + return invoke("exists", { path: this.basePath + path }); } - async readFile(path: string): Promise { - return new Uint8Array(await invoke("read_file", { path })); + async _readFile(path: string): Promise { + return new Uint8Array( + await invoke("read_file", { path: this.basePath + path }), + ); } - async writeFile(path: string, content: Uint8Array | string): Promise { + async _writeFile(path: string, content: Uint8Array | string): Promise { let data: Uint8Array; if (typeof content === "string") { data = new TextEncoder().encode(content); @@ -21,33 +26,36 @@ export class TauriFileSystem extends IFileSystem { data = content; } return invoke("write_file", { - path, + path: this.basePath + path, content: Array.from(data), }); } - async mkdir(path: string, recursive = false): Promise { - return invoke("mkdir", { path, recursive }); + async _mkdir(path: string, recursive = false): Promise { + return invoke("mkdir", { path: this.basePath + path, recursive }); } - async stat(path: string): Promise { - return invoke("stat", { path }); + async _stat(path: string): Promise { + return invoke("stat", { path: this.basePath + path }); } - async readDir(path: string): Promise { - return invoke("read_dir", { path }); + async _readDir(path: string): Promise { + return invoke("read_dir", { path: this.basePath + path }); } - async rename(oldPath: string, newPath: string): Promise { - return invoke("rename", { oldPath, newPath }); + async _rename(oldPath: string, newPath: string): Promise { + return invoke("rename", { + oldPath: this.basePath + oldPath, + newPath: this.basePath + newPath, + }); } - async deleteFile(path: string): Promise { - return invoke("delete_file", { path }); + async _deleteFile(path: string): Promise { + return invoke("delete_file", { path: this.basePath + path }); } - async deleteDirectory(path: string): Promise { - return invoke("delete_directory", { path }); + async _deleteDirectory(path: string): Promise { + return invoke("delete_directory", { path: this.basePath + path }); } /** @@ -109,3 +117,5 @@ export class TauriFileSystem extends IFileSystem { console.log(`Deleted directory: ${dirPath}`); } } + +export const TauriBaseFS = new TauriFileSystem(); diff --git a/src/utils/fs/WebFileApiSystem.tsx b/src/utils/fs/WebFileApiSystem.tsx new file mode 100644 index 00000000..8379d678 --- /dev/null +++ b/src/utils/fs/WebFileApiSystem.tsx @@ -0,0 +1,192 @@ +import { + IFileSystem, + type FileStats, + type DirectoryEntry, +} from "./IFileSystem"; + +type FSAPHandle = FileSystemDirectoryHandle; + +export class WebFileApiSystem extends IFileSystem { + constructor(private rootHandle: FSAPHandle) { + super(); + } + + private async resolvePathComponents(path: string): Promise { + return IFileSystem.normalizePath(path) + .split("/") + .filter((p) => p !== ""); + } + + private async resolveHandle(path: string): Promise { + const parts = await this.resolvePathComponents(path); + let currentHandle: FileSystemHandle = this.rootHandle; + + for (const part of parts) { + if (currentHandle.kind !== "directory") { + throw new Error(`Cannot traverse into non-directory at: ${part}`); + } + + try { + currentHandle = await ( + currentHandle as FileSystemDirectoryHandle + ).getDirectoryHandle(part); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (dirError) { + try { + currentHandle = await ( + currentHandle as FileSystemDirectoryHandle + ).getFileHandle(part); + // 提前终止检查:文件节点不能在路径中间 + if (part !== parts[parts.length - 1]) { + throw new Error(`File node cannot be in path middle: ${path}`); + } + } catch (fileError) { + throw new Error(`Path resolution failed: ${path} (${fileError})`); + } + } + } + return currentHandle; + } + + async _readFile(path: string): Promise { + const handle = await this.resolveHandle(path); + if (handle.kind !== "file") { + throw new Error(`Path is not a file: ${path}`); + } + const file = await (handle as FileSystemFileHandle).getFile(); + return new Uint8Array(await file.arrayBuffer()); + } + + async _writeFile(path: string, content: Uint8Array | string): Promise { + const buffer = + typeof content === "string" ? new TextEncoder().encode(content) : content; + + const parts = await this.resolvePathComponents(path); + const fileName = parts.pop()!; + const parentHandle = await this.ensureDirectoryPath(parts); + + const fileHandle = await parentHandle.getFileHandle(fileName, { + create: true, + }); + const writable = await fileHandle.createWritable(); + await writable.write(buffer); + await writable.close(); + } + + private async ensureDirectoryPath( + parts: string[], + recursive = false, + ): Promise { + let currentHandle = this.rootHandle; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + currentHandle = await currentHandle.getDirectoryHandle(part, { + create: recursive, + }); + } + return currentHandle; + } + + async _readDir(path: string): Promise { + const handle = await this.resolveHandle(path); + if (handle.kind !== "directory") { + throw new Error(`Path is not a directory: ${path}`); + } + + const entries: DirectoryEntry[] = []; + + for await (const [name, entry] of (handle as FileSystemDirectoryHandle) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + .entries()) { + entries.push({ name, isDir: entry.kind === "directory" }); + } + return entries; + } + + async _mkdir(path: string, recursive = false): Promise { + const parts = await this.resolvePathComponents(path); + await this.ensureDirectoryPath(parts, recursive); + } + + async _stat(path: string): Promise { + const handle = await this.resolveHandle(path); + let size = 0; + if (handle.kind === "file") { + const file = await (handle as FileSystemFileHandle).getFile(); + size = file.size; + } + return { + name: path.split("/").pop() || "", + isDir: handle.kind === "directory", + size, + modified: new Date(), // 使用当前时间作为替代方案 + }; + } + + async _rename(oldPath: string, newPath: string): Promise { + // 递归复制函数 + const copyRecursive = async ( + srcHandle: FileSystemHandle, + destDir: FileSystemDirectoryHandle, + newName: string, + ) => { + if (srcHandle.kind === "file") { + const file = await (srcHandle as FileSystemFileHandle).getFile(); + const newFile = await destDir.getFileHandle(newName, { create: true }); + const writable = await newFile.createWritable(); + await writable.write(await file.arrayBuffer()); + await writable.close(); + } else { + const newDir = await destDir.getDirectoryHandle(newName, { + create: true, + }); + const srcDir = srcHandle as FileSystemDirectoryHandle; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + for await (const [name, entry] of srcDir.entries()) { + await copyRecursive(entry, newDir, name); + } + } + }; + + const oldHandle = await this.resolveHandle(oldPath); + const newParts = await this.resolvePathComponents(newPath); + const newName = newParts.pop()!; + const newDirHandle = await this.ensureDirectoryPath(newParts, true); + + await copyRecursive(oldHandle, newDirHandle, newName); + await this._delete(oldHandle.kind, oldPath); + } + + private async _delete( + kind: "file" | "directory", + path: string, + ): Promise { + const parts = await this.resolvePathComponents(path); + const targetName = parts.pop()!; + const parentHandle = await this.ensureDirectoryPath(parts); + + await parentHandle.removeEntry(targetName, { + recursive: kind === "directory", + }); + } + + async _deleteFile(path: string): Promise { + await this._delete("file", path); + } + + async _deleteDirectory(path: string): Promise { + await this._delete("directory", path); + } + + async _exists(path: string): Promise { + try { + await this.resolveHandle(path); + return true; + } catch { + return false; + } + } +} diff --git a/src/utils/fs.tsx b/src/utils/fs/com.tsx similarity index 52% rename from src/utils/fs.tsx rename to src/utils/fs/com.tsx index bfc448a4..5630d479 100644 --- a/src/utils/fs.tsx +++ b/src/utils/fs/com.tsx @@ -1,6 +1,6 @@ -import { invoke } from "@tauri-apps/api/core"; -import { PathString } from "./pathString"; -import { isWeb } from "./platform"; +import { isWeb } from "../platform"; +import { TauriBaseFS } from "./TauriFileSystem"; +import { PathString } from "../pathString"; /** * 检查一个文件是否存在 @@ -11,7 +11,7 @@ export async function exists(path: string): Promise { if (isWeb) { return true; } else { - return invoke("exists", { path }); + return TauriBaseFS.exists(path); } } @@ -40,7 +40,7 @@ export async function readTextFile(path: string): Promise { input.click(); }); } else { - return invoke("read_text_file", { path }); + return TauriBaseFS.readTextFile(path); } } @@ -70,50 +70,44 @@ export async function readFile(path: string): Promise { input.click(); }); } else { - const base64 = await invoke("read_file_base64", { path }); - const binary = atob(base64); - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - return bytes; + return TauriBaseFS.readFile(path); } } -/** - * 读取文件并以 base64 编码返回 - * @param path 文件路径 - * @returns 文件的 base64 编码 - */ -export async function readFileBase64(path: string): Promise { - if (isWeb) { - return new Promise((resolve, reject) => { - const input = document.createElement("input"); - input.type = "file"; - input.onchange = () => { - const file = input.files?.item(0); - if (file) { - const reader = new FileReader(); - reader.onload = () => { - const content = reader.result as string; - resolve(content); - }; - reader.onerror = reject; - reader.readAsDataURL(file); - } - }; - input.click(); - }); - } else { - return invoke("read_file_base64", { path }); - } -} +// /** +// * 读取文件并以 base64 编码返回 +// * @param path 文件路径 +// * @returns 文件的 base64 编码 +// */ +// export async function readFileBase64(path: string): Promise { +// if (isWeb) { +// return new Promise((resolve, reject) => { +// const input = document.createElement("input"); +// input.type = "file"; +// input.onchange = () => { +// const file = input.files?.item(0); +// if (file) { +// const reader = new FileReader(); +// reader.onload = () => { +// const content = reader.result as string; +// resolve(content); +// }; +// reader.onerror = reject; +// reader.readAsDataURL(file); +// } +// }; +// input.click(); +// }); +// } else { +// return invoke("read_file_base64", { path }); +// } +// } -/** - * 将内容写入文本文件 - * @param path 文件路径 - * @param content 文件内容 - */ +// /** +// * 将内容写入文本文件 +// * @param path 文件路径 +// * @param content 文件内容 +// */ export async function writeTextFile( path: string, content: string, @@ -127,7 +121,7 @@ export async function writeTextFile( a.click(); URL.revokeObjectURL(url); } else { - return invoke("write_text_file", { path, content }); + return TauriBaseFS.writeFile(path, content); } } @@ -149,34 +143,28 @@ export async function writeFile( a.click(); URL.revokeObjectURL(url); } else { - const base64url = await new Promise((resolve) => { - const reader = new FileReader(); - reader.onload = () => resolve(reader.result as string); - reader.readAsDataURL(new Blob([content])); - }); - const base64 = btoa(base64url.split(",")[1]); - return invoke("write_file_base64", { path, content: base64 }); + return TauriBaseFS.writeFile(path, content); } } -/** - * 将内容以 base64 编码写入文件 - * @param path 文件路径 - * @param content 文件的 base64 编码内容 - */ -export async function writeFileBase64( - path: string, - content: string, -): Promise { - if (isWeb) { - const blob = new Blob([content], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = PathString.absolute2file(path); - a.click(); - URL.revokeObjectURL(url); - } else { - return invoke("write_file_base64", { path, content }); - } -} +// /** +// * 将内容以 base64 编码写入文件 +// * @param path 文件路径 +// * @param content 文件的 base64 编码内容 +// */ +// export async function writeFileBase64( +// path: string, +// content: string, +// ): Promise { +// if (isWeb) { +// const blob = new Blob([content], { type: "text/plain" }); +// const url = URL.createObjectURL(blob); +// const a = document.createElement("a"); +// a.href = url; +// a.download = PathString.absolute2file(path); +// a.click(); +// URL.revokeObjectURL(url); +// } else { +// return invoke("write_file_base64", { path, content }); +// } +// } diff --git a/src/utils/platform.tsx b/src/utils/platform.tsx index 6de91a0d..f0b5cfcb 100644 --- a/src/utils/platform.tsx +++ b/src/utils/platform.tsx @@ -12,6 +12,10 @@ export const isDesktop = !isMobile; export const isMac = !isWeb && platform() === "macos"; export const appScale = isMobile ? 0.5 : 1; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +//@ts-ignore +export const webFileApiSupport = !!window.showDirectoryPicker; + export function family() { if (isWeb) { // 从userAgent判断unix|windows From a9bc72584c0fbc7cf47fe7fde6ba077fe4b18f59 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:03:45 +0800 Subject: [PATCH 04/22] =?UTF-8?q?build(deps):=20=E6=B7=BB=E5=8A=A0=20jszip?= =?UTF-8?q?=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 package.json 中添加 jszip 依赖,版本为 ^3.10.1 - jszip 是一个用于处理 ZIP 文件的 JavaScript 库,可能用于文件压缩或解压功能 --- package.json | 1 + pnpm-lock.yaml | 134 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/package.json b/package.json index 9fc25d51..ae570bd7 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-updater": "~2", "i18next": "^24.2.0", + "jszip": "^3.10.1", "lucide-react": "^0.469.0", "markdown-it": "^14.1.0", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 844f7ba2..0803d719 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,6 +46,9 @@ importers: i18next: specifier: ^24.2.0 version: 24.2.0(typescript@5.7.2) + jszip: + specifier: ^3.10.1 + version: 3.10.1 lucide-react: specifier: ^0.469.0 version: 0.469.0(react@19.0.0) @@ -1307,6 +1310,7 @@ packages: } cpu: [arm] os: [linux] + libc: [glibc] "@rollup/rollup-linux-arm-musleabihf@4.29.1": resolution: @@ -1315,6 +1319,7 @@ packages: } cpu: [arm] os: [linux] + libc: [musl] "@rollup/rollup-linux-arm64-gnu@4.29.1": resolution: @@ -1323,6 +1328,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-arm64-musl@4.29.1": resolution: @@ -1331,6 +1337,7 @@ packages: } cpu: [arm64] os: [linux] + libc: [musl] "@rollup/rollup-linux-loongarch64-gnu@4.29.1": resolution: @@ -1339,6 +1346,7 @@ packages: } cpu: [loong64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-powerpc64le-gnu@4.29.1": resolution: @@ -1347,6 +1355,7 @@ packages: } cpu: [ppc64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-riscv64-gnu@4.29.1": resolution: @@ -1355,6 +1364,7 @@ packages: } cpu: [riscv64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-s390x-gnu@4.29.1": resolution: @@ -1363,6 +1373,7 @@ packages: } cpu: [s390x] os: [linux] + libc: [glibc] "@rollup/rollup-linux-x64-gnu@4.29.1": resolution: @@ -1371,6 +1382,7 @@ packages: } cpu: [x64] os: [linux] + libc: [glibc] "@rollup/rollup-linux-x64-musl@4.29.1": resolution: @@ -1379,6 +1391,7 @@ packages: } cpu: [x64] os: [linux] + libc: [musl] "@rollup/rollup-win32-arm64-msvc@4.29.1": resolution: @@ -1579,6 +1592,7 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] + libc: [glibc] "@swc/core-linux-arm64-musl@1.10.1": resolution: @@ -1588,6 +1602,7 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] + libc: [musl] "@swc/core-linux-x64-gnu@1.10.1": resolution: @@ -1597,6 +1612,7 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] + libc: [glibc] "@swc/core-linux-x64-musl@1.10.1": resolution: @@ -1606,6 +1622,7 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] + libc: [musl] "@swc/core-win32-arm64-msvc@1.10.1": resolution: @@ -1699,6 +1716,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [glibc] "@tauri-apps/cli-linux-arm64-musl@2.1.0": resolution: @@ -1708,6 +1726,7 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] + libc: [musl] "@tauri-apps/cli-linux-x64-gnu@2.1.0": resolution: @@ -1717,6 +1736,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [glibc] "@tauri-apps/cli-linux-x64-musl@2.1.0": resolution: @@ -1726,6 +1746,7 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] + libc: [musl] "@tauri-apps/cli-win32-arm64-msvc@2.1.0": resolution: @@ -2629,6 +2650,12 @@ packages: } engines: { node: ">=12.13" } + core-util-is@1.0.3: + resolution: + { + integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, + } + cosmiconfig@8.3.6: resolution: { @@ -3506,6 +3533,12 @@ packages: engines: { node: ">=0.10.0" } hasBin: true + immediate@3.0.6: + resolution: + { + integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==, + } + immutable@5.0.3: resolution: { @@ -3526,6 +3559,12 @@ packages: } engines: { node: ">=0.8.19" } + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + internal-slot@1.1.0: resolution: { @@ -3768,6 +3807,12 @@ packages: } engines: { node: ">=12.13" } + isarray@1.0.0: + resolution: + { + integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==, + } + isarray@2.0.5: resolution: { @@ -3894,6 +3939,12 @@ packages: } engines: { node: ">=4.0" } + jszip@3.10.1: + resolution: + { + integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==, + } + keyv@4.5.4: resolution: { @@ -3922,6 +3973,12 @@ packages: } engines: { node: ">= 0.8.0" } + lie@3.3.0: + resolution: + { + integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==, + } + lilconfig@3.1.3: resolution: { @@ -4354,6 +4411,12 @@ packages: integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==, } + pako@1.0.11: + resolution: + { + integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==, + } + parent-module@1.0.1: resolution: { @@ -4645,6 +4708,12 @@ packages: engines: { node: ">=14" } hasBin: true + process-nextick-args@2.0.1: + resolution: + { + integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, + } + prop-types@15.8.1: resolution: { @@ -4749,6 +4818,12 @@ packages: integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==, } + readable-stream@2.3.8: + resolution: + { + integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==, + } + readdirp@3.6.0: resolution: { @@ -4869,6 +4944,12 @@ packages: } engines: { node: ">=0.4" } + safe-buffer@5.1.2: + resolution: + { + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, + } + safe-regex-test@1.1.0: resolution: { @@ -5144,6 +5225,12 @@ packages: } engines: { node: ">= 0.4" } + setimmediate@1.0.5: + resolution: + { + integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==, + } + shebang-command@2.0.0: resolution: { @@ -5332,6 +5419,12 @@ packages: } engines: { node: ">= 0.4" } + string_decoder@1.1.1: + resolution: + { + integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==, + } + stringify-entities@4.0.4: resolution: { @@ -7643,6 +7736,8 @@ snapshots: dependencies: is-what: 4.1.16 + core-util-is@1.0.3: {} + cosmiconfig@8.3.6(typescript@5.7.2): dependencies: import-fresh: 3.3.0 @@ -8284,6 +8379,8 @@ snapshots: image-size@0.5.5: optional: true + immediate@3.0.6: {} + immutable@5.0.3: {} import-fresh@3.3.0: @@ -8293,6 +8390,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -8421,6 +8520,8 @@ snapshots: is-what@4.1.16: {} + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -8505,6 +8606,13 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8530,6 +8638,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -8791,6 +8903,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8897,6 +9011,8 @@ snapshots: prettier@3.4.2: {} + process-nextick-args@2.0.1: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -8952,6 +9068,16 @@ snapshots: dependencies: pify: 2.3.0 + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -9052,6 +9178,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.1.2: {} + safe-regex-test@1.1.0: dependencies: call-bound: 1.0.3 @@ -9193,6 +9321,8 @@ snapshots: functions-have-names: 1.2.3 has-property-descriptors: 1.0.2 + setimmediate@1.0.5: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -9334,6 +9464,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 From a7ac33519fe7ff05a13d2eadb4237e31678d0efd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:04:04 +0800 Subject: [PATCH 05/22] =?UTF-8?q?feat(fs):=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9B=AE=E5=BD=95=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 IndexedDBFileSystem 类,优化文件和目录操作逻辑 - 新增目录创建、读取、重命名和删除功能 - 改进文件重命名和删除,支持自动更新父目录条目 - 优化文件下载时的文件名处理,支持保留文件后缀 --- src/utils/fs/IndexedDBFileSystem.tsx | 353 ++++++++++++++------------- src/utils/fs/com.tsx | 4 +- 2 files changed, 180 insertions(+), 177 deletions(-) diff --git a/src/utils/fs/IndexedDBFileSystem.tsx b/src/utils/fs/IndexedDBFileSystem.tsx index fc329743..4eed1982 100644 --- a/src/utils/fs/IndexedDBFileSystem.tsx +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -6,6 +6,13 @@ import { const DB_VERSION = 1; +function asPromise(i: IDBRequest) { + return new Promise((resolve, reject) => { + i.onsuccess = () => resolve(i.result); + i.onerror = () => reject(i.error); + }); +} + export class IndexedDBFileSystem extends IFileSystem { private db: IDBDatabase | null = null; private DIR_STORE_NAME: string; @@ -19,33 +26,18 @@ export class IndexedDBFileSystem extends IFileSystem { } private async initDB(): Promise { - return new Promise((resolve, reject) => { - const request = indexedDB.open(this.DB_NAME, DB_VERSION); - - request.onupgradeneeded = (event) => { - const db = (event.target as IDBOpenDBRequest).result; - console.log( - this, - db.objectStoreNames.contains(this.STORE_NAME), - db.objectStoreNames.contains(this.DIR_STORE_NAME), - ); - if (!db.objectStoreNames.contains(this.STORE_NAME)) { - db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); - } - if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { - db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); - } - }; + const request = indexedDB.open(this.DB_NAME, DB_VERSION); - request.onsuccess = (event) => { - this.db = (event.target as IDBOpenDBRequest).result; - resolve(); - }; - - request.onerror = (event) => { - reject((event.target as IDBOpenDBRequest).error); - }; - }); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains(this.STORE_NAME)) { + db.createObjectStore(this.STORE_NAME, { keyPath: "path" }); + } + if (!db.objectStoreNames.contains(this.DIR_STORE_NAME)) { + db.createObjectStore(this.DIR_STORE_NAME, { keyPath: "path" }); + } + }; + this.db = await asPromise(request); } private async getTransaction( @@ -65,6 +57,7 @@ export class IndexedDBFileSystem extends IFileSystem { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } + private normalizePath(path: string): string { let normalized = path.replace(/\/+/g, "/").replace(/\/$/, ""); if (normalized === "") return "/"; @@ -88,82 +81,68 @@ export class IndexedDBFileSystem extends IFileSystem { const parentPath = this.getParentPath(childPath); if (!parentPath) return; + // 确保父目录存在(递归创建) + if (!(await this._exists(parentPath))) { + await this._mkdir(parentPath, true); + } + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - const parentDir = await new Promise((resolve, reject) => { - const req = store.get(parentPath); - req.onsuccess = () => resolve(req.result); - req.onerror = () => reject(req.error); - }); + const parentDir = await asPromise(store.get(parentPath)); + const childName = childPath.split("/").pop()!; - if (!parentDir) throw new Error(`Parent directory ${parentPath} not found`); + if (!parentDir) { + throw new Error(`Parent directory ${parentPath} not found`); + } - const childName = childPath.split("/").pop()!; const exists = parentDir.entries.some( (e: DirectoryEntry) => e.name === childName, ); if (!exists) { parentDir.entries.push({ name: childName, isDir }); - await new Promise((resolve, reject) => { - const req = store.put(parentDir); - req.onsuccess = () => resolve(); - req.onerror = () => reject(req.error); - }); + await asPromise(store.put(parentDir)); } } async _exists(path: string): Promise { - if (!this.db) { - await this.initDB(); - } - const transaction = await this.getTransaction( + path = this.normalizePath(path); + const trans = await this.getTransaction( [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", + "readonly", ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve) => { - const request = filesStore.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(true); - } else { - const dirRequest = dirsStore.get(path); - dirRequest.onsuccess = () => resolve(!!dirRequest.result); - dirRequest.onerror = () => resolve(false); - } - }; - request.onerror = () => resolve(false); - }); + const fileExists = !!(await asPromise( + trans.objectStore(this.STORE_NAME).get(path), + )); + if (fileExists) return true; + return !!(await asPromise( + trans.objectStore(this.DIR_STORE_NAME).get(path), + )); } async _readFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME); - return new Promise((resolve, reject) => { - const request = store.get(path); - request.onsuccess = () => { - if (request.result) { - resolve(request.result.content); - } else { - reject(new Error(`File not found: ${path}`)); - } - }; - request.onerror = () => reject(request.error); - }); + const result = await asPromise(store.get(path)); + if (result) return result.content; + throw new Error(`File not found: ${path}`); } async _mkdir(path: string, recursive = false): Promise { path = this.normalizePath(path); + if (path === "/") { + const exists = await this._exists("/"); + if (!exists) { + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + await asPromise(store.put({ path: "/", entries: [] })); + } + return; + } + if (!recursive) { const parentPath = this.getParentPath(path); if (parentPath && !(await this._exists(parentPath))) { throw new Error(`Parent directory does not exist: ${parentPath}`); } const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - await new Promise((resolve, reject) => { - const r = store.put({ path, entries: [] }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); + await asPromise(store.put({ path, entries: [] })); if (parentPath) await this.addEntryToParent(path, true); return; } @@ -174,85 +153,69 @@ export class IndexedDBFileSystem extends IFileSystem { for (const part of parts) { currentPath = currentPath ? `${currentPath}/${part}` : `/${part}`; currentPath = this.normalizePath(currentPath); - if (!(await this._exists(currentPath))) pathsToCreate.push(currentPath); + if (!(await this._exists(currentPath))) { + pathsToCreate.push(currentPath); + } } - if (pathsToCreate.length > 0) { - const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - for (const p of pathsToCreate) { - await new Promise((resolve, reject) => { - const r = store.put({ path: p, entries: [] }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); - const parentPath = this.getParentPath(p); - if (parentPath) await this.addEntryToParent(p, true); - } + const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + for (const p of pathsToCreate) { + await asPromise(store.put({ path: p, entries: [] })); + await this.addEntryToParent(p, true); } } async _writeFile(path: string, content: Uint8Array | string): Promise { path = this.normalizePath(path); + const parentPath = this.getParentPath(path); + if (parentPath) { + await this._mkdir(parentPath, true); + } + const store = await this.getStore(this.STORE_NAME, "readwrite"); const data = typeof content === "string" ? new TextEncoder().encode(content) : content; - await new Promise((resolve, reject) => { - const r = store.put({ path, content: data }); - r.onsuccess = () => resolve(); - r.onerror = () => reject(); - }); - const parentPath = this.getParentPath(path); - if (parentPath) await this.addEntryToParent(path, false); + await asPromise(store.put({ path, content: data })); + await this.addEntryToParent(path, false); } async _readDir(path: string): Promise { path = this.normalizePath(path); const store = await this.getStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const req = store.get(path); - req.onsuccess = () => resolve(req.result?.entries || []); - req.onerror = () => reject(req.error); - }); + const dirEntry = await asPromise(store.get(path)); + if (!dirEntry) { + throw new Error(`Directory not found: ${path}`); + } + return dirEntry.entries; } async _stat(path: string): Promise { - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - - return new Promise((resolve, reject) => { - const fileRequest = filesStore.get(path); - fileRequest.onsuccess = () => { - if (fileRequest.result) { - resolve({ - name: path.split("/").pop() || "", - isDir: false, - size: fileRequest.result.content.byteLength, - modified: new Date(), - }); - } else { - const dirRequest = dirsStore.get(path); - dirRequest.onsuccess = () => { - if (dirRequest.result) { - resolve({ - name: path.split("/").pop() || "", - isDir: true, - size: 0, - modified: new Date(), - }); - } else { - reject(new Error(`Path not found: ${path}`)); - } - }; - } + const fileStore = await this.getStore(this.STORE_NAME); + const dirStore = await this.getStore(this.DIR_STORE_NAME); + const file = await asPromise(fileStore.get(path)); + if (file) { + return { + name: path.split("/").pop() || "", + isDir: false, + size: file.content.byteLength, + modified: new Date(), }; - }); + } + const dir = await asPromise(dirStore.get(path)); + if (dir) { + return { + name: path.split("/").pop() || "", + isDir: true, + size: 0, + modified: new Date(), + }; + } + throw new Error(`Path not found: ${path}`); } async _rename(oldPath: string, newPath: string): Promise { + oldPath = this.normalizePath(oldPath); + newPath = this.normalizePath(newPath); const transaction = await this.getTransaction( [this.STORE_NAME, this.DIR_STORE_NAME], "readwrite", @@ -260,61 +223,101 @@ export class IndexedDBFileSystem extends IFileSystem { const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const moveFile = () => { - const request = filesStore.get(oldPath); - request.onsuccess = () => { - if (request.result) { - const content = request.result.content; - const deleteRequest = filesStore.delete(oldPath); - deleteRequest.onsuccess = () => { - const putRequest = filesStore.put({ path: newPath, content }); - putRequest.onsuccess = () => resolve(); - putRequest.onerror = () => reject(putRequest.error); - }; - } else { - moveDir(); - } - }; - }; - - const moveDir = () => { - const request = dirsStore.get(oldPath); - request.onsuccess = () => { - if (request.result) { - const entries = request.result.entries; - const deleteRequest = dirsStore.delete(oldPath); - deleteRequest.onsuccess = () => { - const putRequest = dirsStore.put({ path: newPath, entries }); - putRequest.onsuccess = () => resolve(); - putRequest.onerror = () => reject(putRequest.error); - }; - } else { - reject(new Error(`Path not found: ${oldPath}`)); + const isDir = !!(await asPromise(dirsStore.get(oldPath))); + if (isDir) { + // 处理目录重命名及子项 + const dir = await asPromise(dirsStore.get(oldPath)); + if (!dir) throw new Error(`Directory not found: ${oldPath}`); + await asPromise(dirsStore.delete(oldPath)); + await asPromise(dirsStore.put({ ...dir, path: newPath })); + + // 更新所有子路径 + const filesCursor = await asPromise(filesStore.openCursor()); + const dirsCursor = await asPromise(dirsStore.openCursor()); + const updateEntries = async (cursor: IDBCursorWithValue | null) => { + while (cursor) { + const oldChildPath = cursor.key as string; + if (oldChildPath.startsWith(`${oldPath}/`)) { + const newChildPath = newPath + oldChildPath.slice(oldPath.length); + if (cursor.source.name === this.STORE_NAME) { + await asPromise(filesStore.delete(oldChildPath)); + await asPromise( + filesStore.put({ ...cursor.value, path: newChildPath }), + ); + } else { + await asPromise(dirsStore.delete(oldChildPath)); + await asPromise( + dirsStore.put({ ...cursor.value, path: newChildPath }), + ); + } } - }; + cursor.continue(); + } }; + await updateEntries(filesCursor); + await updateEntries(dirsCursor); + } else { + // 处理文件重命名 + const file = await asPromise(filesStore.get(oldPath)); + if (!file) throw new Error(`File not found: ${oldPath}`); + await asPromise(filesStore.delete(oldPath)); + await asPromise(filesStore.put({ ...file, path: newPath })); + } - moveFile(); - }); + // 更新父目录条目 + const oldParentPath = this.getParentPath(oldPath); + const newParentPath = this.getParentPath(newPath); + if (oldParentPath) { + const oldParentStore = await this.getStore( + this.DIR_STORE_NAME, + "readwrite", + ); + const oldParent = await asPromise(oldParentStore.get(oldParentPath)); + if (oldParent) { + const entryName = oldPath.split("/").pop()!; + oldParent.entries = oldParent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(oldParentStore.put(oldParent)); + } + } + if (newParentPath) { + await this.addEntryToParent(newPath, isDir); + } } async _deleteFile(path: string): Promise { const store = await this.getStore(this.STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.delete(path); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + await asPromise(store.delete(path)); + const parentPath = this.getParentPath(path); + if (parentPath) { + const parentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parent = await asPromise(parentStore.get(parentPath)); + if (parent) { + const entryName = path.split("/").pop()!; + parent.entries = parent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(parentStore.put(parent)); + } + } } async _deleteDirectory(path: string): Promise { const store = await this.getStore(this.DIR_STORE_NAME, "readwrite"); - return new Promise((resolve, reject) => { - const request = store.delete(path); - request.onsuccess = () => resolve(); - request.onerror = () => reject(request.error); - }); + await asPromise(store.delete(path)); + const parentPath = this.getParentPath(path); + if (parentPath) { + const parentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + const parent = await asPromise(parentStore.get(parentPath)); + if (parent) { + const entryName = path.split("/").pop()!; + parent.entries = parent.entries.filter( + (e: DirectoryEntry) => e.name !== entryName, + ); + await asPromise(parentStore.put(parent)); + } + } } async clear() { diff --git a/src/utils/fs/com.tsx b/src/utils/fs/com.tsx index 5630d479..baa9baee 100644 --- a/src/utils/fs/com.tsx +++ b/src/utils/fs/com.tsx @@ -117,7 +117,7 @@ export async function writeTextFile( const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = PathString.absolute2file(path); + a.download = PathString.absolute2fileWithExt(path); a.click(); URL.revokeObjectURL(url); } else { @@ -139,7 +139,7 @@ export async function writeFile( const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = PathString.absolute2file(path); + a.download = PathString.absolute2fileWithExt(path); a.click(); URL.revokeObjectURL(url); } else { From b55c6c5100e2870731994554994fc380671fad33 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 22:04:22 +0800 Subject: [PATCH 06/22] =?UTF-8?q?feat(file):=20=E6=94=B9=E8=BF=9B=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E6=89=93=E5=BC=80=E5=8A=9F=E8=83=BD=E5=B9=B6=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构文件打开流程,使用 VFileSystem 统一处理文件读取 - 修改文件路径处理逻辑,支持获取文件名和文件名(含后缀) - 更新文件对话框默认文件名,使用 .gp 后缀 --- src/core/service/RecentFileManager.tsx | 10 ++++++---- src/pages/_app_menu.tsx | 2 +- src/utils/pathString.tsx | 22 ++++++++++++++++------ 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/core/service/RecentFileManager.tsx b/src/core/service/RecentFileManager.tsx index 03dc4886..d671e6e6 100644 --- a/src/core/service/RecentFileManager.tsx +++ b/src/core/service/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../types/node"; -import { exists, readTextFile } from "../../utils/fs/com"; +import { exists } from "../../utils/fs/com"; import { createStore } from "../../utils/store"; import { Camera } from "../stage/Camera"; import { Stage } from "../stage/Stage"; @@ -15,6 +15,7 @@ import { Section } from "../stage/stageObject/entity/Section"; import { TextNode } from "../stage/stageObject/entity/TextNode"; import { UrlNode } from "../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "./effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "./VFileSystem"; /** * 管理最近打开的文件列表 @@ -143,16 +144,17 @@ export namespace RecentFileManager { */ export async function openFileByPath(path: string) { StageManager.destroy(); - let content: string; try { - content = await readTextFile(path); + await VFileSystem.loadFromPath(path); } catch (e) { console.error("打开文件失败:", path); console.error(e); return; } - const data = StageLoader.validate(JSON.parse(content)); + const data = StageLoader.validate( + JSON.parse(await VFileSystem.getMetaData()), + ); loadStageByData(data); StageHistoryManager.reset(data); diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index d221fbb7..afaeeaf1 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -143,7 +143,7 @@ export default function AppMenu({ const openFileByDialogWindow = async () => { const path = isWeb - ? "file.json" + ? "file.gp" : await openFileDialog({ title: "打开文件", directory: false, diff --git a/src/utils/pathString.tsx b/src/utils/pathString.tsx index d768ff66..e676e5da 100644 --- a/src/utils/pathString.tsx +++ b/src/utils/pathString.tsx @@ -7,6 +7,21 @@ export namespace PathString { * @returns */ export function absolute2file(path: string): string { + const file = absolute2fileWithExt(path); + const parts = file.split("."); + if (parts.length > 1) { + return parts.slice(0, -1).join("."); + } else { + return file; + } + } + + /** + * 将绝对路径转换为文件名(含文件后缀) + * @param path + * @returns + */ + export function absolute2fileWithExt(path: string): string { const fam = family(); // const fam = "windows"; // vitest 测试时打开此行注释 @@ -17,12 +32,7 @@ export namespace PathString { if (!file) { throw new Error("Invalid path"); } - const parts = file.split("."); - if (parts.length > 1) { - return parts.slice(0, -1).join("."); - } else { - return file; - } + return file; } /** From c7f307be83dd47275b3f70d416267c4c457b82f5 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sat, 1 Feb 2025 23:52:10 +0800 Subject: [PATCH 07/22] =?UTF-8?q?feat(core):=20=E4=BF=AE=E6=94=B9=E8=99=9A?= =?UTF-8?q?=E6=8B=9F=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 VFileSystem 类,增加 pullMetaData 和 saveToPath 方法 - 更新 RecentFileManager 和 StageSaveManager,使用新的文件系统方法 - 修改 ControllerDragFile 和 CopyEngine,使用 VFileSystem 替代直接文件操作 - 更新 StageExportEngine 和 SoundService,使用新的文件读写方法 - 重构 fs/com.tsx,提供统一的文件操作接口 --- src/core/service/VFileSystem.tsx | 15 +++++--- .../concrete/ControllerDragFile.tsx | 6 +-- .../dataFileService/RecentFileManager.tsx | 9 +++-- .../dataFileService/StageSaveManager.tsx | 10 +++-- .../stageExportEngine/stageExportEngine.tsx | 2 +- .../copyEngine/copyEngine.tsx | 37 ++++++++++--------- .../service/feedbackService/SoundService.tsx | 2 +- src/utils/fs/com.tsx | 2 +- 8 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/VFileSystem.tsx index f6751cd3..f6e59db9 100644 --- a/src/core/service/VFileSystem.tsx +++ b/src/core/service/VFileSystem.tsx @@ -1,6 +1,7 @@ import JSZip, * as jszip from "jszip"; import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; -import { readFile } from "../../utils/fs/com"; +import { readFile, writeFile } from "../../utils/fs/com"; +import { StageDumper } from "../stage/StageDumper"; export enum FSType { Tauri = "Tauri", @@ -15,6 +16,9 @@ export namespace VFileSystem { export async function setMetaData(content: string) { return fs.writeTextFile("/meta.json", content); } + export async function pullMetaData() { + setMetaData(JSON.stringify(StageDumper.dump())); + } export async function loadFromPath(path: string) { const data = await readFile(path); const zip = await jszip.loadAsync(data); @@ -35,10 +39,7 @@ export namespace VFileSystem { try { // 分离目录和文件名 const lastSlashIndex = normalizedPath.lastIndexOf("/"); - const parentDir = - lastSlashIndex >= 0 - ? normalizedPath.slice(0, lastSlashIndex) - : ""; + const parentDir = lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ""; // 创建父目录(如果存在) if (parentDir) { @@ -58,6 +59,10 @@ export namespace VFileSystem { await Promise.all(operations); } + export async function saveToPath(path: string) { + await setMetaData(JSON.stringify(StageDumper.dump())); + await writeFile(path, await VFileSystem.exportZipData()); + } export async function exportZipData(): Promise { const zip = new JSZip(); diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index 6ff92533..17010a2c 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -1,6 +1,4 @@ import { v4 as uuidv4 } from "uuid"; -import { writeFileBase64 } from "../../../../../utils/fs"; -import { PathString } from "../../../../../utils/pathString"; import { Color } from "../../../../dataStruct/Color"; import { Vector } from "../../../../dataStruct/Vector"; import { Renderer } from "../../../../render/canvas2d/renderer"; @@ -12,6 +10,7 @@ import { TextNode } from "../../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../../feedbackService/effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; +import { VFileSystem } from "../../../VFileSystem"; /** * BUG: 始终无法触发文件拖入事件 @@ -169,8 +168,7 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - const folderPath = PathString.dirPath(Stage.path.getFilePath()); - writeFileBase64(`${folderPath}${PathString.getSep()}${imageUUID}.png`, fileContent.split(",")[1]); + VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); const imageNode = new ImageNode({ uuid: imageUUID, location: [mouseWorldLocation.x, mouseWorldLocation.y], diff --git a/src/core/service/dataFileService/RecentFileManager.tsx b/src/core/service/dataFileService/RecentFileManager.tsx index d00d0390..b4141c0a 100644 --- a/src/core/service/dataFileService/RecentFileManager.tsx +++ b/src/core/service/dataFileService/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../../types/node"; -import { exists, readTextFile } from "../../../utils/fs"; +import { exists } from "../../../utils/fs/com"; import { createStore } from "../../../utils/store"; import { Camera } from "../../stage/Camera"; import { Stage } from "../../stage/Stage"; @@ -16,6 +16,7 @@ import { TextNode } from "../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { PenStroke } from "../../stage/stageObject/entity/PenStroke"; +import { VFileSystem } from "../VFileSystem"; /** * 管理最近打开的文件列表 @@ -144,16 +145,16 @@ export namespace RecentFileManager { */ export async function openFileByPath(path: string) { StageManager.destroy(); - let content: string; + try { - content = await readTextFile(path); + await VFileSystem.loadFromPath(path); } catch (e) { console.error("打开文件失败:", path); console.error(e); return; } - const data = StageLoader.validate(JSON.parse(content)); + const data = StageLoader.validate(JSON.parse(await VFileSystem.getMetaData())); loadStageByData(data); StageHistoryManager.reset(data); diff --git a/src/core/service/dataFileService/StageSaveManager.tsx b/src/core/service/dataFileService/StageSaveManager.tsx index 3b123bba..fa87231d 100644 --- a/src/core/service/dataFileService/StageSaveManager.tsx +++ b/src/core/service/dataFileService/StageSaveManager.tsx @@ -1,9 +1,10 @@ import { Serialized } from "../../../types/node"; -import { exists, writeTextFile } from "../../../utils/fs"; +import { exists, writeTextFile } from "../../../utils/fs/com"; import { PathString } from "../../../utils/pathString"; import { Stage } from "../../stage/Stage"; import { StageHistoryManager } from "../../stage/stageManager/StageHistoryManager"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; +import { VFileSystem } from "../VFileSystem"; /** * 管理所有和保存相关的内容 @@ -16,7 +17,7 @@ export namespace StageSaveManager { * @param errorCallback */ export async function saveHandle(path: string, data: Serialized.File) { - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.saveToPath(path); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); StageHistoryManager.reset(data); // 重置历史 isCurrentSaved = true; @@ -37,7 +38,7 @@ export namespace StageSaveManager { if (Stage.path.isDraft()) { throw new Error("当前文档的状态为草稿,请您先保存为文件"); } - await writeTextFile(Stage.path.getFilePath(), JSON.stringify(data)); + await VFileSystem.saveToPath(Stage.path.getFilePath()); if (addFlashEffect) { Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } @@ -55,6 +56,7 @@ export namespace StageSaveManager { * @param errorCallback * @returns */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars export async function backupHandle(path: string, data: Serialized.File) { const backupFolderPath = PathString.dirPath(path); const isExists = await exists(backupFolderPath); @@ -62,7 +64,7 @@ export namespace StageSaveManager { throw new Error("备份文件路径错误:" + backupFolderPath); } - await writeTextFile(path, JSON.stringify(data)); + await VFileSystem.saveToPath(path); Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); } /** diff --git a/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx b/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx index 1ee9af64..222038b9 100644 --- a/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx +++ b/src/core/service/dataGenerateService/stageExportEngine/stageExportEngine.tsx @@ -1,4 +1,4 @@ -import { writeTextFile } from "../../../../utils/fs"; +import { writeTextFile } from "../../../../utils/fs/com"; import { GraphMethods } from "../../../stage/stageManager/basicMethods/GraphMethods"; import { ConnectableEntity } from "../../../stage/stageObject/abstract/ConnectableEntity"; import { Entity } from "../../../stage/stageObject/abstract/StageEntity"; diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index f992126d..4d9d274b 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -1,7 +1,6 @@ import { v4 as uuidv4 } from "uuid"; import { Dialog } from "../../../../components/dialog"; import { Serialized } from "../../../../types/node"; -import { writeFileBase64 } from "../../../../utils/fs"; import { PathString } from "../../../../utils/pathString"; import { Rectangle } from "../../../dataStruct/shape/Rectangle"; import { Vector } from "../../../dataStruct/Vector"; @@ -15,6 +14,7 @@ import { ImageNode } from "../../../stage/stageObject/entity/ImageNode"; import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { MouseLocation } from "../../controlService/MouseLocation"; +import { VFileSystem } from "../../VFileSystem"; /** * 专门用来管理节点复制的引擎 @@ -129,13 +129,14 @@ async function readClipboardItems(mouseLocation: Vector) { } const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); - const folder = PathString.dirPath(Stage.path.getFilePath()); - const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; + //const folder = PathString.dirPath(Stage.path.getFilePath()); + VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); + //const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败 // writeFile(imagePath, new Uint8Array(await blob.arrayBuffer())); // 下面这样的写法是没有问题的 - writeFileBase64(imagePath, await convertBlobToBase64(blob)); + // writeFileBase64(imagePath, await convertBlobToBase64(blob)); // 要延迟一下,等待保存完毕 setTimeout(() => { @@ -190,17 +191,17 @@ function blobToText(blob: Blob): Promise { }); } -async function convertBlobToBase64(blob: Blob): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => { - if (typeof reader.result === "string") { - resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 - } else { - reject(new Error("Invalid result type")); - } - }; - reader.onerror = reject; - reader.readAsDataURL(blob); - }); -} +// async function convertBlobToBase64(blob: Blob): Promise { +// return new Promise((resolve, reject) => { +// const reader = new FileReader(); +// reader.onloadend = () => { +// if (typeof reader.result === "string") { +// resolve(reader.result.split(",")[1]); // 去掉"data:image/png;base64,"前缀 +// } else { +// reject(new Error("Invalid result type")); +// } +// }; +// reader.onerror = reject; +// reader.readAsDataURL(blob); +// }); +// } diff --git a/src/core/service/feedbackService/SoundService.tsx b/src/core/service/feedbackService/SoundService.tsx index c4c1cfa5..6fe304f5 100644 --- a/src/core/service/feedbackService/SoundService.tsx +++ b/src/core/service/feedbackService/SoundService.tsx @@ -2,7 +2,7 @@ // @tauri-apps/plugin-fs 只能读取文本文件,不能强行读取流文件并强转为ArrayBuffer // import { readTextFile } from "@tauri-apps/plugin-fs"; -import { readFile } from "../../../utils/fs"; +import { readFile } from "../../../utils/fs/com"; import { StringDict } from "../../dataStruct/StringDict"; import { Settings } from "../Settings"; diff --git a/src/utils/fs/com.tsx b/src/utils/fs/com.tsx index c563bfde..c2af0ac1 100644 --- a/src/utils/fs/com.tsx +++ b/src/utils/fs/com.tsx @@ -156,7 +156,7 @@ export async function writeFileBase64(path: string, content: string): Promise Date: Sat, 1 Feb 2025 23:58:08 +0800 Subject: [PATCH 08/22] =?UTF-8?q?refactor(service):=20=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=20VFileSystem=20=E5=88=B0=E5=90=88=E9=80=82=E7=9A=84=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/concrete/ControllerDragFile.tsx | 2 +- src/core/service/dataFileService/RecentFileManager.tsx | 2 +- src/core/service/dataFileService/StageSaveManager.tsx | 2 +- src/core/service/{ => dataFileService}/VFileSystem.tsx | 6 +++--- .../service/dataManageService/copyEngine/copyEngine.tsx | 2 +- src/core/stage/stageObject/entity/ImageNode.tsx | 2 +- src/main.tsx | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) rename src/core/service/{ => dataFileService}/VFileSystem.tsx (94%) diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index 17010a2c..cc533265 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -10,7 +10,7 @@ import { TextNode } from "../../../../stage/stageObject/entity/TextNode"; import { TextRiseEffect } from "../../../feedbackService/effectEngine/concrete/TextRiseEffect"; import { ViewFlashEffect } from "../../../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { ControllerClassDragFile } from "../ControllerClassDragFile"; -import { VFileSystem } from "../../../VFileSystem"; +import { VFileSystem } from "../../../dataFileService/VFileSystem"; /** * BUG: 始终无法触发文件拖入事件 diff --git a/src/core/service/dataFileService/RecentFileManager.tsx b/src/core/service/dataFileService/RecentFileManager.tsx index b4141c0a..d0f9f1dd 100644 --- a/src/core/service/dataFileService/RecentFileManager.tsx +++ b/src/core/service/dataFileService/RecentFileManager.tsx @@ -16,7 +16,7 @@ import { TextNode } from "../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { PenStroke } from "../../stage/stageObject/entity/PenStroke"; -import { VFileSystem } from "../VFileSystem"; +import { VFileSystem } from "./VFileSystem"; /** * 管理最近打开的文件列表 diff --git a/src/core/service/dataFileService/StageSaveManager.tsx b/src/core/service/dataFileService/StageSaveManager.tsx index fa87231d..2811a87a 100644 --- a/src/core/service/dataFileService/StageSaveManager.tsx +++ b/src/core/service/dataFileService/StageSaveManager.tsx @@ -4,7 +4,7 @@ import { PathString } from "../../../utils/pathString"; import { Stage } from "../../stage/Stage"; import { StageHistoryManager } from "../../stage/stageManager/StageHistoryManager"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; -import { VFileSystem } from "../VFileSystem"; +import { VFileSystem } from "./VFileSystem"; /** * 管理所有和保存相关的内容 diff --git a/src/core/service/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx similarity index 94% rename from src/core/service/VFileSystem.tsx rename to src/core/service/dataFileService/VFileSystem.tsx index f6e59db9..f52282e3 100644 --- a/src/core/service/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -1,7 +1,7 @@ import JSZip, * as jszip from "jszip"; -import { IndexedDBFileSystem } from "../../utils/fs/IndexedDBFileSystem"; -import { readFile, writeFile } from "../../utils/fs/com"; -import { StageDumper } from "../stage/StageDumper"; +import { IndexedDBFileSystem } from "../../../utils/fs/IndexedDBFileSystem"; +import { readFile, writeFile } from "../../../utils/fs/com"; +import { StageDumper } from "../../stage/StageDumper"; export enum FSType { Tauri = "Tauri", diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index 4d9d274b..1e811e35 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -14,7 +14,7 @@ import { ImageNode } from "../../../stage/stageObject/entity/ImageNode"; import { TextNode } from "../../../stage/stageObject/entity/TextNode"; import { UrlNode } from "../../../stage/stageObject/entity/UrlNode"; import { MouseLocation } from "../../controlService/MouseLocation"; -import { VFileSystem } from "../../VFileSystem"; +import { VFileSystem } from "../../dataFileService/VFileSystem"; /** * 专门用来管理节点复制的引擎 diff --git a/src/core/stage/stageObject/entity/ImageNode.tsx b/src/core/stage/stageObject/entity/ImageNode.tsx index 448ce14e..620f2d6f 100644 --- a/src/core/stage/stageObject/entity/ImageNode.tsx +++ b/src/core/stage/stageObject/entity/ImageNode.tsx @@ -5,7 +5,7 @@ import { Vector } from "../../../dataStruct/Vector"; import { Stage } from "../../Stage"; import { ConnectableEntity } from "../abstract/ConnectableEntity"; import { CollisionBox } from "../collisionBox/collisionBox"; -import { VFileSystem } from "../../../service/VFileSystem"; +import { VFileSystem } from "../../../service/dataFileService/VFileSystem"; /** * 一个图片节点 diff --git a/src/main.tsx b/src/main.tsx index 0805447d..985d1c83 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -42,7 +42,7 @@ import { exists } from "./utils/fs/com"; import { exit, openDevtools, writeStderr, writeStdout } from "./utils/otherApi"; import { getCurrentWindow, isDesktop, isWeb } from "./utils/platform"; import { Tourials } from "./core/service/Tourials"; -// import { VFileSystem } from "./core/service/VFileSystem"; +// import { VFileSystem } from "./core/service/dataFileService/VFileSystem"; import { IndexedDBFileSystem } from "./utils/fs/IndexedDBFileSystem"; const router = createMemoryRouter(routes); From 59fe4144aab6853656e116e5c721ad3ce55ccbb8 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:00:23 +0800 Subject: [PATCH 09/22] =?UTF-8?q?feat(core):=20=E4=BC=98=E5=8C=96=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=B3=BB=E7=BB=9F=E5=B9=B6=E6=B7=BB=E5=8A=A0=E6=B8=85?= =?UTF-8?q?=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 VFileSystem 中添加 clear 方法,用于清除文件系统数据 - 优化 IndexedDBFileSystem 中的 getStore、addEntryToParent 和 clear 方法 - 删除多余的空格和括号,提高代码可读性 --- .../service/dataFileService/VFileSystem.tsx | 3 + src/utils/fs/IndexedDBFileSystem.tsx | 94 +++++-------------- 2 files changed, 24 insertions(+), 73 deletions(-) diff --git a/src/core/service/dataFileService/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx index f52282e3..37c1c70a 100644 --- a/src/core/service/dataFileService/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -96,6 +96,9 @@ export namespace VFileSystem { compressionOptions: { level: 6 }, }); } + export async function clear() { + return fs.clear(); + } export function getFS() { return fs; } diff --git a/src/utils/fs/IndexedDBFileSystem.tsx b/src/utils/fs/IndexedDBFileSystem.tsx index 4eed1982..e5c49314 100644 --- a/src/utils/fs/IndexedDBFileSystem.tsx +++ b/src/utils/fs/IndexedDBFileSystem.tsx @@ -1,8 +1,4 @@ -import { - IFileSystem, - type FileStats, - type DirectoryEntry, -} from "./IFileSystem"; +import { IFileSystem, type FileStats, type DirectoryEntry } from "./IFileSystem"; const DB_VERSION = 1; @@ -50,10 +46,7 @@ export class IndexedDBFileSystem extends IFileSystem { return this.db!.transaction(storeNames, mode); } - private async getStore( - storeName: string, - mode: IDBTransactionMode = "readonly", - ): Promise { + private async getStore(storeName: string, mode: IDBTransactionMode = "readonly"): Promise { const transaction = await this.getTransaction(storeName, mode); return transaction.objectStore(storeName); } @@ -74,10 +67,7 @@ export class IndexedDBFileSystem extends IFileSystem { return parts.length === 0 ? "/" : `/${parts.join("/")}`; } - private async addEntryToParent( - childPath: string, - isDir: boolean, - ): Promise { + private async addEntryToParent(childPath: string, isDir: boolean): Promise { const parentPath = this.getParentPath(childPath); if (!parentPath) return; @@ -94,9 +84,7 @@ export class IndexedDBFileSystem extends IFileSystem { throw new Error(`Parent directory ${parentPath} not found`); } - const exists = parentDir.entries.some( - (e: DirectoryEntry) => e.name === childName, - ); + const exists = parentDir.entries.some((e: DirectoryEntry) => e.name === childName); if (!exists) { parentDir.entries.push({ name: childName, isDir }); await asPromise(store.put(parentDir)); @@ -105,17 +93,10 @@ export class IndexedDBFileSystem extends IFileSystem { async _exists(path: string): Promise { path = this.normalizePath(path); - const trans = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readonly", - ); - const fileExists = !!(await asPromise( - trans.objectStore(this.STORE_NAME).get(path), - )); + const trans = await this.getTransaction([this.STORE_NAME, this.DIR_STORE_NAME], "readonly"); + const fileExists = !!(await asPromise(trans.objectStore(this.STORE_NAME).get(path))); if (fileExists) return true; - return !!(await asPromise( - trans.objectStore(this.DIR_STORE_NAME).get(path), - )); + return !!(await asPromise(trans.objectStore(this.DIR_STORE_NAME).get(path))); } async _readFile(path: string): Promise { @@ -173,8 +154,7 @@ export class IndexedDBFileSystem extends IFileSystem { } const store = await this.getStore(this.STORE_NAME, "readwrite"); - const data = - typeof content === "string" ? new TextEncoder().encode(content) : content; + const data = typeof content === "string" ? new TextEncoder().encode(content) : content; await asPromise(store.put({ path, content: data })); await this.addEntryToParent(path, false); } @@ -216,10 +196,7 @@ export class IndexedDBFileSystem extends IFileSystem { async _rename(oldPath: string, newPath: string): Promise { oldPath = this.normalizePath(oldPath); newPath = this.normalizePath(newPath); - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); + const transaction = await this.getTransaction([this.STORE_NAME, this.DIR_STORE_NAME], "readwrite"); const filesStore = transaction.objectStore(this.STORE_NAME); const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); @@ -241,14 +218,10 @@ export class IndexedDBFileSystem extends IFileSystem { const newChildPath = newPath + oldChildPath.slice(oldPath.length); if (cursor.source.name === this.STORE_NAME) { await asPromise(filesStore.delete(oldChildPath)); - await asPromise( - filesStore.put({ ...cursor.value, path: newChildPath }), - ); + await asPromise(filesStore.put({ ...cursor.value, path: newChildPath })); } else { await asPromise(dirsStore.delete(oldChildPath)); - await asPromise( - dirsStore.put({ ...cursor.value, path: newChildPath }), - ); + await asPromise(dirsStore.put({ ...cursor.value, path: newChildPath })); } } cursor.continue(); @@ -268,16 +241,11 @@ export class IndexedDBFileSystem extends IFileSystem { const oldParentPath = this.getParentPath(oldPath); const newParentPath = this.getParentPath(newPath); if (oldParentPath) { - const oldParentStore = await this.getStore( - this.DIR_STORE_NAME, - "readwrite", - ); + const oldParentStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); const oldParent = await asPromise(oldParentStore.get(oldParentPath)); if (oldParent) { const entryName = oldPath.split("/").pop()!; - oldParent.entries = oldParent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + oldParent.entries = oldParent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(oldParentStore.put(oldParent)); } } @@ -295,9 +263,7 @@ export class IndexedDBFileSystem extends IFileSystem { const parent = await asPromise(parentStore.get(parentPath)); if (parent) { const entryName = path.split("/").pop()!; - parent.entries = parent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + parent.entries = parent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(parentStore.put(parent)); } } @@ -312,30 +278,17 @@ export class IndexedDBFileSystem extends IFileSystem { const parent = await asPromise(parentStore.get(parentPath)); if (parent) { const entryName = path.split("/").pop()!; - parent.entries = parent.entries.filter( - (e: DirectoryEntry) => e.name !== entryName, - ); + parent.entries = parent.entries.filter((e: DirectoryEntry) => e.name !== entryName); await asPromise(parentStore.put(parent)); } } } async clear() { - const transaction = await this.getTransaction( - [this.STORE_NAME, this.DIR_STORE_NAME], - "readwrite", - ); - const filesStore = transaction.objectStore(this.STORE_NAME); - const dirsStore = transaction.objectStore(this.DIR_STORE_NAME); - return new Promise((resolve, reject) => { - const r0 = filesStore.clear(); - r0.onsuccess = () => { - const r2 = dirsStore.clear(); - r2.onsuccess = () => resolve(); - r2.onerror = () => reject(); - }; - r0.onerror = () => reject(); - }); + const filesStore = await this.getStore(this.STORE_NAME, "readwrite"); + await asPromise(filesStore.clear()); + const dirsStore = await this.getStore(this.DIR_STORE_NAME, "readwrite"); + await asPromise(dirsStore.clear()); } /** @@ -353,9 +306,7 @@ export class IndexedDBFileSystem extends IFileSystem { const actualContent = new TextDecoder("utf-8").decode(content); if (actualContent !== expectedContent) { throw new Error( - `File content verification failed at ${path}\n` + - `Expected: ${expectedContent}\n` + - `Actual: ${actualContent}`, + `File content verification failed at ${path}\n` + `Expected: ${expectedContent}\n` + `Actual: ${actualContent}`, ); } } @@ -365,10 +316,7 @@ export class IndexedDBFileSystem extends IFileSystem { * @param dbName 测试数据库名称 * @param storeName 测试存储名称 */ - static async testFileSystem( - dbName: string, - storeName: string, - ): Promise { + static async testFileSystem(dbName: string, storeName: string): Promise { const fs = new IndexedDBFileSystem(dbName, storeName); // 初始化数据库 From 9fec9be4af613527643a891534743c8930be45fd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:21:54 +0800 Subject: [PATCH 10/22] =?UTF-8?q?feat(core):=20=E5=9C=A8=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E6=B8=85=E7=90=86=E8=99=9A=E6=8B=9F=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=B9=B6=E4=BC=98=E5=8C=96=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在加载文件前,增加清除虚拟文件系统的操作 - 在多个地方添加 VFileSystem.clear() 调用,以确保文件切换时清理虚拟文件系统 - 修改文件加载逻辑,只允许加载 .gp 后缀的文件 - 更新错误提示信息,明确指出需要选择 .gp 文件 --- src/core/service/dataFileService/VFileSystem.tsx | 1 + src/pages/_app.tsx | 5 +++++ src/pages/_app_menu.tsx | 7 +++++-- src/pages/_recent_files_panel.tsx | 6 +++--- src/pages/_start_file_panel.tsx | 4 ++-- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/core/service/dataFileService/VFileSystem.tsx b/src/core/service/dataFileService/VFileSystem.tsx index 37c1c70a..37dc1a92 100644 --- a/src/core/service/dataFileService/VFileSystem.tsx +++ b/src/core/service/dataFileService/VFileSystem.tsx @@ -20,6 +20,7 @@ export namespace VFileSystem { setMetaData(JSON.stringify(StageDumper.dump())); } export async function loadFromPath(path: string) { + await clear(); const data = await readFile(path); const zip = await jszip.loadAsync(data); const entries = zip.files; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b8bba138..14a80f5a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -21,6 +21,7 @@ import LogicNodePanel from "./_logic_node_panel"; import RecentFilesPanel from "./_recent_files_panel"; import StartFilePanel from "./_start_file_panel"; import TagPanel from "./_tag_panel"; +import { VFileSystem } from "../core/service/dataFileService/VFileSystem"; export default function App() { const [maxmized, setMaxmized] = React.useState(false); @@ -104,6 +105,7 @@ export default function App() { { text: "不保存", onClick: async () => { + await VFileSystem.clear(); await getCurrentWindow().destroy(); }, }, @@ -118,10 +120,12 @@ export default function App() { if (isAutoSave) { // 开启了自动保存,不弹窗 await StageSaveManager.saveHandle(file, StageDumper.dump()); + await VFileSystem.clear(); getCurrentWindow().destroy(); } else { // 没开启自动保存,逐步确认 if (StageSaveManager.isSaved()) { + await VFileSystem.clear(); getCurrentWindow().destroy(); } else { await Dialog.show({ @@ -132,6 +136,7 @@ export default function App() { text: "保存并关闭", onClick: async () => { await StageSaveManager.saveHandle(file, StageDumper.dump()); + await VFileSystem.clear(); await getCurrentWindow().destroy(); }, }, diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 57e67be2..5fcbfaeb 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -46,6 +46,7 @@ import { Stage } from "../core/stage/Stage"; import { GraphMethods } from "../core/stage/stageManager/basicMethods/GraphMethods"; import { TextNode } from "../core/stage/stageObject/entity/TextNode"; import { PathString } from "../utils/pathString"; +import { VFileSystem } from "../core/service/dataFileService/VFileSystem"; export default function AppMenu({ className = "", open = false }: { className?: string; open: boolean }) { const navigate = useNavigate(); @@ -57,9 +58,10 @@ export default function AppMenu({ className = "", open = false }: { className?: /** * 新建草稿 */ - const onNewDraft = () => { + const onNewDraft = async () => { if (StageSaveManager.isSaved() || StageManager.isEmpty()) { StageManager.destroy(); + await VFileSystem.clear(); setFile("Project Graph"); } else { // 当前文件未保存 @@ -76,8 +78,9 @@ export default function AppMenu({ className = "", open = false }: { className?: }, { text: "丢弃当前并直接新开", - onClick: () => { + onClick: async () => { StageManager.destroy(); + await VFileSystem.clear(); setFile("Project Graph"); }, }, diff --git a/src/pages/_recent_files_panel.tsx b/src/pages/_recent_files_panel.tsx index 7cd5b18b..4e1f8eef 100644 --- a/src/pages/_recent_files_panel.tsx +++ b/src/pages/_recent_files_panel.tsx @@ -76,9 +76,9 @@ export default function RecentFilesPanel() { try { const path = file.path; setFile(decodeURIComponent(path)); - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个GP文件", type: "error", }); return; @@ -87,7 +87,7 @@ export default function RecentFilesPanel() { setRecentFilePanelOpen(false); } catch (error) { Dialog.show({ - title: "请选择正确的JSON文件", + title: "请选择正确的GP文件", content: String(error), type: "error", }); diff --git a/src/pages/_start_file_panel.tsx b/src/pages/_start_file_panel.tsx index 2206ffcd..fdc98bb8 100644 --- a/src/pages/_start_file_panel.tsx +++ b/src/pages/_start_file_panel.tsx @@ -157,9 +157,9 @@ export default function StartFilePanel({ open = false }: { open: boolean }) { const checkoutFile = (path: string) => { try { setFile(decodeURIComponent(path)); - if (isDesktop && !path.endsWith(".json")) { + if (isDesktop && !path.endsWith(".gp")) { Dialog.show({ - title: "请选择一个JSON文件", + title: "请选择一个GP文件", type: "error", }); return; From f9aaa3d4a220da3cd457ab6d9179f6a289d22bfd Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Sun, 2 Feb 2025 00:25:59 +0800 Subject: [PATCH 11/22] =?UTF-8?q?fix(file):=20=E4=BF=AE=E5=A4=8Dvfs?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E6=97=B6=E4=B8=A2=E5=A4=B1await=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 dealPngFileDrop 函数中添加 async/await 以确保文件写入操作是异步执行的 - 在 readClipboardItems 函数中添加 await 以确保文件写入操作完成后再继续执行后续代码 --- .../controlService/controller/concrete/ControllerDragFile.tsx | 4 ++-- src/core/service/dataManageService/copyEngine/copyEngine.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx index cc533265..e978d2bb 100644 --- a/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx +++ b/src/core/service/controlService/controller/concrete/ControllerDragFile.tsx @@ -155,7 +155,7 @@ function dealJsonFileDrop(file: File, mouseWorldLocation: Vector) { function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { const reader = new FileReader(); reader.readAsDataURL(file); // 以文本格式读取文件内容 - reader.onload = (e) => { + reader.onload = async (e) => { const fileContent = e.target?.result; // 读取的文件内容 if (typeof fileContent !== "string") { @@ -168,7 +168,7 @@ function dealPngFileDrop(file: File, mouseWorldLocation: Vector) { // data:image/png;base64,iVBORw0KGgoAAAANS... // 在这里处理读取到的内容 const imageUUID = uuidv4(); - VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); + await VFileSystem.getFS().writeFileBase64(`/picture/${imageUUID}.png`, fileContent.split(",")[1]); const imageNode = new ImageNode({ uuid: imageUUID, location: [mouseWorldLocation.x, mouseWorldLocation.y], diff --git a/src/core/service/dataManageService/copyEngine/copyEngine.tsx b/src/core/service/dataManageService/copyEngine/copyEngine.tsx index 1e811e35..185f62e9 100644 --- a/src/core/service/dataManageService/copyEngine/copyEngine.tsx +++ b/src/core/service/dataManageService/copyEngine/copyEngine.tsx @@ -130,7 +130,7 @@ async function readClipboardItems(mouseLocation: Vector) { const blob = await item.getType(item.types[0]); // 获取 Blob 对象 const imageUUID = uuidv4(); //const folder = PathString.dirPath(Stage.path.getFilePath()); - VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); + await VFileSystem.getFS().writeFile(`/picture/${imageUUID}.png`, await blob.bytes()); //const imagePath = `${folder}${PathString.getSep()}${imageUUID}.png`; // 2024.12.31 测试发现这样的写法会导致读取时base64解码失败 From d385e4259bd7819f25c1bb2ef78ccd219eb3a1da Mon Sep 17 00:00:00 2001 From: zty012 Date: Sun, 2 Feb 2025 15:25:25 +0800 Subject: [PATCH 12/22] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20update=20vitest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pnpm-lock.yaml | 183 ++++++++---------- src/__tests__/README.md | 1 - {src/__tests__/unit => tests}/deco.test.tsx | 0 .../unit => tests}/lruCache.test.tsx | 4 +- {src/__tests__/unit => tests}/mod.test.tsx | 4 +- .../unit => tests}/monoStack.test.tsx | 4 +- .../parseMarkdownToJSON.test.tsx | 4 +- .../unit => tests}/validUrl.test.tsx | 4 +- {src/__tests__/unit => tests}/vector.test.tsx | 4 +- {src/__tests__/unit => tests}/vitest.test.tsx | 0 tsconfig.json | 17 +- vite.config.ts | 7 + vitest.config.ts | 17 -- 14 files changed, 115 insertions(+), 136 deletions(-) delete mode 100644 src/__tests__/README.md rename {src/__tests__/unit => tests}/deco.test.tsx (100%) rename {src/__tests__/unit => tests}/lruCache.test.tsx (95%) rename {src/__tests__/unit => tests}/mod.test.tsx (75%) rename {src/__tests__/unit => tests}/monoStack.test.tsx (96%) rename {src/__tests__/unit => tests}/parseMarkdownToJSON.test.tsx (97%) rename {src/__tests__/unit => tests}/validUrl.test.tsx (98%) rename {src/__tests__/unit => tests}/vector.test.tsx (72%) rename {src/__tests__/unit => tests}/vitest.test.tsx (100%) delete mode 100644 vitest.config.ts diff --git a/package.json b/package.json index dcddfc34..5a84a658 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "vite": "^6.0.6", "vite-plugin-svgr": "^4.3.0", "vitepress": "^1.5.0", - "vitest": "^2.1.8", + "vitest": "3.0.4", "vue": "^3.5.13" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a5abf4b..ccfa2bec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,8 +177,8 @@ importers: specifier: ^1.5.0 version: 1.5.0(@algolia/client-search@5.18.0)(@types/react@19.0.2)(less@4.2.1)(lightningcss@1.29.1)(postcss@8.4.49)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass-embedded@1.83.0)(search-insights@2.17.3)(typescript@5.7.2) vitest: - specifier: ^2.1.8 - version: 2.1.8(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) + specifier: 3.0.4 + version: 3.0.4(jiti@2.4.2)(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1) vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.2) @@ -1012,70 +1012,60 @@ packages: { integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A== } cpu: [arm] os: [linux] - libc: [glibc] "@rollup/rollup-linux-arm-musleabihf@4.29.1": resolution: { integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ== } cpu: [arm] os: [linux] - libc: [musl] "@rollup/rollup-linux-arm64-gnu@4.29.1": resolution: { integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA== } cpu: [arm64] os: [linux] - libc: [glibc] "@rollup/rollup-linux-arm64-musl@4.29.1": resolution: { integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA== } cpu: [arm64] os: [linux] - libc: [musl] "@rollup/rollup-linux-loongarch64-gnu@4.29.1": resolution: { integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw== } cpu: [loong64] os: [linux] - libc: [glibc] "@rollup/rollup-linux-powerpc64le-gnu@4.29.1": resolution: { integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w== } cpu: [ppc64] os: [linux] - libc: [glibc] "@rollup/rollup-linux-riscv64-gnu@4.29.1": resolution: { integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ== } cpu: [riscv64] os: [linux] - libc: [glibc] "@rollup/rollup-linux-s390x-gnu@4.29.1": resolution: { integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g== } cpu: [s390x] os: [linux] - libc: [glibc] "@rollup/rollup-linux-x64-gnu@4.29.1": resolution: { integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ== } cpu: [x64] os: [linux] - libc: [glibc] "@rollup/rollup-linux-x64-musl@4.29.1": resolution: { integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA== } cpu: [x64] os: [linux] - libc: [musl] "@rollup/rollup-win32-arm64-msvc@4.29.1": resolution: @@ -1226,7 +1216,6 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] - libc: [glibc] "@swc/core-linux-arm64-musl@1.10.1": resolution: @@ -1234,7 +1223,6 @@ packages: engines: { node: ">=10" } cpu: [arm64] os: [linux] - libc: [musl] "@swc/core-linux-x64-gnu@1.10.1": resolution: @@ -1242,7 +1230,6 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] - libc: [glibc] "@swc/core-linux-x64-musl@1.10.1": resolution: @@ -1250,7 +1237,6 @@ packages: engines: { node: ">=10" } cpu: [x64] os: [linux] - libc: [musl] "@swc/core-win32-arm64-msvc@1.10.1": resolution: @@ -1336,7 +1322,6 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] - libc: [glibc] "@tailwindcss/oxide-linux-arm64-musl@4.0.0": resolution: @@ -1344,7 +1329,6 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] - libc: [musl] "@tailwindcss/oxide-linux-x64-gnu@4.0.0": resolution: @@ -1352,7 +1336,6 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] - libc: [glibc] "@tailwindcss/oxide-linux-x64-musl@4.0.0": resolution: @@ -1360,7 +1343,6 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] - libc: [musl] "@tailwindcss/oxide-win32-arm64-msvc@4.0.0": resolution: @@ -1418,7 +1400,6 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] - libc: [glibc] "@tauri-apps/cli-linux-arm64-musl@2.1.0": resolution: @@ -1426,7 +1407,6 @@ packages: engines: { node: ">= 10" } cpu: [arm64] os: [linux] - libc: [musl] "@tauri-apps/cli-linux-x64-gnu@2.1.0": resolution: @@ -1434,7 +1414,6 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] - libc: [glibc] "@tauri-apps/cli-linux-x64-musl@2.1.0": resolution: @@ -1442,7 +1421,6 @@ packages: engines: { node: ">= 10" } cpu: [x64] os: [linux] - libc: [musl] "@tauri-apps/cli-win32-arm64-msvc@2.1.0": resolution: @@ -1638,41 +1616,41 @@ packages: vite: ^5.0.0 || ^6.0.0 vue: ^3.2.25 - "@vitest/expect@2.1.8": + "@vitest/expect@3.0.4": resolution: - { integrity: sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== } + { integrity: sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg== } - "@vitest/mocker@2.1.8": + "@vitest/mocker@3.0.4": resolution: - { integrity: sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== } + { integrity: sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A== } peerDependencies: msw: ^2.4.9 - vite: ^5.0.0 + vite: ^5.0.0 || ^6.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - "@vitest/pretty-format@2.1.8": + "@vitest/pretty-format@3.0.4": resolution: - { integrity: sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== } + { integrity: sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g== } - "@vitest/runner@2.1.8": + "@vitest/runner@3.0.4": resolution: - { integrity: sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== } + { integrity: sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw== } - "@vitest/snapshot@2.1.8": + "@vitest/snapshot@3.0.4": resolution: - { integrity: sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== } + { integrity: sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w== } - "@vitest/spy@2.1.8": + "@vitest/spy@3.0.4": resolution: - { integrity: sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== } + { integrity: sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q== } - "@vitest/utils@2.1.8": + "@vitest/utils@3.0.4": resolution: - { integrity: sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== } + { integrity: sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ== } "@vue/compiler-core@3.5.13": resolution: @@ -2229,9 +2207,9 @@ packages: { integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w== } engines: { node: ">= 0.4" } - es-module-lexer@1.5.4: + es-module-lexer@1.6.0: resolution: - { integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== } + { integrity: sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ== } es-object-atoms@1.0.0: resolution: @@ -2970,7 +2948,6 @@ packages: engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.29.1: resolution: @@ -2978,7 +2955,6 @@ packages: engines: { node: ">= 12.0.0" } cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.29.1: resolution: @@ -2986,7 +2962,6 @@ packages: engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.29.1: resolution: @@ -2994,7 +2969,6 @@ packages: engines: { node: ">= 12.0.0" } cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.29.1: resolution: @@ -3332,9 +3306,9 @@ packages: { integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== } engines: { node: ">=8" } - pathe@1.1.2: + pathe@2.0.2: resolution: - { integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== } + { integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w== } pathval@2.0.0: resolution: @@ -4024,18 +3998,18 @@ packages: resolution: { integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== } - tinyexec@0.3.1: + tinyexec@0.3.2: resolution: - { integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== } + { integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== } tinypool@1.0.2: resolution: { integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== } engines: { node: ^18.0.0 || >=20.0.0 } - tinyrainbow@1.2.0: + tinyrainbow@2.0.0: resolution: - { integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== } + { integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== } engines: { node: ">=14.0.0" } tinyspy@3.0.2: @@ -4195,10 +4169,10 @@ packages: resolution: { integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== } - vite-node@2.1.8: + vite-node@3.0.4: resolution: - { integrity: sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== } - engines: { node: ^18.0.0 || >=20.0.0 } + { integrity: sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA== } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } hasBin: true vite-plugin-svgr@4.3.0: @@ -4293,21 +4267,24 @@ packages: postcss: optional: true - vitest@2.1.8: + vitest@3.0.4: resolution: - { integrity: sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== } - engines: { node: ^18.0.0 || >=20.0.0 } + { integrity: sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw== } + engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } hasBin: true peerDependencies: "@edge-runtime/vm": "*" - "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 2.1.8 - "@vitest/ui": 2.1.8 + "@types/debug": ^4.1.12 + "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0 + "@vitest/browser": 3.0.4 + "@vitest/ui": 3.0.4 happy-dom: "*" jsdom: "*" peerDependenciesMeta: "@edge-runtime/vm": optional: true + "@types/debug": + optional: true "@types/node": optional: true "@vitest/browser": @@ -5661,45 +5638,45 @@ snapshots: vite: 5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) vue: 3.5.13(typescript@5.7.2) - "@vitest/expect@2.1.8": + "@vitest/expect@3.0.4": dependencies: - "@vitest/spy": 2.1.8 - "@vitest/utils": 2.1.8 + "@vitest/spy": 3.0.4 + "@vitest/utils": 3.0.4 chai: 5.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - "@vitest/mocker@2.1.8(vite@5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0))": + "@vitest/mocker@3.0.4(vite@6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1))": dependencies: - "@vitest/spy": 2.1.8 + "@vitest/spy": 3.0.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) + vite: 6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1) - "@vitest/pretty-format@2.1.8": + "@vitest/pretty-format@3.0.4": dependencies: - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 - "@vitest/runner@2.1.8": + "@vitest/runner@3.0.4": dependencies: - "@vitest/utils": 2.1.8 - pathe: 1.1.2 + "@vitest/utils": 3.0.4 + pathe: 2.0.2 - "@vitest/snapshot@2.1.8": + "@vitest/snapshot@3.0.4": dependencies: - "@vitest/pretty-format": 2.1.8 + "@vitest/pretty-format": 3.0.4 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 - "@vitest/spy@2.1.8": + "@vitest/spy@3.0.4": dependencies: tinyspy: 3.0.2 - "@vitest/utils@2.1.8": + "@vitest/utils@3.0.4": dependencies: - "@vitest/pretty-format": 2.1.8 + "@vitest/pretty-format": 3.0.4 loupe: 3.1.2 - tinyrainbow: 1.2.0 + tinyrainbow: 2.0.0 "@vue/compiler-core@3.5.13": dependencies: @@ -6228,7 +6205,7 @@ snapshots: iterator.prototype: 1.1.4 safe-array-concat: 1.1.3 - es-module-lexer@1.5.4: {} + es-module-lexer@1.6.0: {} es-object-atoms@1.0.0: dependencies: @@ -7206,7 +7183,7 @@ snapshots: path-type@4.0.0: {} - pathe@1.1.2: {} + pathe@2.0.2: {} pathval@2.0.0: {} @@ -7715,11 +7692,11 @@ snapshots: tinybench@2.9.0: {} - tinyexec@0.3.1: {} + tinyexec@0.3.2: {} tinypool@1.0.2: {} - tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} tinyspy@3.0.2: {} @@ -7864,15 +7841,16 @@ snapshots: "@types/unist": 3.0.3 vfile-message: 4.0.2 - vite-node@2.1.8(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0): + vite-node@3.0.4(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1): dependencies: cac: 6.7.14 debug: 4.4.0 - es-module-lexer: 1.5.4 - pathe: 1.1.2 - vite: 5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) + es-module-lexer: 1.6.0 + pathe: 2.0.2 + vite: 6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1) transitivePeerDependencies: - "@types/node" + - jiti - less - lightningcss - sass @@ -7881,6 +7859,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml vite-plugin-svgr@4.3.0(rollup@4.29.1)(typescript@5.7.2)(vite@6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1)): dependencies: @@ -7967,31 +7947,32 @@ snapshots: - typescript - universal-cookie - vitest@2.1.8(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0): + vitest@3.0.4(jiti@2.4.2)(jsdom@25.0.1)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1): dependencies: - "@vitest/expect": 2.1.8 - "@vitest/mocker": 2.1.8(vite@5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)) - "@vitest/pretty-format": 2.1.8 - "@vitest/runner": 2.1.8 - "@vitest/snapshot": 2.1.8 - "@vitest/spy": 2.1.8 - "@vitest/utils": 2.1.8 + "@vitest/expect": 3.0.4 + "@vitest/mocker": 3.0.4(vite@6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1)) + "@vitest/pretty-format": 3.0.4 + "@vitest/runner": 3.0.4 + "@vitest/snapshot": 3.0.4 + "@vitest/spy": 3.0.4 + "@vitest/utils": 3.0.4 chai: 5.1.2 debug: 4.4.0 expect-type: 1.1.0 magic-string: 0.30.17 - pathe: 1.1.2 + pathe: 2.0.2 std-env: 3.8.0 tinybench: 2.9.0 - tinyexec: 0.3.1 + tinyexec: 0.3.2 tinypool: 1.0.2 - tinyrainbow: 1.2.0 - vite: 5.4.11(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) - vite-node: 2.1.8(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0) + tinyrainbow: 2.0.0 + vite: 6.0.6(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1) + vite-node: 3.0.4(jiti@2.4.2)(less@4.2.1)(lightningcss@1.29.1)(sass-embedded@1.83.0)(yaml@2.6.1) why-is-node-running: 2.3.0 optionalDependencies: jsdom: 25.0.1 transitivePeerDependencies: + - jiti - less - lightningcss - msw @@ -8001,6 +7982,8 @@ snapshots: - sugarss - supports-color - terser + - tsx + - yaml void-elements@3.1.0: {} diff --git a/src/__tests__/README.md b/src/__tests__/README.md deleted file mode 100644 index 6e88dac7..00000000 --- a/src/__tests__/README.md +++ /dev/null @@ -1 +0,0 @@ -这里是放测试的地方 diff --git a/src/__tests__/unit/deco.test.tsx b/tests/deco.test.tsx similarity index 100% rename from src/__tests__/unit/deco.test.tsx rename to tests/deco.test.tsx diff --git a/src/__tests__/unit/lruCache.test.tsx b/tests/lruCache.test.tsx similarity index 95% rename from src/__tests__/unit/lruCache.test.tsx rename to tests/lruCache.test.tsx index 8de77368..38540068 100644 --- a/src/__tests__/unit/lruCache.test.tsx +++ b/tests/lruCache.test.tsx @@ -1,6 +1,6 @@ // LruCache.test.ts -import { describe, it, expect } from "vitest"; -import { LruCache } from "../../core/dataStruct/Cache"; +import { describe, expect, it } from "vitest"; +import { LruCache } from "../src/core/dataStruct/Cache"; describe("LruCache", () => { it("对于不存在的键应返回 undefined", () => { diff --git a/src/__tests__/unit/mod.test.tsx b/tests/mod.test.tsx similarity index 75% rename from src/__tests__/unit/mod.test.tsx rename to tests/mod.test.tsx index 6ef8d1e0..00cebb83 100644 --- a/src/__tests__/unit/mod.test.tsx +++ b/tests/mod.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { NumberFunctions } from "../../core/algorithm/numberFunctions"; +import { describe, expect, it } from "vitest"; +import { NumberFunctions } from "../src/core/algorithm/numberFunctions"; describe("mod.test.tsx", () => { it("should pass", () => { diff --git a/src/__tests__/unit/monoStack.test.tsx b/tests/monoStack.test.tsx similarity index 96% rename from src/__tests__/unit/monoStack.test.tsx rename to tests/monoStack.test.tsx index 6f38b24e..9500d361 100644 --- a/src/__tests__/unit/monoStack.test.tsx +++ b/tests/monoStack.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { MonoStack } from "../../core/dataStruct/MonoStack"; +import { describe, expect, it } from "vitest"; +import { MonoStack } from "../src/core/dataStruct/MonoStack"; describe("monoStack", () => { /** * a diff --git a/src/__tests__/unit/parseMarkdownToJSON.test.tsx b/tests/parseMarkdownToJSON.test.tsx similarity index 97% rename from src/__tests__/unit/parseMarkdownToJSON.test.tsx rename to tests/parseMarkdownToJSON.test.tsx index 838c3f17..5419af18 100644 --- a/src/__tests__/unit/parseMarkdownToJSON.test.tsx +++ b/tests/parseMarkdownToJSON.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { parseMarkdownToJSON } from "../../utils/markdownParse"; +import { describe, expect, it } from "vitest"; +import { parseMarkdownToJSON } from "../src/utils/markdownParse"; describe("测试测试框架是否正常运行", () => { it("测试用例1", () => { diff --git a/src/__tests__/unit/validUrl.test.tsx b/tests/validUrl.test.tsx similarity index 98% rename from src/__tests__/unit/validUrl.test.tsx rename to tests/validUrl.test.tsx index d25ed3fb..fb7ad684 100644 --- a/src/__tests__/unit/validUrl.test.tsx +++ b/tests/validUrl.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { PathString } from "../../utils/pathString"; +import { describe, expect, it } from "vitest"; +import { PathString } from "../src/utils/pathString"; describe("PathString", () => { it("URL有效性检测", () => { diff --git a/src/__tests__/unit/vector.test.tsx b/tests/vector.test.tsx similarity index 72% rename from src/__tests__/unit/vector.test.tsx rename to tests/vector.test.tsx index e2ae8934..ccd3cacb 100644 --- a/src/__tests__/unit/vector.test.tsx +++ b/tests/vector.test.tsx @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { Vector } from "../../core/dataStruct/Vector"; +import { describe, expect, it } from "vitest"; +import { Vector } from "../src/core/dataStruct/Vector"; describe("Vector", () => { it("1+1=2", () => { diff --git a/src/__tests__/unit/vitest.test.tsx b/tests/vitest.test.tsx similarity index 100% rename from src/__tests__/unit/vitest.test.tsx rename to tests/vitest.test.tsx diff --git a/tsconfig.json b/tsconfig.json index 2e0c745f..3245d4e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,9 +23,16 @@ // 其他 "experimentalDecorators": true // 开启装饰器 }, - "include": ["src"], - "references": [ - { "path": "./tsconfig.node.json" }, - { "path": "./tsconfig.docs.json" } - ] + "include": [ + "src", + "tests/deco.test.tsx", + "tests/lruCache.test.tsx", + "tests/mod.test.tsx", + "tests/monoStack.test.tsx", + "tests/parseMarkdownToJSON.test.tsx", + "tests/validUrl.test.tsx", + "tests/vector.test.tsx", + "tests/vitest.test.tsx" + ], + "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.docs.json" }] } diff --git a/vite.config.ts b/vite.config.ts index b630bc34..10c6ab50 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,5 @@ +/// + import generouted from "@generouted/react-router/plugin"; import ViteYaml from "@modyfi/vite-plugin-yaml"; import tailwindcss from "@tailwindcss/vite"; @@ -59,4 +61,9 @@ export default defineConfig(async () => ({ // 只有名字以LR_开头的环境变量才会被注入到前端 // import.meta.env.LR_xxx envPrefix: "LR_", + + test: { + environment: "jsdom", + include: ["./tests/**/*.test.tsx"], + }, })); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index 95189d86..00000000 --- a/vitest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - // 配置测试环境 - environment: "jsdom", // 用jsdom模拟浏览器环境 - // 全局设置 - globals: true, - include: ["src/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], - // 设置覆盖率报告 - // coverage: { - // provider: 'c8', - // reporter: ['text', 'json', 'html'] - // }, - // 其他配置选项 - }, -}); From 0f076d5894688ac2e5468d713748eded54fbe3f0 Mon Sep 17 00:00:00 2001 From: littlefean <2028140990@qq.com> Date: Sun, 2 Feb 2025 16:41:16 +0800 Subject: [PATCH 13/22] =?UTF-8?q?=F0=9F=93=9D=20=E4=BF=AE=E6=94=B9toolbar?= =?UTF-8?q?=E8=BF=9E=E7=BA=BF=E9=A2=9C=E8=89=B2=E6=9B=B4=E6=94=B9=E7=9A=84?= =?UTF-8?q?=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/_toolbar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_toolbar.tsx b/src/pages/_toolbar.tsx index 5d95bc0a..7e7d3091 100644 --- a/src/pages/_toolbar.tsx +++ b/src/pages/_toolbar.tsx @@ -351,7 +351,7 @@ export default function Toolbar({ className = "" }: { className?: string }) { {(isHaveSelectedNode || isHaveSelectedEdge) && ( } handleFunction={() => Popup.show()} /> From b8250674990ed58b0ce775a271ee57d763cccd8e Mon Sep 17 00:00:00 2001 From: littlefean <2028140990@qq.com> Date: Sun, 2 Feb 2025 17:15:12 +0800 Subject: [PATCH 14/22] =?UTF-8?q?=F0=9F=90=9B=20=E4=B8=B4=E6=97=B6?= =?UTF-8?q?=E6=80=A7=E8=A7=A3=E5=86=B3toolbar=E8=BF=87=E9=AB=98=E9=81=AE?= =?UTF-8?q?=E6=8C=A1=E5=8F=B3=E4=B8=8A=E8=A7=92=E6=8C=89=E9=92=AE=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/_toolbar.tsx | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pages/_toolbar.tsx b/src/pages/_toolbar.tsx index 7e7d3091..16cc5dca 100644 --- a/src/pages/_toolbar.tsx +++ b/src/pages/_toolbar.tsx @@ -274,7 +274,8 @@ function AlignNodePanel() { * @returns */ export default function Toolbar({ className = "" }: { className?: string }) { - const [isCopyClearShow, setIsCopyClearShow] = useState(false); + // 是否显示清空粘贴板 + const [isClipboardClearShow, setIsCopyClearShow] = useState(false); const [isHaveSelectedNode, setSsHaveSelectedNode] = useState(false); const [isHaveSelectedNodeOverTwo, setSsHaveSelectedNodeOverTwo] = useState(false); const [isHaveSelectedEdge, setSsHaveSelectedEdge] = useState(false); @@ -314,13 +315,13 @@ export default function Toolbar({ className = "" }: { className?: string }) { // 因为报错窗口可能会被它遮挡住导致无法在右上角关闭报错窗口 return (
@@ -364,7 +365,7 @@ export default function Toolbar({ className = "" }: { className?: string }) { handleFunction={() => Popup.show()} /> )} - {isCopyClearShow && ( + {isClipboardClearShow && ( } @@ -469,6 +470,28 @@ export default function Toolbar({ className = "" }: { className?: string }) { StageManager.autoLayoutFastTreeMode(); }} /> + {/* 测试占位符 */} + {/* } + handleFunction={() => { + StageManager.autoLayoutFastTreeMode(); + }} + /> + } + handleFunction={() => { + StageManager.autoLayoutFastTreeMode(); + }} + /> + } + handleFunction={() => { + StageManager.autoLayoutFastTreeMode(); + }} + /> */}
); From c7b0aa27b63d4b68a2e664c41ba865479c7bcfc9 Mon Sep 17 00:00:00 2001 From: littlefean <2028140990@qq.com> Date: Sun, 2 Feb 2025 17:28:18 +0800 Subject: [PATCH 15/22] =?UTF-8?q?=F0=9F=90=9B=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=B3=A8=E9=87=8A=E5=88=A0=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=B9=B2=E5=87=80=E5=AF=BC=E8=87=B4=E5=8F=B3=E4=B8=8A=E8=A7=92?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=B8=80=E7=9B=B4=E6=98=BE=E7=A4=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canvas2d/entityRenderer/EntityDetailsButtonRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/render/canvas2d/entityRenderer/EntityDetailsButtonRenderer.tsx b/src/core/render/canvas2d/entityRenderer/EntityDetailsButtonRenderer.tsx index b8c2b0d8..a9dccc39 100644 --- a/src/core/render/canvas2d/entityRenderer/EntityDetailsButtonRenderer.tsx +++ b/src/core/render/canvas2d/entityRenderer/EntityDetailsButtonRenderer.tsx @@ -10,7 +10,7 @@ import { Renderer } from "../renderer"; * 仅仅渲染一个节点右上角的按钮 */ export function EntityDetailsButtonRenderer(entity: Entity) { - if (!entity.details) { + if (!entity.details.trim()) { return; } // ShapeRenderer.renderRect( From e06ddedb72b7cf1cacf3bc189c7ce83e67cda293 Mon Sep 17 00:00:00 2001 From: littlefean <2028140990@qq.com> Date: Sun, 2 Feb 2025 20:24:48 +0800 Subject: [PATCH 16/22] =?UTF-8?q?=F0=9F=90=9B=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E9=80=BB=E8=BE=91=E8=8A=82=E7=82=B9=EF=BC=9A?= =?UTF-8?q?=E6=A0=B9=E6=8D=AE=E9=A2=9C=E8=89=B2=E6=94=B6=E9=9B=86=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E6=97=B6=E9=81=87=E5=88=B0=E7=A9=BA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E4=B8=8D=E6=94=B6=E9=9B=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../autoComputeEngine/functions/nodeLogic.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/service/dataGenerateService/autoComputeEngine/functions/nodeLogic.tsx b/src/core/service/dataGenerateService/autoComputeEngine/functions/nodeLogic.tsx index 597e6c81..4c95d2e0 100644 --- a/src/core/service/dataGenerateService/autoComputeEngine/functions/nodeLogic.tsx +++ b/src/core/service/dataGenerateService/autoComputeEngine/functions/nodeLogic.tsx @@ -263,6 +263,9 @@ export namespace NodeLogic { if (AutoComputeUtils.isNodeConnectedWithLogicNode(node)) { continue; } + if (node.text.trim() === "") { + continue; + } // 匹配颜色 if (node.color.equals(matchColor)) { matchNodes.push(node); @@ -306,6 +309,9 @@ export namespace NodeLogic { if (AutoComputeUtils.isNodeConnectedWithLogicNode(node)) { continue; } + if (node.details.trim() === "") { + continue; + } // 匹配颜色 if (node.color.equals(matchColor)) { matchNodes.push(node); From 3c734e3da2dd201314e164d529a9869e8d59b216 Mon Sep 17 00:00:00 2001 From: littlefean <2028140990@qq.com> Date: Sun, 2 Feb 2025 20:54:03 +0800 Subject: [PATCH 17/22] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E8=AF=A6=E7=BB=86=E4=BF=A1=E6=81=AF=E5=AE=BD?= =?UTF-8?q?=E5=BA=A6=E7=9A=84=E8=87=AA=E5=AE=9A=E4=B9=89=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../render/canvas2d/entityRenderer/EntityRenderer.tsx | 2 +- src/core/render/canvas2d/renderer.tsx | 10 +++++++--- src/core/service/Settings.tsx | 2 ++ src/locales/en.yml | 4 ++++ src/locales/zh_CN.yml | 4 ++++ src/locales/zh_TW.yml | 4 ++++ src/pages/settings/visual.tsx | 9 +++++++++ 7 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx b/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx index 53fe6338..c9e508ff 100644 --- a/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx +++ b/src/core/render/canvas2d/entityRenderer/EntityRenderer.tsx @@ -195,7 +195,7 @@ export namespace EntityRenderer { ), Renderer.FONT_SIZE_DETAILS * Camera.currentScale, Math.max( - Renderer.NODE_DETAILS_WIDTH * Camera.currentScale, + Renderer.ENTITY_DETAILS_WIDTH * Camera.currentScale, entity.collisionBox.getRectangle().size.x * Camera.currentScale, ), StageStyleManager.currentStyle.NodeDetailsTextColor, diff --git a/src/core/render/canvas2d/renderer.tsx b/src/core/render/canvas2d/renderer.tsx index 07576a04..f653709e 100644 --- a/src/core/render/canvas2d/renderer.tsx +++ b/src/core/render/canvas2d/renderer.tsx @@ -4,11 +4,12 @@ import { Color, mixColors } from "../../dataStruct/Color"; import { Vector } from "../../dataStruct/Vector"; import { Rectangle } from "../../dataStruct/shape/Rectangle"; import { Settings } from "../../service/Settings"; +import { MouseLocation } from "../../service/controlService/MouseLocation"; import { Controller } from "../../service/controlService/controller/Controller"; +import { KeyboardOnlyEngine } from "../../service/controlService/keyboardOnlyEngine/keyboardOnlyEngine"; import { CopyEngine } from "../../service/dataManageService/copyEngine/copyEngine"; import { sine } from "../../service/feedbackService/effectEngine/mathTools/animateFunctions"; import { StageStyleManager } from "../../service/feedbackService/stageStyle/StageStyleManager"; -import { KeyboardOnlyEngine } from "../../service/controlService/keyboardOnlyEngine/keyboardOnlyEngine"; import { Camera } from "../../stage/Camera"; import { Canvas } from "../../stage/Canvas"; import { Stage } from "../../stage/Stage"; @@ -29,7 +30,6 @@ import { renderHorizonBackground, renderVerticalBackground, } from "./utilsRenderer/backgroundRenderer"; -import { MouseLocation } from "../../service/controlService/MouseLocation"; /** * 渲染器 @@ -50,10 +50,11 @@ export namespace Renderer { export const NODE_PADDING = 14; /// 节点的圆角半径 export const NODE_ROUNDED_RADIUS = 8; + /** * 节点详细信息最大宽度 */ - export const NODE_DETAILS_WIDTH = 200; + export let ENTITY_DETAILS_WIDTH = 200; export let w = 0; export let h = 0; @@ -114,6 +115,9 @@ export namespace Renderer { Settings.watch("entityDetailsLinesLimit", (value) => { ENTITY_DETAILS_LIENS_LIMIT = value; }); + Settings.watch("entityDetailsWidthLimit", (value) => { + ENTITY_DETAILS_WIDTH = value; + }); Settings.watch("showDebug", (value) => (isShowDebug = value)); Settings.watch("showBackgroundHorizontalLines", (value) => { isShowBackgroundHorizontalLines = value; diff --git a/src/core/service/Settings.tsx b/src/core/service/Settings.tsx index 3e26ac09..a545709f 100644 --- a/src/core/service/Settings.tsx +++ b/src/core/service/Settings.tsx @@ -30,6 +30,7 @@ export namespace Settings { useNativeTitleBar: boolean; entityDetailsFontSize: number; entityDetailsLinesLimit: number; + entityDetailsWidthLimit: number; limitCameraInCycleSpace: boolean; cameraCycleSpaceSizeX: number; cameraCycleSpaceSizeY: number; @@ -91,6 +92,7 @@ export namespace Settings { useNativeTitleBar: false, entityDetailsFontSize: 18, entityDetailsLinesLimit: 4, + entityDetailsWidthLimit: 200, limitCameraInCycleSpace: false, cameraCycleSpaceSizeX: 1000, cameraCycleSpaceSizeY: 1000, diff --git a/src/locales/en.yml b/src/locales/en.yml index 9d245e5a..b4240597 100644 --- a/src/locales/en.yml +++ b/src/locales/en.yml @@ -173,6 +173,10 @@ settings: title: Line Limit for Entity Details description: | Limit the maximum number of lines for entity details. Exceeding this limit will result in the omission of the excess content. + entityDetailsWidthLimit: + title: Width Limit for Entity Details + description: | + Limit the maximum width for entity details. Exceeding this limit will result in the excess content wrapping to the next line. limitCameraInCycleSpace: title: Enable Camera Movement Limitation in Cycle Space description: | diff --git a/src/locales/zh_CN.yml b/src/locales/zh_CN.yml index e7c7d53a..0c888587 100644 --- a/src/locales/zh_CN.yml +++ b/src/locales/zh_CN.yml @@ -158,6 +158,10 @@ settings: title: 实体详细信息行数限制 description: | 限制实体详细信息的最大行数,超过限制的部分将被省略 + entityDetailsWidthLimit: + title: 实体详细信息宽度限制 + description: | + 限制实体详细信息的最大宽度,超过限制的部分将被换行 limitCameraInCycleSpace: title: 开启循环空间限制摄像机移动 description: | diff --git a/src/locales/zh_TW.yml b/src/locales/zh_TW.yml index b9a8f6b9..8fdea587 100644 --- a/src/locales/zh_TW.yml +++ b/src/locales/zh_TW.yml @@ -145,6 +145,10 @@ settings: title: 实体详细信息行数限制 description: | 限制实体详细信息的最大行数,超过限制的部分将被省略 + entityDetailsWidthLimit: + title: 实体详细信息宽度限制 + description: | + 限制实体详细信息的最大宽度,超过限制的部分将被换行 limitCameraInCycleSpace: title: 开启循环空间限制摄像机移动 description: | diff --git a/src/pages/settings/visual.tsx b/src/pages/settings/visual.tsx index d375eed1..09cb2a2d 100644 --- a/src/pages/settings/visual.tsx +++ b/src/pages/settings/visual.tsx @@ -14,6 +14,7 @@ import { Ratio, Rows4, Scaling, + Space, Spline, VenetianMask, } from "lucide-react"; @@ -53,6 +54,14 @@ export default function Visual() { max={200} step={2} /> + } + settingKey="entityDetailsWidthLimit" + type="slider" + min={200} + max={2000} + step={100} + /> } settingKey="limitCameraInCycleSpace" type="switch" /> Date: Mon, 3 Feb 2025 00:29:50 +0800 Subject: [PATCH 18/22] =?UTF-8?q?feat(utils):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=B0=86=E7=BB=9D=E5=AF=B9=E8=B7=AF=E5=BE=84=E8=BD=AC=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 absolute2Ext 函数,用于将绝对路径转换为文件后缀 --- src/utils/pathString.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/utils/pathString.tsx b/src/utils/pathString.tsx index 7fd5d979..e1a345e5 100644 --- a/src/utils/pathString.tsx +++ b/src/utils/pathString.tsx @@ -47,6 +47,20 @@ export namespace PathString { } return file; } + /** + * 将绝对路径转换为文件后缀 + * @param path + * @returns + */ + export function absolute2Ext(path: string): string { + const fam = family(); + // const fam = "windows"; // vitest 测试时打开此行注释 + + if (fam === "windows") { + path = path.replace(/\\/g, "/"); + } + return path.split("/").pop()?.split(".").pop() || ""; + } /** * 根据文件的绝对路径,获取当前文件所在目录的路径 From 1f5bb89eccf92f10d2119b237bbc09905c869ed7 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Mon, 3 Feb 2025 00:32:01 +0800 Subject: [PATCH 19/22] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=20legacy?= =?UTF-8?q?=20=E6=96=87=E4=BB=B6=E6=89=93=E5=BC=80=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 RecentFileManager.openLegacyFileByPath 方法,用于打开旧版文件格式 - 在 AppMenu 中添加了"打开旧版文件"选项 - 优化了文件打开流程,支持选择 legacy 文件 --- .../dataFileService/RecentFileManager.tsx | 31 ++++++++++++- src/pages/_app_menu.tsx | 45 ++++++++++++++++--- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/src/core/service/dataFileService/RecentFileManager.tsx b/src/core/service/dataFileService/RecentFileManager.tsx index d0f9f1dd..3320fb42 100644 --- a/src/core/service/dataFileService/RecentFileManager.tsx +++ b/src/core/service/dataFileService/RecentFileManager.tsx @@ -1,7 +1,7 @@ import { Store } from "@tauri-apps/plugin-store"; // import { exists } from "@tauri-apps/plugin-fs"; // 导入文件相关函数 import { Serialized } from "../../../types/node"; -import { exists } from "../../../utils/fs/com"; +import { exists, readFile, readTextFile } from "../../../utils/fs/com"; import { createStore } from "../../../utils/store"; import { Camera } from "../../stage/Camera"; import { Stage } from "../../stage/Stage"; @@ -17,6 +17,7 @@ import { UrlNode } from "../../stage/stageObject/entity/UrlNode"; import { ViewFlashEffect } from "../feedbackService/effectEngine/concrete/ViewFlashEffect"; import { PenStroke } from "../../stage/stageObject/entity/PenStroke"; import { VFileSystem } from "./VFileSystem"; +import { PathString } from "../../../utils/pathString"; /** * 管理最近打开的文件列表 @@ -166,6 +167,34 @@ export namespace RecentFileManager { time: new Date().getTime(), }); } + export async function openLegacyFileByPath(path: string) { + StageManager.destroy(); + const ext = PathString.absolute2Ext(path); + if (ext !== "json") { + throw new Error("不兼容的文件格式"); + } + + const data = StageLoader.validate(JSON.parse(await readTextFile(path))); + const dirPath = PathString.dirPath(path); + const operations = data.entities + .filter((entity) => entity.type === "core:image_node") + .map(async (entity) => { + const ud = await readFile(`${dirPath}${PathString.getSep()}${entity.uuid}.png`); + await VFileSystem.getFS().writeFile(`/picture/${entity.uuid}.png`, ud); + }); + await Promise.all(operations); + loadStageByData(data); + await VFileSystem.pullMetaData(); + + StageHistoryManager.reset(data); + + Camera.reset(); + Stage.effectMachine.addEffect(ViewFlashEffect.SaveFile()); + // RecentFileManager.addRecentFile({ + // path: path, + // time: new Date().getTime(), + // }); + } export function loadStageByData(data: Serialized.File) { for (const entity of data.entities) { diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 5fcbfaeb..41d098ce 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -90,12 +90,12 @@ export default function AppMenu({ className = "", open = false }: { className?: } }; - const onOpen = async () => { + const onOpen = async (legacy: boolean = false) => { if (!StageSaveManager.isSaved()) { if (StageManager.isEmpty()) { //空项目不需要保存 StageManager.destroy(); - openFileByDialogWindow(); + openFileByDialogWindow(legacy); } else if (Stage.path.isDraft()) { Dialog.show({ title: "草稿未保存", @@ -107,7 +107,7 @@ export default function AppMenu({ className = "", open = false }: { className?: text: "丢弃并打开新文件", onClick: () => { StageManager.destroy(); - openFileByDialogWindow(); + openFileByDialogWindow(legacy); }, }, ], @@ -120,7 +120,7 @@ export default function AppMenu({ className = "", open = false }: { className?: { text: "保存并打开新文件", onClick: () => { - onSave().then(openFileByDialogWindow); + onSave().then(() => openFileByDialogWindow(legacy)); }, }, { text: "我再想想" }, @@ -129,11 +129,37 @@ export default function AppMenu({ className = "", open = false }: { className?: } } else { // 直接打开文件 - openFileByDialogWindow(); + openFileByDialogWindow(legacy); } }; - const openFileByDialogWindow = async () => { + const openLegacyFileByDialogWindow = async () => { + const path = isWeb + ? "file.json" + : await openFileDialog({ + title: "打开文件", + directory: false, + multiple: false, + filters: [], + }); + if (!path) { + return; + } + try { + await RecentFileManager.openLegacyFileByPath(path); // 已经包含历史记录重置功能 + // 设置为草稿 + setFile("Project Graph"); + } catch (e) { + Dialog.show({ + title: "请选择正确的文件", + content: String(e), + type: "error", + }); + } + }; + + const openFileByDialogWindow = async (legacy: boolean = false) => { + if (legacy) return openLegacyFileByDialogWindow(); const path = isWeb ? "file.gp" : await openFileDialog({ @@ -386,7 +412,7 @@ export default function AppMenu({ className = "", open = false }: { className?: } onClick={onNewDraft}> {t("file.items.new")} - } onClick={onOpen}> + } onClick={() => onOpen()}> {t("file.items.open")} {!isWeb && ( @@ -408,6 +434,11 @@ export default function AppMenu({ className = "", open = false }: { className?: {t("file.items.backup")} )} + {!isWeb && ( + } onClick={() => onOpen(true)}> + {t("file.items.openLegacy")} + + )} {!isWeb && ( } title={t("location.title")}> From 3b3b154f7808762de81e43b49eee330c28038f33 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Mon, 3 Feb 2025 00:32:38 +0800 Subject: [PATCH 20/22] =?UTF-8?q?style(fs):=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F=E7=9B=B8=E5=85=B3=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/fs/IFileSystem.tsx | 10 ++-------- src/utils/fs/TauriFileSystem.tsx | 14 +++---------- src/utils/fs/WebFileApiSystem.tsx | 33 +++++++------------------------ 3 files changed, 12 insertions(+), 45 deletions(-) diff --git a/src/utils/fs/IFileSystem.tsx b/src/utils/fs/IFileSystem.tsx index 4f8ac636..eed04683 100644 --- a/src/utils/fs/IFileSystem.tsx +++ b/src/utils/fs/IFileSystem.tsx @@ -47,10 +47,7 @@ export abstract class IFileSystem { // 抽象原始方法(带下划线版本) abstract _readFile(path: string): Promise; - abstract _writeFile( - path: string, - content: Uint8Array | string, - ): Promise; + abstract _writeFile(path: string, content: Uint8Array | string): Promise; abstract _readDir(path: string): Promise; abstract _mkdir(path: string, recursive?: boolean): Promise; abstract _stat(path: string): Promise; @@ -81,10 +78,7 @@ export abstract class IFileSystem { } rename(oldPath: string, newPath: string) { - return this._rename( - IFileSystem.normalizePath(oldPath), - IFileSystem.normalizePath(newPath), - ); + return this._rename(IFileSystem.normalizePath(oldPath), IFileSystem.normalizePath(newPath)); } deleteFile(path: string) { diff --git a/src/utils/fs/TauriFileSystem.tsx b/src/utils/fs/TauriFileSystem.tsx index b56a42b0..e0edec86 100644 --- a/src/utils/fs/TauriFileSystem.tsx +++ b/src/utils/fs/TauriFileSystem.tsx @@ -13,9 +13,7 @@ export class TauriFileSystem extends IFileSystem { } async _readFile(path: string): Promise { - return new Uint8Array( - await invoke("read_file", { path: this.basePath + path }), - ); + return new Uint8Array(await invoke("read_file", { path: this.basePath + path })); } async _writeFile(path: string, content: Uint8Array | string): Promise { @@ -64,18 +62,12 @@ export class TauriFileSystem extends IFileSystem { * @param path 文件路径 * @param expectedContent 预期内容 */ - private static async verifyFileContent( - fs: TauriFileSystem, - path: string, - expectedContent: string, - ): Promise { + private static async verifyFileContent(fs: TauriFileSystem, path: string, expectedContent: string): Promise { const content = await fs.readFile(path); const actualContent = new TextDecoder("utf-8").decode(content); if (actualContent !== expectedContent) { throw new Error( - `File content verification failed at ${path}\n` + - `Expected: ${expectedContent}\n` + - `Actual: ${actualContent}`, + `File content verification failed at ${path}\n` + `Expected: ${expectedContent}\n` + `Actual: ${actualContent}`, ); } } diff --git a/src/utils/fs/WebFileApiSystem.tsx b/src/utils/fs/WebFileApiSystem.tsx index 8379d678..7f89615a 100644 --- a/src/utils/fs/WebFileApiSystem.tsx +++ b/src/utils/fs/WebFileApiSystem.tsx @@ -1,8 +1,4 @@ -import { - IFileSystem, - type FileStats, - type DirectoryEntry, -} from "./IFileSystem"; +import { IFileSystem, type FileStats, type DirectoryEntry } from "./IFileSystem"; type FSAPHandle = FileSystemDirectoryHandle; @@ -27,15 +23,11 @@ export class WebFileApiSystem extends IFileSystem { } try { - currentHandle = await ( - currentHandle as FileSystemDirectoryHandle - ).getDirectoryHandle(part); + currentHandle = await (currentHandle as FileSystemDirectoryHandle).getDirectoryHandle(part); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (dirError) { try { - currentHandle = await ( - currentHandle as FileSystemDirectoryHandle - ).getFileHandle(part); + currentHandle = await (currentHandle as FileSystemDirectoryHandle).getFileHandle(part); // 提前终止检查:文件节点不能在路径中间 if (part !== parts[parts.length - 1]) { throw new Error(`File node cannot be in path middle: ${path}`); @@ -58,8 +50,7 @@ export class WebFileApiSystem extends IFileSystem { } async _writeFile(path: string, content: Uint8Array | string): Promise { - const buffer = - typeof content === "string" ? new TextEncoder().encode(content) : content; + const buffer = typeof content === "string" ? new TextEncoder().encode(content) : content; const parts = await this.resolvePathComponents(path); const fileName = parts.pop()!; @@ -73,10 +64,7 @@ export class WebFileApiSystem extends IFileSystem { await writable.close(); } - private async ensureDirectoryPath( - parts: string[], - recursive = false, - ): Promise { + private async ensureDirectoryPath(parts: string[], recursive = false): Promise { let currentHandle = this.rootHandle; for (let i = 0; i < parts.length; i++) { @@ -127,11 +115,7 @@ export class WebFileApiSystem extends IFileSystem { async _rename(oldPath: string, newPath: string): Promise { // 递归复制函数 - const copyRecursive = async ( - srcHandle: FileSystemHandle, - destDir: FileSystemDirectoryHandle, - newName: string, - ) => { + const copyRecursive = async (srcHandle: FileSystemHandle, destDir: FileSystemDirectoryHandle, newName: string) => { if (srcHandle.kind === "file") { const file = await (srcHandle as FileSystemFileHandle).getFile(); const newFile = await destDir.getFileHandle(newName, { create: true }); @@ -160,10 +144,7 @@ export class WebFileApiSystem extends IFileSystem { await this._delete(oldHandle.kind, oldPath); } - private async _delete( - kind: "file" | "directory", - path: string, - ): Promise { + private async _delete(kind: "file" | "directory", path: string): Promise { const parts = await this.resolvePathComponents(path); const targetName = parts.pop()!; const parentHandle = await this.ensureDirectoryPath(parts); From bffda711e93fc92ddbd994d3b130ddb0c9de8c74 Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Mon, 3 Feb 2025 00:35:14 +0800 Subject: [PATCH 21/22] =?UTF-8?q?feat(i18n):=20=E6=B7=BB=E5=8A=A0=E2=80=9C?= =?UTF-8?q?=E4=BB=8E=E6=97=A7=E7=89=88=E6=96=87=E4=BB=B6=E6=89=93=E5=BC=80?= =?UTF-8?q?=E2=80=9D=E5=8A=9F=E8=83=BD=E7=9A=84=E5=A4=9A=E8=AF=AD=E8=A8=80?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在英文、简体中文和繁体中文的本地化文件中添加了“Open Legacy Project”及其翻译 - 此更新支持了新开辟的功能,使用户能够从旧版本的文件中打开项目 --- src/locales/en.yml | 1 + src/locales/zh_CN.yml | 1 + src/locales/zh_TW.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/src/locales/en.yml b/src/locales/en.yml index 63050561..3fba22b4 100644 --- a/src/locales/en.yml +++ b/src/locales/en.yml @@ -356,6 +356,7 @@ appMenu: saveAs: Save As recent: Recents backup: Backup + openLegacy: Open Legacy Project location: title: Dirs items: diff --git a/src/locales/zh_CN.yml b/src/locales/zh_CN.yml index be58bc0d..2346b4dd 100644 --- a/src/locales/zh_CN.yml +++ b/src/locales/zh_CN.yml @@ -352,6 +352,7 @@ appMenu: saveAs: 另存为 recent: 最近打开 backup: 备份 + openLegacy: 从旧版文件打开 location: title: 位置 items: diff --git a/src/locales/zh_TW.yml b/src/locales/zh_TW.yml index 393809b1..7c7595a0 100644 --- a/src/locales/zh_TW.yml +++ b/src/locales/zh_TW.yml @@ -328,6 +328,7 @@ appMenu: saveAs: 另存為 recent: 最近打開 backup: 備份 + openLegacy: 打開舊版 location: title: 位置 items: From 1170901853dcf5f6a4cdff03443045a3b3e625db Mon Sep 17 00:00:00 2001 From: Jorylee <1747358809@qq.com> Date: Mon, 3 Feb 2025 00:36:55 +0800 Subject: [PATCH 22/22] =?UTF-8?q?style(icon):=20=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E6=89=93=E5=BC=80=5Flegacy=5F=E6=96=87=E4=BB=B6=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E7=9A=84=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 Database 图标替换为 FileText 图标 - 优化用户体验和视觉一致性 --- src/pages/_app_menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/_app_menu.tsx b/src/pages/_app_menu.tsx index 41d098ce..3c455462 100644 --- a/src/pages/_app_menu.tsx +++ b/src/pages/_app_menu.tsx @@ -435,7 +435,7 @@ export default function AppMenu({ className = "", open = false }: { className?: )} {!isWeb && ( - } onClick={() => onOpen(true)}> + } onClick={() => onOpen(true)}> {t("file.items.openLegacy")} )}