commands.rs 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. use crate::app_state::AppState;
  2. use crate::poller;
  3. use crate::settings::SettingsStore;
  4. use crate::state::{PetState, PetStatePayload};
  5. use crate::token::TokenStore;
  6. use tauri::{AppHandle, Emitter, Manager, State};
  7. #[tauri::command]
  8. pub async fn on_interact(app: AppHandle, action: String, _data: Option<String>) -> Result<(), String> {
  9. match action.as_str() {
  10. "click" => {
  11. // 有可一键完成的待办任务时优先完成它,否则回退到喝水打卡
  12. let pending = app
  13. .try_state::<AppState>()
  14. .and_then(|s| *s.pending_task.lock().unwrap());
  15. match pending {
  16. Some(id) => complete_task_by_id(&app, id).await,
  17. None => complete_water(&app).await,
  18. }
  19. }
  20. "pet" => {
  21. let _ = app.emit("pet://bubble", serde_json::json!({"text": "嘻嘻~", "durationMs": 2000}));
  22. Ok(())
  23. }
  24. _ => Ok(()),
  25. }
  26. }
  27. #[tauri::command]
  28. pub async fn drag_start(app: AppHandle) -> Result<(), String> {
  29. if let Some(win) = app.get_webview_window("main") {
  30. win.start_dragging().map_err(|e| e.to_string())?;
  31. }
  32. Ok(())
  33. }
  34. #[tauri::command]
  35. pub async fn drag_end() -> Result<(), String> {
  36. Ok(())
  37. }
  38. #[tauri::command]
  39. pub fn open_settings(app: AppHandle) -> Result<(), String> {
  40. if let Some(win) = app.get_webview_window("settings") {
  41. let _ = win.show();
  42. let _ = win.set_focus();
  43. }
  44. Ok(())
  45. }
  46. #[tauri::command]
  47. pub fn quit_app(app: AppHandle) -> Result<(), String> {
  48. app.exit(0);
  49. Ok(())
  50. }
  51. #[tauri::command]
  52. pub fn get_settings(
  53. _state: State<AppState>,
  54. settings: State<SettingsStore>,
  55. ) -> Result<serde_json::Value, String> {
  56. let cfg = settings.load();
  57. let has_token = TokenStore::new().load().map(|t| t.is_some()).unwrap_or(false);
  58. Ok(serde_json::json!({
  59. "baseUrl": cfg.base_url,
  60. "hasToken": has_token,
  61. "customMascotUrl": cfg.custom_mascot_url
  62. }))
  63. }
  64. #[tauri::command]
  65. pub async fn upload_mascot(
  66. app: AppHandle,
  67. settings: State<'_, SettingsStore>,
  68. filename: String,
  69. mime: String,
  70. data_base64: String,
  71. ) -> Result<String, String> {
  72. use base64::Engine;
  73. let bytes = base64::engine::general_purpose::STANDARD
  74. .decode(data_base64.as_bytes())
  75. .map_err(|e| format!("图片解码失败: {e}"))?;
  76. if bytes.is_empty() {
  77. return Err("图片内容为空".into());
  78. }
  79. if bytes.len() > 10 * 1024 * 1024 {
  80. return Err("图片过大(上限 10MB)".into());
  81. }
  82. let state = app.state::<AppState>();
  83. let record = state
  84. .api
  85. .upload_media(&filename, bytes, &mime)
  86. .await
  87. .map_err(|e| format!("上传失败: {e}"))?;
  88. let url = record.url.ok_or_else(|| "上传响应缺少 url".to_string())?;
  89. let mut cfg = settings.load();
  90. cfg.custom_mascot_url = Some(url.clone());
  91. settings.save(&cfg)?;
  92. let _ = app.emit("pet://custom-image", serde_json::json!({ "url": url }));
  93. Ok(url)
  94. }
  95. #[tauri::command]
  96. pub fn clear_custom_mascot(app: AppHandle, settings: State<SettingsStore>) -> Result<(), String> {
  97. let mut cfg = settings.load();
  98. cfg.custom_mascot_url = None;
  99. settings.save(&cfg)?;
  100. let _ = app.emit("pet://custom-image", serde_json::json!({ "url": null }));
  101. Ok(())
  102. }
  103. /// 微信扫码登录:取二维码链接 → 用系统浏览器打开 → 后台轮询换取 JWT → 写入 keyring。
  104. /// 结果通过 `pet://wechat-login` 事件回传
  105. /// (status: pending / success / notfound / expired / error / timeout)。
  106. #[tauri::command]
  107. pub async fn start_wechat_login(app: AppHandle) -> Result<serde_json::Value, String> {
  108. let qr = {
  109. let state = app.state::<AppState>();
  110. state
  111. .api
  112. .wechat_web_qr()
  113. .await
  114. .map_err(|e| format!("获取二维码失败: {e}"))?
  115. };
  116. open_url(&qr.url)?;
  117. let api = app.state::<AppState>().api.clone();
  118. let state_token = qr.state.clone();
  119. let handle = app.clone();
  120. tauri::async_runtime::spawn(async move {
  121. // 最多轮询约 3 分钟(2s 间隔 × 90 次)
  122. for _ in 0..90 {
  123. tokio::time::sleep(std::time::Duration::from_secs(2)).await;
  124. match api.wechat_web_poll(&state_token).await {
  125. Ok(resp) => match resp.status.as_str() {
  126. "pending" => continue,
  127. "success" => {
  128. let Some(token) = resp.token else {
  129. let _ = handle.emit(
  130. "pet://wechat-login",
  131. serde_json::json!({"status": "error", "message": "响应缺少 token"}),
  132. );
  133. return;
  134. };
  135. if let Err(e) = TokenStore::new().save(&token) {
  136. let _ = handle.emit(
  137. "pet://wechat-login",
  138. serde_json::json!({"status": "error", "message": format!("保存令牌失败: {e}")}),
  139. );
  140. return;
  141. }
  142. api.update_token(token);
  143. if let Some(s) = handle.try_state::<AppState>() {
  144. restart_polling(&handle, &s);
  145. }
  146. let _ = handle.emit(
  147. "pet://wechat-login",
  148. serde_json::json!({"status": "success", "nickname": resp.nickname}),
  149. );
  150. return;
  151. }
  152. other => {
  153. let _ = handle.emit(
  154. "pet://wechat-login",
  155. serde_json::json!({"status": other, "message": resp.message}),
  156. );
  157. return;
  158. }
  159. },
  160. Err(_) => continue, // 瞬时网络错误:继续轮询
  161. }
  162. }
  163. let _ = handle.emit("pet://wechat-login", serde_json::json!({"status": "timeout"}));
  164. });
  165. Ok(serde_json::json!({ "status": "pending", "url": qr.url }))
  166. }
  167. /// 用系统默认浏览器打开 URL(Windows: cmd start / macOS: open / 其它: xdg-open)。
  168. fn open_url(url: &str) -> Result<(), String> {
  169. #[cfg(target_os = "windows")]
  170. {
  171. std::process::Command::new("cmd")
  172. .args(["/C", "start", "", url])
  173. .spawn()
  174. .map_err(|e| e.to_string())?;
  175. }
  176. #[cfg(target_os = "macos")]
  177. {
  178. std::process::Command::new("open")
  179. .arg(url)
  180. .spawn()
  181. .map_err(|e| e.to_string())?;
  182. }
  183. #[cfg(not(any(target_os = "windows", target_os = "macos")))]
  184. {
  185. std::process::Command::new("xdg-open")
  186. .arg(url)
  187. .spawn()
  188. .map_err(|e| e.to_string())?;
  189. }
  190. Ok(())
  191. }
  192. #[tauri::command]
  193. pub fn save_settings(
  194. app: AppHandle,
  195. state: State<AppState>,
  196. settings: State<SettingsStore>,
  197. base_url: String,
  198. token: Option<String>,
  199. ) -> Result<(), String> {
  200. let mut cfg = settings.load();
  201. cfg.base_url = base_url.trim().to_string();
  202. settings.save(&cfg)?;
  203. if let Some(t) = token {
  204. if !t.trim().is_empty() {
  205. TokenStore::new().save(t.trim())?;
  206. }
  207. }
  208. // 更新运行时客户端并重启轮询(对齐现 ShowSettings 的 Stop/Start)
  209. state.api.set_base_url(cfg.base_url.clone());
  210. if let Some(t) = TokenStore::new().load()? {
  211. state.api.update_token(t);
  212. }
  213. restart_polling(&app, &state);
  214. if let Some(win) = app.get_webview_window("settings") {
  215. let _ = win.hide();
  216. }
  217. Ok(())
  218. }
  219. fn restart_polling(app: &AppHandle, state: &State<AppState>) {
  220. if let Some(h) = state.poll_handle.lock().unwrap().take() {
  221. h.abort();
  222. }
  223. let handle = poller::spawn(app.clone(), state.api.clone());
  224. *state.poll_handle.lock().unwrap() = Some(handle);
  225. }
  226. async fn complete_water(app: &AppHandle) -> Result<(), String> {
  227. let state = app.state::<AppState>();
  228. let api = state.api.clone();
  229. match api.complete_water().await {
  230. Ok(_) => {
  231. let payload = PetStatePayload {
  232. state: PetState::Happy,
  233. reason: Some("drink_complete".into()),
  234. data: None,
  235. };
  236. let _ = app.emit("pet://state", payload);
  237. let _ = app.emit("pet://bubble", serde_json::json!({"text": "喝水打卡成功!太棒了~", "durationMs": 4000}));
  238. Ok(())
  239. }
  240. Err(_) => {
  241. let _ = app.emit("pet://bubble", serde_json::json!({"text": "打卡失败,请稍后再试", "durationMs": 4000}));
  242. Err("checkin failed".into())
  243. }
  244. }
  245. }
  246. async fn complete_task_by_id(app: &AppHandle, task_id: i64) -> Result<(), String> {
  247. let state = app.state::<AppState>();
  248. let api = state.api.clone();
  249. match api.complete_task(task_id).await {
  250. Ok(_) => {
  251. if let Some(s) = app.try_state::<AppState>() {
  252. *s.pending_task.lock().unwrap() = None;
  253. }
  254. let payload = PetStatePayload {
  255. state: PetState::Happy,
  256. reason: Some("task_complete".into()),
  257. data: None,
  258. };
  259. let _ = app.emit("pet://state", payload);
  260. let _ = app.emit("pet://bubble", serde_json::json!({"text": "任务完成!太棒了~", "durationMs": 4000}));
  261. Ok(())
  262. }
  263. Err(_) => {
  264. let _ = app.emit("pet://bubble", serde_json::json!({"text": "完成任务失败,请稍后再试", "durationMs": 4000}));
  265. Err("task complete failed".into())
  266. }
  267. }
  268. }