|
@@ -0,0 +1,118 @@
|
|
|
|
|
+use crate::api::{models::DailyCheckin, ApiClient};
|
|
|
|
|
+use crate::config::PetConfig;
|
|
|
|
|
+use crate::state::{map_state, PetState, PetStatePayload};
|
|
|
|
|
+use chrono::Timelike;
|
|
|
|
|
+use std::sync::Arc;
|
|
|
|
|
+use std::time::Duration;
|
|
|
|
|
+use tauri::{AppHandle, Emitter, Manager};
|
|
|
|
|
+
|
|
|
|
|
+/// 启动轮询任务:启动即同步吉祥物+喝水检查,然后 30s 任务 / 60s 健康 / 1h 喝水。
|
|
|
|
|
+/// 返回 JoinHandle 以便 save_settings 时 abort 重启。
|
|
|
|
|
+pub fn spawn(app: AppHandle, api: Arc<ApiClient>) -> tauri::async_runtime::JoinHandle<()> {
|
|
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
|
|
+ sync_mascot(&app, &api).await;
|
|
|
|
|
+ check_water(&app, &api).await;
|
|
|
|
|
+
|
|
|
|
|
+ let mut tasks_iv = tokio::time::interval(Duration::from_secs(30));
|
|
|
|
|
+ let mut health_iv = tokio::time::interval(Duration::from_secs(60));
|
|
|
|
|
+ let mut water_iv = tokio::time::interval(Duration::from_secs(3600));
|
|
|
|
|
+
|
|
|
|
|
+ loop {
|
|
|
|
|
+ tokio::select! {
|
|
|
|
|
+ _ = tasks_iv.tick() => poll_tasks(&app, &api).await,
|
|
|
|
|
+ _ = health_iv.tick() => poll_health(&app, &api).await,
|
|
|
|
|
+ _ = water_iv.tick() => check_water(&app, &api).await,
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn in_active_hour(hour: u32) -> bool {
|
|
|
|
|
+ (10..=20).contains(&hour)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async fn sync_mascot(app: &AppHandle, api: &ApiClient) {
|
|
|
|
|
+ let Ok(mascot) = api.mascot_get().await else { return };
|
|
|
|
|
+ let pet_name = mascot.mascot_id.as_deref().unwrap_or("xibao");
|
|
|
|
|
+ let path = format!("DefaultPets/{pet_name}/pet-config.json");
|
|
|
|
|
+
|
|
|
|
|
+ // 优先读嵌入资产;失败回退 resource_dir
|
|
|
|
|
+ let bytes = app
|
|
|
|
|
+ .asset_resolver()
|
|
|
|
|
+ .get(path.clone())
|
|
|
|
|
+ .map(|a| a.bytes)
|
|
|
|
|
+ .or_else(|| std::fs::read(app.path().resource_dir().unwrap_or_default().join(&path)).ok());
|
|
|
|
|
+ let Some(bytes) = bytes else { return };
|
|
|
|
|
+
|
|
|
|
|
+ if let Ok(json) = String::from_utf8(bytes) {
|
|
|
|
|
+ if let Ok(cfg) = PetConfig::parse(&json) {
|
|
|
|
|
+ if let Ok(payload) = serde_json::to_string(&cfg) {
|
|
|
|
|
+ let _ = app.emit("pet://config", payload);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async fn poll_tasks(app: &AppHandle, api: &ApiClient) {
|
|
|
|
|
+ match api.tasks_today().await {
|
|
|
|
|
+ Ok(tasks) => {
|
|
|
|
|
+ let state = map_state(Some(&tasks), None, None, false);
|
|
|
|
|
+ let payload = PetStatePayload { state, reason: Some("tasks".into()), data: None };
|
|
|
|
|
+ let _ = app.emit("pet://state", payload);
|
|
|
|
|
+ }
|
|
|
|
|
+ Err(_) => {
|
|
|
|
|
+ let _ = app.emit(
|
|
|
|
|
+ "pet://bubble",
|
|
|
|
|
+ serde_json::json!({"text": "连接后端失败,请检查服务", "durationMs": 5000}),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async fn poll_health(app: &AppHandle, api: &ApiClient) {
|
|
|
|
|
+ match (api.energy_overview().await, api.daily_checkin().await) {
|
|
|
|
|
+ (Ok(energy), Ok(checkin)) => {
|
|
|
|
|
+ let state = map_state(None, Some(&energy), Some(&checkin), false);
|
|
|
|
|
+ let payload = PetStatePayload { state, reason: Some("health".into()), data: None };
|
|
|
|
|
+ let _ = app.emit("pet://state", payload);
|
|
|
|
|
+ }
|
|
|
|
|
+ _ => { /* 静默,与现版一致 */ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async fn check_water(app: &AppHandle, api: &ApiClient) {
|
|
|
|
|
+ let hour = chrono::Local::now().hour();
|
|
|
|
|
+ if !in_active_hour(hour) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ let checkin: DailyCheckin = match api.daily_checkin().await {
|
|
|
|
|
+ Ok(c) => c,
|
|
|
|
|
+ Err(_) => return,
|
|
|
|
|
+ };
|
|
|
|
|
+ if !checkin.items.iter().any(|i| i == "water") {
|
|
|
|
|
+ let payload = PetStatePayload {
|
|
|
|
|
+ state: PetState::WaitingForInteraction,
|
|
|
|
|
+ reason: Some("drink_water".into()),
|
|
|
|
|
+ data: None,
|
|
|
|
|
+ };
|
|
|
|
|
+ let _ = app.emit("pet://state", payload);
|
|
|
|
|
+ let _ = app.emit(
|
|
|
|
|
+ "pet://bubble",
|
|
|
|
|
+ serde_json::json!({"text": "该喝水啦~点我确认完成!", "durationMs": 15000}),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[cfg(test)]
|
|
|
|
|
+mod tests {
|
|
|
|
|
+ use super::*;
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn active_hour_window_is_10_to_20_inclusive() {
|
|
|
|
|
+ assert!(!in_active_hour(9));
|
|
|
|
|
+ assert!(in_active_hour(10));
|
|
|
|
|
+ assert!(in_active_hour(15));
|
|
|
|
|
+ assert!(in_active_hour(20));
|
|
|
|
|
+ assert!(!in_active_hour(21));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|