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); // 三段降级: // 1) 现代路径 Clipboard.SetDataObject(copy: true),用 IDataObject COM 接口(可能跟 OLE 路径分开排队) // 2) 失败回退到 Clipboard.SetText + 10 次 50ms 重试(常见 race,RDP/杀软短暂持有) // 3) 还失败(典型:网易UU远程 / 剪贴板工具长期持有)弹手动复制窗口,文本预填 + 全选,让用户自己 Ctrl+C Exception? lastEx = null; bool ok = false; try { Clipboard.SetDataObject(text, true); ok = true; } catch (Exception ex) { lastEx = ex; _logger?.Warn($"Clipboard.SetDataObject failed: {ex.GetType().Name} 0x{ex.HResult:X8}"); } if (!ok) { const int MaxAttempts = 10; const int DelayMs = 50; 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 { // 长期占用的兜底:弹个预填 + 全选的窗口,用户自己 Ctrl+C _logger?.Error("Clipboard locked persistently. Showing fallback copy dialog.", lastEx); var dialog = new CopyFallbackWindow(text) { Owner = this }; dialog.ShowDialog(); } } 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); } } }