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/TODO.md b/TODO.md index b4e3cf9..22a171c 100644 --- a/TODO.md +++ b/TODO.md @@ -50,3 +50,32 @@ locally on a Snip when something one-off is needed. from shared scope. This is the single biggest content-quality-of-life feature after v1 ships. + +--- + +## Carried over from the phase stack + +These were trimmed out of Phase 4–6 to keep the PRs reviewable. None are +load-bearing for the v1 demo, but they're the obvious next-pulls. + +- **Hotkey rebinding UI.** The setting is editable in `AppConfig` already; + what's missing is a key-capture control on the Settings page and the call + to `IHotkeyService.TryRegister` after the change. Tooling: a small custom + `Control` that listens for a single key chord then displays it formatted. +- **Storage path: move / adopt / warn-on-conflict.** Per `CLAUDE.md`, when + the user changes the storage path we need three flows: move the existing + store to the new path; adopt a store already at the new path; warn when + both exist. UI: a "Change…" button next to the read-only path display. +- **Backup retention configurable.** Plumbing: `BackupService` takes + retention at construction time today; either re-create it on the relevant + config change or have it read from `AppConfig` lazily. +- **CLI delete.** Settle cascade semantics — must-be-empty vs trash-all-child-snips — + before wiring the UI. +- **Nerdbank.GitVersioning.** Right now `InformationalVersion` falls back to + the assembly's compile-time version. NBGV would give us a real git-tag-derived + string at build time (`v1.2.3+gabcdef0`). +- **Markdown rendering for Snip descriptions.** Stored as plain text right + now; render via a markdown control on the parameter-fill / detail view. +- **Trash UI.** Soft-deleted Snips currently just vanish from the views. + Need a "Trash" entry in the pane footer that lists trashed Snips with a + restore action and a hard-delete option. 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..2c49936 --- /dev/null +++ b/src/Snipdeck.App/Services/WindowsUpdateService.cs @@ -0,0 +1,68 @@ +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); + } + + return _pendingUpdate is null + ? new UpdateCheckResult(UpdateAvailable: false, AvailableVersion: null) + : 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, cancelToken: cancellationToken).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."> + + - + + + + + + + +