From ce8d0dfe8d9b32eaefb8b49f8ec4034e15281588 Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Fri, 29 May 2026 17:57:04 +0000 Subject: [PATCH 1/3] Phase 6: editable settings + Velopack update check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings page becomes interactive: - Theme combo (System/Light/Dark) applies live via IThemeApplier -> MainWindow.Content.RequestedTheme. - Close behaviour combo (Hide-to-tray vs Exit) persists immediately. - Hotkey + storage location shown read-only with a note about future rebind / move support. About expander reads real assembly metadata (InformationalVersion, copyright) from Directory.Build.props. WindowsUpdateService wraps Velopack's UpdateManager pointed at the StuartMeeks/Snipdeck GitHub releases. Check button surfaces the available version; Apply button (visible only when an update is available) downloads and restarts. Dev builds gracefully no-op via UpdateManager.IsInstalled. ShellViewModel.OpenSettings now takes a SettingsViewModel parameter — ShellPage code-behind resolves it from DI on each click so the page reads fresh config state. New abstractions: IThemeApplier, IUpdateService. New App services: WindowsThemeApplier, WindowsUpdateService. This PR is the fourth and last in the stack. Base: feat/phase-5-platform. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 15 ++ src/Snipdeck.App/Bootstrap.cs | 3 + .../Services/WindowsThemeApplier.cs | 33 ++++ .../Services/WindowsUpdateService.cs | 71 +++++++ src/Snipdeck.App/Views/ShellPage.xaml | 56 ++++-- src/Snipdeck.App/Views/ShellPage.xaml.cs | 4 +- .../Abstractions/IThemeApplier.cs | 13 ++ .../Abstractions/IUpdateService.cs | 15 ++ .../ViewModels/SettingsViewModel.cs | 175 +++++++++++++++++- .../ViewModels/ShellViewModel.cs | 5 +- .../Support/FakePathProvider.cs | 15 ++ .../Support/FakeSettingsStore.cs | 24 +++ .../Support/FakeThemeApplier.cs | 15 ++ .../Support/FakeUpdateService.cs | 27 +++ .../ViewModels/ShellViewModelTests.cs | 14 +- 15 files changed, 457 insertions(+), 28 deletions(-) create mode 100644 src/Snipdeck.App/Services/WindowsThemeApplier.cs create mode 100644 src/Snipdeck.App/Services/WindowsUpdateService.cs create mode 100644 src/Snipdeck.Core/Abstractions/IThemeApplier.cs create mode 100644 src/Snipdeck.Core/Abstractions/IUpdateService.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakePathProvider.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakeSettingsStore.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakeThemeApplier.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakeUpdateService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e468ab..dad9efa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added — Phase 6: Settings page + Velopack updater +- **Settings page becomes editable.** Theme switches live (System/Light/Dark + apply immediately via `IThemeApplier` → `MainWindow`'s content tree). Close + behaviour (Hide-to-tray vs Exit) persists to `AppConfig` as you change it. + Global hotkey is shown read-only for now — rebinding lands later. +- **About expander** is populated from real assembly metadata: name, + `InformationalVersion`, and copyright (from `Directory.Build.props`). +- **Manual update check** via `IUpdateService` (Velopack-backed, + `WindowsUpdateService`). Points at the GitHub releases for this repo, + catches dev-build / network-failure cases, and exposes a Check / Apply + pair on the Settings page. +- `SettingsViewModel` now lives in the DI container as transient — a fresh + instance is resolved each time the Settings entry is clicked so reading + config state is always current. + ### Added — Phase 5: Platform services - **Global hotkey** via Win32 `RegisterHotKey`. Default Ctrl+Alt+S; pressed anywhere brings the existing Snipdeck window to the foreground. diff --git a/src/Snipdeck.App/Bootstrap.cs b/src/Snipdeck.App/Bootstrap.cs index c454872..9059fdb 100644 --- a/src/Snipdeck.App/Bootstrap.cs +++ b/src/Snipdeck.App/Bootstrap.cs @@ -45,11 +45,14 @@ public static IServiceProvider Build() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() .AddSingleton(settingsStore) .AddSingleton(snipStore) .AddSingleton(backupService) .AddSingleton(iconStorage) .AddSingleton(config) + .AddTransient() .AddSingleton() .AddSingleton() .AddSingleton(); diff --git a/src/Snipdeck.App/Services/WindowsThemeApplier.cs b/src/Snipdeck.App/Services/WindowsThemeApplier.cs new file mode 100644 index 0000000..f91346f --- /dev/null +++ b/src/Snipdeck.App/Services/WindowsThemeApplier.cs @@ -0,0 +1,33 @@ +using Microsoft.UI.Xaml; + +using Snipdeck.Core.Abstractions; +using Snipdeck.Core.Models; + +namespace Snipdeck.App.Services +{ + internal sealed class WindowsThemeApplier : IThemeApplier + { + private readonly IServiceProvider _services; + + public WindowsThemeApplier(IServiceProvider services) + { + ArgumentNullException.ThrowIfNull(services); + _services = services; + } + + public void Apply(ThemePreference theme) + { + var mainWindow = (MainWindow?)_services.GetService(typeof(MainWindow)); + if (mainWindow?.Content is FrameworkElement root) + { + root.RequestedTheme = theme switch + { + ThemePreference.Light => ElementTheme.Light, + ThemePreference.Dark => ElementTheme.Dark, + ThemePreference.System => ElementTheme.Default, + _ => ElementTheme.Default, + }; + } + } + } +} diff --git a/src/Snipdeck.App/Services/WindowsUpdateService.cs b/src/Snipdeck.App/Services/WindowsUpdateService.cs new file mode 100644 index 0000000..1c2bd91 --- /dev/null +++ b/src/Snipdeck.App/Services/WindowsUpdateService.cs @@ -0,0 +1,71 @@ +using Snipdeck.Core.Abstractions; + +using Velopack; +using Velopack.Sources; + +namespace Snipdeck.App.Services +{ + /// + /// Velopack-backed updater. Points at the GitHub releases for Snipdeck; + /// silently no-ops when running from a dev tree (not installed) so the + /// dev experience matches release-build expectations. + /// + internal sealed class WindowsUpdateService : IUpdateService + { + private const string _githubRepoUrl = "https://github.com/StuartMeeks/Snipdeck"; + + private readonly UpdateManager _manager; + private UpdateInfo? _pendingUpdate; + + public WindowsUpdateService() + { + _manager = new UpdateManager(new GithubSource(_githubRepoUrl, accessToken: null, prerelease: false)); + } + + public async Task CheckForUpdatesAsync(CancellationToken cancellationToken = default) + { + if (!_manager.IsInstalled) + { + return new UpdateCheckResult(UpdateAvailable: false, AvailableVersion: null); + } + + try + { + _pendingUpdate = await _manager.CheckForUpdatesAsync().ConfigureAwait(false); + } + catch (Exception) + { + // Network errors / missing release feed shouldn't crash the app — surface as "no update". + return new UpdateCheckResult(UpdateAvailable: false, AvailableVersion: null); + } + + if (_pendingUpdate is null) + { + return new UpdateCheckResult(UpdateAvailable: false, AvailableVersion: null); + } + + return new UpdateCheckResult( + UpdateAvailable: true, + AvailableVersion: _pendingUpdate.TargetFullRelease.Version.ToString()); + } + + public async Task ApplyUpdateAndRestartAsync(CancellationToken cancellationToken = default) + { + if (_pendingUpdate is null || !_manager.IsInstalled) + { + return false; + } + + try + { + await _manager.DownloadUpdatesAsync(_pendingUpdate).ConfigureAwait(false); + _manager.ApplyUpdatesAndRestart(_pendingUpdate); + return true; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/src/Snipdeck.App/Views/ShellPage.xaml b/src/Snipdeck.App/Views/ShellPage.xaml index cf87796..ba97529 100644 --- a/src/Snipdeck.App/Views/ShellPage.xaml +++ b/src/Snipdeck.App/Views/ShellPage.xaml @@ -144,25 +144,51 @@ Style="{ThemeResource TitleTextBlockStyle}" Margin="0,0,0,8" /> - - - - + Description="Applies immediately. System follows your Windows preference."> + + System + Light + Dark + + + + + + Hide to tray + Exit on close + + + Description="Rebinding lands in a future phase."> + + - + + + + + + + +