From f52c04fa954213529f0e800969406baa82ab0b2a Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Sat, 30 May 2026 10:20:45 +0000 Subject: [PATCH 1/5] Add change-storage-location with move / adopt / restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Storage location gains a "Change…" button. Picking a folder classifies the change (Core StorageRelocationService): - target already has a store -> adopt it (current store left in place; this is the conflict/warn case, surfaced in the confirm copy) - target empty, current has a store -> move store + icons there - neither -> just adopt the empty target On confirm, the new path is persisted and the app restarts (IAppRestartService -> AppInstance.Restart). The storage path is only read at startup, so apply-and-restart keeps the immutable startup singletons consistent and avoids the running app writing snips to the old location after the switch (a live-repoint would need a store-indirection refactor + Windows runtime verification — deferred deliberately). New Core: IStorageRelocationService/StorageRelocationService, IFolderPickerService, IAppRestartService; SettingsViewModel gains ChangeStoragePathCommand and an observable StorageDirectory. App: WindowsFolderPickerService, WindowsAppRestartService, DI + XAML wiring. 20 new Core tests (relocation outcomes + move; VM move/adopt/cancel/picker-cancel). Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 7 + src/Snipdeck.App/Bootstrap.cs | 3 + .../Services/WindowsAppRestartService.cs | 16 ++ .../Services/WindowsFolderPickerService.cs | 33 +++++ src/Snipdeck.App/Views/ShellPage.xaml | 15 +- .../Abstractions/IAppRestartService.cs | 12 ++ .../Abstractions/IFolderPickerService.cs | 11 ++ .../Abstractions/IStorageRelocationService.cs | 41 ++++++ .../Services/StorageRelocationService.cs | 85 +++++++++++ .../ViewModels/SettingsViewModel.cs | 85 ++++++++++- .../Services/StorageRelocationServiceTests.cs | 100 +++++++++++++ .../Support/FakeAppRestartService.cs | 11 ++ .../Support/FakeFolderPickerService.cs | 11 ++ .../ViewModels/SettingsViewModelTests.cs | 139 +++++++++++++++++- .../ViewModels/ShellViewModelTests.cs | 5 + 15 files changed, 567 insertions(+), 7 deletions(-) create mode 100644 src/Snipdeck.App/Services/WindowsAppRestartService.cs create mode 100644 src/Snipdeck.App/Services/WindowsFolderPickerService.cs create mode 100644 src/Snipdeck.Core/Abstractions/IAppRestartService.cs create mode 100644 src/Snipdeck.Core/Abstractions/IFolderPickerService.cs create mode 100644 src/Snipdeck.Core/Abstractions/IStorageRelocationService.cs create mode 100644 src/Snipdeck.Core/Services/StorageRelocationService.cs create mode 100644 tests/Snipdeck.Core.Tests/Services/StorageRelocationServiceTests.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakeAppRestartService.cs create mode 100644 tests/Snipdeck.Core.Tests/Support/FakeFolderPickerService.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 6be3a53..e8bb1af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 IL2104 on the WinAppSDK/WinRT/Jdenticon assemblies, which aren't trim-safe. ### Added +- **Change the storage location.** A "Change…" button on Settings → Storage + location lets you pick a new folder for your snips. If the folder already + contains a Snipdeck store it's adopted (your current snips are left where + they are); otherwise your store and icons are moved there. The choice is + confirmed first, and Snipdeck restarts to apply it — the storage path is + read at startup, so restarting keeps everything consistent and avoids + writing to the old location after the switch. - **Rebindable global hotkey.** The global hotkey is now editable from Settings: click the capture box and press a shortcut (at least one of Ctrl/Alt/Shift plus a key). The new binding registers and persists diff --git a/src/Snipdeck.App/Bootstrap.cs b/src/Snipdeck.App/Bootstrap.cs index 52bc1f4..d2e976b 100644 --- a/src/Snipdeck.App/Bootstrap.cs +++ b/src/Snipdeck.App/Bootstrap.cs @@ -42,6 +42,9 @@ public static IServiceProvider Build() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(new StorageRelocationService(_snipStoreFileName)) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/Snipdeck.App/Services/WindowsAppRestartService.cs b/src/Snipdeck.App/Services/WindowsAppRestartService.cs new file mode 100644 index 0000000..a62978b --- /dev/null +++ b/src/Snipdeck.App/Services/WindowsAppRestartService.cs @@ -0,0 +1,16 @@ +using Snipdeck.Core.Abstractions; + +using Microsoft.Windows.AppLifecycle; + +namespace Snipdeck.App.Services +{ + internal sealed class WindowsAppRestartService : IAppRestartService + { + public void Restart() + { + // Windows App SDK gracefully terminates and relaunches the current + // instance. Used to apply a storage-location change cleanly. + _ = AppInstance.Restart(string.Empty); + } + } +} diff --git a/src/Snipdeck.App/Services/WindowsFolderPickerService.cs b/src/Snipdeck.App/Services/WindowsFolderPickerService.cs new file mode 100644 index 0000000..3a7116f --- /dev/null +++ b/src/Snipdeck.App/Services/WindowsFolderPickerService.cs @@ -0,0 +1,33 @@ +using Snipdeck.Core.Abstractions; + +using Windows.Storage.Pickers; + +namespace Snipdeck.App.Services +{ + internal sealed class WindowsFolderPickerService : IFolderPickerService + { + private readonly IServiceProvider _services; + + public WindowsFolderPickerService(IServiceProvider services) + { + ArgumentNullException.ThrowIfNull(services); + _services = services; + } + + public async Task PickFolderAsync() + { + var picker = new FolderPicker + { + SuggestedStartLocation = PickerLocationId.DocumentsLibrary, + }; + picker.FileTypeFilter.Add("*"); + + var mainWindow = (MainWindow)_services.GetService(typeof(MainWindow))!; + var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(mainWindow); + WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd); + + var folder = await picker.PickSingleFolderAsync(); + return folder?.Path; + } + } +} diff --git a/src/Snipdeck.App/Views/ShellPage.xaml b/src/Snipdeck.App/Views/ShellPage.xaml index 55bd998..e4f81df 100644 --- a/src/Snipdeck.App/Views/ShellPage.xaml +++ b/src/Snipdeck.App/Views/ShellPage.xaml @@ -187,11 +187,16 @@ - + Description="Where your snips live. Changing it restarts Snipdeck to load the new location."> + + +