#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] //! 预览 Host 只开放本地文件命令;未接通的 AI / 同步 / 凭据能力明确返回不可用。 use notesagent_host::workspace::{Document, Entry, Workspace}; use serde::Serialize; use std::sync::Mutex; use tauri::{Emitter, Manager, State}; #[derive(Default)] struct Host(Mutex>); #[derive(Serialize)] struct VaultInfo { vault_id: String, path: String, name: String, } fn info(ws: &Workspace) -> VaultInfo { VaultInfo { vault_id: ws.vault_id.clone(), path: ws.root.to_string_lossy().into(), name: ws .root .file_name() .unwrap_or_default() .to_string_lossy() .into(), } } fn with_workspace( host: &Host, f: impl FnOnce(&mut Workspace) -> notesagent_host::workspace::Result, ) -> Result { let mut guard = host.0.lock().map_err(|_| "HOST_BUSY")?; let ws = guard.as_mut().ok_or("VAULT_NOT_OPEN")?; f(ws).map_err(|e| e.code) } #[tauri::command] fn host_capabilities() -> serde_json::Value { serde_json::json!({"protocol":1,"workspace":true,"core":false,"sync":false,"credentials":false,"extensions":false,"release":"preview"}) } #[tauri::command] fn editor_capabilities(app: tauri::AppHandle, import_enabled: bool) -> Result<(), String> { app.state::>() .set_enabled(import_enabled) .map_err(|_| "MENU_UNAVAILABLE".into()) } #[tauri::command] fn workspace_choose(host: State<'_, Host>) -> Result, String> { let Some(path) = rfd::FileDialog::new() .set_title("选择本地 Vault") .pick_folder() else { return Ok(None); }; let mut guard = host.0.lock().map_err(|_| "HOST_BUSY")?; if guard .as_ref() .is_some_and(|ws| ws.root == path.canonicalize().unwrap_or_default()) { return Ok(guard.as_ref().map(info)); } let workspace = Workspace::open(&path).map_err(|e| e.code)?; let result = info(&workspace); *guard = Some(workspace); Ok(Some(result)) } #[tauri::command] fn workspace_recent(host: State<'_, Host>) -> Result, String> { let guard = host.0.lock().map_err(|_| "HOST_BUSY")?; Ok(guard.as_ref().map(info).into_iter().collect()) } #[tauri::command] fn workspace_revoke(host: State<'_, Host>) -> Result<(), String> { *host.0.lock().map_err(|_| "HOST_BUSY")? = None; Ok(()) } #[tauri::command] fn workspace_tree(host: State<'_, Host>) -> Result, String> { with_workspace(&host, |ws| ws.scan()) } #[tauri::command] fn workspace_read(host: State<'_, Host>, path: String) -> Result { with_workspace(&host, |ws| ws.read(&path)) } #[tauri::command] fn workspace_write( host: State<'_, Host>, path: String, expected: String, content: String, ) -> Result { with_workspace(&host, |ws| { ws.write(&path, &expected, content.as_bytes(), "local") }) } #[tauri::command] fn workspace_rename( host: State<'_, Host>, path: String, destination: String, expected: String, ) -> Result { with_workspace(&host, |ws| ws.rename(&path, &destination, &expected)) } #[tauri::command] fn workspace_delete(host: State<'_, Host>, path: String, expected: String) -> Result<(), String> { with_workspace(&host, |ws| ws.delete(&path, &expected)) } #[tauri::command] fn workspace_mkdir(host: State<'_, Host>, path: String) -> Result<(), String> { with_workspace(&host, |ws| ws.mkdir(&path)) } fn main() { tauri::Builder::default() .manage(Host::default()) .setup(|app| { use tauri::menu::{Menu, MenuItem, Submenu}; let import = MenuItem::with_id( app, "editor.import-note-properties", "导入为笔记属性…", false, None::<&str>, )?; let paragraph = Submenu::with_items(app, "段落", true, &[&import])?; app.manage(import); app.set_menu(Menu::with_items(app, &[¶graph])?)?; Ok(()) }) .on_menu_event(|app, event| { // 主窗口独占预览 Vault;扩展窗口没有命令 capability。 if let Some(window) = app.get_webview_window("main") { let _ = window.emit("editor-command", event.id().as_ref()); } }) .on_window_event(|window, event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { api.prevent_close(); let _ = window.emit("host-close-requested", ()); } }) .invoke_handler(tauri::generate_handler![ host_capabilities, editor_capabilities, workspace_choose, workspace_recent, workspace_revoke, workspace_tree, workspace_read, workspace_write, workspace_rename, workspace_delete, workspace_mkdir ]) .run(tauri::generate_context!()) .expect("桌面 Host 启动失败"); }