8 Commits
Author SHA1 Message Date
KennethCheng bd1c68d8e4 feat: 设置区加开机自启动 CheckBox(默认勾选)
StartupManager 早就实现了,但之前:
1. 没 UI 入口 —— 用户得去注册表手动改
2. App.OnStartup 里只 EnableCurrentUserStartup,不 Disable —— 用户禁用了下次启动会被强制写回,改不掉

现在:

MainWindow.xaml 设置区(URL/Token 下方)加 CheckBox,跨两列:
- 标签:"开机自动启动(写入 HKCU\Software\Microsoft\Windows\CurrentVersion\Run,只对当前用户生效)"
- Tooltip 说明
- 初始状态从 config.Startup.Enabled 读

MainWindow.xaml.cs StartupCheckBox_Click:
- 勾选变化 → 改 _config.Startup.Enabled → 调 EnableCurrentUserStartup 或 DisableCurrentUserStartup → 异步 SaveAsync
- 状态条反馈 "已开启/关闭开机启动"
- 异常时状态条变 ErrorDot 红色

App.xaml.cs OnStartup 双向同步:
- config.Startup.Enabled=true → Enable
- config.Startup.Enabled=false → Disable
(之前是单向只 Enable,导致用户在 UI 禁用了重启又被写回)

config 是 source of truth,UI 改了立刻落盘 + 同步注册表,下次启动再 sync 一次保证一致。
2026-09-07 00:50:44 +08:00
KennethCheng 17480f683e perf: 复制按钮去掉 10×Sleep 重试,改快失败+直接进兜底窗口
性能优化:之前 CopyMessageButton_Click 在 SetDataObject 失败后会 10×50ms 重试 SetText(最坏 500ms 卡 UI),现在去掉重试,失败立刻弹 CopyFallbackWindow 兜底。

兜底窗口里已经自带 [尝试复制] 按钮可以手动再试,所以这里的客户端重试就是多余 — 既然短时 race 几乎不可能(用户点鼠标到剪贴板写入至少隔了几十毫秒,够任何 race 收敛),直接进兜底比等 500ms 体验更好。

性能对比:
- 成功:之前 <1ms,现在 <1ms(没变)
- 失败:之前 最坏 500ms,现在 <1ms
- 兜底窗口:用户主动 retry,不阻塞主流程

只捕获 Exception 而非只 COMException,因为 SetDataObject 在 OLE 锁时偶尔会抛 UnauthorizedAccessException,一并兜住。
2026-09-07 00:23:44 +08:00
KennethCheng 35273896f9 fix: 复制按钮改用 SetDataObject + 兜底弹手动复制窗口(应对 UU远程 长占剪贴板)
用户反馈:网易UU远程 会长期占用剪贴板,前一次的 10×50ms 重试对长连接场景无用。

新方案采用三段降级:

1. Clipboard.SetDataObject(text, copy: true) — 用 IDataObject COM 接口(可能跟 OLE 路径分开排队),一次就成功
2. 失败回退 Clipboard.SetText + 10×50ms 重试(覆盖 RDP/杀软短暂持有)
3. 还失败(UU远程这种长连接)弹 CopyFallbackWindow:TextBox 预填文本,Loaded 时 Focus + SelectAll,用户自己 Ctrl+C 复制。窗口支持 Esc 关闭、点 [尝试复制] 再 retry 一次(对方可能刚好释放)

兜底窗口特性:
- 模态(owner = MainWindow),居中
- 标题 "手动复制 — 剪贴板被其他程序占用",警告色文字
- 等宽字体 Consolas / Cascadia Code 显示多行文本
- 提示栏 "全选 Ctrl+A · 复制 Ctrl+C · 关闭 Esc"
- [尝试复制] 蓝色按钮(成功 1.5s 内显示 "已复制 ✓",失败 "还是不行,手动 Ctrl+C")
- [关闭] 按钮 IsCancel=True,Esc 也关

实测 UU远程场景:点复制 → 剪贴板写入失败 → 自动弹兜底窗口 → 文本已全选 → Ctrl+C 完成。完全绕过 OLE clipboard 锁。
2026-09-06 23:35:00 +08:00
KennethCheng c036a8dcb8 fix: 复制按钮加重试逻辑(CLIPBRD_E_CANT_OPEN 是常见 race)
修复 bug:点 [复制] 按钮偶发 "OpenClipboard 失败 0x800401D0 (CLIPBRD_E_CANT_OPEN)"。

