Skip to content

fix(grid): order startup layout - #71

Merged
keyldev merged 3 commits into
mainfrom
fix/grid-order-startup-layout
Sep 23, 2026
Merged

keyldev merged 3 commits into
mainfrom
fix/grid-order-startup-layout

Conversation

@keyldev

@keyldev keyldev commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes #69, closes #70.

  • Grid: tiles reused across a layout switch kept the previous layout's order, and drag-reorder persisted by page index, so a mismatched grid wrote a scrambled order to the DB. Tiles are now re-sorted to the stored order after each refresh, and reorder is persisted by camera id.
  • Settings → Appearance: start page (Library / Live) and the layout the Live grid opens on at startup (default: last used).
    Note: orders already scrambled by the old bug need one re-drag.

Related

Type

  • Bug fix
  • Feature
  • Refactor / cleanup
  • Docs / CI
  • Other:

Checklist

  • Builds with 0 warnings (TreatWarningsAsErrors=true).
  • Tests pass (dotnet test); new Core logic has unit tests.
  • No layering violation — App references Core only (Infrastructure / Video / Devices wired via DI in a head).
  • Scope stays within one phase (didn't pull work from a later phase's "Не входит").
  • README / docs updated if public commands, options, or setup changed.

Platforms tested

  • Windows
  • Linux
  • macOS
  • Android
  • iOS
  • CI build only

Screenshots / notes

…69)

- Reorder kept tiles to the layout's stored order after a refresh
- Persist drag-reorder by camera id instead of page offset + index
- Settings > Appearance: start in Library or Live
- Optional layout the Live grid opens on at startup (default: last used)
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Fix grid ordering and add startup page and layout preferences

🐞 Bug fix ✨ Enhancement ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Restores each layout’s stored camera order after tile refreshes and layout switches.
• Persists drag reordering by camera identity, avoiding page-index corruption from missing tiles.
• Adds configurable startup page and optional Live layout, with localized Appearance controls.
Diagram

graph TD
  appearance["Appearance UI"] --> settings["Settings VM"] --> prefs[("User Settings")] --> shell["Main Window"] --> grid["Live Grid"] --> tiles["Ordered Tiles"]
  settings --> layouts[("Layout Repository")] --> grid
  prefs --> grid --> layouts
Loading
High-Level Assessment

The current approach is appropriately scoped: preserving reusable tile view models avoids stream teardown while explicitly restoring repository order, and camera-ID-based persistence remains correct when pagination or closed tiles create gaps. Rebuilding every tile on refresh would simplify ordering but unnecessarily churn live sessions, while retaining page-offset persistence would preserve the underlying corruption risk.

Files changed (7) +144 / -20

Enhancement (5) +92 / -3
Localizer.csLocalize startup page and layout settings +6/-0

Localize startup page and layout settings

• Adds English and Russian labels for the start-page selector, startup-layout selector, and last-used layout option.

src/OpenIPC.Viewer.App/Services/Localizer.cs

MainWindowViewModel.csSelect the configured initial page +3/-1

Select the configured initial page

• Injects user settings and initializes the main window on Live when requested, retaining Library as the default.

src/OpenIPC.Viewer.App/ViewModels/MainWindowViewModel.cs

SettingsPageViewModel.csManage startup page and layout choices +56/-1

Manage startup page and layout choices

• Adds selectable start-page and startup-layout options, loads current layouts from the repository, and persists selections. Missing or deleted layouts fall back to the last-used option.

src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs

SettingsPage.axamlAdd Appearance startup controls +20/-0

Add Appearance startup controls

• Adds start-page and conditional startup-layout combo boxes to Appearance settings. The layout selector is shown only when Live is selected as the start page.

src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml

SettingsPage.axaml.csRefresh startup layouts when settings opens +7/-1

Refresh startup layouts when settings opens

• Reloads available layouts when the settings view receives its view model or reattaches to the visual tree, keeping the selector synchronized with Live-page changes.

src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml.cs

Bug fix (1) +46 / -16
GridPageViewModel.csRestore layout order and apply the startup layout +46/-16

