commands.rs 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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!({
  149. "status": "success",
  150. "nickname": resp.nickname,
  151. "userId": resp.user_id,
  152. "role": resp.role
  153. }),
  154. );
  155. return;
  156. }
  157. other => {
  158. let _ = handle.emit(
  159. "pet://wechat-login",
  160. serde_json::json!({"status": other, "message": resp.message}),
  161. );
  162. return;
  163. }
  164. },
  165. Err(_) => continue, // 瞬时网络错误:继续轮询
  166. }
  167. }
  168. let _ = handle.emit("pet://wechat-login", serde_json::json!({"status": "timeout"}));
  169. });
  170. Ok(serde_json::json!({ "status": "pending", "url": qr.url }))
  171. }
  172. /// 用系统默认浏览器打开 URL(Windows: cmd start / macOS: open / 其它: xdg-open)。
  173. fn open_url(url: &str) -> Result<(), String> {
  174. #[cfg(target_os = "windows")]
  175. {
  176. std::process::Command::new("cmd")
  177. .args(["/C", "start", "", url])
  178. .spawn()
  179. .map_err(|e| e.to_string())?;
  180. }
  181. #[cfg(target_os = "macos")]
  182. {
  183. std::process::Command::new("open")
  184. .arg(url)
  185. .spawn()
  186. .map_err(|e| e.to_string())?;
  187. }
  188. #[cfg(not(any(target_os = "windows", target_os = "macos")))]
  189. {
  190. std::process::Command::new("xdg-open")
  191. .arg(url)
  192. .spawn()
  193. .map_err(|e| e.to_string())?;
  194. }
  195. Ok(())
  196. }
  197. #[tauri::command]
  198. pub fn save_settings(
  199. app: AppHandle,
  200. state: State<AppState>,
  201. settings: State<SettingsStore>,
  202. base_url: String,
  203. token: Option<String>,
  204. ) -> Result<(), String> {
  205. let mut cfg = settings.load();
  206. cfg.base_url = base_url.trim().to_string();
  207. settings.save(&cfg)?;
  208. if let Some(t) = token {
  209. if !t.trim().is_empty() {
  210. TokenStore::new().save(t.trim())?;
  211. }
  212. }
  213. // 更新运行时客户端并重启轮询(对齐现 ShowSettings 的 Stop/Start)
  214. state.api.set_base_url(cfg.base_url.clone());
  215. if let Some(t) = TokenStore::new().load()? {
  216. state.api.update_token(t);
  217. }
  218. restart_polling(&app, &state);
  219. if let Some(win) = app.get_webview_window("settings") {
  220. let _ = win.hide();
  221. }
  222. Ok(())
  223. }
  224. fn restart_polling(app: &AppHandle, state: &State<AppState>) {
  225. if let Some(h) = state.poll_handle.lock().unwrap().take() {
  226. h.abort();
  227. }
  228. let handle = poller::spawn(app.clone(), state.api.clone());
  229. *state.poll_handle.lock().unwrap() = Some(handle);
  230. }
  231. async fn complete_water(app: &AppHandle) -> Result<(), String> {
  232. let state = app.state::<AppState>();
  233. let api = state.api.clone();
  234. match api.complete_water().await {
  235. Ok(_) => {
  236. let payload = PetStatePayload {
  237. state: PetState::Happy,
  238. reason: Some("drink_complete".into()),
  239. data: None,
  240. };
  241. let _ = app.emit("pet://state", payload);
  242. let _ = app.emit("pet://bubble", serde_json::json!({"text": "喝水打卡成功!太棒了~", "durationMs": 4000}));
  243. Ok(())
  244. }
  245. Err(_) => {
  246. let _ = app.emit("pet://bubble", serde_json::json!({"text": "打卡失败,请稍后再试", "durationMs": 4000}));
  247. Err("checkin failed".into())
  248. }
  249. }
  250. }
  251. async fn complete_task_by_id(app: &AppHandle, task_id: i64) -> Result<(), String> {
  252. let state = app.state::<AppState>();
  253. let api = state.api.clone();
  254. match api.complete_task(task_id).await {
  255. Ok(_) => {
  256. if let Some(s) = app.try_state::<AppState>() {
  257. *s.pending_task.lock().unwrap() = None;
  258. }
  259. let payload = PetStatePayload {
  260. state: PetState::Happy,
  261. reason: Some("task_complete".into()),
  262. data: None,
  263. };
  264. let _ = app.emit("pet://state", payload);
  265. let _ = app.emit("pet://bubble", serde_json::json!({"text": "任务完成!太棒了~", "durationMs": 4000}));
  266. Ok(())
  267. }
  268. Err(_) => {
  269. let _ = app.emit("pet://bubble", serde_json::json!({"text": "完成任务失败,请稍后再试", "durationMs": 4000}));
  270. Err("task complete failed".into())
  271. }
  272. }
  273. }