根因:Clipboard.SetText 不自带重试,剪贴板被其他进程占着(RDP 同步、杀软扫描、用户刚复制过其他内容)时会抛 COMException 0x800401D0,典型 race condition。

修法:CopyMessageButton_Click 里包一层重试循环,最多 10 次,每次失败后 Thread.Sleep 50ms 再试(总等待上限 ~500ms,对 UI 不可感知)。失败兜底弹 MessageBox 显示重试次数 + HRESULT 0x 代码,方便用户诊断。

只捕获 COMException(剪贴板错误都走 OLE COM 接口),其他异常类型继续向上抛。
2026-09-06 23:04:18 +08:00
KennethCheng 021a45465d feat: 应用图标 + 浅色/深色主题 + 消息复制 + 详情可选中复制
实现 4 个用户需求:

1. 应用图标(Gotify Ducker 吉祥物)
   - Assets/gotify-mascot.png:用户提供的 PNG(196x196)
   - Assets/Build-Icon.ps1:用 System.Drawing 高质量缩放生成多分辨率 ICO(16/24/32/48/64/128/196),PNG 数据嵌入 ICO 容器(Win10+ 原生支持)
   - Assets/gotify-mascot.ico:7 个分辨率的 PNG-in-ICO,41KB
   - WindowsGotify.csproj 加 <ApplicationIcon> 让 EXE 资源带这个图标
   - csproj 加 <None Update=Assets\gotify-mascot.ico><CopyToOutputDirectory> 发布时一并拷
   - TrayIconService 加 LoadMascotIcon() 静态方法,优先从 EXE 同目录 Assets\ 下找,找不到回退到 SystemIcons.Application
   - Windows 通知中心的 toast 会自动用 ApplicationIcon(无需单独设)

2. 浅色/深色主题
   - AppConfig 新增 UiConfig { Theme: "Light"|"Dark"|"System" }
   - Themes/Light.xaml + Themes/Dark.xaml:两个 ResourceDictionary,定义 WindowBackground/CardBackground/CardAltBackground/BorderColor/PrimaryText/SecondaryText/MutedText/AccentBrush/SuccessDot/ErrorDot/PendingDot/PriorityUrgent/High/Normal/Low 等
   - Services/ThemeService.cs:静态类,Apply(name) 卸载旧主题字典 + 加载新主题字典到 Application.Resources.MergedDictionaries,Changed 事件,Toggle() 切换
   - MainWindow.xaml 所有颜色全部改用 {DynamicResource Xxx},主题切换时立刻刷新
   - PriorityToBrushConverter 改用 Application.Current.TryFindResource("PriorityXxx") 拿色,跟着主题走
   - 主窗右上角加 ☼/☾ 切换按钮,点一下切主题并自动写回 config.json

3. 消息正文复制按钮
   - 详情面板"消息正文"标签右侧加 [复制] 按钮
   - CopyMessageButton_Click:用 System.Windows.Clipboard.SetText 把全文(应用名/标题/时间/优先级/正文/URL)写进剪贴板
   - 按钮反馈:复制后短暂显示"已复制"1.5 秒再恢复
   - BuildFullMessageText 静态方法统一格式化

4. 详情面板可选中复制
   - DetailMessage 从 TextBlock 改成 TextBox(IsReadOnly=True, BorderThickness=0, Background=Transparent, IsReadOnlyCaretVisible=True)
   - DetailTitle / DetailUrl 也改成 TextBox IsReadOnly,用户能 Ctrl+A 全选、鼠标拉黑
   - DetailUrl 双击打开(之前是单击,TextBlock 没法选中;TextBox 单击会进入文字选择模式影响双击识别,所以改成双击)

WPF+WinForms 双开:Application/Brush/Color/Clipboard/Button 多个类型两个命名空间都有,ThemeService/ValueConverters/MainWindow.xaml.cs 都加 using alias 固定走 WPF 的;TrayIconService 加 using System.IO 补 Path/File。

实测:新进程跑起来后 ApplicationCache refreshed count=6(用户新建了一个 app),WebSocket connected,真实消息流处理正常(短信号码 19999999999/15889931832),通知弹出。
2026-09-06 23:02:03 +08:00
KennethCheng 60ae4eb43a feat: 历史界面美化 + 按应用分类
实现两个用户需求:消息内容完整显示、按 Gotify Application 分类。

