Skip to content

Feature: FoliCon icon overlay plugin ecosystem - #312

Open
DineshSolanki wants to merge 17 commits into
masterfrom
feature/plugin-eco
Open

Feature: FoliCon icon overlay plugin ecosystem#312
DineshSolanki wants to merge 17 commits into
masterfrom
feature/plugin-eco

Conversation

@DineshSolanki

@DineshSolanki DineshSolanki commented Jul 18, 2026

Copy link
Copy Markdown
Owner

What

Folicon Plugin ecosystem rebuilt from scratch after the previous discarded dll approch.

Why

This is part of folicon plugin feature ecosystem.

How

Implemented multiple overlay definitions with respective properties, updated the PreviewerViewModel for dynamic loading, and enhanced the PosterIconConfigViewModel for better data binding. Refactored the UI for improved scalability for multiple overlay, overlays are now controlled by json

Testing

  • Builds successfully (dotnet build)
  • Tested manually on Windows
  • No regressions observed

Screenshots

Summary by CodeRabbit

  • New Features
    • Added an Overlay Store to discover, search, install, update, and remove overlays.
    • Added an Overlay Designer with templates, drafts, previews, editing controls, validation, undo/redo, export, and installation.
    • Added dynamic overlay previews and support for built-in and custom overlay packages.
  • Improvements
    • Updated poster icon configuration to manage overlays dynamically.
    • Added startup update checks and safer package validation.
  • Localization
    • Added translated overlay Store and Designer text across supported languages.
  • Documentation
    • Refreshed setup, overlay, localization, and contribution guidance.

- Introduced multiple new overlay definitions including "Alternate", "Faelpessoal", "Legacy", "Liaher", and "Windows 11" with respective properties and configurations.
- Updated the PreviewerViewModel to load available overlays dynamically.
- Enhanced PosterIconConfigViewModel to manage overlay selection with a new OverlayItemViewModel for better data binding.
- Implemented a dynamic poster icon renderer that builds its visual tree based on the selected overlay definition.
- Refactored the poster icon configuration view to utilize an ItemsControl for overlay selection, improving UI scalability and maintainability.
- Add Tests for the plugin feature
@codacy-production

codacy-production Bot commented Jul 18, 2026

Copy link
Copy Markdown

Not up to standards ⛔

🔴 Issues 1 high · 4 medium · 5 minor

Alerts:
⚠ 10 issues (≤ 0 issues of at least minor severity)

Results:
10 new issues

Category Results
BestPractice 4 medium
ErrorProne 1 high
CodeStyle 5 minor

View in Codacy

🟢 Metrics 1488 complexity · 64 duplication

Metric Results
Complexity 1488
Duplication 64

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 1: Core Plugin System (MVP) — ✅ COMPLETE

All 7 phases done. Build verified, 58 tests passing.

Phase Status Key deliverables
Phase 1: Data Model ✅ Done PosterOverlayDefinition, OverlayLayerConfig, 6 overlay.json files, JSON schema
Phase 2: Overlay Loader ✅ Done IOverlayProvider, OverlayProvider, OverlayValidator
Phase 3: Dynamic Renderer ✅ Done DynamicPosterIcon, OverlayPreviewCache, StaRenderer integration
Phase 4: Migrate Overlays ✅ Done 6 overlay definitions, IconOverlay deprecated
Phase 5: UI Integration ✅ Done Dynamic PosterIconConfig, Previewer, DI registration
Phase 7: Settings & Migration ✅ Done String-based overlay IDs, enum→string migration
Testing ✅ Done 58/58 tests passing, golden-image parity with tight 100px threshold

…y overlays via a GitHub-backed catalog.

```
GitHub (FoliCon-Overlays repo)
  catalog.json ──→ OverlayRepositoryService ──→ OverlayStoreViewModel ──→ OverlayStore.xaml
  overlays/{id}/                                   ↕
     manifest.json                              OverlayProvider (reads from %AppData%/FoliCon/Overlays/)
     overlay.json                               DynamicPosterIcon (renders previews)
     preview.png                                OverlayPreviewCache (caches rendered previews)
     *.png (assets)
```
@DineshSolanki DineshSolanki self-assigned this Jul 19, 2026
@DineshSolanki DineshSolanki added the enhancement New feature or request label Jul 19, 2026
@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 2: Overlay Store + Repository — ✅ COMPLETE

Phase 1: Data Models — ✅ DONE

Deliverable Status Notes
OverlayManifest.cs ✅ Done Per-overlay manifest with metadata, assets, SHA256, ToCatalogEntry() converter
manifest-schema.json ✅ Done JSON Schema for manifest validation (5MB max, semver, SHA256 hex)

Phase 2: Repository Service — ✅ DONE

Deliverable Status Notes
IOverlayRepositoryService.cs ✅ Done Interface: FetchCatalog, FetchManifest, Install, Update, Uninstall, MarkUpdateAvailable
OverlayRepositoryService.cs ✅ Done HTTP via Services.HttpC, disk cache (24h TTL), atomic install (tmp→validate→rename), SHA256 verification, backup/rollback, injectable paths/URL, env var + file override for local dev

Phase 3: ViewModels — ✅ DONE

Deliverable Status Notes
OverlayStoreViewModel.cs ✅ Done Catalog loading, search/filter by query+tag, install/update/uninstall commands, card state persistence across filter changes
OverlayCardViewModel.cs ✅ Done Preview lazy-loading from URL, SizeDisplay/VersionDisplay formatting, INotifyPropertyChanged

Phase 4: Overlay Store UI — ✅ DONE

Deliverable Status Notes
OverlayStore.xaml + .cs ✅ Done HandyControl dialog: SearchBar, tag ComboBox, WrapPanel card grid, BusyIndicator, status bar with error display

Phase 5: Wire Up UI + DI — ✅ DONE

Deliverable Status Notes
DI registrations in App.xaml.cs ✅ Done IOverlayRepositoryService singleton, OverlayUpdateChecker singleton, OverlayStore dialog
DialogServiceExtensions.cs ✅ Done ShowOverlayStore() extension method
PosterIconConfigViewModel ✅ Done IDialogService injection, BrowseOverlayStoreCommand, DemoIconPath returns frozen BitmapImage for community overlays (no file lock)
PosterIconConfig.xaml ✅ Done "Browse Overlay Store..." button enabled with command binding
GlobalVariables.SetOverlayProvider() ✅ Done Ensures DI singleton and static accessor share same IOverlayProvider instance

Phase 6: GitHub Repo Prep — ✅ DONE

  • FoliCon-Overlays

    Deliverable Status Notes
    FoliCon-Overlays/README.md ✅ Done Contribution guide with directory structure, manifest format, validation limits
    FoliCon-Overlays/catalog.json ✅ Done Initial catalog with example-dvd-case overlay
    FoliCon-Overlays/overlays/example-dvd-case/ ✅ Done Test overlay: overlay.json, manifest.json, base.png, front.png, preview.png
    .github/workflows/generate-catalog.yml ✅ Done Auto-generates catalog.json from manifest files on push to main
    .agents/skill ✅ Done AI agent skill to help user interactively generate the overlays

Phase 7: Update Checker — ✅ DONE

Deliverable Status Notes
OverlayUpdateChecker.cs ✅ Done Non-blocking background check on app start, marks updates via MarkUpdateAvailable(), uses shared OverlayConstants.TryCompareVersions()

Community Overlay Support — ✅ DONE

Deliverable Status Notes
PosterOverlayDefinition.OverlayFolderPath ✅ Done [JsonIgnore] property set by OverlayProvider when loading user overlays
DynamicPosterIcon.ResolveImageSource ✅ Done Now non-static, resolves relative paths against _overlayFolderPath for community overlays
DemoIconPath for community overlays ✅ Done Returns frozen BitmapImage (no file lock) instead of file path string

Testing — ✅ DONE

Deliverable Status Notes
OverlayManifestTests.cs ✅ Done 6 tests: defaults, JSON round-trip, serialize, ToCatalogEntry mapping
OverlayRepositoryServiceTests.cs ✅ Done 11 tests: install/uninstall/cache/version with injectable temp paths
OverlayStoreViewModelTests.cs ✅ Done 6 tests: field mapping, size formatting, property notifications, filtering (uses StubRepositoryService)
Total test run ✅ Done 81 tests, 81 passed, 0 failed (verified 2026-07-20)

