From 1ea8e4a05f57c5280fee90cdc2437c574ae0e11c Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Mon, 1 Jun 2026 08:00:00 +0000 Subject: [PATCH] Icon picker: move catalogue to LocalAppData, grow default to 100 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The catalogue now lives at %LOCALAPPDATA%\Snipdeck\icon-catalogue.json (renamed from appsettings.json) so user edits survive Velopack updates. It's seeded on first run from a default embedded in the app — delete the file to restore it — and the embedded default also serves as the fallback when the user copy is missing or malformed. Read fresh on each open. The default set grows from 50 to 100 glyphs, all code points grounded against the official Segoe Fluent Icons list, and now covers billing, customer, contract, invoice, person, marketplace, orders, preview, price list, renewal, report and transfer. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 6 +- src/Snipdeck.App/Assets/icon-catalogue.json | 105 ++++++++++++++++++ src/Snipdeck.App/Bootstrap.cs | 2 +- .../Services/GlyphCatalogueProvider.cs | 93 ++++++++++++++-- .../Services/WindowsPathProvider.cs | 2 + .../Services/WindowsShellInteractions.cs | 2 +- src/Snipdeck.App/Snipdeck.App.csproj | 11 +- src/Snipdeck.App/Views/GlyphPickerDialog.xaml | 2 +- src/Snipdeck.App/appsettings.json | 55 --------- .../Abstractions/IGlyphCatalogueProvider.cs | 6 +- .../Abstractions/IPathProvider.cs | 2 + .../Models/GlyphCatalogueEntry.cs | 2 +- src/Snipdeck.Core/Services/DefaultPaths.cs | 8 ++ .../Support/FakePathProvider.cs | 2 + 14 files changed, 222 insertions(+), 76 deletions(-) create mode 100644 src/Snipdeck.App/Assets/icon-catalogue.json delete mode 100644 src/Snipdeck.App/appsettings.json diff --git a/CHANGELOG.md b/CHANGELOG.md index c1642f5..d1ed379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 each tag that opens a searchable grid of icons to pick from, so you no longer have to know a Segoe Fluent Icons code point. Search filters by name, keyword or code point, and the free-text field remains for pasting a glyph directly. The - catalogue is a curated set in `appsettings.json` beside the app — edit that file - to add or remove icons, and the picker reflects the change the next time it opens. + catalogue ships with 100 curated icons and is a plain JSON file + (`icon-catalogue.json`) in the Snipdeck app-data folder — edit it to add or + remove icons (the picker reflects the change the next time it opens), and your + edits survive app updates. Delete it to restore the default set. ### Changed - **Home, navigation and shared-parameters polish.** The Home page leads with a diff --git a/src/Snipdeck.App/Assets/icon-catalogue.json b/src/Snipdeck.App/Assets/icon-catalogue.json new file mode 100644 index 0000000..a4f3a57 --- /dev/null +++ b/src/Snipdeck.App/Assets/icon-catalogue.json @@ -0,0 +1,105 @@ +{ + "$comment": "Default icon catalogue for the icon picker. Each entry is a Segoe Fluent Icons glyph: 'code' is the hex code point, 'name' is shown and searched, 'keywords' add extra search terms. This file is the bundled default; on first run it is copied to %LOCALAPPDATA%\\Snipdeck\\icon-catalogue.json, which is what the app actually reads. Edit that copy to add or remove glyphs — the picker re-reads it each time it opens, and your edits survive app updates. Delete the copy to restore this default. The free-text icon field remains available for anything not listed here.", + "glyphCatalogue": [ + { "code": "E80F", "name": "Home", "keywords": [ "house", "dashboard", "start" ] }, + { "code": "E713", "name": "Settings", "keywords": [ "gear", "cog", "config", "preferences", "options" ] }, + { "code": "E74E", "name": "Save", "keywords": [ "disk", "store", "floppy" ] }, + { "code": "E74D", "name": "Delete", "keywords": [ "trash", "bin", "remove", "discard" ] }, + { "code": "E70F", "name": "Edit", "keywords": [ "pencil", "modify", "change", "write" ] }, + { "code": "E710", "name": "Add", "keywords": [ "plus", "new", "create" ] }, + { "code": "E711", "name": "Cancel", "keywords": [ "close", "cross", "dismiss", "remove" ] }, + { "code": "E8FB", "name": "Accept", "keywords": [ "check", "tick", "done", "confirm", "ok" ] }, + { "code": "E930", "name": "Completed", "keywords": [ "check circle", "success", "passed", "approved" ] }, + { "code": "E721", "name": "Search", "keywords": [ "find", "magnify", "lookup", "query" ] }, + { "code": "E72C", "name": "Refresh", "keywords": [ "reload", "sync", "update", "retry" ] }, + { "code": "E895", "name": "Sync", "keywords": [ "synchronise", "refresh", "two way", "update" ] }, + { "code": "E8EE", "name": "Renewal", "keywords": [ "renew", "recurring", "subscription", "repeat", "auto renew" ] }, + { "code": "E81C", "name": "History", "keywords": [ "recent", "past", "log", "timeline", "audit" ] }, + { "code": "E8B7", "name": "Folder", "keywords": [ "directory", "files", "path" ] }, + { "code": "E8E5", "name": "Open file", "keywords": [ "open", "document", "load" ] }, + { "code": "E8DA", "name": "Open folder", "keywords": [ "browse", "local", "directory" ] }, + { "code": "E8A5", "name": "Document", "keywords": [ "file", "page", "text", "doc" ] }, + { "code": "E8C8", "name": "Copy", "keywords": [ "duplicate", "clipboard", "clone" ] }, + { "code": "E8C6", "name": "Cut", "keywords": [ "scissors", "clipboard", "move" ] }, + { "code": "E77F", "name": "Paste", "keywords": [ "clipboard", "insert" ] }, + { "code": "E715", "name": "Mail", "keywords": [ "email", "envelope", "message", "inbox" ] }, + { "code": "E724", "name": "Send", "keywords": [ "submit", "forward", "deliver" ] }, + { "code": "E8BD", "name": "Message", "keywords": [ "chat", "comment", "speech", "reply" ] }, + { "code": "E72D", "name": "Share", "keywords": [ "distribute", "export", "send" ] }, + { "code": "E72A", "name": "Forward", "keywords": [ "next", "right", "advance" ] }, + { "code": "E72B", "name": "Back", "keywords": [ "previous", "left", "return" ] }, + { "code": "E8B5", "name": "Import", "keywords": [ "ingest", "load", "bring in" ] }, + { "code": "E8AB", "name": "Transfer", "keywords": [ "move", "swap", "migrate", "switch", "exchange" ] }, + { "code": "E774", "name": "Globe", "keywords": [ "world", "web", "internet", "network", "online" ] }, + { "code": "E753", "name": "Cloud", "keywords": [ "remote", "storage", "sky", "hosted" ] }, + { "code": "E896", "name": "Download", "keywords": [ "pull", "save", "fetch", "arrow down" ] }, + { "code": "E898", "name": "Upload", "keywords": [ "push", "publish", "arrow up" ] }, + { "code": "E74A", "name": "Move up", "keywords": [ "promote", "top", "arrow up", "raise" ] }, + { "code": "E7B8", "name": "Package", "keywords": [ "box", "parcel", "shipment", "bundle", "release" ] }, + { "code": "E768", "name": "Play", "keywords": [ "run", "start", "execute", "go" ] }, + { "code": "E769", "name": "Pause", "keywords": [ "hold", "suspend", "wait" ] }, + { "code": "E71A", "name": "Stop", "keywords": [ "halt", "end", "kill", "cancel" ] }, + { "code": "E787", "name": "Calendar", "keywords": [ "date", "schedule", "event", "day" ] }, + { "code": "EC92", "name": "Date and time", "keywords": [ "datetime", "schedule", "timestamp" ] }, + { "code": "E917", "name": "Clock", "keywords": [ "time", "recent", "timer", "duration" ] }, + { "code": "E72E", "name": "Lock", "keywords": [ "secure", "private", "locked", "protected" ] }, + { "code": "E785", "name": "Unlock", "keywords": [ "open", "unsecure", "unlocked" ] }, + { "code": "EA18", "name": "Shield", "keywords": [ "security", "protect", "safe", "defend", "guard" ] }, + { "code": "E77B", "name": "Person", "keywords": [ "contact", "user", "individual", "account", "profile" ] }, + { "code": "E779", "name": "Customer", "keywords": [ "client", "contact", "account", "buyer", "tenant" ] }, + { "code": "E716", "name": "People", "keywords": [ "users", "team", "members", "audience" ] }, + { "code": "E902", "name": "Group", "keywords": [ "team", "people", "members", "organisation unit" ] }, + { "code": "E748", "name": "Switch account", "keywords": [ "switch user", "change account", "impersonate", "tenant" ] }, + { "code": "EC06", "name": "Organisation", "keywords": [ "company", "building", "city", "tenant", "enterprise" ] }, + { "code": "E821", "name": "Work", "keywords": [ "briefcase", "job", "business", "portfolio" ] }, + { "code": "E8C7", "name": "Billing", "keywords": [ "payment", "credit card", "charge", "pay", "card" ] }, + { "code": "E9F9", "name": "Invoice", "keywords": [ "bill", "statement", "receipt", "charges", "document" ] }, + { "code": "E8EF", "name": "Calculator", "keywords": [ "calculate", "maths", "sum", "total" ] }, + { "code": "E94C", "name": "Discount", "keywords": [ "percentage", "percent", "rate", "tax", "margin" ] }, + { "code": "EB95", "name": "Contract", "keywords": [ "agreement", "certificate", "terms", "signed", "licence" ] }, + { "code": "E719", "name": "Marketplace", "keywords": [ "shop", "store", "catalogue", "storefront", "buy" ] }, + { "code": "E7BF", "name": "Orders", "keywords": [ "cart", "shopping", "purchase", "basket", "checkout" ] }, + { "code": "E8FD", "name": "Price list", "keywords": [ "rate card", "pricing", "list", "catalogue", "tariff" ] }, + { "code": "E8EC", "name": "Tag", "keywords": [ "label", "category", "default" ] }, + { "code": "E718", "name": "Pin", "keywords": [ "pushpin", "attach", "stick" ] }, + { "code": "E723", "name": "Attach", "keywords": [ "paperclip", "attachment", "file" ] }, + { "code": "E71B", "name": "Link", "keywords": [ "url", "chain", "hyperlink", "connect" ] }, + { "code": "E734", "name": "Favourite", "keywords": [ "star", "favorite", "bookmark", "important" ] }, + { "code": "E8A4", "name": "Bookmark", "keywords": [ "save", "ribbon", "mark", "saved" ] }, + { "code": "EB51", "name": "Heart", "keywords": [ "like", "love", "wishlist" ] }, + { "code": "E7C1", "name": "Flag", "keywords": [ "mark", "report", "milestone" ] }, + { "code": "E71C", "name": "Filter", "keywords": [ "funnel", "refine", "narrow" ] }, + { "code": "E8CB", "name": "Sort", "keywords": [ "order", "arrange", "rank" ] }, + { "code": "E8D7", "name": "Permissions", "keywords": [ "access", "roles", "rights", "key", "security", "scope" ] }, + { "code": "E9D5", "name": "Checklist", "keywords": [ "tasks", "todo", "checkboxes", "steps" ] }, + { "code": "E71D", "name": "Apps", "keywords": [ "all apps", "grid", "applications", "tiles" ] }, + { "code": "E890", "name": "View", "keywords": [ "show", "see", "display", "look" ] }, + { "code": "E8FF", "name": "Preview", "keywords": [ "view", "look", "inspect", "peek" ] }, + { "code": "E8A9", "name": "View all", "keywords": [ "list view", "show all", "details" ] }, + { "code": "ED1A", "name": "Hide", "keywords": [ "hidden", "invisible", "conceal", "off" ] }, + { "code": "E9D2", "name": "Report", "keywords": [ "chart", "analytics", "graph", "metrics", "area chart", "dashboard" ] }, + { "code": "EB05", "name": "Pie chart", "keywords": [ "chart", "analytics", "breakdown", "share", "distribution" ] }, + { "code": "E749", "name": "Print", "keywords": [ "printer", "paper" ] }, + { "code": "E722", "name": "Camera", "keywords": [ "photo", "snapshot", "capture" ] }, + { "code": "E8B9", "name": "Picture", "keywords": [ "image", "photo", "gallery", "media" ] }, + { "code": "E714", "name": "Video", "keywords": [ "film", "movie", "record", "media" ] }, + { "code": "E720", "name": "Microphone", "keywords": [ "mic", "record", "audio", "voice" ] }, + { "code": "E707", "name": "Location", "keywords": [ "map pin", "place", "geo", "region", "address" ] }, + { "code": "E717", "name": "Phone", "keywords": [ "call", "telephone", "mobile", "contact" ] }, + { "code": "E8CC", "name": "Mobile", "keywords": [ "tablet", "device", "phone", "handset" ] }, + { "code": "E772", "name": "Devices", "keywords": [ "pc", "computer", "hardware", "machines" ] }, + { "code": "E968", "name": "Server", "keywords": [ "host", "backend", "rack", "infrastructure" ] }, + { "code": "EBE8", "name": "Bug", "keywords": [ "issue", "defect", "error", "debug" ] }, + { "code": "E7BA", "name": "Warning", "keywords": [ "alert", "caution", "attention", "exclamation" ] }, + { "code": "E783", "name": "Error", "keywords": [ "important", "alert", "danger", "critical" ] }, + { "code": "E946", "name": "Info", "keywords": [ "information", "about", "details", "note" ] }, + { "code": "E897", "name": "Help", "keywords": [ "question", "support", "faq", "assist" ] }, + { "code": "EA80", "name": "Lightbulb", "keywords": [ "idea", "tip", "hint", "suggestion" ] }, + { "code": "E82D", "name": "Dictionary", "keywords": [ "book", "glossary", "reference", "definitions" ] }, + { "code": "E8F1", "name": "Library", "keywords": [ "books", "collection", "catalogue", "docs" ] }, + { "code": "E929", "name": "Notes", "keywords": [ "handwriting", "signature", "annotation", "scribble" ] }, + { "code": "E756", "name": "Terminal", "keywords": [ "command", "prompt", "shell", "console", "cli" ] }, + { "code": "E943", "name": "Code", "keywords": [ "script", "source", "developer", "programming" ] }, + { "code": "E90F", "name": "Repair", "keywords": [ "wrench", "tools", "fix", "maintenance", "spanner" ] } + ] +} diff --git a/src/Snipdeck.App/Bootstrap.cs b/src/Snipdeck.App/Bootstrap.cs index d2a2a1d..32ebf9d 100644 --- a/src/Snipdeck.App/Bootstrap.cs +++ b/src/Snipdeck.App/Bootstrap.cs @@ -49,7 +49,7 @@ public static IServiceProvider Build() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() + .AddSingleton(new GlyphCatalogueProvider(pathProvider.IconCatalogueFilePath)) .AddSingleton() .AddSingleton() .AddSingleton(settingsStore) diff --git a/src/Snipdeck.App/Services/GlyphCatalogueProvider.cs b/src/Snipdeck.App/Services/GlyphCatalogueProvider.cs index 0f8952f..ffffbde 100644 --- a/src/Snipdeck.App/Services/GlyphCatalogueProvider.cs +++ b/src/Snipdeck.App/Services/GlyphCatalogueProvider.cs @@ -8,23 +8,78 @@ namespace Snipdeck.App.Services { /// - /// Reads the curated glyph catalogue from appsettings.json (next to the - /// executable). Re-reads on every call so editing the file is reflected the - /// next time the picker opens. Degrades to an empty catalogue — never throws — - /// when the file is missing or malformed, so a bad edit can't break the picker. + /// Supplies the icon picker's catalogue from a user-editable JSON file in + /// LocalAppData (so edits survive Velopack updates). On first run the file is + /// seeded from a default bundled into the app; thereafter the user's copy is + /// read, re-read on every call so edits take effect without a restart. + /// Degrades to the bundled default — never throws — when the user file is + /// missing or malformed, so a bad edit can't break the picker. /// internal sealed class GlyphCatalogueProvider : IGlyphCatalogueProvider { + // The bundled default, embedded so it can't be deleted and ships fresh + // with each release. Its logical name is pinned in the csproj. + private const string _defaultResourceName = "Snipdeck.App.icon-catalogue.json"; + private readonly string _path; - public GlyphCatalogueProvider() + public GlyphCatalogueProvider(string catalogueFilePath) { - _path = Path.Combine(AppContext.BaseDirectory, "appsettings.json"); + ArgumentException.ThrowIfNullOrWhiteSpace(catalogueFilePath); + _path = catalogueFilePath; } public IReadOnlyList GetEntries() { - GlyphCatalogueFile? file; + SeedIfMissing(); + + // The user's copy wins; fall back to the bundled default if it's + // absent or unreadable (e.g. mid-edit or malformed). + var fromFile = ReadFile(); + return fromFile.Count > 0 ? fromFile : ReadBundledDefault(); + } + + private void SeedIfMissing() + { + // Only seed when truly absent — never overwrite a user's file, even a + // broken one (they can fix it, or delete it to restore the default). + try + { + if (File.Exists(_path)) + { + return; + } + + var directory = Path.GetDirectoryName(_path); + if (!string.IsNullOrEmpty(directory)) + { + _ = Directory.CreateDirectory(directory); + } + + using var resource = OpenBundledDefault(); + if (resource is null) + { + return; + } + + // Write-then-rename so an interrupted seed can't leave a partial file. + var tempPath = _path + ".tmp"; + using (var file = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + resource.CopyTo(file); + file.Flush(); + } + + File.Move(tempPath, _path, overwrite: true); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) + { + // Seeding is best-effort: the bundled default still serves the picker. + } + } + + private List ReadFile() + { try { if (!File.Exists(_path)) @@ -33,13 +88,33 @@ public IReadOnlyList GetEntries() } using var stream = File.OpenRead(_path); - file = JsonSerializer.Deserialize(stream, GlyphCatalogueJsonContext.Default.GlyphCatalogueFile); + return Parse(stream); } catch (Exception ex) when (ex is IOException or JsonException or UnauthorizedAccessException) { return []; } + } + + private static List ReadBundledDefault() + { + try + { + using var stream = OpenBundledDefault(); + return stream is null ? [] : Parse(stream); + } + catch (JsonException) + { + return []; + } + } + private static Stream? OpenBundledDefault() => + typeof(GlyphCatalogueProvider).Assembly.GetManifestResourceStream(_defaultResourceName); + + private static List Parse(Stream stream) + { + var file = JsonSerializer.Deserialize(stream, GlyphCatalogueJsonContext.Default.GlyphCatalogueFile); if (file?.GlyphCatalogue is not { Count: > 0 } raw) { return []; @@ -63,7 +138,7 @@ public IReadOnlyList GetEntries() } } - /// The appsettings.json shape the catalogue is read from. + /// The icon-catalogue JSON shape the catalogue is read from. internal sealed class GlyphCatalogueFile { public List? GlyphCatalogue { get; set; } diff --git a/src/Snipdeck.App/Services/WindowsPathProvider.cs b/src/Snipdeck.App/Services/WindowsPathProvider.cs index a2103ce..5680b7d 100644 --- a/src/Snipdeck.App/Services/WindowsPathProvider.cs +++ b/src/Snipdeck.App/Services/WindowsPathProvider.cs @@ -14,6 +14,8 @@ internal sealed class WindowsPathProvider : IPathProvider public string SettingsFilePath => DefaultPaths.SettingsFilePath; + public string IconCatalogueFilePath => DefaultPaths.IconCatalogueFilePath; + public string DefaultStorageDirectory => DefaultPaths.DefaultStorageDirectory; public string DefaultBackupDirectory => DefaultPaths.DefaultBackupDirectory; diff --git a/src/Snipdeck.App/Services/WindowsShellInteractions.cs b/src/Snipdeck.App/Services/WindowsShellInteractions.cs index 6bd71a0..c174208 100644 --- a/src/Snipdeck.App/Services/WindowsShellInteractions.cs +++ b/src/Snipdeck.App/Services/WindowsShellInteractions.cs @@ -133,7 +133,7 @@ public async Task NotifyAsync(string title, string message, string buttonText = public async Task PickGlyphAsync(string? currentGlyph) { - // Re-read the catalogue each open, so edits to appsettings.json take + // Re-read the catalogue each open, so edits to icon-catalogue.json take // effect without a restart. var picker = new GlyphPickerViewModel(_glyphCatalogue.GetEntries(), currentGlyph); var dialog = new GlyphPickerDialog(picker) diff --git a/src/Snipdeck.App/Snipdeck.App.csproj b/src/Snipdeck.App/Snipdeck.App.csproj index 0e4ccad..e678005 100644 --- a/src/Snipdeck.App/Snipdeck.App.csproj +++ b/src/Snipdeck.App/Snipdeck.App.csproj @@ -28,10 +28,15 @@ - + - + + Snipdeck.App.icon-catalogue.json + diff --git a/src/Snipdeck.App/Views/GlyphPickerDialog.xaml b/src/Snipdeck.App/Views/GlyphPickerDialog.xaml index ca13ecc..00be502 100644 --- a/src/Snipdeck.App/Views/GlyphPickerDialog.xaml +++ b/src/Snipdeck.App/Views/GlyphPickerDialog.xaml @@ -59,7 +59,7 @@ Visibility="{x:Bind ViewModel.HasNoResults, Mode=OneWay, Converter={StaticResource GlyphBoolToVisibility}}" /> - /// Supplies the glyph picker's browsable catalogue. The implementation lives - /// in the App project and reads the user-editable appsettings.json, so editing - /// that file (adding or removing glyphs) is reflected the next time the picker - /// opens — no rebuild required. + /// in the App project and reads the user-editable icon-catalogue.json, so + /// editing that file (adding or removing glyphs) is reflected the next time + /// the picker opens — no rebuild required. /// public interface IGlyphCatalogueProvider { diff --git a/src/Snipdeck.Core/Abstractions/IPathProvider.cs b/src/Snipdeck.Core/Abstractions/IPathProvider.cs index e422dfa..2ad2c65 100644 --- a/src/Snipdeck.Core/Abstractions/IPathProvider.cs +++ b/src/Snipdeck.Core/Abstractions/IPathProvider.cs @@ -6,6 +6,8 @@ public interface IPathProvider string SettingsFilePath { get; } + string IconCatalogueFilePath { get; } + string DefaultStorageDirectory { get; } string DefaultBackupDirectory { get; } diff --git a/src/Snipdeck.Core/Models/GlyphCatalogueEntry.cs b/src/Snipdeck.Core/Models/GlyphCatalogueEntry.cs index 72d12e6..c15c67a 100644 --- a/src/Snipdeck.Core/Models/GlyphCatalogueEntry.cs +++ b/src/Snipdeck.Core/Models/GlyphCatalogueEntry.cs @@ -5,7 +5,7 @@ namespace Snipdeck.Core.Models /// /// One entry in the glyph picker's browsable catalogue: a Segoe Fluent Icons /// glyph, a friendly name, and optional search keywords. The catalogue is a - /// curated, user-editable subset (see the App's appsettings.json) — not the + /// curated, user-editable subset (see the App's icon-catalogue.json) — not the /// full ~1.5k font, which would need heavier virtualisation to stay usable. /// /// The resolved glyph character to render (e.g. ""). diff --git a/src/Snipdeck.Core/Services/DefaultPaths.cs b/src/Snipdeck.Core/Services/DefaultPaths.cs index 998909d..4b0c8f5 100644 --- a/src/Snipdeck.Core/Services/DefaultPaths.cs +++ b/src/Snipdeck.Core/Services/DefaultPaths.cs @@ -13,6 +13,7 @@ public static class DefaultPaths { public const string AppFolderName = "Snipdeck"; public const string SettingsFileName = "settings.json"; + public const string IconCatalogueFileName = "icon-catalogue.json"; public const string StoreDirectoryName = "store"; public const string StoreFileName = "store.json"; public const string BackupsDirectoryName = "backups"; @@ -26,6 +27,13 @@ public static class DefaultPaths /// The app-config file, stored separately from the snip store. public static string SettingsFilePath { get; } = Path.Combine(AppDataDirectory, SettingsFileName); + /// + /// The user-editable icon picker catalogue. Lives here (not beside the + /// executable) so it survives Velopack updates and is editable per machine; + /// seeded from a bundled default on first run. + /// + public static string IconCatalogueFilePath { get; } = Path.Combine(AppDataDirectory, IconCatalogueFileName); + /// The default directory that holds the snip store document. public static string DefaultStorageDirectory { get; } = Path.Combine(AppDataDirectory, StoreDirectoryName); diff --git a/tests/Snipdeck.Core.Tests/Support/FakePathProvider.cs b/tests/Snipdeck.Core.Tests/Support/FakePathProvider.cs index 7d3f89d..b98b27c 100644 --- a/tests/Snipdeck.Core.Tests/Support/FakePathProvider.cs +++ b/tests/Snipdeck.Core.Tests/Support/FakePathProvider.cs @@ -8,6 +8,8 @@ public sealed class FakePathProvider : IPathProvider public string SettingsFilePath { get; init; } = "/data/settings.json"; + public string IconCatalogueFilePath { get; init; } = "/data/icon-catalogue.json"; + public string DefaultStorageDirectory { get; init; } = "/data/store"; public string DefaultBackupDirectory { get; init; } = "/data/backups";