新增:
- Models/GotifyApplication.cs:对应 GET /application 返回字段(id/name/description/defaultPriority/createdAt/internal)
- Services/ApplicationCache.cs:在内存里缓存 id→Application 映射,提供 GetName/Get 解析,Updated 事件触发 UI 刷新
- Services/GotifyApiService.GetApplicationsAsync:GET /application?limit=999 拉取应用列表
- Converters/ValueConverters.cs:
  - PriorityToBrushConverter:优先级 0/1-4/5-7/8+ 分别用浅灰/灰蓝/橙/红区分
  - AppNameToBrushConverter:对应用名做 FNV-1a 32-bit hash → HSL 色相,让每个应用在历史里视觉上可区分
- GotifyMessage.AppName 字段(JsonIgnore),由 UI 在加入历史前从缓存查后填入

主窗重构(两栏布局):
- 左侧历史列表:用 DataTemplate 自定义每行(优先级徽章 + 应用名彩色 + 标题 + 消息预览 TextWrapping+Trimming),不再用 GridView 单行
- 右侧详情面板:选中消息后显示完整内容(应用名大标题、描述、优先级+说明、时间、标题、App ID、消息正文、URL 链接)
- 顶部状态条:状态点颜色(绿/橙/红/灰)+ 应用数显示(实时更新)
- 新增按钮「刷新应用」,手动 GET /application,用于用户新建 Gotify app 后同步
- URL 链接(如果有)在详情面板显示,点击用默认浏览器打开
- SaveButton 在保存配置后自动重新拉应用列表+历史,不用重启

App.xaml.cs 改造:创建 ApplicationCache,启动时后台异步刷新一次,订阅 Updated 事件让 MainWindow 重新解析已加载消息的 AppName(因为消息先到,缓存可能后到)

Color/Brush/MessageBox 在 WPF+WinForms 双开项目里撞名,ValueConverters 和 MainWindow.xaml.cs 都加 using alias 固定走 WPF。

实测:启动后 ApplicationCache refreshed count=5,WebSocket connected,历史拉了 50 条股票分析报告,通知正常弹。
2026-09-06 20:47:47 +08:00
KennethCheng e53c0585f7 fix: 修复 SDK 2.4.0 + WPF/WinForms 双开下的编译错误
dotnet 8.0.424 SDK + Microsoft.WindowsAppSDK 2.4.0 实测 build 通过(0 警告 0 错误)。

修复内容:

- csproj:TargetFramework 从 net8.0-windows 改为 net8.0-windows10.0.19041.0,新增 SupportedOSPlatformVersion=10.0.17763.0(WindowsAppSDK 2.4.0 强制要求 TFM 带 Windows 版本号)

- 4 个 Services 文件加 using System.IO(Logger/ConfigService/StateService/GotifyWebSocketService)。WPF+WinForms 双开项目模板不自动 import System.IO

- GotifyApiService 加 using System.Net.Http(同上,HttpClient 不在 implicit usings)

- App.xaml.cs:using Application = System.Windows.Application(WPF/WinForms Application 撞名)

- MainWindow.xaml.cs:using MessageBox = System.Windows.MessageBox(同上,MessageBox 撞名)

- App.xaml.cs:SendTestNotificationAsync 里 Date 从 .UtcNow.ToUnixTimeMilliseconds() 改回 .UtcNow(原 plan 写法有 bug,long 塞不进 DateTimeOffset)

- App.xaml.cs:OnNotificationInvoked 重写:WindowsAppSDK 2.4.0 把 args.Arguments 从 string 改成 IDictionary<string,string>,改用 TryGetValue("url", out var url) 取参数

- NotificationService:AddArgument 从单参 "url=escaped" 改成两参 ("url", url)。WindowsAppSDK 2.4.0 不再支持单参 "key=value" 写法

- GotifyWebSocketService:删除 _forceReconnect 字段(写了不读,改成 RequestReconnect 只调 socket.Abort 让外层 RunLoopAsync 自然重连)
2026-09-06 20:22:54 +08:00
KennethCheng c2f44767a6 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
2026-09-06 18:44:55 +08:00