- Updated OverlayDesigner.xaml.cs to use localized title for color picker.
- Enhanced OverlayStore.xaml with localization for various UI elements including buttons and tooltips.
- Modified posterIconConfig.xaml to utilize localized strings for tooltips and button content.
- Added localization tests to ensure all overlay strings are translated across multiple cultures.
- Introduced smoke tests for OverlayDesigner and OverlayStore views to validate XAML loading and layout.
- Implemented a new XamlLoadingCollection to prevent concurrent loading issues in tests.
- Improved WpfTestHost to set up application resources for testing localized strings.
@sonarqubecloud

Copy link
Copy Markdown

@DineshSolanki

Copy link
Copy Markdown
Owner Author

Release 3: Overlay Designer (v1.1) — ✅ COMPLETE (5 of 5 steps)

Scope changed during planning: GitHub PR automation moved out to Release 3.1 (externally blocked on OAuth App
registration, and public_repo scope warrants its own review), and "New from template" was added because the original
scope could only open existing packages — a designer that presents a dead end to anyone without an overlay folder.

Step Status Notes
1. Document + validation foundation ✅ Done Typed edit state, undo/redo with baseline dirty tracking, centralized margin↔bounds geometry, template cloning with pack-resource extraction, read-only package loading, structured validation
2. Designer dialog + live preview ✅ Done Template picker with rendered thumbnails, drag/resize canvas, selection-driven properties, debounced STA preview, keyboard operability, validation gating
3. Drafts + package export ✅ Done OverlayDraftStore, OverlayExporter, camelCase serializer, deterministic preview.png + manifest.json
4. Guided manual submission ✅ Done Submission panel, local install, live catalog clash check
5. Launch points + localization ✅ Done Main-menu + store entries, resx keys, regression sweep (translations outstanding)
Deliverable Status
Modules/Overlays/Designer/ (12 files) ✅ Done
OverlayDesignerViewModel.cs + OverlayElementViewModel.cs + OverlayTemplateCardViewModel.cs ✅ Done
OverlayDesigner.xaml + .xaml.cs ✅ Done
OverlayExporter.cs + OverlayPackageSerializer.cs ✅ Done
OverlayDraftStore.cs ✅ Done
OverlaySubmissionGuide.cs ✅ Done
GitHub device auth + publisher ⏸️ Deferred to Release 3.1

An author can now create an overlay from a template, edit it on a canvas, save drafts, export a store-ready package,
install it for their own use, and be walked through submitting it — without leaving FoliCon.

Tests: 453 total (81 from Releases 1–2 + 372 added across Release 3 and the store rework), 0 warnings, 0 errors,
stable across repeated full runs.

Completion pass (2026-07-26): layer reordering and per-corner clip radius editing exposed in the designer;
OverlayStore / OverlayDesigner translated into all 7 locales; the 6 obsolete PosterIcon*.xaml views and the
callerless ReferenceImageExporter removed (13 files) with app startup verified. The [Obsolete] IconOverlay enum is
deliberately retained — now unreferenced, but removing a public type is a breaking change for a major version.

Store tag filter reworked (2026-07-26): was a single-select ComboBox — a Release 2 gap the review had already
flagged (hc:TagContainer was planned) and which contradicted DESIGN.md's Chips component. Now multi-select **
hc:Tag** chips on their own wrapping row: per-tag counts, popularity ordering, AND semantics so "dvd + classic"
narrows rather than widens, a "Clear tags" action, and selection preserved across refresh. SelectedTag/AvailableTags
are gone.

Defects found and fixed

Thirteen in total across Release 3 — three by tests, ten by manual use, which is the ratio worth remembering:
automated coverage caught structural faults, but every visual and workflow defect needed a human.

Found by Count Examples
Automated tests 3 Null-command crash on dialog open; layer-reorder undo not rebuilding the rail; export determinism
Manual QA round 1 4 Clipped template cards; invisible card text; Close navigating to the wrong place; Close doing nothing at all when dirty
Manual QA round 2 7 No draft resume; no uninstall for locally-installed overlays; chips rendering as plain text; rating number not following its badge; blank numeric editors; radius only applying on focus loss; title text never drawn

Two of these were more than UI polish:

  • Title text was never rendered when layerOrder omitted "title" — a DynamicPosterIcon bug affecting all
    community overlays, not just the designer. The renderer now appends any element that exists but is unlisted; a
    companion test confirms layerOrder still governs z-order for listed layers so built-in parity is undisturbed.
  • Locally-installed overlays were unremovable. The store can only uninstall what it knows from the catalog, and a
    designer-installed overlay has no catalog entry. Uninstall now lives in OverlayExporter.UninstallLocal beside
    install, with a ✕ in Change Poster Icon Overlay.

@DineshSolanki

Copy link
Copy Markdown
Owner Author

This feature was worked in 3 Releases and many phases of each release, though it will be merged only once everything is completed

Release 1: Core Plugin System (MVP)

Release 2: Overlay Store + Repository

  • New GitHub repository: FoliCon-Overlays with per-overlay manifests + GH Actions

Release 3: Overlay Designer

@DineshSolanki DineshSolanki changed the title Add overlay definitions and enhance poster icon configuration Feature: FoliCon icon overlay plugin ecosystem Jul 27, 2026
@DineshSolanki
DineshSolanki requested a review from Copilot July 28, 2026 05:12

This comment was marked as off-topic.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d78a0712-7dfd-4b61-8d9e-19f41c21d6a5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (23)
FoliCon/internal-nlog.txt-1-5 (1)

1-5: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the generated runtime log from the repository.

FoliCon/internal-nlog.txt contains verbose NLog diagnostics and a developer-specific absolute path on Line 5. Remove the file from version control and ignore it in .gitignore to prevent future reintroduction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/internal-nlog.txt` around lines 1 - 5, Remove the generated runtime
log file FoliCon/internal-nlog.txt from version control, then add that filename
to the repository’s .gitignore so future NLog diagnostics are not reintroduced.
FoliCon/Modules/Overlays/OverlayProvider.cs-67-76 (1)

67-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return an absolute path for built-in overlays.

GetOverlayFolderPath returns Resources\Overlays\<id> for built-in IDs. That path is relative. IOverlayProvider.GetOverlayFolderPath documents "the full path to an overlay's folder". Any caller that passes the result to File.Exists, Directory.GetFiles, or Path.GetFullPath resolves it against the current working directory. The working directory is not the application directory when the app starts from a shortcut, from a shell with a different cwd, or from a file-dialog callback.

Base the built-in path on AppContext.BaseDirectory.

🐛 Proposed fix
         if (OverlayConstants.BuiltInOverlayIds.Contains(id, StringComparer.OrdinalIgnoreCase))
         {
-            return Path.Combine("Resources", "Overlays", id);
+            return Path.Combine(AppContext.BaseDirectory, "Resources", "Overlays", id);
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayProvider.cs` around lines 67 - 76, Update
GetOverlayFolderPath so built-in overlay IDs return an absolute path rooted at
AppContext.BaseDirectory, while preserving the existing Resources/Overlays/<id>
structure; leave user overlay path handling unchanged.
FoliCon/Modules/Overlays/Internal/OverlayValidator.cs-182-189 (1)

182-189: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict the pack-path bypass to built-in overlays.

ValidateAssetReference returns early for any assetPath that starts with /. ValidateDetailed runs for user-installed packages too, through OverlayProvider.LoadUserOverlays. A community overlay can therefore set "imagePath": "/Resources/Overlays/liaher/base.png" and skip every asset check: relative-path safety, PNG extension, existence, and per-image size limit. The overlay then renders app-internal resources that the package does not ship.

Gate the early return on definition.IsBuiltIn, or reject leading-slash paths when the overlay folder is a user overlay folder.

