Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 19 additions & 13 deletions src/Snipdeck.App/Services/HNotifyIconTrayService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -89,18 +100,13 @@ private void RaiseShowRequested()
ShowRequested?.Invoke(this, EventArgs.Empty);
}

private static async Task<BitmapImage> BuildTrayBitmapAsync()
private async Task<string> 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
Expand Down
Loading