17 个源文件 + .gitignore + 默认 config.json 模板,严格按方案文档第 6 节实现。
用户核心场景:
- Windows 接收 Gotify 通知(WS /stream → AppNotificationBuilder)
- 应用内查看历史消息(打开 MainWindow 触发 GetRecentMessagesAsync,实时通过 MessageReceived 事件追加)
- 列表条目双击打开 extras.client::notification.click.url
关键变更点:
- GotifyMessage.Extras 类型由 Dictionary<string,object> 改为 Dictionary<string,JsonElement>,避免 System.Text.Json 反序列化时丢失嵌套结构
- 新增 HistoryConfig.ViewerLimit(默认 50,UI 硬上限 200)
- GotifyApiService.GetRecentMessagesAsync:since=0 拉最近 N 条,OrderByDescending(Id) 保证顺序
- GotifyWebSocketService 新增 MessageReceived 事件,MainWindow 用 Dispatcher.Invoke 切回 UI 线程追加
- NotificationService.AddArgument('url=...') 透传 click URL 给 OnNotificationInvoked
- MainWindow.xaml 增加 ListView + 刷新按钮,使用 ObservableCollection<GotifyMessage>
- 默认 config.json 用 https://gotify.example.com 占位 URL,ClientToken 留空,首启动自动写入 %LOCALAPPDATA%
依赖:Microsoft.WindowsAppSDK 2.4.0(方案文档固定版本)。
构建:本机 .NET 8 SDK 未安装,build 待用户在装好 SDK 后执行: dotnet restore && dotnet build -c Release
206 lines
6.1 KiB
C#
206 lines
6.1 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
using System.Windows;
|
|
using System.Windows.Controls;
|
|
using WindowsGotify.Models;
|
|
using WindowsGotify.Services;
|
|
using WindowsGotify.Tray;
|
|
|
|
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 readonly ObservableCollection<GotifyMessage> _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)
|
|
{
|
|
_config = config;
|
|
_configService = configService;
|
|
_logger = logger;
|
|
_tray = tray;
|
|
_webSocket = webSocket;
|
|
_apiService = apiService;
|
|
|
|
UrlTextBox.Text = config.Gotify.Url;
|
|
TokenBox.Password = config.Gotify.ClientToken;
|
|
StatusText.Text = "状态:等待连接";
|
|
UpdateHistoryCount();
|
|
|
|
// 实时消息追加到历史
|
|
_webSocket.MessageReceived += OnLiveMessage;
|
|
}
|
|
|
|
public void ShowFromTray()
|
|
{
|
|
Show();
|
|
WindowState = WindowState.Normal;
|
|
Activate();
|
|
// 每次打开都拉一次最新历史,确保和远端同步
|
|
_ = LoadHistoryAsync();
|
|
}
|
|
|
|
private void OnLiveMessage(GotifyMessage msg)
|
|
{
|
|
// WebSocket 回调可能在非 UI 线程,切回 UI 线程
|
|
Dispatcher.Invoke(() =>
|
|
{
|
|
_history.Insert(0, msg);
|
|
TrimHistory();
|
|
UpdateHistoryCount();
|
|
});
|
|
}
|
|
|
|
private void TrimHistory()
|
|
{
|
|
while (_history.Count > MaxHistoryInUi)
|
|
{
|
|
_history.RemoveAt(_history.Count - 1);
|
|
}
|
|
}
|
|
|
|
private void UpdateHistoryCount()
|
|
{
|
|
HistoryCountText.Text = $"共 {_history.Count} 条(本地缓存上限 {MaxHistoryInUi})";
|
|
}
|
|
|
|
private async Task LoadHistoryAsync()
|
|
{
|
|
if (_apiService is null || _config is null) return;
|
|
if (string.IsNullOrWhiteSpace(_config.Gotify.ClientToken))
|
|
{
|
|
StatusText.Text = "状态:未配置 Client Token,无法拉取历史";
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
StatusText.Text = "状态:正在拉取历史...";
|
|
var limit = Math.Clamp(_config.History.ViewerLimit, 1, 100);
|
|
var messages = await _apiService.GetRecentMessagesAsync(limit, CancellationToken.None);
|
|
|
|
// 用清空 + 批量 Insert 让 UI 一次性刷新,避免多次 CollectionChanged
|
|
_history.Clear();
|
|
foreach (var m in messages)
|
|
{
|
|
_history.Add(m);
|
|
}
|
|
UpdateHistoryCount();
|
|
StatusText.Text = $"状态:已加载 {messages.Count} 条历史消息";
|
|
_logger?.Info($"History loaded. count={messages.Count}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error("Load history failed.", ex);
|
|
StatusText.Text = $"状态:拉取历史失败 - {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
await LoadHistoryAsync();
|
|
}
|
|
|
|
private void HistoryList_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
{
|
|
if (HistoryList.SelectedItem is not GotifyMessage msg) return;
|
|
var url = msg.ClickUrl;
|
|
if (string.IsNullOrWhiteSpace(url)) return;
|
|
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = url,
|
|
UseShellExecute = true
|
|
});
|
|
_logger?.Info($"Opened click url. id={msg.Id}, url={url}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.Error("Failed to open click url.", ex);
|
|
MessageBox.Show($"无法打开链接:{ex.Message}", "Windows Gotify",
|
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|