🛡️ Proposed fix
-    private static void ValidateAssetReference(string overlayFolder, string assetPath, string field, OverlayValidationResult result)
+    private static void ValidateAssetReference(string overlayFolder, string assetPath, string field,
+        OverlayValidationResult result, bool isBuiltIn)
     {
         // Built-in overlays reference embedded resources with a leading slash; those are
         // resolved by DynamicPosterIcon against pack URIs and never touch the overlay folder.
-        if (assetPath.StartsWith('/'))
+        if (isBuiltIn && assetPath.StartsWith('/'))
         {
             return;
         }

Pass definition.IsBuiltIn down from ValidateDetailed through ValidateLayers, ValidateLayer, and ValidatePosterConfig.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Internal/OverlayValidator.cs` around lines 182 -
189, Restrict the leading-slash asset bypass in ValidateAssetReference to
built-in overlays only. Propagate definition.IsBuiltIn from ValidateDetailed
through ValidateLayers, ValidateLayer, and ValidatePosterConfig into
ValidateAssetReference, and require it for the early return so user-installed
overlays still undergo all path, extension, existence, and size validation.
FoliCon/Modules/Overlays/OverlayConstants.cs-58-61 (1)

58-61: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use a case-insensitive, read-only set for BuiltInOverlayIds.

The set uses the default ordinal comparer and is a mutable HashSet<string> exposed as a public static field.

Two consequences follow:

  1. OverlayProvider.LoadUserOverlays (line 145) calls OverlayConstants.BuiltInOverlayIds.Contains(definition.Id). That call is case-sensitive. A community overlay with "id": "Liaher" passes the reserved-ID check. GetOverlayById matches with OrdinalIgnoreCase, so the community overlay can then shadow or conflict with the built-in liaher. Note that OverlayValidator.IdRegex rejects uppercase IDs only when the overlay reaches validation, which happens after this check and only produces a warning-level skip for other reasons.
  2. Any caller can mutate the shared set at runtime.
🛡️ Proposed fix
-    public static readonly HashSet<string> BuiltInOverlayIds =
-    [
-        "legacy", "alternate", "liaher", "faelpessoal", "faelpessoal-horizontal", "windows11"
-    ];
+    public static readonly IReadOnlySet<string> BuiltInOverlayIds =
+        new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+        {
+            "legacy", "alternate", "liaher", "faelpessoal", "faelpessoal-horizontal", "windows11"
+        };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayConstants.cs` around lines 58 - 61, Update
BuiltInOverlayIds to use OrdinalIgnoreCase comparison and expose it as a
read-only set rather than a mutable public HashSet. Preserve the existing
built-in IDs and ensure OverlayProvider.LoadUserOverlays.Contains applies the
same case-insensitive comparer.
FoliCon/Modules/Overlays/OverlayProvider.cs-114-177 (1)

114-177: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Guard _userOverlays against concurrent reload and reads.

OverlayProvider is registered as a singleton in FoliCon/App.xaml.cs. Refresh() calls LoadUserOverlays(), which clears _userOverlays and refills it item by item. OverlayUpdateChecker.CheckForUpdatesAsync runs on a background task and calls GetUserOverlays(), and the store view models call GetAllOverlays(). A read that overlaps a refresh can throw InvalidOperationException from the enumerator, or observe an empty or partial list and drop installed overlays from the UI.

Two changes are needed:

  1. Build the new list locally and publish it with a single reference assignment, or protect all reads and writes with a lock.
  2. Set definition.OverlayFolderPath before you add the definition to the list. Line 167 adds first and assigns at line 168, so a concurrent reader can observe a definition whose OverlayFolderPath is still null. DynamicPosterIcon then cannot resolve relative image paths for that overlay.
🔒️ Proposed fix sketch
-    private readonly List<PosterOverlayDefinition> _userOverlays = [];
+    private volatile IReadOnlyList<PosterOverlayDefinition> _userOverlays = [];
     private void LoadUserOverlays()
     {
-        _userOverlays.Clear();
+        var loaded = new List<PosterOverlayDefinition>();
 
         if (!Directory.Exists(_userOverlaysPath))
         {
             Logger.Debug("User overlays directory does not exist: {Path}", _userOverlaysPath);
+            _userOverlays = loaded;
             return;
         }
@@
-                _userOverlays.Add(definition);
-                definition.OverlayFolderPath = folder;
+                definition.OverlayFolderPath = folder;
+                loaded.Add(definition);
             }
             catch (Exception ex)
             {
                 Logger.Error(ex, "Failed to load overlay from '{Path}'", jsonPath);
             }
         }
 
-        Logger.Info("Loaded {Count} user overlays", _userOverlays.Count);
+        _userOverlays = loaded;
+        Logger.Info("Loaded {Count} user overlays", loaded.Count);
     }

Apply the same pattern to _builtInOverlays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayProvider.cs` around lines 114 - 177, Update
LoadUserOverlays to build a complete local collection, set
definition.OverlayFolderPath before adding each definition, then publish the
collection with one reference assignment instead of clearing and mutating
_userOverlays. Apply the same atomic publication pattern to _builtInOverlays,
and ensure GetUserOverlays and GetAllOverlays read stable published collections
so refreshes cannot expose partial data or invalidate enumeration.
FoliCon/Modules/Overlays/IOverlayRepositoryService.cs-45-70 (1)

45-70: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make _availableUpdates thread-safe.

OverlayRepositoryService is a singleton. OverlayUpdateChecker writes _availableUpdates while the store reads it through IsUpdateAvailable. UninstallOverlayAsync and InvalidateCache also mutate it. Use ConcurrentDictionary or protect every access with one lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/IOverlayRepositoryService.cs` around lines 45 - 70,
Make the _availableUpdates accesses in OverlayRepositoryService thread-safe
because the singleton is concurrently read and mutated by OverlayUpdateChecker,
IsUpdateAvailable, UninstallOverlayAsync, and InvalidateCache. Replace the
backing collection with ConcurrentDictionary or consistently protect every read,
write, and removal with a shared lock while preserving existing update-state
behavior.
FoliCon/Modules/Overlays/OverlayPreviewCache.cs-103-115 (1)

103-115: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Dispose the PosterIcon after each render.

PosterIcon implements IDisposable and holds a MemoryStream with the full poster bytes (FoliCon/Models/Data/PosterIcon.cs, lines 61-69). This method creates one per overlay and never disposes it, so GetPreviewsAsync leaks one stream for every overlay rendered. OverlayDesignerPreviewRenderer.RenderOnStaAsync already uses using var posterIcon for the same reason.

Create the PosterIcon once per GetPreviewsAsync call, or dispose it here.

