| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- 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<String>) -> Result<(), String> {
- match action.as_str() {
- "click" => {
- // 有可一键完成的待办任务时优先完成它,否则回退到喝水打卡
- let pending = app
- .try_state::<AppState>()
- .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<AppState>,
- settings: State<SettingsStore>,
- ) -> Result<serde_json::Value, String> {
- 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<String, String> {
- 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::<AppState>();
- 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<SettingsStore>) -> 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<serde_json::Value, String> {
- let qr = {
- let state = app.state::<AppState>();
- state
- .api
- .wechat_web_qr()
- .await
- .map_err(|e| format!("获取二维码失败: {e}"))?
- };
- open_url(&qr.url)?;
- let api = app.state::<AppState>().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::<AppState>() {
- 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<AppState>,
- settings: State<SettingsStore>,
- base_url: String,
- token: Option<String>,
- ) -> 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<AppState>) {
- 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::<AppState>();
- 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::<AppState>();
- let api = state.api.clone();
- match api.complete_task(task_id).await {
- Ok(_) => {
- if let Some(s) = app.try_state::<AppState>() {
- *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())
- }
- }
- }
|