use crate::app_state::AppState; use crate::poller; use crate::settings::SettingsStore; use crate::state::{PetState, PetStatePayload}; use crate::token::TokenStore; use tauri::{AppHandle, Emitter, Manager, State}; #[tauri::command] pub async fn on_interact(app: AppHandle, action: String, _data: Option) -> Result<(), String> { match action.as_str() { "click" => { // 有可一键完成的待办任务时优先完成它,否则回退到喝水打卡 let pending = app .try_state::() .and_then(|s| *s.pending_task.lock().unwrap()); match pending { Some(id) => complete_task_by_id(&app, id).await, None => complete_water(&app).await, } } "pet" => { let _ = app.emit("pet://bubble", serde_json::json!({"text": "嘻嘻~", "durationMs": 2000})); Ok(()) } _ => Ok(()), } } #[tauri::command] pub async fn drag_start(app: AppHandle) -> Result<(), String> { if let Some(win) = app.get_webview_window("main") { win.start_dragging().map_err(|e| e.to_string())?; } Ok(()) } #[tauri::command] pub async fn drag_end() -> Result<(), String> { Ok(()) } #[tauri::command] pub fn open_settings(app: AppHandle) -> Result<(), String> { if let Some(win) = app.get_webview_window("settings") { let _ = win.show(); let _ = win.set_focus(); } Ok(()) } #[tauri::command] pub fn quit_app(app: AppHandle) -> Result<(), String> { app.exit(0); Ok(()) } #[tauri::command] pub fn get_settings( _state: State, settings: State, ) -> Result { let cfg = settings.load(); let has_token = TokenStore::new().load().map(|t| t.is_some()).unwrap_or(false); Ok(serde_json::json!({ "baseUrl": cfg.base_url, "hasToken": has_token, "customMascotUrl": cfg.custom_mascot_url })) } #[tauri::command] pub async fn upload_mascot( app: AppHandle, settings: State<'_, SettingsStore>, filename: String, mime: String, data_base64: String, ) -> Result { use base64::Engine; let bytes = base64::engine::general_purpose::STANDARD .decode(data_base64.as_bytes()) .map_err(|e| format!("图片解码失败: {e}"))?; if bytes.is_empty() { return Err("图片内容为空".into()); } if bytes.len() > 10 * 1024 * 1024 { return Err("图片过大(上限 10MB)".into()); } let state = app.state::(); let record = state .api .upload_media(&filename, bytes, &mime) .await .map_err(|e| format!("上传失败: {e}"))?; let url = record.url.ok_or_else(|| "上传响应缺少 url".to_string())?; let mut cfg = settings.load(); cfg.custom_mascot_url = Some(url.clone()); settings.save(&cfg)?; let _ = app.emit("pet://custom-image", serde_json::json!({ "url": url })); Ok(url) } #[tauri::command] pub fn clear_custom_mascot(app: AppHandle, settings: State) -> Result<(), String> { let mut cfg = settings.load(); cfg.custom_mascot_url = None; settings.save(&cfg)?; let _ = app.emit("pet://custom-image", serde_json::json!({ "url": null })); Ok(()) } /// 微信扫码登录:取二维码链接 → 用系统浏览器打开 → 后台轮询换取 JWT → 写入 keyring。 /// 结果通过 `pet://wechat-login` 事件回传 /// (status: pending / success / notfound / expired / error / timeout)。 #[tauri::command] pub async fn start_wechat_login(app: AppHandle) -> Result { let qr = { let state = app.state::(); state .api .wechat_web_qr() .await .map_err(|e| format!("获取二维码失败: {e}"))? }; open_url(&qr.url)?; let api = app.state::().api.clone(); let state_token = qr.state.clone(); let handle = app.clone(); tauri::async_runtime::spawn(async move { // 最多轮询约 3 分钟(2s 间隔 × 90 次) for _ in 0..90 { tokio::time::sleep(std::time::Duration::from_secs(2)).await; match api.wechat_web_poll(&state_token).await { Ok(resp) => match resp.status.as_str() { "pending" => continue, "success" => { let Some(token) = resp.token else { let _ = handle.emit( "pet://wechat-login", serde_json::json!({"status": "error", "message": "响应缺少 token"}), ); return; }; if let Err(e) = TokenStore::new().save(&token) { let _ = handle.emit( "pet://wechat-login", serde_json::json!({"status": "error", "message": format!("保存令牌失败: {e}")}), ); return; } api.update_token(token); if let Some(s) = handle.try_state::() { restart_polling(&handle, &s); } let _ = handle.emit( "pet://wechat-login", serde_json::json!({"status": "success", "nickname": resp.nickname}), ); return; } other => { let _ = handle.emit( "pet://wechat-login", serde_json::json!({"status": other, "message": resp.message}), ); return; } }, Err(_) => continue, // 瞬时网络错误:继续轮询 } } let _ = handle.emit("pet://wechat-login", serde_json::json!({"status": "timeout"})); }); Ok(serde_json::json!({ "status": "pending", "url": qr.url })) } /// 用系统默认浏览器打开 URL(Windows: cmd start / macOS: open / 其它: xdg-open)。 fn open_url(url: &str) -> Result<(), String> { #[cfg(target_os = "windows")] { std::process::Command::new("cmd") .args(["/C", "start", "", url]) .spawn() .map_err(|e| e.to_string())?; } #[cfg(target_os = "macos")] { std::process::Command::new("open") .arg(url) .spawn() .map_err(|e| e.to_string())?; } #[cfg(not(any(target_os = "windows", target_os = "macos")))] { std::process::Command::new("xdg-open") .arg(url) .spawn() .map_err(|e| e.to_string())?; } Ok(()) } #[tauri::command] pub fn save_settings( app: AppHandle, state: State, settings: State, base_url: String, token: Option, ) -> Result<(), String> { let mut cfg = settings.load(); cfg.base_url = base_url.trim().to_string(); settings.save(&cfg)?; if let Some(t) = token { if !t.trim().is_empty() { TokenStore::new().save(t.trim())?; } } // 更新运行时客户端并重启轮询(对齐现 ShowSettings 的 Stop/Start) state.api.set_base_url(cfg.base_url.clone()); if let Some(t) = TokenStore::new().load()? { state.api.update_token(t); } restart_polling(&app, &state); if let Some(win) = app.get_webview_window("settings") { let _ = win.hide(); } Ok(()) } fn restart_polling(app: &AppHandle, state: &State) { if let Some(h) = state.poll_handle.lock().unwrap().take() { h.abort(); } let handle = poller::spawn(app.clone(), state.api.clone()); *state.poll_handle.lock().unwrap() = Some(handle); } async fn complete_water(app: &AppHandle) -> Result<(), String> { let state = app.state::(); let api = state.api.clone(); match api.complete_water().await { Ok(_) => { let payload = PetStatePayload { state: PetState::Happy, reason: Some("drink_complete".into()), data: None, }; let _ = app.emit("pet://state", payload); let _ = app.emit("pet://bubble", serde_json::json!({"text": "喝水打卡成功!太棒了~", "durationMs": 4000})); Ok(()) } Err(_) => { let _ = app.emit("pet://bubble", serde_json::json!({"text": "打卡失败,请稍后再试", "durationMs": 4000})); Err("checkin failed".into()) } } } async fn complete_task_by_id(app: &AppHandle, task_id: i64) -> Result<(), String> { let state = app.state::(); let api = state.api.clone(); match api.complete_task(task_id).await { Ok(_) => { if let Some(s) = app.try_state::() { *s.pending_task.lock().unwrap() = None; } let payload = PetStatePayload { state: PetState::Happy, reason: Some("task_complete".into()), data: None, }; let _ = app.emit("pet://state", payload); let _ = app.emit("pet://bubble", serde_json::json!({"text": "任务完成!太棒了~", "durationMs": 4000})); Ok(()) } Err(_) => { let _ = app.emit("pet://bubble", serde_json::json!({"text": "完成任务失败,请稍后再试", "durationMs": 4000})); Err("task complete failed".into()) } } }