diff --git a/TASKS.md b/TASKS.md index b8d9a96..5842b71 100644 --- a/TASKS.md +++ b/TASKS.md @@ -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) | | 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) | +| 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 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 - adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. Next - slices (user-queued): multi-camera webcam selector in the App Settings dialog (gear), static (+) - catalog rows (Background, YouTubeEvent, quoted labels) with reason-greying. + adopts a solo camera as default. TASK 26's app-wide gate superseded by creator directive. The App + Settings webcam selector is now **TASK 40 Unit A** (plan saved 2026-09-22). Remaining next slice + (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) 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 diff --git a/TASKS/task-40-app-settings-round.md b/TASKS/task-40-app-settings-round.md new file mode 100644 index 0000000..92fbac8 --- /dev/null +++ b/TASKS/task-40-app-settings-round.md @@ -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 \ No newline at end of file diff --git a/ai.md b/ai.md index 0b0c0bd..4fd1666 100644 --- a/ai.md +++ b/ai.md @@ -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** (`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** β†’ - 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 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,