🐛 Proposed fix
         return await StaRenderer.Default.EnqueueRender(() =>
         {
             // Create PosterIcon on the STA thread (WPF objects require STA)
-            var posterIcon = CreatePosterIcon(posterPath, rating, ratingVisibility, mockupVisibility);
+            using var posterIcon = CreatePosterIcon(posterPath, rating, ratingVisibility, mockupVisibility);
 
             var dynamicIcon = new DynamicPosterIcon(overlay, posterIcon);
             using var bitmap = dynamicIcon.RenderToBitmap();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayPreviewCache.cs` around lines 103 - 115,
Dispose each PosterIcon created during the render flow to release its underlying
MemoryStream. Update the render lambda in GetPreviewsAsync (or the enclosing
render helper) to scope the CreatePosterIcon result with disposal, while
preserving the existing DynamicPosterIcon and bitmap conversion behavior.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-417-428 (1)

417-428: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Protect the rollback so a failure does not delete the previous version.

RollbackUpdate runs inside a catch block. If Directory.Delete or Directory.Move throws, for example because a file is locked by the running application, the new exception replaces the original failure. The user then loses the installed overlay, and the backup stays behind as {overlayId}_previous. UninstallOverlayAsync later deletes that folder, so the previous version is unrecoverable.

Catch and log inside RollbackUpdate so the original exception still surfaces.

🛡️ Proposed fix
-    private static void RollbackUpdate(string finalDir, string backupDir)
+    private static void RollbackUpdate(string finalDir, string backupDir)
     {
-        if (Directory.Exists(finalDir))
-        {
-            Directory.Delete(finalDir, true);
-        }
-
-        if (Directory.Exists(backupDir))
-        {
-            Directory.Move(backupDir, finalDir);
-        }
+        try
+        {
+            if (Directory.Exists(finalDir))
+            {
+                Directory.Delete(finalDir, true);
+            }
+
+            if (Directory.Exists(backupDir))
+            {
+                Directory.Move(backupDir, finalDir);
+            }
+        }
+        catch (Exception ex)
+        {
+            Logger.Error(ex, "Rollback failed. Backup remains at {BackupDir}", backupDir);
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 417 - 428,
Update RollbackUpdate to handle exceptions from deleting finalDir or moving
backupDir without allowing rollback failures to replace the original update
exception. Catch and log rollback errors within RollbackUpdate, preserving the
backup directory when restoration cannot complete so it remains recoverable by
later cleanup or recovery logic.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-313-333 (1)

313-333: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject assets without a declared hash, and cap the overlay.json size.

Two gaps exist here:

  1. The SHA256 check runs only when manifest.Sha256 contains the asset key. A manifest that omits an entry installs the file with no integrity check. Fail the install instead when a hash is missing.
  2. The size check excludes OverlayConstants.overlayJsonFileName, so overlay.json has no upper bound. DownloadAssetsAsync buffers the whole response with GetByteArrayAsync, so a large file consumes unbounded memory.
🛡️ Proposed change
-        if (bytes.Length > OverlayConstants.maxImageSizeBytes && asset != OverlayConstants.overlayJsonFileName)
+        var maxBytes = asset == OverlayConstants.overlayJsonFileName
+            ? OverlayConstants.maxDefinitionSizeBytes
+            : OverlayConstants.maxImageSizeBytes;
+        if (bytes.Length > maxBytes)
         {
             throw new InvalidOperationException(string.Format(
-                Lang.OverlayInstallAssetTooLarge, asset, OverlayConstants.maxImageSizeBytes / 1024 / 1024));
+                Lang.OverlayInstallAssetTooLarge, asset, maxBytes / 1024 / 1024));
         }
 
-        // SHA256 verification
-        if (manifest.Sha256.TryGetValue(asset, out var expectedHash))
+        if (!manifest.Sha256.TryGetValue(asset, out var expectedHash))
         {
-            var actualHash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
-            if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
-            {
-                Logger.Error("SHA256 mismatch for '{Asset}': expected {Expected}, got {Actual}",
-                    asset, expectedHash, actualHash);
-                throw new InvalidOperationException(
-                    string.Format(Lang.OverlayInstallHashMismatch, asset));
-            }
+            Logger.Error("Manifest declares no SHA256 for '{Asset}'", asset);
+            throw new InvalidOperationException(string.Format(Lang.OverlayInstallHashMismatch, asset));
+        }
+
+        var actualHash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();
+        if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase))
+        {
+            Logger.Error("SHA256 mismatch for '{Asset}': expected {Expected}, got {Actual}",
+                asset, expectedHash, actualHash);
+            throw new InvalidOperationException(
+                string.Format(Lang.OverlayInstallHashMismatch, asset));
         }

OverlayConstants.maxDefinitionSizeBytes is illustrative. Use the existing constant if one is already defined.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 313 - 333,
Update the asset validation in the method containing the “Size check” and
“SHA256 verification” blocks to enforce the maximum size for every asset,
including OverlayConstants.overlayJsonFileName, using the existing applicable
size constant. Require manifest.Sha256 to contain every asset; when an entry is
missing, reject the install with the existing hash-mismatch error flow, and
retain the current case-insensitive comparison and logging for declared hashes
that do not match.
FoliCon/Modules/Overlays/OverlayRepositoryService.cs-509-534 (1)

509-534: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

CheckForUpdates never runs on the cached catalog paths.

FetchCatalogFromNetworkAsync is the only caller. FetchCatalogAsync returns early from the in-memory cache at line 108 and from the disk cache at line 132. After an application restart the disk cache is fresh for 24 hours, so _availableUpdates stays empty and IsUpdateAvailable returns false for every overlay. The store then reports no updates although newer versions exist in the catalog.

Call CheckForUpdates on the cached return paths as well.

🐛 Proposed fix
         if (_cachedCatalog != null && DateTime.UtcNow - _cacheTimestamp < CacheTtl)
         {
             Logger.Debug("Returning in-memory cached catalog");
+            CheckForUpdates(_cachedCatalog);
             return _cachedCatalog;
         }

Apply the same call before the disk-cache return at line 132.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 509 - 534,
Update FetchCatalogAsync so both the in-memory cache return and the disk-cache
return invoke CheckForUpdates with the cached catalog before returning. Preserve
the existing FetchCatalogFromNetworkAsync behavior and ensure _availableUpdates
is refreshed for every cached catalog path.
FoliCon/ViewModels/PreviewerViewModel.cs-47-51 (1)

47-51: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

MediaTitle no longer affects the rendered previews.

The MediaTitle setter only calls SetProperty. It does not call RebuildPreviewsAsync. OverlayPreviewCache.GetPreviewsAsync is called with the poster path, rating, and the two visibility values, but not the title.

OverlayPreviewContext carries a MediaTitle field (see FoliCon/Modules/Overlays/Designer/OverlayDesignerPreviewRenderer.cs Lines 8-33), so the renderer supports a title. The previewer's title input is now inert.

Pass the title into the preview request and rebuild on change, or remove the input from Previewer.xaml.

🐛 Proposed fix
         public string MediaTitle
         {
             get => _mediaTitle;
-            set => SetProperty(ref _mediaTitle, value);
+            set
+            {
+                if (SetProperty(ref _mediaTitle, value))
+                {
+                    _ = RebuildPreviewsAsync();
+                }
+            }
         }

Also applies to: 84-89

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/PreviewerViewModel.cs` around lines 47 - 51, Update
PreviewerViewModel.MediaTitle so changing the title triggers
RebuildPreviewsAsync, and include the current title when calling
OverlayPreviewCache.GetPreviewsAsync to populate
OverlayPreviewContext.MediaTitle. Preserve the existing preview rebuild behavior
and ensure title changes are reflected in rendered previews.
FoliCon/Views/OverlayDesigner.xaml-64-76 (1)

64-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Arrow-key KeyBindings at UserControl level will block text editing.

InputBindings on the UserControl are evaluated on the bubbling route from the focused element. The arrow keys therefore fire NudgeCommand while focus is inside a TextBox or an hc:NumericUpDown.

NudgeCommand executes whenever SelectedElement != null (OverlayDesignerViewModel.cs Line 135), which is the normal editor state. The command marks the key handled, so the caret does not move in DisplayName (Line 492), OverlayId (Line 496), Description (Line 511), TagsText (Line 516), or the geometry editors (Lines 457-480).

Move the arrow-key bindings onto CanvasRoot, or gate NudgeCommand on canvas focus.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Views/OverlayDesigner.xaml` around lines 64 - 76, Move the arrow-key
KeyBindings from UserControl.InputBindings to the CanvasRoot input scope so they
only invoke NudgeCommand while the canvas is focused. Preserve the existing
direction and shift CommandParameters, and leave the UndoCommand, RedoCommand,
and OpenHelpCommand bindings unchanged.
FoliCon/ViewModels/posterIconConfigViewModel.cs-47-56 (1)

47-56: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset IconOverlay when the store removes the active overlay.

RemoveOverlay falls back to the default when it deletes the active overlay (Lines 105-110). The store callback at Lines 50-55 does not.

If a user uninstalls the active overlay inside the overlay store, LoadOverlays rebuilds AvailableOverlays without that ID. IconOverlay keeps the stale ID, so no item has IsActive == true and the RadioButton group in FoliCon/Views/posterIconConfig.xaml (Lines 63-75) shows nothing selected. Icon generation then falls back silently at render time.

Add the same check to LoadOverlays, which covers both callbacks.

🐛 Proposed fix
             AvailableOverlays.Clear();
             foreach (var overlay in allOverlays)
             {
                 ...
                 AvailableOverlays.Add(item);
             }
+
+            // A removed overlay must not stay selected: the picker would show no
+            // checked item and icon generation would fall back without telling anyone.
+            if (AvailableOverlays.Count > 0 && AvailableOverlays.All(o => !o.IsActive))
+            {
+                IconOverlay = OverlayConstants.DefaultOverlayId;
+            }
         }

Also applies to: 142-167

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/posterIconConfigViewModel.cs` around lines 47 - 56, Update
LoadOverlays to detect when the current IconOverlay is no longer present in
AvailableOverlays and reset it to the default overlay, matching RemoveOverlay’s
fallback behavior. Keep the existing store callback and removal flow unchanged
so both paths use the centralized validation.
FoliCon/ViewModels/PreviewerViewModel.cs-79-109 (1)

79-109: 🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Guard LoadPreviewsAsync against overlapping invocations.

Rating, RatingVisibility, and OverlayVisibility each start RebuildPreviewsAsync without awaiting it. SelectImage starts another. Two runs can therefore be in flight at once.

Both runs execute OverlayPreviewItems.Clear() and then Add per item around an await. If the second run clears the collection while the first run is still adding, the bound list ends with a partial or duplicated set. InvalidateAll in one run also discards cache entries the other run just populated, so every overlay is re-rendered.

If Rating is bound with UpdateSourceTrigger=PropertyChanged, each keystroke starts a full re-render of every overlay.

Add a CancellationTokenSource per request and a short debounce, then apply the results only when the token is still current.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/PreviewerViewModel.cs` around lines 79 - 109, Update
LoadPreviewsAsync and RebuildPreviewsAsync to serialize preview refresh requests
using a per-request CancellationTokenSource and short debounce. Cancel and
replace the previous request when Rating, RatingVisibility, OverlayVisibility,
or SelectImage triggers a rebuild, pass the current token through loading, and
only clear/add OverlayPreviewItems if that request remains current; ensure
cancelled or stale requests do not apply results or invalidate newer cache data.
FoliCon/Views/OverlayDesigner.xaml.cs-65-81 (1)

65-81: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

OnLoaded subscribes the canvas handlers on every Loaded event.

The Contains guard at Line 67 protects the adorner creation only. Lines 73-76 run unconditionally.

FrameworkElement.Loaded can raise more than once for the same instance, for example after the element is removed from and re-added to the visual tree. Each raise adds another set of handlers to CanvasRoot. OnCanvasMouseMove then calls ViewModel.ApplyGesture twice per pointer move, and the handlers are never removed.

🐛 Proposed fix
     private void OnLoaded(object sender, RoutedEventArgs e)
     {
         if (!AdornerLayer.Children.Contains(_selectionOutline))
         {
             AdornerLayer.Children.Add(_selectionOutline);
             CreateHandles();
+
+            CanvasRoot.MouseLeftButtonDown += OnCanvasMouseDown;
+            CanvasRoot.MouseMove += OnCanvasMouseMove;
+            CanvasRoot.MouseLeftButtonUp += OnCanvasMouseUp;
+            CanvasRoot.MouseLeave += OnCanvasMouseLeave;
         }
 
-        CanvasRoot.MouseLeftButtonDown += OnCanvasMouseDown;
-        CanvasRoot.MouseMove += OnCanvasMouseMove;
-        CanvasRoot.MouseLeftButtonUp += OnCanvasMouseUp;
-        CanvasRoot.MouseLeave += OnCanvasMouseLeave;
-
         // Arrow-key nudge is bound at the dialog level, so focus must land here.
         Focus();
         UpdateAdorner();
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Views/OverlayDesigner.xaml.cs` around lines 65 - 81, Update OnLoaded
so CanvasRoot mouse handlers are subscribed only once, guarding the
subscriptions with the same initialization state used for the adorner or a
dedicated flag; ensure repeated Loaded events do not duplicate handlers while
preserving the existing adorner setup and focus/update behavior.
FoliCon/ViewModels/posterIconConfigViewModel.cs-220-267 (1)

220-267: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Cache the community preview instead of decoding it in the property getter.

DemoIconPath is a computed property with no backing field and no change notification. Every read of the _ branch calls LoadCommunityOverlayPreview, which performs File.Exists and a full PNG decode.

WPF re-reads a bound getter whenever the binding refreshes or the container is re-templated, for example when the ItemsControl in FoliCon/Views/posterIconConfig.xaml (Lines 48-100) virtualizes or re-applies its template. The decode then runs again for each visible overlay.

Store the result in a lazily initialized field.

♻️ Proposed fix
+    private object? _demoIconPath;
+
     public object DemoIconPath => OverlayId switch
     {
         "legacy" => "/Resources/mockup_demos/simple/PosterIcon.ico",
         ...
-        _ => (object?)LoadCommunityOverlayPreview(OverlayId, IsBuiltIn) ?? "/Resources/icons/NoPosterAvailable.png"
+        _ => _demoIconPath ??=
+            (object?)LoadCommunityOverlayPreview(OverlayId, IsBuiltIn)
+            ?? "/Resources/icons/NoPosterAvailable.png"
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/posterIconConfigViewModel.cs` around lines 220 - 267,
Cache the community preview result used by DemoIconPath in a lazily initialized
backing field so repeated getter reads do not rerun LoadCommunityOverlayPreview,
including its file check and PNG decode. Preserve the existing built-in mappings
and fallback behavior, and ensure the cached value is initialized only for the
current OverlayId/IsBuiltIn state using the existing LoadCommunityOverlayPreview
method.
FoliCon/ViewModels/OverlayDesignerViewModel.cs-144-144 (1)

144-144: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Wrap the async command body so exceptions cannot escape as async void.

new DelegateCommand(async () => await ExportPackageAsync(), ...) compiles the lambda to async void. Any exception that ExportPackageAsync does not catch is rethrown on the synchronization context and terminates the process.

ExportPackageAsync filters only IOException and UnauthorizedAccessException. OpenSubmissionPanelAsync (Line 1086) awaits _submissionGuide.CheckAsync, which performs network work and can throw HttpRequestException or TaskCanceledException. Those escape the filter.

🛡️ Proposed fix
-        ExportPackageCommand = new DelegateCommand(async () => await ExportPackageAsync(), () => CanExport && !IsBusy);
+        ExportPackageCommand = new DelegateCommand(() => _ = RunExportAsync(), () => CanExport && !IsBusy);

Add the guarded wrapper:

private async Task RunExportAsync()
{
    try
    {
        await ExportPackageAsync();
    }
    catch (Exception ex)
    {
        Logger.Error(ex, "Unhandled error while exporting overlay '{Id}'", _document.Id);
        StatusMessage = string.Format(Lang.OverlayDesignerExportFailedWithReason, ex.Message);
        IsBusy = false;
    }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/ViewModels/OverlayDesignerViewModel.cs` at line 144, Update
ExportPackageCommand to invoke a guarded Task-returning wrapper instead of
passing ExportPackageAsync directly as an async void lambda. Add RunExportAsync
near the command-related methods, await ExportPackageAsync inside it, catch any
unhandled Exception, log it with the document ID, update the export failure
status, and reset IsBusy before returning.
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs-157-177 (1)

157-177: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Constrain every path component to the drafts root before the store writes or deletes. OverlayDraftStore builds file and folder paths from values that originate in overlay.json — the asset names and the overlay ID — and neither value is checked for path separators, .. segments, or a rooted path. Path.Combine then resolves outside DraftsRoot, and the store copies, moves, or recursively deletes there.

  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs#L157-L177: reject rooted asset paths and verify Path.GetFullPath(target) stays under stagingPath before File.Copy.
  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs#L53-L59: reject a document.Id that is not a plain folder name, so finalPath, Commit, and Delete cannot act outside _draftsRoot.
  • FoliconTest/OverlayDraftStoreTests.cs#L37-L46: add tests that save a document with an escaping asset path and with an escaping ID, and assert nothing is created outside DraftsRoot.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs` around lines 157 -
177, Constrain all paths derived from overlay.json to DraftsRoot: in
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs lines 157-177, reject
rooted or escaping asset paths and verify the full target remains under
stagingPath before File.Copy; in
FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs lines 53-59, validate
document.Id is a plain folder name so finalPath, Commit, and Delete remain
within _draftsRoot; in FoliconTest/OverlayDraftStoreTests.cs lines 37-46, add
save tests for escaping asset paths and IDs and assert nothing is created
outside DraftsRoot.
FoliCon/Modules/Overlays/Designer/OverlayExporter.cs-235-251 (1)

235-251: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Nested asset paths are copied but never manifested or installed.

CopyReferencedAssets creates subdirectories at line 248, so an asset such as art/base.png is copied into {staging}/art/base.png.

Two later steps only handle a flat layout:

  • WriteManifest at line 277 uses Directory.GetFiles(stagingPath) without recursion. A nested asset is missing from Assets and from Sha256.
  • InstallLocally at line 164 uses Directory.GetFiles(packagePath) without recursion. A nested asset is not installed, so the installed overlay fails to render.

Choose one of two fixes. Either reject nested asset paths at export time, or enumerate recursively in both places and store relative paths.

🐛 Option A: recursive enumeration with relative keys
-        var files = Directory.GetFiles(stagingPath)
-            .Select(Path.GetFileName)
-            .OfType<string>()
+        var files = Directory.GetFiles(stagingPath, "*", SearchOption.AllDirectories)
+            .Select(f => Path.GetRelativePath(stagingPath, f).Replace('\\', '/'))
             // Stable order so the manifest is byte-identical across exports.
             .OrderBy(f => f, StringComparer.Ordinal)
             .ToArray();
-            foreach (var file in Directory.GetFiles(packagePath))
+            foreach (var file in Directory.GetFiles(packagePath, "*", SearchOption.AllDirectories))
             {
-                File.Copy(file, Path.Combine(staging, Path.GetFileName(file)), overwrite: true);
+                var target = Path.Combine(staging, Path.GetRelativePath(packagePath, file));
+                Directory.CreateDirectory(Path.GetDirectoryName(target)!);
+                File.Copy(file, target, overwrite: true);
             }

Run the following script to check whether the validator already forbids nested asset paths:

#!/bin/bash
# Look for path-separator rules on imagePath / opacityMaskPath / fontSource.
fd -i 'OverlayValidator*.cs' --exec rg -n -C 6 'imagePath|ImagePath|OpacityMaskPath|FontSource|DirectorySeparator|Contains\(.[/\\]' {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Designer/OverlayExporter.cs` around lines 235 - 251,
Update WriteManifest and InstallLocally to enumerate staging/package files
recursively so nested assets are included in the manifest, hashes, and local
installation. Store each file using its path relative to the respective root,
preserving nested paths such as art/base.png; keep the existing flat-file
behavior unchanged.
FoliconTest/OverlayTemplateProviderTests.cs-12-15 (1)

12-15: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add the XamlLoadingCollection attribute to this class.

This class creates a WpfTestHost on line 15 and calls WpfTestHost.Invoke in ten tests. Every other suite in this change that uses WpfTestHost declares [Collection(XamlLoadingCollection.name)]: OverlayTemplatePickerTests line 13, OverlayDesignerGeometryTests line 14, OverlayDesignerIntegrationTests line 22, OverlayDesignerPreviewRendererTests line 12, and OverlayDesignerViewSmokeTests line 22.

Without the attribute, xUnit runs this class in parallel with those collections. It then initializes a second WPF host and queues work on the shared STA thread while another collection uses it. That causes intermittent, order-dependent failures.

💚 Proposed fix
+[Collection(XamlLoadingCollection.name)]
 public class OverlayTemplateProviderTests : IDisposable
 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayTemplateProviderTests.cs` around lines 12 - 15, Add the
xUnit [Collection(XamlLoadingCollection.name)] attribute to the
OverlayTemplateProviderTests class so its WpfTestHost usage is serialized with
the other XAML-loading test suites; leave the existing test implementation
unchanged.
FoliconTest/OverlayDesignerViewSmokeTests.cs-62-86 (1)

62-86: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore the Prism view-model factory after the test.

SetDefaultViewModelFactory changes process-wide static state. This test leaves a factory that returns the disposed viewModel. A later AutoWireViewModel path can receive that instance. Restore Prism 9's default factory in finally:

             finally
             {
+                ViewModelLocationProvider.SetDefaultViewModelFactory(
+                    type => Activator.CreateInstance(type));
                 viewModel.Dispose();
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayDesignerViewSmokeTests.cs` around lines 62 - 86, Restore
Prism’s default view-model factory in the finally block after disposing the test
view model, using the Prism 9 reset/default-factory API. Keep the existing
SetDefaultViewModelFactory setup and test flow unchanged, ensuring later
AutoWireViewModel calls cannot receive the disposed viewModel.
FoliconTest/OverlayStoreViewModelTests.cs-405-409 (1)

405-409: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Run the tag-filter helpers on the dispatcher thread.

VisibleOverlays is created by CollectionViewSource.GetDefaultView(Overlays) inside the view-model constructor, and LoadTagFixtureAsync constructs the view model through WpfTestHost.Invoke (Line 427). The resulting ICollectionView has thread affinity to the STA dispatcher thread. Visible and Select run on the xUnit thread instead, as do vm.SearchQuery (Line 398), vm.ClearTagFiltersCommand.Execute() (Line 364), and vm.RefreshCommand.Execute() (Line 385). Each of these reaches the collection view from the wrong thread and can throw at runtime. The tests earlier in this file (Lines 105-115, 132-134) already wrap the same operations in WpfTestHost.Invoke. Make the tag-filter tests consistent.

🔒️ Proposed fix
     private static List<OverlayCardViewModel> Visible(OverlayStoreViewModel vm) =>
-        [.. vm.VisibleOverlays.Cast<OverlayCardViewModel>()];
+        WpfTestHost.Invoke(() => vm.VisibleOverlays.Cast<OverlayCardViewModel>().ToList());
 
     private static void Select(OverlayStoreViewModel vm, string tag, bool selected = true) =>
-        vm.TagFilters.Single(t => string.Equals(t.Tag, tag, StringComparison.OrdinalIgnoreCase)).IsSelected = selected;
+        WpfTestHost.Invoke(() =>
+            vm.TagFilters.Single(t => string.Equals(t.Tag, tag, StringComparison.OrdinalIgnoreCase)).IsSelected = selected);

Wrap the command executions and the SearchQuery assignment in WpfTestHost.Invoke as well.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/OverlayStoreViewModelTests.cs` around lines 405 - 409, Update the
tag-filter tests to access the dispatcher-affine collection view only through
WpfTestHost.Invoke. Wrap the Visible and Select helper bodies, plus SearchQuery
assignments and ClearTagFiltersCommand.Execute and RefreshCommand.Execute calls,
in dispatcher invocations while preserving their existing assertions and test
behavior.
FoliconTest/WpfTestHost.cs-24-69 (1)

24-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

An initialization failure hangs the whole test run.

ready.Set() runs at Line 58, after the application setup at Lines 33-54. If the reflection call or a theme ResourceDictionary load throws, Set() never runs and ready.Wait() at Line 67 blocks forever. The test process then hangs instead of failing. Capture the exception, signal the event in a finally, and rethrow on the calling thread.

🛡️ Proposed fix
         Dispatcher dispatcher = null!;
+        Exception? startupFailure = null;
         using var ready = new ManualResetEventSlim(false);
         var staThread = new Thread(() =>
         {
-            // Creating Application sets Application.Current (required for relative pack URIs).
-            // Set BaseUri to FoliCon assembly so relative URIs like /Resources/... resolve
-            // against FoliCon.dll resources, not the test runner assembly.
-            if (Application.Current == null)
+            try
             {
-                ...
+                // Creating Application sets Application.Current (required for relative pack URIs).
+                if (Application.Current == null)
+                {
+                    // ... existing setup ...
+                }
+                dispatcher = Dispatcher.CurrentDispatcher;
             }
-
-            dispatcher = Dispatcher.CurrentDispatcher;
-            // ReSharper disable once AccessToDisposedClosure - Set() runs before ready is disposed
-            ready.Set();
+            catch (Exception ex)
+            {
+                startupFailure = ex;
+            }
+            finally
+            {
+                // ReSharper disable once AccessToDisposedClosure - Set() runs before ready is disposed
+                ready.Set();
+            }
+
+            if (startupFailure != null)
+            {
+                return;
+            }
             Dispatcher.Run();
         })
@@
         ready.Wait();
+        if (startupFailure != null)
+        {
+            throw new InvalidOperationException("WPF test host failed to start.", startupFailure);
+        }
         return dispatcher;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliconTest/WpfTestHost.cs` around lines 24 - 69, Update
StartDispatcherThread so exceptions during application setup, including BaseUri
reflection or ThemeSources resource loading, are captured from the STA thread,
and always signal ready in a finally block. After ready.Wait() returns, rethrow
the captured exception on the calling thread before returning the dispatcher,
while preserving normal Dispatcher.Run startup behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 53e3c24e-db2d-4c7a-9232-50573b38f66f

📥 Commits

Reviewing files that changed from the base of the PR and between b18ca2f and 8b48c97.

⛔ Files ignored due to path filters (6)
  • FoliconTest/Resources/ReferenceOverlays/alternate_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/faelpessoal-horizontal_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/faelpessoal_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/legacy_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/liaher_reference.png is excluded by !**/*.png
  • FoliconTest/Resources/ReferenceOverlays/windows11_reference.png is excluded by !**/*.png
📒 Files selected for processing (118)
  • .gitignore
  • FoliCon/App.xaml
  • FoliCon/App.xaml.cs
  • FoliCon/FoliCon.csproj
  • FoliCon/Models/Constants/GlobalVariables.cs
  • FoliCon/Models/Data/OverlayCatalog.cs
  • FoliCon/Models/Data/OverlayLayerConfig.cs
  • FoliCon/Models/Data/OverlayManifest.cs
  • FoliCon/Models/Data/OverlayStatusFilterOption.cs
  • FoliCon/Models/Data/PosterOverlayDefinition.cs
  • FoliCon/Models/Enums/IconOverlay.cs
  • FoliCon/Models/Enums/OverlayStatusFilter.cs
  • FoliCon/Models/Enums/OverlayStoreSection.cs
  • FoliCon/Models/Usings.cs
  • FoliCon/Modules/Convertor/LocalizedFormatConverter.cs
  • FoliCon/Modules/Extension/DialogServiceExtensions.cs
  • FoliCon/Modules/Extension/StreamExtension.cs
  • FoliCon/Modules/LangProvider.cs
  • FoliCon/Modules/Overlays/Designer/IOverlayEditCommand.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDesignerDocument.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDesignerPreviewRenderer.cs
  • FoliCon/Modules/Overlays/Designer/OverlayDraftStore.cs
  • FoliCon/Modules/Overlays/Designer/OverlayEditHistory.cs
  • FoliCon/Modules/Overlays/Designer/OverlayElementKind.cs
  • FoliCon/Modules/Overlays/Designer/OverlayExporter.cs
  • FoliCon/Modules/Overlays/Designer/OverlayGeometry.cs
  • FoliCon/Modules/Overlays/Designer/OverlayPackageLoader.cs
  • FoliCon/Modules/Overlays/Designer/OverlayPackageSerializer.cs
  • FoliCon/Modules/Overlays/Designer/OverlaySubmissionGuide.cs
  • FoliCon/Modules/Overlays/Designer/OverlayTemplateProvider.cs
  • FoliCon/Modules/Overlays/IOverlayProvider.cs
  • FoliCon/Modules/Overlays/IOverlayRepositoryService.cs
  • FoliCon/Modules/Overlays/Internal/OverlayValidator.cs
  • FoliCon/Modules/Overlays/OverlayConstants.cs
  • FoliCon/Modules/Overlays/OverlayPreviewCache.cs
  • FoliCon/Modules/Overlays/OverlayProvider.cs
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs
  • FoliCon/Modules/Overlays/OverlayUpdateChecker.cs
  • FoliCon/Modules/Overlays/OverlayValidationResult.cs
  • FoliCon/Modules/Validation/ApiKeyValidator.cs
  • FoliCon/Modules/utils/IconUtils.cs
  • FoliCon/Properties/Langs/Lang.Designer.cs
  • FoliCon/Properties/Langs/Lang.ar.resx
  • FoliCon/Properties/Langs/Lang.es.resx
  • FoliCon/Properties/Langs/Lang.hi.resx
  • FoliCon/Properties/Langs/Lang.ja.resx
  • FoliCon/Properties/Langs/Lang.pt.resx
  • FoliCon/Properties/Langs/Lang.resx
  • FoliCon/Properties/Langs/Lang.ru.resx
  • FoliCon/Properties/Langs/Lang.zh.resx
  • FoliCon/Resources/Overlays/alternate/overlay.json
  • FoliCon/Resources/Overlays/faelpessoal-horizontal/overlay.json
  • FoliCon/Resources/Overlays/faelpessoal/overlay.json
  • FoliCon/Resources/Overlays/legacy/overlay.json
  • FoliCon/Resources/Overlays/liaher/overlay.json
  • FoliCon/Resources/Overlays/windows11/overlay.json
  • FoliCon/ViewModels/MainWindowViewModel.cs
  • FoliCon/ViewModels/OverlayCardViewModel.cs
  • FoliCon/ViewModels/OverlayDesignerViewModel.cs
  • FoliCon/ViewModels/OverlayElementViewModel.cs
  • FoliCon/ViewModels/OverlayStoreViewModel.cs
  • FoliCon/ViewModels/OverlayTagFilterViewModel.cs
  • FoliCon/ViewModels/OverlayTemplateCardViewModel.cs
  • FoliCon/ViewModels/PreviewerViewModel.cs
  • FoliCon/ViewModels/posterIconConfigViewModel.cs
  • FoliCon/Views/DynamicPosterIcon.xaml
  • FoliCon/Views/DynamicPosterIcon.xaml.cs
  • FoliCon/Views/MainWindow.xaml
  • FoliCon/Views/OverlayDesigner.xaml
  • FoliCon/Views/OverlayDesigner.xaml.cs
  • FoliCon/Views/OverlayStore.xaml
  • FoliCon/Views/OverlayStore.xaml.cs
  • FoliCon/Views/PosterIcon.xaml
  • FoliCon/Views/PosterIcon.xaml.cs
  • FoliCon/Views/PosterIconAlt.xaml
  • FoliCon/Views/PosterIconAlt.xaml.cs
  • FoliCon/Views/PosterIconFaelpessoal.xaml
  • FoliCon/Views/PosterIconFaelpessoal.xaml.cs
  • FoliCon/Views/PosterIconFaelpessoalHorizontal.xaml
  • FoliCon/Views/PosterIconFaelpessoalHorizontal.xaml.cs
  • FoliCon/Views/PosterIconLiaher.xaml
  • FoliCon/Views/PosterIconLiaher.xaml.cs
  • FoliCon/Views/PosterIconWindows11.xaml
  • FoliCon/Views/PosterIconWindows11.xaml.cs
  • FoliCon/Views/Previewer.xaml
  • FoliCon/Views/posterIconConfig.xaml
  • FoliCon/internal-nlog.txt
  • Folicon.sln
  • FoliconTest/DynamicPosterIconParityTests.cs
  • FoliconTest/FoliconTest.csproj
  • FoliconTest/GlobalUsings.cs
  • FoliconTest/GoldenImageParityTests.cs
  • FoliconTest/OverlayDesignerDocumentTests.cs
  • FoliconTest/OverlayDesignerGeometryTests.cs
  • FoliconTest/OverlayDesignerIntegrationTests.cs
  • FoliconTest/OverlayDesignerPreviewRendererTests.cs
  • FoliconTest/OverlayDesignerViewModelTests.cs
  • FoliconTest/OverlayDesignerViewSmokeTests.cs
  • FoliconTest/OverlayDraftStoreTests.cs
  • FoliconTest/OverlayEditCommandTests.cs
  • FoliconTest/OverlayExporterTests.cs
  • FoliconTest/OverlayLocalizationTests.cs
  • FoliconTest/OverlayManifestTests.cs
  • FoliconTest/OverlayPackageLoaderTests.cs
  • FoliconTest/OverlayProviderTests.cs
  • FoliconTest/OverlayRepositoryServiceTests.cs
  • FoliconTest/OverlayStoreStyleResolutionTests.cs
  • FoliconTest/OverlayStoreViewModelTests.cs
  • FoliconTest/OverlayStoreViewSmokeTests.cs
  • FoliconTest/OverlaySubmissionGuideTests.cs
  • FoliconTest/OverlayTemplatePickerTests.cs
  • FoliconTest/OverlayTemplateProviderTests.cs
  • FoliconTest/OverlayValidatorDetailedTests.cs
  • FoliconTest/OverlayValidatorTests.cs
  • FoliconTest/PosterIconConfigViewSmokeTests.cs
  • FoliconTest/WpfTestHost.cs
  • FoliconTest/XamlLoadingCollection.cs
  • README.md
💤 Files with no reviewable changes (2)
  • FoliCon/Modules/Validation/ApiKeyValidator.cs
  • FoliCon/Models/Enums/IconOverlay.cs

Comment on lines +87 to +99
var finalPath = Path.Combine(destinationRoot, document.Id);
if (Directory.Exists(finalPath) && !overwrite)
{
return OverlayExportResult.Failure(
string.Format(Lang.OverlayExportFolderExists, document.Id));
}

// Staged as a sibling so the final move is a rename on the same volume.
var stagingPath = Path.Combine(destinationRoot, $".{document.Id}.export-tmp");

try
{
PrepareStagingFolder(stagingPath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Validate document.Id before you compose any path.

Line 87 and line 95 place document.Id directly into Path.Combine and into an interpolated folder name. The ID is only validated at line 107, after PrepareStagingFolder already ran. PrepareStagingFolder calls SafeDelete, which performs Directory.Delete(path, recursive: true).

If document.Id contains a path separator or .., the staging path escapes destinationRoot. The recursive delete then targets a folder outside the destination. The author controls this value through the designer.

The existing test uses "Not A Valid Id", which contains no separator, so this case is not covered.

Add an ID format check before line 87.

🔒️ Proposed fix: reject unsafe IDs first
         if (string.IsNullOrWhiteSpace(document.Id))
         {
             return OverlayExportResult.Failure(Lang.OverlayExportIdRequired);
         }
 
+        // The ID becomes a folder name. Reject anything that could escape destinationRoot
+        // before it reaches Path.Combine or the recursive staging cleanup.
+        if (document.Id != Path.GetFileName(document.Id) ||
+            document.Id is "." or ".." ||
+            document.Id.AsSpan().IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
+        {
+            return OverlayExportResult.Failure(Lang.OverlayExportIdRequired);
+        }
+
         var finalPath = Path.Combine(destinationRoot, document.Id);

Run the following script to check whether any caller validates the ID before calling ExportAsync:

#!/bin/bash
# Find ExportAsync call sites and any ID validation rules.
rg -n -C 6 --type=cs '\.ExportAsync\s*\('

# Inspect the validator's ID rules.
fd -i 'OverlayValidator*.cs' --exec rg -n -C 4 -i 'id' {}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/Designer/OverlayExporter.cs` around lines 87 - 99,
Validate document.Id before constructing finalPath or stagingPath in
OverlayExporter’s export flow, rejecting path separators, traversal segments,
and other values not permitted by the existing overlay ID rules. Reuse the
established validator or validation logic, and return the appropriate
OverlayExportResult.Failure without invoking PrepareStagingFolder when
validation fails.

Comment on lines +227 to +228
var tmpDir = Path.Combine(_userOverlaysDir, $"{entry.Id}.tmp");
var finalDir = Path.Combine(_userOverlaysDir, entry.Id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Path traversal through remote-controlled identifiers in OverlayRepositoryService. The catalog and the manifest are fetched over HTTP, and their Id and asset-name strings are passed straight to Path.Combine without confinement. Path.Combine returns the second argument when it is rooted, and it does not collapse .. segments. A compromised repository, a hijacked FOLICON_OVERLAY_REPO_URL, or a malicious .overlay-repo-url file can therefore delete and write files anywhere the user can.

  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs#L227-L228: validate entry.Id against a strict slug pattern before you build tmpDir and finalDir, and apply the same validation in UpdateOverlayAsync, UninstallOverlayAsync, IsOverlayInstalled, and GetInstalledVersion.
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs#L305-L307: resolve Path.Combine(targetDir, asset) with Path.GetFullPath and reject the asset when the result is not under targetDir, before Directory.CreateDirectory and File.WriteAllBytesAsync run.
📍 Affects 1 file
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs#L227-L228 (this comment)
  • FoliCon/Modules/Overlays/OverlayRepositoryService.cs#L305-L307
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/Overlays/OverlayRepositoryService.cs` around lines 227 - 228,
Constrain remote-controlled paths in
FoliCon/Modules/Overlays/OverlayRepositoryService.cs at lines 227-228 by
validating entry.Id against a strict slug pattern before constructing tmpDir and
finalDir, and apply the same validation in UpdateOverlayAsync,
UninstallOverlayAsync, IsOverlayInstalled, and GetInstalledVersion. At lines
305-307, resolve each asset path with Path.GetFullPath against targetDir, reject
paths outside targetDir before Directory.CreateDirectory or
File.WriteAllBytesAsync, and preserve safe asset writes.

Comment on lines +140 to +148
if (overlayDefinition == null)
{
IconOverlay.Legacy => await StaRenderer.Default.EnqueueRender(() =>
new Views.PosterIcon(new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility))
.RenderToBitmap()),
IconOverlay.Alternate => await StaRenderer.Default.EnqueueRender(() =>
new PosterIconAlt(new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility))
.RenderToBitmap()),
IconOverlay.Liaher => await StaRenderer.Default.EnqueueRender(() =>
new PosterIconLiaher(new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility))
.RenderToBitmap()),
IconOverlay.Faelpessoal => await StaRenderer.Default.EnqueueRender(() => new PosterIconFaelpessoal(new PosterIcon(
filmFolderPath, rating,
ratingVisibility, mockupVisibility, mediaTitle)).RenderToBitmap()),
IconOverlay.FaelpessoalHorizontal => await StaRenderer.Default.EnqueueRender(() => new PosterIconFaelpessoalHorizontal(
new PosterIcon(
filmFolderPath, rating,
ratingVisibility, mockupVisibility, mediaTitle)).RenderToBitmap()),
IconOverlay.Windows11 => await StaRenderer.Default.EnqueueRender(() =>
new PosterIconWindows11(new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility))
.RenderToBitmap()),
_ => await StaRenderer.Default.EnqueueRender(() =>
new Views.PosterIcon(new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility))
.RenderToBitmap())
};
Logger.Warn("No overlay definition provided for {FilmFolderPath}. Skipping icon creation.", filmFolderPath);
return;
}

icon = await StaRenderer.Default.EnqueueRender(() =>
new DynamicPosterIcon(overlayDefinition, new PosterIcon(filmFolderPath, rating, ratingVisibility, mockupVisibility, mediaTitle))
.RenderToBitmap());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

The caller treats this early return as success and deletes the source PNG.

When overlayDefinition is null, BuildFolderIco returns before it creates the .ico. BuildFolderIco returns Task, so TryCreateIconFromPng cannot detect the skip. Lines 87-90 then log "Icon Created", delete the downloaded PNG, and return true. The processed count reported to the user is wrong, and the poster is lost with no icon produced. The PNG File Not Found early return at line 119 has the same effect.

Return a status from BuildFolderIco and act on it.

🐛 Proposed fix
-    private static async Task BuildFolderIco(IconProperties iconProperties, PosterOverlayDefinition? overlayDefinition)
+    private static async Task<bool> BuildFolderIco(IconProperties iconProperties, PosterOverlayDefinition? overlayDefinition)
             if (overlayDefinition == null)
             {
                 Logger.Warn("No overlay definition provided for {FilmFolderPath}. Skipping icon creation.", filmFolderPath);
-                return;
+                return false;
             }

Return false at line 119 as well, return true at the end, and gate the caller on the result:

         var iconProperties = new IconProperties(iconMode, pngFilePath, item.Rating, ratingVisibility, mockupVisibility, item.Title);
-        await BuildFolderIco(iconProperties, overlayDefinition);
+        if (!await BuildFolderIco(iconProperties, overlayDefinition))
+        {
+            return false;
+        }
 
         Logger.Info("Icon Created for Folder: {Folder}", item.FolderName);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@FoliCon/Modules/utils/IconUtils.cs` around lines 140 - 148, Change
BuildFolderIco to return a success status: return false when the PNG is missing
or overlayDefinition is null, and return true only after the ICO is created.
Update TryCreateIconFromPng to await this result and log, delete the source PNG,
increment processing state, and return true only on success; preserve the
failure path without deleting the PNG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants