feat: 落地 V1 全部源码 — WebSocket 通知 + 应用内历史查看
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
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user