From 52abffc3c406e5f49941cb7c4a2bbf5a50256507 Mon Sep 17 00:00:00 2001 From: cirry <812852553@qq.com> Date: Wed, 8 Jul 2026 16:44:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=87=E6=8D=A2=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E7=9B=AE=E5=BD=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .reasonix/desktop-topic-titles.json | 2 +- src-tauri/src/lib.rs | 60 +- src/App.vue | 226 +++++-- src/components/DirectorySidebar.vue | 938 +++++++++++++++++++++++++++- 4 files changed, 1172 insertions(+), 54 deletions(-) diff --git a/.reasonix/desktop-topic-titles.json b/.reasonix/desktop-topic-titles.json index 6526aeb..d6054a9 100644 --- a/.reasonix/desktop-topic-titles.json +++ b/.reasonix/desktop-topic-titles.json @@ -36,5 +36,5 @@ "topic_20260707-092315_b8c3939bfc696802": "当我点击目录树切换显示文件的时候,右…", "topic_20260707-092711_af706c3fcc7e435e": "帮我检查一下app.vue里的too…", "topic_20260707-095503_bf879b65e4666516": "帮我移除markdownEditor…", - "topic_20260708-020616_9ab54d98daf65053": "新的会话" + "topic_20260708-020616_9ab54d98daf65053": "帮我分析一下现在的markdownE…" } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7a2b77d..165eb62 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,29 @@ struct DirEntry { is_dir: bool, } +fn is_hidden_entry(entry: &fs::DirEntry) -> bool { + let name = entry.file_name(); + let is_dot_hidden = name.to_string_lossy().starts_with('.'); + + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + + const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; + let is_windows_hidden = entry + .metadata() + .map(|metadata| metadata.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0) + .unwrap_or(false); + + return is_dot_hidden || is_windows_hidden; + } + + #[cfg(not(windows))] + { + is_dot_hidden + } +} + #[tauri::command] fn pick_folder(app: tauri::AppHandle) -> Option { app.dialog() @@ -22,13 +45,19 @@ fn pick_folder(app: tauri::AppHandle) -> Option { #[tauri::command] fn read_dir(path: String) -> Result, String> { let entries = fs::read_dir(&path).map_err(|e| format!("无法读取目录: {}", e))?; - let allowed_extensions = ["md", "txt", "png", "jpg", "jpeg", "gif", "svg", "webp", "bmp"]; + let allowed_extensions = [ + "md", "png", "jpg", "jpeg", "gif", "webp", "avif", "mp4", "webm", "mov", "m4v", "ogv", + ]; let mut result = Vec::new(); for entry in entries { let entry = entry.map_err(|e| format!("读取条目失败: {}", e))?; + if is_hidden_entry(&entry) { + continue; + } + let path = entry.path(); let is_dir = path.is_dir(); - // Always include directories; filter files by extension + // Keep the workspace tree focused on markdown documents and embeddable media. if !is_dir { let ext = path .extension() @@ -39,10 +68,7 @@ fn read_dir(path: String) -> Result, String> { continue; } } - let name = entry - .file_name() - .to_string_lossy() - .to_string(); + let name = entry.file_name().to_string_lossy().to_string(); result.push(DirEntry { name, path: path.to_string_lossy().to_string(), @@ -82,7 +108,14 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) - .invoke_handler(tauri::generate_handler![greet, pick_folder, read_dir, read_file, write_file, pick_save_file]) + .invoke_handler(tauri::generate_handler![ + greet, + pick_folder, + read_dir, + read_file, + write_file, + pick_save_file + ]) .setup(|app| { // ---- File menu ---- let new = MenuItemBuilder::with_id("new", "新建") @@ -151,12 +184,9 @@ pub fn run() { .build()?; // ---- Help menu ---- - let about = MenuItemBuilder::with_id("about", "关于") - .build(app)?; + let about = MenuItemBuilder::with_id("about", "关于").build(app)?; - let help_menu = SubmenuBuilder::new(app, "帮助") - .item(&about) - .build()?; + let help_menu = SubmenuBuilder::new(app, "帮助").item(&about).build()?; // ---- Build the menu bar ---- let menu = MenuBuilder::new(app) @@ -180,11 +210,7 @@ pub fn run() { let handle = app_handle.clone(); // Spawn because dialog is blocking std::thread::spawn(move || { - if let Some(folder) = handle - .dialog() - .file() - .blocking_pick_folder() - { + if let Some(folder) = handle.dialog().file().blocking_pick_folder() { let path = folder.to_string(); let _ = handle.emit("folder-opened", path); } diff --git a/src/App.vue b/src/App.vue index ee9da51..9546ccd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -73,9 +73,17 @@ interface DirEntry { children?: DirEntry[]; } +interface WorkspaceEntry { + name: string; + path: string; +} + const folderPath = ref(""); const dirEntries = ref([]); const LAST_WORKSPACE_PATH_KEY = "yurou:last-workspace-path"; +const RECENT_WORKSPACES_KEY = "yurou:recent-workspaces"; +const MAX_RECENT_WORKSPACES = 12; +const recentWorkspaces = ref(loadRecentWorkspaces()); const editorResetKey = ref(0); // ---- editor zoom (Ctrl+scroll) ---- @@ -384,34 +392,178 @@ function clearLastWorkspacePath() { } } -async function loadDir(path: string): Promise { - folderPath.value = path; - dirEntries.value = []; +function normalizeWorkspacePath(path: string) { + const trimmed = path.trim(); + if (!trimmed) return ""; + + const normalized = trimmed.replace(/[\\/]+$/, ""); + if (!normalized) return trimmed[0] || ""; + if (/^[A-Za-z]:$/.test(normalized) && /^[A-Za-z]:[\\/]+$/.test(trimmed)) { + return `${normalized}\\`; + } + + return normalized; +} + +function getWorkspaceIdentity(path: string) { + return normalizeWorkspacePath(path).toLocaleLowerCase(); +} + +function getWorkspaceName(path: string): string { + const normalized = normalizeWorkspacePath(path); + return normalized.split(/[/\\]/).pop() || normalized || path; +} + +function createWorkspaceEntry(path: string): WorkspaceEntry { + const normalized = normalizeWorkspacePath(path); + return { + name: getWorkspaceName(normalized), + path: normalized, + }; +} + +function getStoredWorkspacePath(item: unknown) { + if (typeof item === "string") return item; + if (item && typeof item === "object") { + const maybeWorkspace = item as { path?: unknown }; + if (typeof maybeWorkspace.path === "string") return maybeWorkspace.path; + } + return null; +} + +function dedupeWorkspaces(paths: string[]) { + const seen = new Set(); + const workspaces: WorkspaceEntry[] = []; + + for (const path of paths) { + const normalized = normalizeWorkspacePath(path); + if (!normalized) continue; + + const identity = getWorkspaceIdentity(normalized); + if (seen.has(identity)) continue; + + seen.add(identity); + workspaces.push(createWorkspaceEntry(normalized)); + } + + return workspaces.slice(0, MAX_RECENT_WORKSPACES); +} + +function loadRecentWorkspaces(): WorkspaceEntry[] { try { - dirEntries.value = await invoke("read_dir", {path}); + const stored = localStorage.getItem(RECENT_WORKSPACES_KEY); + if (!stored) return []; + + const parsed = JSON.parse(stored) as unknown; + if (!Array.isArray(parsed)) return []; + + return dedupeWorkspaces( + parsed + .map(getStoredWorkspacePath) + .filter((path): path is string => Boolean(path)), + ); + } catch { + return []; + } +} + +function saveRecentWorkspaces(workspaces: WorkspaceEntry[] = recentWorkspaces.value) { + try { + localStorage.setItem(RECENT_WORKSPACES_KEY, JSON.stringify(workspaces)); + } catch { /* noop */ + } +} + +function rememberWorkspace(path: string) { + const normalized = normalizeWorkspacePath(path); + if (!normalized) return; + + recentWorkspaces.value = dedupeWorkspaces([ + normalized, + ...recentWorkspaces.value.map((workspace) => workspace.path), + ]); + saveRecentWorkspaces(); +} + +function forgetWorkspace(path: string) { + const identity = getWorkspaceIdentity(path); + recentWorkspaces.value = recentWorkspaces.value.filter((workspace) => { + return getWorkspaceIdentity(workspace.path) !== identity; + }); + saveRecentWorkspaces(); +} + +async function removeRecentWorkspace(path: string) { + const isCurrent = Boolean(folderPath.value) && getWorkspaceIdentity(folderPath.value) === getWorkspaceIdentity(path); + + if (isCurrent) { + const saved = await saveCurrentWorkspaceDocumentBeforeSwitch(); + if (!saved) return; + + resetWorkspaceAndEditorState(); + clearLastWorkspacePath(); + } + + forgetWorkspace(path); +} + +async function loadDir(path: string): Promise { + const normalized = normalizeWorkspacePath(path); + try { + const entries = await invoke("read_dir", {path: normalized}); + folderPath.value = normalized; + dirEntries.value = entries; return true; } catch (e) { console.error("Failed to read directory:", e); - folderPath.value = ""; - dirEntries.value = []; return false; } } +async function saveCurrentWorkspaceDocumentBeforeSwitch() { + if (!currentFilePath.value && !hasDraftDocument.value) return true; + + const contentToSave = getLatestEditorContent(); + if (!currentFilePath.value && !contentToSave.trim()) return true; + + return doSave(contentToSave); +} + +function resetWorkspaceAndEditorState() { + folderPath.value = ""; + dirEntries.value = []; + resetMarkdownEditorState(); +} + async function switchWorkspace(path: string, options: { persist?: boolean; resetEditor?: boolean } = {}) { + const normalized = normalizeWorkspacePath(path); + if (options.resetEditor !== false) { - resetMarkdownEditorState(); + const saved = await saveCurrentWorkspaceDocumentBeforeSwitch(); + if (!saved) return false; + resetWorkspaceAndEditorState(); } - const loaded = await loadDir(path); + const loaded = await loadDir(normalized); - if (loaded && options.persist !== false) { - saveLastWorkspacePath(path); - } else if (!loaded) { - clearLastWorkspacePath(); + if (!loaded) { + forgetWorkspace(normalized); + + const lastWorkspacePath = getLastWorkspacePath(); + if (lastWorkspacePath && getWorkspaceIdentity(lastWorkspacePath) === getWorkspaceIdentity(normalized)) { + clearLastWorkspacePath(); + } + + return false; } - return loaded; + rememberWorkspace(normalized); + + if (options.persist !== false) { + saveLastWorkspacePath(normalized); + } + + return true; } async function restoreLastWorkspace() { @@ -486,11 +638,11 @@ async function doSave(contentOverride?: string) { // No file path yet — prompt user to pick a save location try { const chosen = await invoke("pick_save_file"); - if (!chosen) return; // user cancelled + if (!chosen) return false; // user cancelled currentFilePath.value = chosen; } catch (e) { console.error("Save dialog failed:", e); - return; + return false; } } @@ -500,9 +652,12 @@ async function doSave(contentOverride?: string) { setTimeout(() => { saveStatus.value = ""; }, 3000); + hasDraftDocument.value = false; + return true; } catch (e) { console.error("Save failed:", e); saveStatus.value = `保存失败: ${e}`; + return false; } } @@ -580,7 +735,10 @@ registerTauriListener("menu-event", (event) => { :settings-initial-page="settingsInitialPage" :default-document-mode="defaultDocumentMode" :app-theme="appTheme" + :recent-workspaces="recentWorkspaces" @open-folder="onOpenFolder" + @switch-workspace="(path: string) => switchWorkspace(path)" + @remove-workspace="removeRecentWorkspace" @toggle-folder="onToggleFolder" @open-file="onOpenFile" @update-default-document-mode="updateDefaultDocumentMode" @@ -760,27 +918,27 @@ registerTauriListener("menu-event", (event) => { -
-
+ + - - {{ saveStatus }} - - {{ currentFilePath.split(/[/\\]/).pop() }} - - 未保存 + + + + + + + + + + - + - {{ wordCount.toLocaleString() }} 词 - - {{ charCount.toLocaleString() }} 字 -
-
+ + + + + @@ -1030,5 +1188,3 @@ registerTauriListener("menu-event", (event) => { outline: none; } - - diff --git a/src/components/DirectorySidebar.vue b/src/components/DirectorySidebar.vue index b853da0..548ef8f 100644 --- a/src/components/DirectorySidebar.vue +++ b/src/components/DirectorySidebar.vue @@ -1,5 +1,5 @@