|
|
@@ -0,0 +1,207 @@
|
|
|
+using System;
|
|
|
+using System.IO;
|
|
|
+using System.Text.Json;
|
|
|
+using System.Timers;
|
|
|
+using System.Windows;
|
|
|
+using System.Windows.Forms;
|
|
|
+using XafPet.Desktop.PetEngine;
|
|
|
+
|
|
|
+namespace XafPet.Desktop;
|
|
|
+
|
|
|
+public partial class MainWindow : Window
|
|
|
+{
|
|
|
+ private NotifyIcon? _trayIcon;
|
|
|
+ private ApiClient? _api;
|
|
|
+ private Timer? _taskTimer;
|
|
|
+ private Timer? _energyTimer;
|
|
|
+ private Timer? _waterTimer;
|
|
|
+ private string _baseUrl = "http://localhost:9082";
|
|
|
+
|
|
|
+ public MainWindow()
|
|
|
+ {
|
|
|
+ InitializeComponent();
|
|
|
+ RootGrid.MouseLeftButtonDown += (s, e) =>
|
|
|
+ {
|
|
|
+ if (e.ButtonState == MouseButtonState.Pressed) DragMove();
|
|
|
+ };
|
|
|
+ PetHost.InteractionReceived += OnInteractionReceived;
|
|
|
+ Loaded += OnLoaded;
|
|
|
+ Closed += OnClosed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnLoaded(object sender, RoutedEventArgs e)
|
|
|
+ {
|
|
|
+ var token = TokenStore.Load();
|
|
|
+ _api = new ApiClient(new HttpClient(), _baseUrl, token ?? "dev-token");
|
|
|
+ StartPolling();
|
|
|
+ SetupTray();
|
|
|
+ _ = SyncMascotAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void StartPolling()
|
|
|
+ {
|
|
|
+ _taskTimer = new Timer(30_000) { AutoReset = true };
|
|
|
+ _taskTimer.Elapsed += async (_, _) => await PollTasksAsync();
|
|
|
+ _taskTimer.Start();
|
|
|
+
|
|
|
+ _energyTimer = new Timer(60_000) { AutoReset = true };
|
|
|
+ _energyTimer.Elapsed += async (_, _) => await PollHealthAsync();
|
|
|
+ _energyTimer.Start();
|
|
|
+
|
|
|
+ StartWaterReminder();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task PollTasksAsync()
|
|
|
+ {
|
|
|
+ if (_api == null) return;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var tasks = await _api.GetTodayTasksAsync();
|
|
|
+ var state = StateManager.MapState(tasks ?? new List<CfcTask>(), null, null, false);
|
|
|
+ PushState(state, "tasks");
|
|
|
+ }
|
|
|
+ catch (ApiException ex) { PushBubble($"服务暂时不可用({ex.ApiCode})"); }
|
|
|
+ catch (Exception) { PushBubble("连接后端失败,请检查服务"); }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task PollHealthAsync()
|
|
|
+ {
|
|
|
+ if (_api == null) return;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var energy = await _api.GetEnergyOverviewAsync();
|
|
|
+ var checkin = await _api.GetDailyCheckinAsync();
|
|
|
+ var state = StateManager.MapState(null, energy, checkin, false);
|
|
|
+ PushState(state, "health");
|
|
|
+ }
|
|
|
+ catch (Exception) { /* 静默 */ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void PushState(PetState state, string reason)
|
|
|
+ {
|
|
|
+ var payload = new PetStatePayload(StateManager.StateToCamelCase(state), reason, null);
|
|
|
+ PetHost.NotifyState(JsonSerializer.Serialize(payload));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void PushBubble(string text)
|
|
|
+ {
|
|
|
+ PetHost.ShowBubble(text, 5000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void SetupTray()
|
|
|
+ {
|
|
|
+ var menu = new ContextMenuStrip();
|
|
|
+ menu.Items.Add("设置", null, (_, _) => ShowSettings());
|
|
|
+ menu.Items.Add("显示/隐藏", null, (_, _) => ToggleVisible());
|
|
|
+ menu.Items.Add("退出", null, (_, _) => { _trayIcon?.Dispose(); Application.Current.Shutdown(); });
|
|
|
+
|
|
|
+ _trayIcon = new NotifyIcon
|
|
|
+ {
|
|
|
+ Text = "浠艾福桌面宠物",
|
|
|
+ Icon = SystemIcons.Application,
|
|
|
+ ContextMenuStrip = menu,
|
|
|
+ Visible = true
|
|
|
+ };
|
|
|
+ _trayIcon.DoubleClick += (_, _) => ToggleVisible();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ToggleVisible()
|
|
|
+ {
|
|
|
+ Visibility = Visibility == Visibility.Visible ? Visibility.Hidden : Visibility.Visible;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ShowSettings()
|
|
|
+ {
|
|
|
+ var win = new SettingsWindow(TokenStore.Load(), _baseUrl) { Owner = this };
|
|
|
+ if (win.ShowDialog() == true)
|
|
|
+ {
|
|
|
+ _baseUrl = win.BaseUrl;
|
|
|
+ if (!string.IsNullOrEmpty(win.Token)) TokenStore.Save(win.Token);
|
|
|
+ _api = new ApiClient(new HttpClient(), _baseUrl, TokenStore.Load() ?? "dev-token");
|
|
|
+ StartPolling();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async void OnClosed(object? sender, EventArgs e)
|
|
|
+ {
|
|
|
+ _taskTimer?.Stop(); _energyTimer?.Stop(); _waterTimer?.Stop();
|
|
|
+ _taskTimer?.Dispose(); _energyTimer?.Dispose(); _waterTimer?.Dispose();
|
|
|
+ _trayIcon?.Dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 喝水提醒 ─────────────────────────────────────────────
|
|
|
+ private void StartWaterReminder()
|
|
|
+ {
|
|
|
+ _waterTimer = new Timer(60 * 60 * 1000) { AutoReset = true };
|
|
|
+ _waterTimer.Elapsed += async (_, _) => await CheckWaterAsync();
|
|
|
+ _waterTimer.Start();
|
|
|
+ _ = CheckWaterAsync();
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task CheckWaterAsync()
|
|
|
+ {
|
|
|
+ if (_api == null) return;
|
|
|
+ var hour = DateTime.Now.Hour;
|
|
|
+ if (hour < 10 || hour > 20) return;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var checkin = await _api.GetDailyCheckinAsync();
|
|
|
+ if (checkin != null && !checkin.Items.Contains("water"))
|
|
|
+ {
|
|
|
+ PushState(PetState.WaitingForInteraction, "drink_water");
|
|
|
+ PetHost.ShowBubble("该喝水啦~点我确认完成!", 15000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception) { }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void OnInteractionReceived(string action, string? dataJson)
|
|
|
+ {
|
|
|
+ switch (action)
|
|
|
+ {
|
|
|
+ case "click":
|
|
|
+ _ = HandleClickAsync();
|
|
|
+ break;
|
|
|
+ case "pet":
|
|
|
+ PetHost.ShowBubble("嘻嘻~", 2000);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async Task HandleClickAsync()
|
|
|
+ {
|
|
|
+ if (_api == null) return;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var result = await _api.CompleteDrinkWaterAsync();
|
|
|
+ if (result?.IsSuccess == true)
|
|
|
+ {
|
|
|
+ PushState(PetState.Happy, "drink_complete");
|
|
|
+ PetHost.ShowBubble("喝水打卡成功!太棒了~", 4000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception)
|
|
|
+ {
|
|
|
+ PushBubble("打卡失败,请稍后再试");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ── 吉祥物同步 ───────────────────────────────────────────
|
|
|
+ private async Task SyncMascotAsync()
|
|
|
+ {
|
|
|
+ if (_api == null) return;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ var mascot = await _api.GetMascotAsync();
|
|
|
+ var petName = mascot?.MascotId ?? "xibao";
|
|
|
+ var cfgPath = Path.Combine(AppContext.BaseDirectory, "DefaultPets", petName, "pet-config.json");
|
|
|
+ if (File.Exists(cfgPath))
|
|
|
+ {
|
|
|
+ var json = File.ReadAllText(cfgPath);
|
|
|
+ var parsed = PetConfig.Parse(json);
|
|
|
+ PetHost.LoadPetConfig(JsonSerializer.Serialize(parsed));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ catch (Exception) { /* 默认浠宝 */ }
|
|
|
+ }
|
|
|
+}
|