//! 后端 HTTP API 封装 + 窗口控制命令。 use serde_json::{json, Value}; use std::sync::atomic::{AtomicBool, Ordering}; use tauri::{AppHandle, Manager}; /// 全局暂停开关(11B 托盘菜单"暂停使用"):暂停时快捷键不再触发听写。 #[derive(Default)] pub struct Paused(AtomicBool); pub fn is_app_paused(app: &AppHandle) -> bool { app.state::().0.load(Ordering::Relaxed) } #[tauri::command] pub fn is_paused(app: AppHandle) -> bool { is_app_paused(&app) } /// 切换暂停状态,返回新状态。 #[tauri::command] pub fn toggle_pause(app: AppHandle) -> bool { let paused = &app.state::().0; let next = !paused.load(Ordering::Relaxed); paused.store(next, Ordering::Relaxed); if next { crate::dictation::stop(&app, true); // 录音中暂停 → 直接取消 } if let Some(tray) = app.tray_by_id("main") { let _ = tray.set_tooltip(Some(if next { "dudu — 已暂停" } else { "dudu — 就绪" })); } next } fn base(app: &AppHandle) -> (String, String) { let s = app.state::().get(); (s.server_url, s.token) } async fn get_json(app: &AppHandle, path: &str, authed: bool) -> Option { let (url, token) = base(app); let mut req = reqwest::Client::new().get(format!("{url}{path}")); if authed { if token.is_empty() { return None; } req = req.bearer_auth(token); } let resp = req.send().await.ok()?; if !resp.status().is_success() { return None; } resp.json().await.ok() } #[tauri::command] pub async fn fetch_me(app: AppHandle) -> Option { get_json(&app, "/v1/me", true).await } #[tauri::command] pub async fn fetch_packs(app: AppHandle) -> Option { let v = get_json(&app, "/v1/packs", false).await?; v.get("packs").cloned() } #[tauri::command] pub async fn login_qr_create(app: AppHandle) -> Option { let (url, _) = base(&app); reqwest::Client::new() .post(format!("{url}/v1/auth/qr")) .send() .await .ok()? .json() .await .ok() } #[tauri::command] pub async fn login_qr_poll(app: AppHandle, state: String) -> Option { let v = get_json(&app, &format!("/v1/auth/qr/{state}"), false).await?; if v.get("status").and_then(|s| s.as_str()) == Some("confirmed") { if let Some(token) = v.get("token").and_then(|t| t.as_str()) { let store = app.state::(); let mut s = store.get(); s.token = token.to_string(); store.set(s); // 通知 ws 重连携带新 token let _ = app.state::().tx.send(crate::ws::WsCmd::Reconnect); } } Some(v) } #[tauri::command] pub async fn create_order(app: AppHandle, pack_id: String) -> Option { let (url, token) = base(&app); reqwest::Client::new() .post(format!("{url}/v1/orders")) .bearer_auth(token) .json(&serde_json::json!({"pack_id": pack_id, "channel": "native"})) .send() .await .ok()? .json() .await .ok() } #[tauri::command] pub async fn order_status(app: AppHandle, order_id: String) -> Option { get_json(&app, &format!("/v1/orders/{order_id}"), true).await } // ─── 反馈(11E)────────────────────────────────────────────────────────────── #[derive(serde::Deserialize)] pub struct FeedbackImage { pub name: String, /// base64(不含 data: 前缀) pub data: String, } fn image_mime(name: &str) -> &'static str { let lower = name.to_ascii_lowercase(); if lower.ends_with(".png") { "image/png" } else if lower.ends_with(".webp") { "image/webp" } else { "image/jpeg" } } #[cfg(target_os = "macos")] fn os_version() -> String { std::process::Command::new("sw_vers") .arg("-productVersion") .output() .ok() .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) .unwrap_or_default() } #[cfg(not(target_os = "macos"))] fn os_version() -> String { String::new() } /// multipart POST /v1/feedback:文字 + 图片(≤3 张 ≤5MB)+ 可选诊断信息。 /// 错误以字符串 code 返回:NOT_LOGGED_IN / FEEDBACK_RATE_LIMITED / HTTP_xxx / 网络错误描述。 #[tauri::command] pub async fn submit_feedback( app: AppHandle, content: String, images: Vec, include_diagnostics: bool, ) -> Result { let (url, token) = base(&app); if token.is_empty() { return Err("NOT_LOGGED_IN".into()); } let app_version = app.package_info().version.to_string(); let mut form = reqwest::multipart::Form::new().text("content", content); for img in images.into_iter().take(3) { use base64::Engine; let bytes = base64::engine::general_purpose::STANDARD .decode(img.data.as_bytes()) .map_err(|e| e.to_string())?; let part = reqwest::multipart::Part::bytes(bytes) .mime_str(image_mime(&img.name)) .map_err(|e| e.to_string())? .file_name(img.name); form = form.part("images[]", part); } if include_diagnostics { let s = app.state::().get(); let diag = json!({ "app_version": app_version, "platform": std::env::consts::OS, "os_version": os_version(), "device_id": s.device_id, }); form = form.text("diagnostics", diag.to_string()); } let resp = reqwest::Client::new() .post(format!("{url}/v1/feedback")) .bearer_auth(token) .header("X-Platform", std::env::consts::OS) .header("X-App-Version", app_version) .multipart(form) .send() .await .map_err(|e| e.to_string())?; match resp.status().as_u16() { 200 => resp.json().await.map_err(|e| e.to_string()), 429 => Err("FEEDBACK_RATE_LIMITED".into()), s => Err(format!("HTTP_{s}")), } } // ─── 窗口控制 ──────────────────────────────────────────────────────────────── #[tauri::command] pub fn open_settings(app: AppHandle) { if let Some(w) = app.get_webview_window("settings") { let _ = w.show(); let _ = w.set_focus(); } } #[tauri::command] pub fn open_login(app: AppHandle) { if let Some(w) = app.get_webview_window("login") { let _ = w.show(); let _ = w.set_focus(); } } #[tauri::command] pub fn close_login(app: AppHandle) { if let Some(w) = app.get_webview_window("login") { let _ = w.hide(); } } #[tauri::command] pub fn open_feedback(app: AppHandle) { if let Some(w) = app.get_webview_window("feedback") { let _ = w.show(); let _ = w.set_focus(); } } #[tauri::command] pub fn close_feedback(app: AppHandle) { if let Some(w) = app.get_webview_window("feedback") { let _ = w.hide(); } } /// 完成首次启动引导(11F):置 onboarding_done 并关闭引导窗。 #[tauri::command] pub fn finish_onboarding(app: AppHandle) { let store = app.state::(); let mut s = store.get(); if !s.onboarding_done { s.onboarding_done = true; store.set(s); } if let Some(w) = app.get_webview_window("onboarding") { let _ = w.hide(); } } /// 检查更新:GET /v1/app/latest,有新版则打开下载页;返回结果供 UI 提示。 #[tauri::command] pub async fn check_update(app: AppHandle) -> Option { let current = app.package_info().version.to_string(); #[cfg(target_os = "macos")] let platform = "mac"; #[cfg(target_os = "windows")] let platform = "win"; #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] let platform = "linux"; let latest = get_json(&app, &format!("/v1/app/latest?platform={platform}"), false).await?; let version = latest.get("version").and_then(|v| v.as_str()).unwrap_or_default(); let dl = latest.get("url").and_then(|v| v.as_str()).unwrap_or_default(); let available = !version.is_empty() && version != current; if available && !dl.is_empty() { open_external(dl); } Some(json!({"current": current, "latest": version, "update_available": available, "url": dl})) } /// 用系统默认方式打开外部链接。 fn open_external(url: &str) { #[cfg(target_os = "macos")] let _ = std::process::Command::new("open").arg(url).spawn(); #[cfg(target_os = "windows")] let _ = std::process::Command::new("cmd").args(["/C", "start", "", url]).spawn(); #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] let _ = std::process::Command::new("xdg-open").arg(url).spawn(); } #[tauri::command] pub fn quit_app(app: AppHandle) { app.exit(0); }