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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
29 changes: 29 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 3 additions & 0 deletions src/Snipdeck.App/Bootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ public static IServiceProvider Build()
.AddSingleton<IHotkeyService, WindowsHotkeyService>()
.AddSingleton<ITrayService, HNotifyIconTrayService>()
.AddSingleton<IShellInteractions, WindowsShellInteractions>()
.AddSingleton<IThemeApplier, WindowsThemeApplier>()
.AddSingleton<IUpdateService, WindowsUpdateService>()
.AddSingleton<ISettingsStore>(settingsStore)
.AddSingleton<ISnipStore>(snipStore)
.AddSingleton<IBackupService>(backupService)
.AddSingleton<IIconAssetStorage>(iconStorage)
.AddSingleton(config)
.AddTransient<SettingsViewModel>()
.AddSingleton<ShellViewModel>()
.AddSingleton<ShellPage>()
.AddSingleton<MainWindow>();
Expand Down
33 changes: 33 additions & 0 deletions src/Snipdeck.App/Services/WindowsThemeApplier.cs
Original file line number Diff line number Diff line change
@@ -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,
};
}
}
}
}
68 changes: 68 additions & 0 deletions src/Snipdeck.App/Services/WindowsUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Snipdeck.Core.Abstractions;

using Velopack;
using Velopack.Sources;

namespace Snipdeck.App.Services
{
/// <summary>
/// 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.
/// </summary>
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<UpdateCheckResult> 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<bool> 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;
}
}
}
}
56 changes: 41 additions & 15 deletions src/Snipdeck.App/Views/ShellPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,25 +144,51 @@
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,0,0,8" />

<tk:SettingsCard Header="Storage location"
Description="Where snipdeck keeps your snips. Configurable in Phase 6."
IsEnabled="False" />

<tk:SettingsCard Header="Backups"
Description="Backup directory and retention. Configurable in Phase 6."
IsEnabled="False" />

<tk:SettingsCard Header="Theme"
Description="Light, Dark, or System. Live switching in Phase 6."
IsEnabled="False" />
Description="Applies immediately. System follows your Windows preference.">
<ComboBox SelectedIndex="{x:Bind ThemeIndex, Mode=TwoWay}" MinWidth="160">
<x:String>System</x:String>
<x:String>Light</x:String>
<x:String>Dark</x:String>
</ComboBox>
</tk:SettingsCard>

<tk:SettingsCard Header="Close behaviour"
Description="Hide-to-tray keeps the hotkey live. Exit closes the process.">
<ComboBox SelectedIndex="{x:Bind CloseBehaviourIndex, Mode=TwoWay}" MinWidth="200">
<x:String>Hide to tray</x:String>
<x:String>Exit on close</x:String>
</ComboBox>
</tk:SettingsCard>

<tk:SettingsCard Header="Global hotkey"
Description="Default Ctrl+Alt+S. Rebinding in Phase 6."
IsEnabled="False" />
Description="Rebinding lands in a future phase.">
<TextBlock Text="{x:Bind HotkeyDisplay}"
Style="{ThemeResource BodyStrongTextBlockStyle}" />
</tk:SettingsCard>

<tk:SettingsCard Header="Close behaviour"
Description="Hide-to-tray vs exit. Configurable in Phase 6."
IsEnabled="False" />
<tk:SettingsCard Header="Storage location"
Description="Where snips and backups live. Move support lands in a future phase.">
<TextBlock Text="{x:Bind StorageDirectory}"
Style="{ThemeResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis"
MaxWidth="320" />
</tk:SettingsCard>

<tk:SettingsCard Header="Updates"
Description="Check the GitHub releases feed for a newer build.">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="{x:Bind UpdateStatusMessage, Mode=OneWay}"
VerticalAlignment="Center"
Style="{ThemeResource CaptionTextBlockStyle}" />
<Button Content="Check for updates"
Command="{x:Bind CheckForUpdatesCommand}" />
<Button Content="Apply &amp; restart"
Command="{x:Bind ApplyUpdateCommand}"
Style="{ThemeResource AccentButtonStyle}"
Visibility="{x:Bind UpdateAvailable, Mode=OneWay, Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
</tk:SettingsCard>

<tk:SettingsExpander Header="About">
<tk:SettingsExpander.Items>
Expand Down
4 changes: 3 additions & 1 deletion src/Snipdeck.App/Views/ShellPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;

Expand All @@ -24,7 +25,8 @@ private async void OnLoaded(object sender, RoutedEventArgs e)

private void OnSettingsClicked(object sender, RoutedEventArgs e)
{
ViewModel.OpenSettings();
var settings = App.Services.GetRequiredService<SettingsViewModel>();
ViewModel.OpenSettings(settings);
}

private void OnNavigationSelectionChanged(
Expand Down
13 changes: 13 additions & 0 deletions src/Snipdeck.Core/Abstractions/IThemeApplier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Snipdeck.Core.Models;

namespace Snipdeck.Core.Abstractions
{
/// <summary>
/// Applies a <see cref="ThemePreference"/> to the live UI. Implemented in
/// the App project so view models in Core stay WinUI-free.
/// </summary>
public interface IThemeApplier
{
void Apply(ThemePreference theme);
}
}
15 changes: 15 additions & 0 deletions src/Snipdeck.Core/Abstractions/IUpdateService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Snipdeck.Core.Abstractions
{
public sealed record UpdateCheckResult(bool UpdateAvailable, string? AvailableVersion);

/// <summary>
/// Thin wrapper around Velopack so the UI can request an update check
/// without referencing the SDK directly.
/// </summary>
public interface IUpdateService
{
Task<UpdateCheckResult> CheckForUpdatesAsync(CancellationToken cancellationToken = default);

Task<bool> ApplyUpdateAndRestartAsync(CancellationToken cancellationToken = default);
}
}
Loading
Loading