docs(plan): TASK 40 App Settings round — saved plan (units A-D, one Good Dog test each)

This commit is contained in:
2026-09-22 08:37:05 -07:00
parent 197ee815a0
commit 4d9188ab2e
3 changed files with 140 additions and 4 deletions
+11 -3
View File
@@ -55,6 +55,7 @@
| 37 | Defaults vs current layout split | ☐ Queued (2026-09-20) | [`TASKS/task-37-defaults-current-split.md`](TASKS/task-37-defaults-current-split.md) | | 37 | Defaults vs current layout split | ☐ Queued (2026-09-20) | [`TASKS/task-37-defaults-current-split.md`](TASKS/task-37-defaults-current-split.md) |
| 38 | Capture Window… backdrop (in-app window picker) | ✅ Done — shipped 2026-09-21 | [`TASKS/task-38-window-backdrop.md`](TASKS/task-38-window-backdrop.md) | | 38 | Capture Window… backdrop (in-app window picker) | ✅ Done — shipped 2026-09-21 | [`TASKS/task-38-window-backdrop.md`](TASKS/task-38-window-backdrop.md) |
| 39 | YPP journey tracker (slice 1: current-scope data) | 🔶 Slice 1 ✅ — 2026-09-22; slice 2 (Analytics + ETA) queued | [`TASKS/task-39-ypp-journey-tracker.md`](TASKS/task-39-ypp-journey-tracker.md) | | 39 | YPP journey tracker (slice 1: current-scope data) | 🔶 Slice 1 ✅ — 2026-09-22; slice 2 (Analytics + ETA) queued | [`TASKS/task-39-ypp-journey-tracker.md`](TASKS/task-39-ypp-journey-tracker.md) |
| 40 | App Settings round: active-camera picker + config dialog, gear move, defaults, accent | ☐ Queued (2026-09-22) — plan saved | [`TASKS/task-40-app-settings-round.md`](TASKS/task-40-app-settings-round.md) |
--- ---
@@ -73,9 +74,16 @@
slice shipped same day — webcam = app-level default, one per stream per scene, Add places the slice shipped same day — webcam = app-level default, one per stream per scene, Add places the
default directly, offered only when a camera is ATTAINABLE (identity + live session, not just a default directly, offered only when a camera is ATTAINABLE (identity + live session, not just a
saved identity), app base lock keeps the session after the last placement is removed, startup saved identity), app base lock keeps the session after the last placement is removed, startup
adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. Next adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. The App
slices (user-queued): multi-camera webcam selector in the App Settings dialog (gear), static (+) Settings webcam selector is now **TASK 40 Unit A** (plan saved 2026-09-22). Remaining next slice
catalog rows (Background, YouTubeEvent, quoted labels) with reason-greying. (user-queued): static (+) catalog rows (Background, YouTubeEvent, quoted labels) with reason-greying.
- **TASK 40 — App Settings round** — plan saved 2026-09-22 (`TASKS/task-40-app-settings-round.md`),
four units, one Good Dog test each: **A** active web camera section under Recordings (Change…
sub-menu = live enumeration incl. vcams; Tune… config dialog via `ICameraControlProbe`, live-only
tweaks), **B** gear moved top-left right of the wordmark (left-click App Settings, right-click
Bug/Feature/About), **C** persist output resolution default + surface default scene
transition/duration + confirm-before-End, **D** accent/theme color (consolidate 69 hardcoded
accent sites into a DynamicResource brush + picker).
- **TASK 37** — queued (2026-09-20): split the layout data into a `default` set (the established DB) - **TASK 37** — queued (2026-09-20): split the layout data into a `default` set (the established DB)
and a `current` set carrying the build-id; a saved `current` is honored only by the same build-id, and a `current` set carrying the build-id; a saved `current` is honored only by the same build-id,
otherwise defaults load — enables one-click revert to defaults too. Capture needed work OUT of otherwise defaults load — enables one-click revert to defaults too. Capture needed work OUT of
+127
View File
@@ -0,0 +1,127 @@
# TASK 40 — App Settings round: active camera + config dialog, gear move, defaults, accent
**Status:** ☐ Queued — **plan saved 2026-09-22** (creator: "save this plan to the memory
files"; implementation to start on the next task spec). Sub-units A → B → C → D, each a
separate scope-checked work unit with its own Good Dog integration test.
## Creator asks (2026-09-22, three)
1. **Settings → App Settings:** add a new section **under "Default Location for Recordings"**
for the *active web camera* — (a) triggering it queries Windows for the **current** list of
webcam resources **including vcams**, presented as a **sub-menu list**; (b) selecting a
camera opens a **dialog with the available configuration params for tweaking**.
2. The settings **gear** (bottom bar, lower-left, icon-only, context-menu) "looks lost" —
re-locate and/or label it.
3. "Surely there's more tweakables?" — add other settings.
## Locked decisions (user answers, 2026-09-22)
- **Three units A → B → C** (recommended split honored; one Good Dog test + one commit each).
- Camera tweaks are **live-only** — no persistence, no schema change in Unit A. A
per-camera "re-apply on preview start" preset stays a follow-up if ever wanted.
- Gear: **top-left, to the right of the "LlamaCasty" wordmark, gap ≈ 3 wordmark letters**,
icon-only; **left-click = App Settings** (direct), **right-click = Report Bug / Feature
Request / About** menu (relocated with the button off BottomBar).
- Unit C picks: **default output resolution**, **default scene transition + duration**,
**Confirm before End Stream**.
- **Accent/theme color** was picked too, but it is heavy (69 hardcoded
`#e94560`/`#ef4444`/`#22c55e` sites across 15 files + VM brush literals + the _persisted_
per-source `Source.ChatMemberColor`) → split out as **Unit D** with its own scope + test.
## Facts this plan sits on (all verified in code, 2026-09-22)
- Webcam identity is **app-level**: `LayoutStore.Webcam` (Webcam table), picked via
`CameraPickerDialog` on first Add, swapped via "Change Webcam…" context menu
(`MainViewModel.Webcam.cs` → `ChangeWebcamAsync` / `SwapWebcamIdentityAsync`: release old,
re-acquire per scene config, persist).
- Startup resource check auto-locks only when **exactly one** camera is found; **≥2 cameras →
deliberately no auto-lock, selection left to the App Settings dialog (gear)** (ai.md §Startup
resource lifecycle) — this plan's Unit A IS that selector.
- Enumeration is already "current list incl. vcams": `MediaCaptureCameraEnumerator` →
`DeviceInformation.FindAllAsync(DeviceClass.VideoCapture)` (same source OBS consumes;
caveat: a vcam must be a *registered capture device* to appear).
- **Default scene transition type + duration are persisted (`TransitionType` /
`TransitionDurationMs`) and loaded every start, but editable NOWHERE in the UI** (nothing in
XAML binds `SelectedTransitionType`/`TransitionDurationMs`) — Unit C surfaces an existing knob.
- **Output resolution resets every launch** (`_selectedQuality = QualityOptions[0]` in the
MainViewModel ctor; bottom-bar ComboBox not persisted).
- Gear today: `Controls/BottomBar.xaml:13` (far-left column 0), icon-only, ContextMenu
(App Settings / Report Bug / Feature Request / About); click handler `BottomBar.xaml.cs`.
## Unit A — App Settings "Active Web Camera" (your ask 1)
New group **under** Recordings in the App Settings overlay (`OverlayHost.xaml:39` settings
StackPanel): current camera display + **Change…** + **Tune…**.
- **Change…** → **fresh enumeration per trigger** → ContextMenu **sub-menu** listing cameras
(current = checkmarked; disabled "No cameras found" item when empty). Selection:
`_webcam == null` → adopt + base-lock (mirrors the single-camera startup path); else →
`SwapWebcamIdentityAsync` (release old / re-acquire per scene / Webcam row persisted).
- **Tune…** (enabled when a camera is active) → **CameraConfigDialog** (new window; mirrors
`CameraPickerDialog.xaml` owner/geometry/theme): probes the device's
`VideoDeviceController` **MediaDeviceControl**s — Brightness, Contrast, Hue, Saturation,
Sharpness, Gamma, WhiteBalance, BacklightCompensation, Exposure, Focus, Pan, Tilt, Zoom —
showing only the ones the device reports. Each row: **Auto checkbox** (when
`Capabilities.AutoSupported`) + **Slider Min/Max/Step** applying live via
`TrySetValue`/`TrySetAuto`; empty state "No adjustable controls on this camera" (common on
cheap cams and vcams).
- **⚠ Tech risk, verified during implementation (not assumed):** whether MediaDeviceControl
writes succeed under our `MediaCaptureSharingMode.SharedReadOnly` init (the MJPG
stream-property negotiation already works there, but extended controls are a separate
path). Mitigation ladder: (1) probe-capture `SharedReadOnly` → (2) the running frame
source's own `VideoDeviceController` → (3) read-only controls + an "applies to a stopped
preview" note. **Spin-guard:** a second failed attempt = research the established answer
(OBS does this via DirectShow VideoProcAmp) before writing a third.
- **Test seam:** `Services/CameraControlProbe.cs` — `ICameraControlProbe` (capability/value/
auto read + `TrySet*`) with a MediaCapture-backed implementation; the Good Dog test drives a
fake that records calls. (The WinRT/hardware probe itself is not unit-testable.)
| Unit A — declared files | |
|---|---|
| touch | `Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Webcam.cs`, `ViewModels/MainViewModel.cs` |
| new | `Services/CameraControlProbe.cs`, `CameraConfigDialog.xaml(+.cs)`, `ViewModels/CameraConfigViewModel.cs`, `ytLive.Tests/AppSettingsWebcamTests.cs` |
| docs | `TASKS.md`, `ai.md`, `TASKS/task-40-app-settings-round.md`, `HANDOFF.md` |
**Good Dog test `AppSettingsWebcamTests`** (RealApp + temp DB, reuses existing
`CameraEnumeratorOverride` / `CameraFrameSourceFactoryOverride` seams):
(1) settings panel → Change… lists the fake cameras (incl. vcam-styled names); (2) pick B when
A is current → identity swapped, A released, B acquired, Webcam row persisted, B checkmarked;
(3) pick when `_webcam == null` → adopt + base-lock acquired; (4) fake `ICameraControlProbe`
drives `CameraConfigViewModel` — Auto + slider changes forward to the probe.
## Unit B — gear to top-left (your ask 2)
- `Controls/TopBar.xaml`: `IconButton` gear **right of the brand Button**, gap ≈ 3 wordmark
letters; left-click `OpenSettingsCommand`; ContextMenu keeps Report Bug / Feature
Request / About. Remove the gear (+ its click handler) from `Controls/BottomBar.xaml(.cs)`.
- Bonus: TopBar sits inside the overlay scrim span (`OverlayHost Grid.RowSpan=3` covers rows
0–2), so the gear is correctly covered while an overlay is open — today it sits below the scrim.
## Unit C — the small defaults (your ask 3, rest)
- **Default output resolution** — persist `SelectedQuality` (e.g. Settings key
`Output.Resolution`), restore at startup.
- **Default scene transition + duration** — surface the persisted-but-uneditable
`TransitionType`/`TransitionDurationMs` as controls in App Settings.
- **Confirm before End Stream** — toggle; when on, `EndStreamCommand` confirms before stopping
live/recording.
- One Good Dog test for the unit + docs in the same commit.
## Unit D — accent / theme color (own unit; not merged into C)
- Consolidate the hardcoded accent literals into a `Themes/Controls.xaml` accent brush,
consumed via `DynamicResource` across the ~15 files (brand "ty" run, YtButton primary,
selected rings, live toolbar/glow, meters, trax, etc.). `Source.ChatMemberColor` (persisted
per-source) stays per-source.
- Picker + persistence via a Settings key (`App.AccentColor`).
- Own Good Dog test (accent propagates) + docs (`ai.md` gains the hardcoded-accent →
resource-brush consolidation note).
- Caveat for the plan file: this is the widest diff of the round; the scope-check file list
will be long by necessity and every touch is a genuine dependency of the consolidation.
## Memory updates (this plan's save, 2026-09-22)
- [`TASKS/task-40-app-settings-round.md`](task-40-app-settings-round.md) (new — this file)
- `TASKS.md` — row 40 + webcam "next slices" bullet (selector → TASK 40) + TASK 40 open item
- `ai.md` — ≥2-camera bullet points at TASK 40 Unit A
- `HANDOFF.md` — in-flight plan + next step
+2 -1
View File
@@ -453,7 +453,8 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
`CameraManager.AcquireAsync(device)` as an app-wide lock — on failure a **persistent red alert** `CameraManager.AcquireAsync(device)` as an app-wide lock — on failure a **persistent red alert**
(`WebcamLockAlert`, bottom of the Layers panel with a Retry button) re-polls every 5 s (`WebcamLockAlert`, bottom of the Layers panel with a Retry button) re-polls every 5 s
(`_webcamLockPollTimer`) until a lock succeeds, and any first real frame also clears it; **≥2** → (`_webcamLockPollTimer`) until a lock succeeds, and any first real frame also clears it; **≥2** →
deliberately no auto-lock — selection belongs to the App Settings dialog (gear) **next slice**. deliberately no auto-lock — selection belongs to the App Settings dialog (gear) **queued — TASK 40
Unit A (App Settings round, plan saved 2026-09-22)**.
The single-camera branch **always acquires** (it no longer skips when configs already hold the The single-camera branch **always acquires** (it no longer skips when configs already hold the
session): that acquire is the app's own BASE ref, so removing every scene's placement leaves session): that acquire is the app's own BASE ref, so removing every scene's placement leaves
`RefCount = 1` and the session alive — the gate stays satisfied and the Web Cam row stays offered, `RefCount = 1` and the session alive — the gate stays satisfied and the Web Cam row stays offered,