Restore layout order and apply the startup layout

• Applies a configured Live layout once during the session’s first grid load. Reorders reused tile view models to repository order after refresh and persists drag changes by camera ID rather than page-relative indexes.

src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs

Other (1) +6 / -1
UserSettings.csPersist startup navigation preferences +6/-1

Persist startup navigation preferences

• Extends user settings with a startup page and optional startup layout ID. Defaults preserve the existing Library start page and last-used Live layout behavior.

src/OpenIPC.Viewer.App/Services/UserSettings.cs

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Fast navigation corrupts saved layouts ✓ Resolved 📎 Requirement gap ≡ Correctness
Description
MoveTileAsync captures the active layout before its first await but derives pageIds from the
mutable Tiles collection only after that await completes. If the user switches layouts or pages
while the repository read is pending, IDs from the newly displayed grid can be written into the
previous layout, altering an independent saved arrangement.
Code

src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs[R575-579]

+            var pageIds = Tiles.Select(t => t.Camera.Id).ToList();
+            var positions = pageIds.Select(id => full.IndexOf(id)).Where(p => p >= 0).OrderBy(p => p).ToList();
+            if (positions.Count != pageIds.Count) return;
+            for (var i = 0; i < positions.Count; i++)
+                full[positions[i]] = pageIds[i];
Evidence
Compliance rule 1 requires each layout's ordering to remain independent. The changed reorder path
captures layout a, awaits GetTilesAsync, and only afterward reads Tiles; navigation can
refresh that mutable collection during the await, while SetTilesAsync(a.Id, ...) still targets the
original layout.

Persist camera positions independently for each grid layout
src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs[566-580]
src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs[250-257]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteLayoutRepository.cs[91-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MoveTileAsync` reads the mutable `Tiles` collection after awaiting the repository, allowing navigation during that await to substitute another layout or page's camera order.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs[566-580]
## Recommended Fix
Snapshot the active layout ID and reordered camera IDs before the first await, then use only those snapshots when calculating and persisting positions. Serialize overlapping reorder writes if necessary so completion order cannot overwrite a newer drag operation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Layout picker failures leave no trace ✓ Resolved 🐞 Bug ◔ Observability
Description
LoadStartupLayoutsAsync catches every repository exception and returns without logging it or
notifying the view. If opening or querying the layout database fails on the first settings visit,
the picker remains limited to “Last used,” and neither the user nor logs explain why named layouts
are absent.
Code

src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs[R339-340]

+        try { all = await _layouts.GetAllAsync(CancellationToken.None).ConfigureAwait(true); }
+        catch (Exception) { return; }
Evidence
The newly added loader suppresses all exceptions from GetAllAsync, while the repository
implementation opens a database connection and executes a query that can fail. The settings page
invokes this loader asynchronously when its data context is assigned and when it attaches, so no
caller observes or reports the swallowed failure.

src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs[334-351]
src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteLayoutRepository.cs[21-28]
src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml.cs[24-29]
src/OpenIPC.Viewer.App/Views/Pages/SettingsPage.axaml.cs[33-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`LoadStartupLayoutsAsync` silently catches repository failures, so the startup-layout picker can remain incomplete without any diagnostic information.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs[336-340]
- src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs[163-188]
## Recommended Fix
Inject an `ILogger<SettingsPageViewModel>` and log the caught exception with context before returning. Preserve the current fallback behavior so a failed repository read does not break the settings page.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/OpenIPC.Viewer.App/ViewModels/GridPageViewModel.cs Outdated
Comment thread src/OpenIPC.Viewer.App/ViewModels/SettingsPageViewModel.cs Outdated
- Snapshot layout id and tile order before awaiting in MoveTileAsync
- Serialize tile-order writes so quick drags can't land out of order
- Log layout-picker load failures instead of swallowing them
@keyldev
keyldev merged commit 4a2ccd9 into main Sep 23, 2026
5 checks passed
@keyldev
keyldev deleted the fix/grid-order-startup-layout branch September 26, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add option to select layout to start with Camera positions in grid is not kept

1 participant