feat(ui/ux): UI rework & upgrade - #75
Conversation
- Live | Stills segmented switch replaces the camera toggle - Grid size is one dropdown on the tabs row (per-layout), showing the current shape - Layout actions move into the active tab's menu (… / right-click); + Layout closes the tab row - Center wide Lucide glyphs (camera, volume) on tile and camera-page buttons
- Re-importing a backup (or importing onto a fresh install) no longer duplicates tabs - Existing same-named layout gets the imported grid size and tiles - Test: importing twice keeps one layout per name
- Bare TcpClient() is a dual-mode IPv6 socket; with IPv6 off or dropped by a VPN it fails on IPv4 cameras (WSAEADDRNOTAVAIL) - Every camera read offline in the library, grid and Health Center while streams played fine - Resolve the host and try each address with a matching IPv4/IPv6 socket
- Search by name/IP/group, status chips with live counts, 20 cameras per page - One row per camera: preview slot + status dot, host, layouts, web/SSH/more - Row menu (… / right-click) holds edit, device, files, delete and a Layouts submenu that replaces the "in grid" checkbox - Header regrouped: Find on network, QR, Add camera; groups and refresh as icons
…calendar - Camera / period / motion-only filters, day headers with totals, 20 rows per page - Dense rows: play, camera, time range, duration, size, motion / in-progress badges, menu (show in folder, copy file, delete) - Calendar fills recording days by count, dots event-only days, follows the camera/motion filter, localized weekdays - Empty month offers a jump to the nearest month with recordings
- Camera / type / period filters (default 7 days), day headers, 20 rows per page - Bursts of one camera and kind within a minute fold into one episode with merged class counts - Rows show what was detected (localized COCO classes), play the covering recording from the event moment, open the camera - Simulate motion moves into the header overflow; chip style no longer picks up Fluent's checked fill
- Camera dropdown becomes a picker: search by name/IP/group, tick several cameras, per-camera counts with a bar - Defaults to cameras that have events in the period/type; "All cameras" lists every one - Selected cameras show as removable chips; clicking a camera name in a row filters to it
- CameraPickerViewModel + Controls/CameraPickerView carry the search, counts, multi-select and chips - Events moves onto it unchanged; Recordings swaps its camera dropdown for it (counts = recordings per camera) - Calendar highlight follows the ticked cameras
- Status badge + stat tiles; when the engine isn't running, explain how it starts - Per-camera detection list: on/off switch, class chips, fps/threshold, REC, 24 h count, Configure (camera editor via shared CameraEditService); enable on more cameras from a searchable list - Last 24 h class summary with a link to Events filtered to AI detections - Detected classes render as icon chips (person, car, truck/bus, bike) here and in Events rows
- Header shows camera + date/time/length/size; previous/next recording and a ⋯ menu (folder, copy, delete) - Transport: ±10 s, wall-clock time, 0.5×–8× speed (IPlaybackSession.Rate), Space/←/→/Home/End hotkeys - Clip mode replaces the export bar; save the current frame into snapshots - Event sidebar hidden when empty, localized filter, class-icon rows; auto-continue into an adjacent recording
- IPlaybackSession.StepFrame: forward decodes one frame, backward re-seeks to the previous one - Seek target now survives across packets, so seeks land on the exact frame instead of the keyframe - Shared FfmpegAudioDecoder (live + file); file audio plays at 1x only; MediaInfo.HasAudio
- Frame step buttons and , / . keys; PgUp/PgDn for previous/next recording - Mute + volume shared with the live page (M key), shown when the file has audio - Fullscreen via button, F or F11; Esc exits; survives previous/next and auto-continue - Centre the header ellipsis; event sidebar now really hides below the compact breakpoint
- Address + login first; Connect detects OpenIPC/ONVIF, fills streams, ports and name, and shows a live frame - Failure card says what went wrong (unreachable, credentials, no video) - Advanced settings in collapsible rows with one-line summaries; per-field validation - Pasted rtsp:// links are split into fields; new group inline; dark header replaces the title bar
- Quick ONVIF/mDNS scan starts on open; rows show kind, model, address and their own Add - Cameras already in the library listed separately; empty state points to deep scan or manual entry - Deep scan in a collapsible footer: subnets, extra range, start/stop, live progress - Add hands the device to the camera editor, which connects straight away with the session login
PR Summary by QodoRedesign camera workflows, events, recordings, and playback
AI Description
Diagram
High-Level Assessment
Files changed (59)
|
Code Review by Qodo
1. Imports leave stale cameras in layouts
|
| var detections = await _events.ListAsync(null, EventKind.Detection, since, 10000, ct).ConfigureAwait(true); | ||
| var perCamera = detections.GroupBy(e => e.CameraId).ToDictionary(g => g.Key, g => g.Count()); |
There was a problem hiding this comment.
2. Busy sites show understated detections 🐞 Bug ≡ Correctness
ReloadAsync calculates the 24-hour camera and class totals from a detection query capped at 10,000 rows. Once a site records more than 10,000 detections in that window, older qualifying events are excluded from both displayed summaries without any indication of truncation.
Agent Prompt
## Issue description
The 24-hour detection summaries silently undercount when more than 10,000 events exist.
## Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/AnalyticsPageViewModel.cs[113-133]
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteEventRepository.cs[24-44]
## Recommended Fix
Aggregate camera and class counts over the entire time window, using a repository aggregation or paginated reads rather than a fixed-limit event list.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| var ap = fmtCtx->streams[i]->codecpar; | ||
| // Only audio this build can actually decode counts. | ||
| if (ap->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO && ffmpeg.avcodec_find_decoder(ap->codec_id) != null) | ||
| hasAudio = true; |
There was a problem hiding this comment.
3. Some recordings offer sound but play silent 🐞 Bug ≡ Correctness
FfmpegMediaProbe sets HasAudio when any audio stream has a decoder, while FfmpegAudioDecoder.TryOpen selects only the first audio stream. For a recording whose first audio stream is unsupported but a later one is supported, the player enables listening even though its playback session produces no audio frames.
Agent Prompt
## Issue description
Audio probing can advertise a supported stream that playback never selects.
## Fix Focus Areas
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegMediaProbe.cs[45-51]
- src/OpenIPC.Viewer.Video/Pipeline/FfmpegAudioDecoder.cs[44-62]
## Recommended Fix
Make probing and decoder setup select audio streams consistently, preferably by trying subsequent streams when the first cannot be decoded.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| if (tileIds.Count > 0) | ||
| await _layouts.SetTilesAsync(newId, tileIds, ct).ConfigureAwait(false); | ||
| await _layouts.SetTilesAsync(layoutId, tileIds, ct).ConfigureAwait(false); |
There was a problem hiding this comment.
1. Imports leave stale cameras in layouts 🐞 Bug ≡ Correctness
ImportAsync reuses a same-named layout but calls SetTilesAsync only when the filtered imported tileIds collection is non-empty. When the imported layout has no tiles or none of its camera IDs exist locally, the call that would delete the existing LayoutTiles rows is skipped, leaving the layout's previous tiles in place.
Agent Prompt
## Issue description
`ImportAsync` skips replacing tile membership when an imported layout has no locally valid camera IDs, preserving stale tiles on a same-named layout.
## Fix Focus Areas
- src/OpenIPC.Viewer.Infrastructure/Persistence/SqliteConfigBackupService.cs[156-174]
- tests/OpenIPC.Viewer.Infrastructure.Tests/SqliteConfigBackupServiceTests.cs[1-100]
## Recommended Fix
Call `SetTilesAsync(layoutId, tileIds, ct)` unconditionally after filtering imported tile IDs, including when the list is empty; it already clears existing membership before inserting replacement rows. Add a test that imports an empty layout over a same-named layout with existing tiles.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Period changes what is loaded; cameras, kind and grouping only re-slice. | ||
| partial void OnPeriodChanged(EventPeriod value) => _ = ReloadAsync(CancellationToken.None); | ||
| partial void OnKindFilterChanged(EventKindFilter value) => ApplyFilter(resetPage: true); | ||
| partial void OnGroupBurstsChanged(bool value) => ApplyFilter(resetPage: true); |
There was a problem hiding this comment.
4. Event filters can show an older period 🐞 Bug ≡ Correctness
OnPeriodChanged starts ReloadAsync fire-and-forget without cancelling or versioning an earlier reload, while ReloadAsync writes its fetched events into shared _events after multiple awaited repository calls. When a user selects periods in quick succession, an older request can finish last and replace the list with its old time range even though Period now holds the newer selection.
Agent Prompt
Issue description
Period changes can overlap event reloads, allowing a stale request to overwrite the results for the user's current period.
Fix Focus Areas
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[105-108]
- src/OpenIPC.Viewer.App/ViewModels/EventsPageViewModel.cs[211-243]
Recommended Fix
Maintain a reload cancellation token source or monotonically increasing reload generation. Capture it before each reload and, after awaited repository calls, only update `_events`, `_recordings`, loading state, and filtered rows if that request remains current; cancel the preceding request when the period changes.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
UI redesign for couple of screens.
Related
Type
Checklist
TreatWarningsAsErrors=true).dotnet test); new Core logic has unit tests.AppreferencesCoreonly (Infrastructure / Video / Devices wired via DI in a head).Platforms tested
Screenshots / notes