using System.Collections.ObjectModel; using System.Diagnostics; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using WindowsGotify.Models; using WindowsGotify.Services; using WindowsGotify.Tray; // 双开 WPF + WinForms 时,这几个类型两个命名空间都有,固定走 WPF 的。 using MessageBox = System.Windows.MessageBox; using Color = System.Windows.Media.Color; using Brush = System.Windows.Media.Brush; using Brushes = System.Windows.Media.Brushes; using Clipboard = System.Windows.Clipboard; using Button = System.Windows.Controls.Button; namespace WindowsGotify; public partial class MainWindow : Window { private const int MaxHistoryInUi = 200; private AppConfig? _config; private ConfigService? _configService; private Logger? _logger; private TrayIconService? _tray; private GotifyWebSocketService? _webSocket; private GotifyApiService? _apiService; private ApplicationCache? _appCache; private readonly ObservableCollection _history = new(); public MainWindow() { InitializeComponent(); HistoryList.ItemsSource = _history; Closing += (_, e) => { e.Cancel = true; Hide(); }; } public void Initialize( AppConfig config, ConfigService configService, Logger logger, TrayIconService tray, GotifyWebSocketService webSocket, GotifyApiService apiService, ApplicationCache appCache) { _config = config; _configService = configService; _logger = logger; _tray = tray; _webSocket = webSocket; _apiService = apiService; _appCache = appCache; UrlTextBox.Text = config.Gotify.Url; TokenBox.Password = config.Gotify.ClientToken; UpdateAppCount(); SetStatus("等待连接", StatusKind.Pending); UpdateHistoryCount(); UpdateThemeButtonLabel(); // 实时消息追加到历史 _webSocket.MessageReceived += OnLiveMessage; // 应用缓存更新时,刷新已加载消息的 AppName if (_appCache != null) { _appCache.Updated += (_, _) => Dispatcher.Invoke(() => { UpdateAppCount(); // 已存在的消息重新解析 AppName(缓存里可能新增了之前缺失的应用) foreach (var m in _history) m.AppName = _appCache.GetName(m.AppId); // 详情面板如果当前选中的,刷新 if (HistoryList.SelectedItem is GotifyMessage sel) RenderDetail(sel); }); } } public void ShowFromTray() { Show(); WindowState = WindowState.Normal; Activate(); // 每次打开都拉一次最新历史和应用,确保和远端同步 _ = LoadHistoryAsync(); _ = RefreshAppsAsync(silent: true); } private void OnLiveMessage(GotifyMessage msg) { Enrich(msg); Dispatcher.Invoke(() => { _history.Insert(0, msg); TrimHistory(); UpdateHistoryCount(); }); } /// /// 拿到一条消息后,补 AppName(从缓存查),返回原对象。 /// private GotifyMessage Enrich(GotifyMessage msg) { if (_appCache != null && string.IsNullOrEmpty(msg.AppName)) { msg.AppName = _appCache.GetName(msg.AppId); } return msg; } private void TrimHistory() { while (_history.Count > MaxHistoryInUi) { _history.RemoveAt(_history.Count - 1); } } private void UpdateHistoryCount() { HistoryCountText.Text = $"共 {_history.Count} 条"; } private void UpdateAppCount() { if (_appCache == null) { AppCountText.Text = " · 应用:?"; return; } var n = _appCache.Count; AppCountText.Text = n == 0 ? " · 应用:0(未拉取)" : $" · 应用:{n}"; } private enum StatusKind { Pending, Connected, Error, Info } private void SetStatus(string text, StatusKind kind) { StatusText.Text = $"状态:{text}"; StatusDot.Fill = kind switch { StatusKind.Connected => new SolidColorBrush(Color.FromRgb(0x10, 0x81, 0x16)), StatusKind.Error => new SolidColorBrush(Color.FromRgb(0xD1, 0x34, 0x38)), StatusKind.Pending => new SolidColorBrush(Color.FromRgb(0xCA, 0x50, 0x10)), _ => new SolidColorBrush(Color.FromRgb(0x88, 0x88, 0x88)) }; } private async Task LoadHistoryAsync() { if (_apiService is null || _config is null) return; if (string.IsNullOrWhiteSpace(_config.Gotify.ClientToken)) { SetStatus("未配置 Client Token,无法拉取历史", StatusKind.Error); return; } try { SetStatus("正在拉取历史...", StatusKind.Pending); var limit = Math.Clamp(_config.History.ViewerLimit, 1, 100); var messages = await _apiService.GetRecentMessagesAsync(limit, CancellationToken.None); foreach (var m in messages) Enrich(m); _history.Clear(); foreach (var m in messages) _history.Add(m); UpdateHistoryCount(); SetStatus($"已加载 {messages.Count} 条历史消息", StatusKind.Connected); _logger?.Info($"History loaded. count={messages.Count}"); } catch (Exception ex) { _logger?.Error("Load history failed.", ex); SetStatus($"拉取历史失败 - {ex.Message}", StatusKind.Error); } } private async Task RefreshAppsAsync(bool silent = false) { if (_appCache is null) return; if (_config is null || string.IsNullOrWhiteSpace(_config.Gotify.ClientToken)) { if (!silent) SetStatus("未配置 Client Token,无法拉取应用", StatusKind.Error); return; } try { if (!silent) SetStatus("正在拉取应用列表...", StatusKind.Pending); await _appCache.RefreshAsync(); UpdateAppCount(); if (!silent) SetStatus($"已加载 {_appCache.Count} 个应用", StatusKind.Connected); } catch (Exception ex) { _logger?.Error("Refresh applications failed.", ex); if (!silent) SetStatus($"拉取应用失败 - {ex.Message}", StatusKind.Error); } } private async void RefreshAppsButton_Click(object sender, RoutedEventArgs e) { await RefreshAppsAsync(); } private async void RefreshButton_Click(object sender, RoutedEventArgs e) { await LoadHistoryAsync(); } private void HistoryList_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (HistoryList.SelectedItem is GotifyMessage msg) { RenderDetail(msg); } else { DetailPanel.Visibility = Visibility.Collapsed; DetailEmptyHint.Visibility = Visibility.Visible; } } private void RenderDetail(GotifyMessage msg) { DetailEmptyHint.Visibility = Visibility.Collapsed; DetailPanel.Visibility = Visibility.Visible; DetailAppName.Text = string.IsNullOrWhiteSpace(msg.AppName) ? msg.TitleDisplay : msg.AppName; DetailAppName.Foreground = (Brush)(new AppNameToBrushConverter().Convert(msg.AppName, typeof(Brush), null, System.Globalization.CultureInfo.CurrentCulture) ?? Brushes.Black); var app = _appCache?.Get(msg.AppId); DetailAppDesc.Text = app?.Description ?? "(无应用描述)"; DetailAppDesc.Visibility = string.IsNullOrWhiteSpace(app?.Description) ? Visibility.Collapsed : Visibility.Visible; DetailPriority.Text = $"{msg.PriorityDisplay} ({(msg.Priority >= 8 ? "紧急-告警音" : msg.Priority >= 5 ? "重要" : "一般")})"; DetailDate.Text = msg.Date.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"); DetailTitle.Text = msg.TitleDisplay; DetailAppId.Text = msg.AppId.ToString(); DetailMessage.Text = msg.Message ?? ""; var url = msg.ClickUrl; if (!string.IsNullOrWhiteSpace(url)) { DetailUrl.Text = url; DetailUrl.Tag = url; DetailUrlLabel.Visibility = Visibility.Visible; DetailUrl.Visibility = Visibility.Visible; } else { DetailUrlLabel.Visibility = Visibility.Collapsed; DetailUrl.Visibility = Visibility.Collapsed; } } private void DetailUrl_Click(object sender, MouseButtonEventArgs e) { if (DetailUrl.Tag is string url && !string.IsNullOrWhiteSpace(url)) { OpenUrl(url); } } private void DetailUrl_DoubleClick(object sender, MouseButtonEventArgs e) { if (DetailUrl.Text is string url && !string.IsNullOrWhiteSpace(url)) { OpenUrl(url); } } private void OpenUrl(string url) { try { Process.Start(new ProcessStartInfo { FileName = url, UseShellExecute = true }); } catch (Exception ex) { _logger?.Error("Failed to open click url.", ex); MessageBox.Show($"无法打开链接:{ex.Message}", "Windows Gotify", MessageBoxButton.OK, MessageBoxImage.Warning); } } private void CopyMessageButton_Click(object sender, RoutedEventArgs e) { if (HistoryList.SelectedItem is not GotifyMessage msg) return; var text = BuildFullMessageText(msg); // 剪贴板常被其他进程占着(RDP、杀软、其他复制操作),CLIPBRD_E_CANT_OPEN 是常见 race。 // Clipboard.SetText 不自带重试,自己包一层:最多 10 次,每次等 50ms。 const int MaxAttempts = 10; const int DelayMs = 50; Exception? lastEx = null; bool ok = false; for (int attempt = 1; attempt <= MaxAttempts; attempt++) { try { Clipboard.SetText(text); ok = true; break; } catch (System.Runtime.InteropServices.COMException ex) { lastEx = ex; System.Threading.Thread.Sleep(DelayMs); } } if (ok) { _logger?.Info($"Copied message to clipboard. id={msg.Id}"); if (sender is Button btn) { var origContent = btn.Content; btn.Content = "已复制"; var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) }; timer.Tick += (_, _) => { btn.Content = origContent; timer.Stop(); }; timer.Start(); } } else { _logger?.Error("Copy to clipboard failed after retries.", lastEx); MessageBox.Show( $"复制失败:剪贴板被其他程序占用,重试 {MaxAttempts} 次仍未成功。\n请关闭正在使用剪贴板的程序后重试。\n\n错误:0x{(lastEx?.HResult ?? 0):X8}", "Windows Gotify", MessageBoxButton.OK, MessageBoxImage.Warning); } } private static string BuildFullMessageText(GotifyMessage msg) { var sb = new System.Text.StringBuilder(); sb.Append("["); sb.Append(string.IsNullOrWhiteSpace(msg.AppName) ? $"App {msg.AppId}" : msg.AppName); sb.Append("] "); sb.Append(string.IsNullOrWhiteSpace(msg.Title) ? "(无标题)" : msg.Title); sb.AppendLine(); sb.Append("时间: "); sb.AppendLine(msg.Date.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")); sb.Append("优先级: P"); sb.AppendLine(msg.Priority.ToString()); sb.AppendLine(); sb.AppendLine(msg.Message ?? ""); if (!string.IsNullOrWhiteSpace(msg.ClickUrl)) { sb.AppendLine(); sb.AppendLine(msg.ClickUrl); } return sb.ToString(); } private void ThemeToggleButton_Click(object sender, RoutedEventArgs e) { ThemeService.Toggle(); UpdateThemeButtonLabel(); // 持久化 if (_config != null && _configService != null) { _config.Ui.Theme = ThemeService.Current; _ = _configService.SaveAsync(_config); } } private void UpdateThemeButtonLabel() { ThemeToggleButton.Content = ThemeService.Current == ThemeService.Dark ? "☾ 深色" : "☼ 浅色"; } private async void SaveButton_Click(object sender, RoutedEventArgs e) { if (_config is null || _configService is null) return; var url = UrlTextBox.Text.Trim(); var token = TokenBox.Password.Trim(); if (!Uri.TryCreate(url, UriKind.Absolute, out var uri) || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) { MessageBox.Show("Gotify URL 无效。", "Windows Gotify", MessageBoxButton.OK, MessageBoxImage.Warning); return; } if (string.IsNullOrWhiteSpace(token)) { MessageBox.Show("Client Token 不能为空。", "Windows Gotify", MessageBoxButton.OK, MessageBoxImage.Warning); return; } _config.Gotify.Url = url.TrimEnd('/'); _config.Gotify.ClientToken = token; await _configService.SaveAsync(_config); _webSocket?.RequestReconnect(); // URL/Token 变了,重新拉应用和历史 _ = RefreshAppsAsync(silent: true); await LoadHistoryAsync(); MessageBox.Show("配置已保存,正在重新连接。", "Windows Gotify", MessageBoxButton.OK, MessageBoxImage.Information); } private async void TestButton_Click(object sender, RoutedEventArgs e) { try { var notification = new NotificationService(_config!, _logger!); await notification.ShowAsync(new GotifyMessage { Id = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), Message = "Windows Gotify Agent 测试成功。", Title = "Gotify 测试", Priority = 5, Date = DateTimeOffset.UtcNow }); } catch (Exception ex) { MessageBox.Show(ex.Message, "测试通知失败", MessageBoxButton.OK, MessageBoxImage.Error); } } }