diff --git a/CHANGELOG.md b/CHANGELOG.md index bf1935e..c885b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `LogsDirectory`. The log rotates at 5 MB. ### Fixed +- Tray icon initialisation crash (`NullReferenceException` inside + `H.NotifyIcon.ImageExtensions.ToStream(Uri)`): H.NotifyIcon resolves + `TaskbarIcon.IconSource` by reading `BitmapImage.UriSource`, but the + tray service was loading the identicon via `BitmapImage.SetSourceAsync` + from an in-memory stream — pixels populated, URI null. The service now + persists the identicon to `%LOCALAPPDATA%\Snipdeck\tray-icon.png` and + loads the `BitmapImage` from that URI, which is the shape H.NotifyIcon + expects. - First-run crash on startup (`RPC_E_WRONG_THREAD` / `0x8001010E`): `ShellViewModel.LoadAsync` resumed on a thread-pool thread after loading the store and then mutated `CliChoices`, an `ObservableCollection` already diff --git a/src/Snipdeck.App/Services/HNotifyIconTrayService.cs b/src/Snipdeck.App/Services/HNotifyIconTrayService.cs index ebfd1f7..8465513 100644 --- a/src/Snipdeck.App/Services/HNotifyIconTrayService.cs +++ b/src/Snipdeck.App/Services/HNotifyIconTrayService.cs @@ -6,18 +6,24 @@ using Snipdeck.Core.Abstractions; using Snipdeck.Core.Services; -using Windows.Storage.Streams; - namespace Snipdeck.App.Services { internal sealed partial class HNotifyIconTrayService : ITrayService { // Stable seed so the tray icon never changes between runs of Snipdeck. private static readonly Guid _iconSeed = Guid.Parse("5147DEC0-0000-0000-0000-000000000001"); + private const string _trayIconFileName = "tray-icon.png"; + private readonly IPathProvider _paths; private TaskbarIcon? _icon; private bool _disposed; + public HNotifyIconTrayService(IPathProvider paths) + { + ArgumentNullException.ThrowIfNull(paths); + _paths = paths; + } + public event EventHandler? ShowRequested; public event EventHandler? ExitRequested; @@ -31,7 +37,12 @@ public async Task InitialiseAsync() return; } - var image = await BuildTrayBitmapAsync(); + // H.NotifyIcon resolves IconSource by reading BitmapImage.UriSource, + // not the pixel data — a stream-loaded BitmapImage NREs deep inside + // the library. Persist the identicon to a stable file path and load + // the BitmapImage from that URI instead. + var iconPath = await WriteTrayIconFileAsync(); + var image = new BitmapImage(new Uri(iconPath, UriKind.Absolute)); _icon = new TaskbarIcon { @@ -89,18 +100,13 @@ private void RaiseShowRequested() ShowRequested?.Invoke(this, EventArgs.Empty); } - private static async Task BuildTrayBitmapAsync() + private async Task WriteTrayIconFileAsync() { var bytes = IdenticonService.GeneratePng(_iconSeed, size: 32); - var image = new BitmapImage(); - using var stream = new InMemoryRandomAccessStream(); - var writer = new DataWriter(stream); - writer.WriteBytes(bytes); - _ = await writer.StoreAsync(); - _ = writer.DetachStream(); - stream.Seek(0); - await image.SetSourceAsync(stream); - return image; + _ = Directory.CreateDirectory(_paths.AppDataDirectory); + var path = Path.Combine(_paths.AppDataDirectory, _trayIconFileName); + await File.WriteAllBytesAsync(path, bytes); + return path; } private sealed partial class RelayCommand(Action execute) : System.Windows.Input.ICommand