Webcam menu gates on the app-wide identity (TASK 26)
This commit is contained in:
+25
-35
@@ -1,54 +1,44 @@
|
|||||||
# HANDOFF — Session State
|
# HANDOFF — Session State
|
||||||
|
|
||||||
## Branch
|
## Branch
|
||||||
`task25-backgrounds` @ `7dd140a` — **committed and pushed** (includes TASK 24 `b400ab5`). Working tree clean.
|
`task26-webcam-menu-gate` @ working tree — **code + docs complete, NOT yet committed** (commit on your word). Parent: `task25-backgrounds` @ `7dd140a`, committed and pushed, clean.
|
||||||
|
|
||||||
## Shipped: TASK 25 background consolidation (+ TASK 24 toasts beneath it)
|
## Shipped: TASK 26 — webcam (+) menu gates on the app-wide identity
|
||||||
|
|
||||||
Locked model (user directive): exactly ONE "Background" per screen at index 0, non-deletable/
|
Creator's visual pass of TASK 25 came back green except one item: the (+) menu's Webcam row wasn't
|
||||||
non-reorderable, NOT in the (+) menu; capture context-sensitivity only on the Live screen
|
greyed out when a webcam was already deployed in another scene. Root cause: `CanAddWebcamToStagedScene`
|
||||||
(Show Desktop/monitor switching kept intact); exactly five default images in the DB; stale rows cleaned.
|
only checked the staged scene's config; the truth is the `_webcam` singleton.
|
||||||
|
|
||||||
1. **Seeders consolidated** (`ViewModels/MainViewModel.cs`): five clone seeders + `EnforceBackgroundPolicy`
|
1. **Renamed + regated:** `CanAddWebcamToStagedScene` → **`CanAddWebcam`** =
|
||||||
deleted. Now: `EnsureBackground` → `CreateBackground(name)` — Live = DisplayCapture row, others =
|
`StagedScene != null && _webcam == null` (`MainViewModel.cs:308`). Raised at all five sites:
|
||||||
static Background art row, all named "Background". `NormalizeBackgrounds(scenes)` heals any layout:
|
StagedScene setter, `ReacquireWebcam` (layout load), `OnElementsChanged`, `_webcam` create
|
||||||
correct-flavor keep (in-place Type conversion re-asserts `IsBackground` because `Source.Type`'s setter
|
(`AddWebcamToStagedSceneAsync`), last-config removal (`RemoveElement`).
|
||||||
derives it), duplicates dropped, missing seeded + INSERTED, index 0, non-canonical stripped.
|
2. **XAML:** the Webcam MenuItem's `IsEnabled` binding renamed to match; tooltip verbatim.
|
||||||
`HealBackgrounds()` = Normalize + `StampDefaultBackgroundAsset` (pack-resource art via `AddAsset`;
|
3. **Test (Good Dog Rule — ONE):** `WebcamMenuGateTests.CanAddWebcam_Gates_On_The_AppWide_Webcam_Identity`
|
||||||
custom Browse wins). Called from LoadLayout, StagedScene setter, ReacquireScreenCaptures.
|
— real window + temp DB seeded with a `Webcam` row + one config in Starting; asserts greyed while
|
||||||
2. **Settings purge:** `LayoutStore.Save` deletes orphaned `BackgroundUseDefault_{id}`/`BackgroundPath_{id}`
|
Live is staged (the old gate's false positive), greyed while Starting staged, re-enabled after
|
||||||
keys (ids gone from Source). Fixes the real DB's 16 stale keys.
|
`RemoveSourceCommand` drops the last config, and zero `Webcam` rows after save.
|
||||||
3. **UI:** (+) menu Screen+Background items removed (`MainWindow.xaml`); `AddSource` refuses capture/background
|
4. **Suite: 224 total, 223 pass.** The only failure is the known pre-existing
|
||||||
params. `CanChangeBackground` = staged-is-Live → gates preview CanvasGrid menu directly and the layer-row
|
`AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` (unrelated).
|
||||||
menu via new MultiBinding converter `Helpers/AllTrueToVisibilityConverter.cs` (row Tag carries VM bool).
|
Build 0 warnings.
|
||||||
4. **Mini rule:** minis never render captures. `LoadBackgroundImage` is flavor-blind (`IsBackground`) — fixes
|
5. **Docs same commit:** ai.md (gate rule + rename; dropped the stale "empty-canvas right-click
|
||||||
green mini bug (was `Type==Background`, missed Live's DisplayCapture row → placeholder twin at old :951 too);
|
Show Webcam" claim — that XAML never shipped), TASKS.md TASK 3 row 27, this file.
|
||||||
`RefreshSnapshotsAsync` shows green placeholder only while Live is staged, otherwise its stored art.
|
6. **Dead code flagged, not touched:** `ShowWebcamCommand` / `CanShowWebcamInStagedScene` are wired
|
||||||
5. **Tests:** suite 221/218 → **223 total, 220 pass**. The ONE integration test:
|
in the VM but bound nowhere — audit item, separate branch if wanted.
|
||||||
`BackgroundHealIntegrationTests.Dirty_Layout_Heals_To_One_Locked_Background_Per_Scene` (seeds dirty temp DB,
|
|
||||||
real window, asserts heal + save purges keys). Unit: SceneCatalogTests rewritten (NormalizeBackgrounds heal +
|
|
||||||
strip + per-scene flavor), BackgroundTests empty-scene flavor updated, SourceNamingTests filters IsBackground.
|
|
||||||
The two stale landmine tests (#2/#3) are healed by this branch — see below.
|
|
||||||
6. **Docs same commit:** ai.md (TASK 25 model section replaces backdrop-era text), TASKS.md TASK 25 entry,
|
|
||||||
Services/index.md (LayoutStore purge), Helpers/index.md (new converter), this file.
|
|
||||||
7. Build **0 warnings**.
|
|
||||||
|
|
||||||
## ⚠️ Landmines
|
## ⚠️ Landmines
|
||||||
|
|
||||||
- **User verdict (2026-08-23):** "The AI wrote fucked up code after repeatedly and intentionally misunderstanding instructions." Extends the ai.md 2026-08-22 audit flag — trust nothing from those sessions without manual verification.
|
- **User verdict (2026-08-23):** "The AI wrote fucked up code after repeatedly and intentionally misunderstanding instructions." Extends the ai.md 2026-08-22 audit flag — trust nothing from those sessions without manual verification.
|
||||||
- **Pre-existing test failure is now ONE (not caused by these branches):**
|
- **Pre-existing test failure is ONE** (`AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback`) — not caused by this branch.
|
||||||
1. `AudioPipelineTests.Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback`
|
|
||||||
- Former #2 SourceNaming / #3 SceneCatalog policy tests were repaired BY TASK 25 (expectation updates to the locked model) — not skipped, not deleted.
|
|
||||||
- **Real-MainWindow tests MUST set `LayoutPathOverride`** (ai.md) — DB pollution wiped real data once.
|
- **Real-MainWindow tests MUST set `LayoutPathOverride`** (ai.md) — DB pollution wiped real data once.
|
||||||
- **`Source.Type` setter derives `IsBackground`** (Models/Source.cs:40) — any Type change must re-set IsBackground after.
|
- **`Source.Type` setter derives `IsBackground`** (Models/Source.cs:40) — any Type change must re-set IsBackground after.
|
||||||
- **vstest quirk:** only one `--tests:` param allowed; plain substrings work (`--tests:BackgroundHeal`), alternation regexes silently match nothing.
|
- **vstest quirk:** only one `--tests:` param allowed; plain substrings work, alternation regexes silently match nothing.
|
||||||
- Notification.Wpf gotchas (ai.md/TASKS.md): area routing by XAML `Name`; unknown AreaName silently drops; `NeverExpires()` = `TimeSpan.MaxValue`; `ToHex()` = `#AARRGGBB`.
|
- Notification.Wpf gotchas (ai.md/TASKS.md): area routing by XAML `Name`; unknown AreaName silently drops; `NeverExpires()` = `TimeSpan.MaxValue`; `ToHex()` = `#AARRGGBB`.
|
||||||
|
|
||||||
## Next steps
|
## Next steps
|
||||||
1. Commit this branch (code + docs, one commit), then merge/PR per Good Dog Rule (one integration test ✓).
|
1. Commit this branch (code + docs, one commit), then merge/PR per Good Dog Rule (one integration test ✓).
|
||||||
2. User visual pass on Windows: minis show art when unstaged (Live included), green placeholder while staged;
|
2. Creator visual pass on Windows: add a webcam to any scene → the (+) menu's Webcam row greys out everywhere; remove it → row re-enables.
|
||||||
no Game Capture rows in layer lists; (+) menu trimmed; capture controls only on staged Live.
|
3. Then merge `task25-backgrounds` → main (still pending from last session), relaunch app once so the real `%APPDATA%` DB self-heals.
|
||||||
3. Then relaunch app once so the user's real `%APPDATA%` DB self-heals on load + first save purges stale keys.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+1
-1
@@ -276,7 +276,7 @@
|
|||||||
<Button.ContextMenu>
|
<Button.ContextMenu>
|
||||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||||
<MenuItem Header="Webcam" Command="{Binding AddWebcamCommand}"
|
<MenuItem Header="Webcam" Command="{Binding AddWebcamCommand}"
|
||||||
IsEnabled="{Binding CanAddWebcamToStagedScene}"
|
IsEnabled="{Binding CanAddWebcam}"
|
||||||
ToolTip="One webcam at a time — it's already in your stream"/>
|
ToolTip="One webcam at a time — it's already in your stream"/>
|
||||||
<MenuItem Header="Image" Command="{Binding AddImageCommand}"/>
|
<MenuItem Header="Image" Command="{Binding AddImageCommand}"/>
|
||||||
<MenuItem Header="Text" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.TextOverlay}"/>
|
<MenuItem Header="Text" Command="{Binding AddSourceCommand}" CommandParameter="{x:Static models:SourceType.TextOverlay}"/>
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ Lifecycle: `created → ready → [testing] → live → complete` (transitional
|
|||||||
26. ✅ **Left panel spacing + context menu reorder** — HR margins matched, Row 4 changed from `*` to `Auto`. Context menu reordered: Border Thickness first, then Opacity, Color, Effect.
|
26. ✅ **Left panel spacing + context menu reorder** — HR margins matched, Row 4 changed from `*` to `Auto`. Context menu reordered: Border Thickness first, then Opacity, Color, Effect.
|
||||||
21. ✅ **Logo + richer in-app About (2026-08-13, queued → 2026-08-14 SHIPPED)** — the ytLlive wordmark in the top bar opens the About overlay (already wired); the About overlay is now the **creator hub**: the real logo (the "llama fortnite superman logo" from the creator's vault, copied to `Assets/llama-logo.png` — the creator's own art, no third-party license), plus **creator-hub links** — llama chile shop on YouTube (`MainViewModel.ChannelUrl`), Mastodon (`https://mastodon.llamachile.tube/@gramps`), **Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — live), and **Unlock Premium** (greyed "coming soon" — the **billing** product URL is a tabled seam, `PremiumUrl`, until TASK 10 picks the subscription provider). A **Licenses & legal** button flips the About overlay to an in-app scrolling panel that loads the full `THIRD-PARTY-NOTICES.txt` text (`MainViewModel.ShowLicenses` reads the shipped file from the executable directory on first open; graceful "not found" fallback — **never the OS viewer, everything stays in-app**); "← Back to About" returns. The About overlay is also the in-app home of the notices — the top-bar About button that opened the file in the OS viewer was removed on 2026-08-13 for exactly this reason. Integration test (Good Dog Rule — ONE): `AboutHubTests.About_Opens_InApp_Licensing_Loads_Shipped_Notices` drives the real window + VM, asserting the hub opens, the link URLs are real, the licensing panel loads the shipped notices text (contains "Third-Party Notices" + "LGPL"), and back returns to the hub. 174 tests passing, 0 warnings
|
21. ✅ **Logo + richer in-app About (2026-08-13, queued → 2026-08-14 SHIPPED)** — the ytLlive wordmark in the top bar opens the About overlay (already wired); the About overlay is now the **creator hub**: the real logo (the "llama fortnite superman logo" from the creator's vault, copied to `Assets/llama-logo.png` — the creator's own art, no third-party license), plus **creator-hub links** — llama chile shop on YouTube (`MainViewModel.ChannelUrl`), Mastodon (`https://mastodon.llamachile.tube/@gramps`), **Buy me a coffee** (`https://buymeacoffee.com/llamachiley` — live), and **Unlock Premium** (greyed "coming soon" — the **billing** product URL is a tabled seam, `PremiumUrl`, until TASK 10 picks the subscription provider). A **Licenses & legal** button flips the About overlay to an in-app scrolling panel that loads the full `THIRD-PARTY-NOTICES.txt` text (`MainViewModel.ShowLicenses` reads the shipped file from the executable directory on first open; graceful "not found" fallback — **never the OS viewer, everything stays in-app**); "← Back to About" returns. The About overlay is also the in-app home of the notices — the top-bar About button that opened the file in the OS viewer was removed on 2026-08-13 for exactly this reason. Integration test (Good Dog Rule — ONE): `AboutHubTests.About_Opens_InApp_Licensing_Loads_Shipped_Notices` drives the real window + VM, asserting the hub opens, the link URLs are real, the licensing panel loads the shipped notices text (contains "Third-Party Notices" + "LGPL"), and back returns to the hub. 174 tests passing, 0 warnings
|
||||||
22. ✅ **Voice filters on the mic channel (2026-08-13 queued → SHIPPED 2026-08-14 inside TASK 9, the audio milestone)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Noise suppression = a **pure-C# noise gate** (creator chose over RNNoise / a second ffmpeg `afftdn` pipe, 2026-08-14 — KISS). Always-on — no UI knobs; the mic stays the creator's single audio control
|
22. ✅ **Voice filters on the mic channel (2026-08-13 queued → SHIPPED 2026-08-14 inside TASK 9, the audio milestone)** — the standard four applied to the sound input path (before the meter/encoder mix): **bass boost, treble, noise suppression, compressor** (set decided with the creator 2026-08-13). Noise suppression = a **pure-C# noise gate** (creator chose over RNNoise / a second ffmpeg `afftdn` pipe, 2026-08-14 — KISS). Always-on — no UI knobs; the mic stays the creator's single audio control
|
||||||
|
27. ✅ **Webcam row gates on the app-wide identity (2026-08-24, TASK 26)** — the (+) menu's Webcam item greys out whenever a webcam exists **anywhere** (`CanAddWebcam` = `StagedScene != null && _webcam == null`; renamed from `CanAddWebcamToStagedScene` whose per-scene rule let a second picker run from a scene lacking the config), raised at both `_webcam` mutation sites (create / last-config removal) + scene staging + elements change. Creator's visual pass found it: minis/no-capture-rows/capture-controls all good. ONE integration test `WebcamMenuGateTests.CanAddWebcam_Gates_On_The_AppWide_Webcam_Identity` (real window + temp DB seeded with a webcam in Starting; asserts greyed while Live staged, re-enabled after `RemoveSourceCommand` clears the last config + the `Webcam` DB row). Stale map fixed in the same commit: ai.md's "empty-canvas right-click Show Webcam" claim dropped — that XAML never shipped (`ShowWebcamCommand`/`CanShowWebcamInStagedScene` are wired but unbound dead code, audit item). 224 tests (223 pass; the pre-existing AudioPipelineTests failure is unrelated)
|
||||||
|
|
||||||
### The Minimal Source Set (design decision — do not expand casually)
|
### The Minimal Source Set (design decision — do not expand casually)
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
|
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
|
||||||
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
|
||||||
OnPropertyChanged(nameof(CanChangeBackground));
|
OnPropertyChanged(nameof(CanChangeBackground));
|
||||||
OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||||
OnPropertyChanged(nameof(IsCountdownAvailable));
|
OnPropertyChanged(nameof(IsCountdownAvailable));
|
||||||
UpdateActiveBackground();
|
UpdateActiveBackground();
|
||||||
@@ -303,8 +303,9 @@ public class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The Add → Webcam menu item: enabled when the active scene doesn't show the webcam yet.</summary>
|
/// <summary>The Add → Webcam menu item: enabled only while no webcam exists anywhere —
|
||||||
public bool CanAddWebcamToStagedScene => StagedScene?.WebcamConfig == null;
|
/// one camera identity app-wide; re-adding always opens the picker.</summary>
|
||||||
|
public bool CanAddWebcam => StagedScene != null && _webcam == null;
|
||||||
|
|
||||||
// ─── Transitions (TASK 19/23) ───
|
// ─── Transitions (TASK 19/23) ───
|
||||||
|
|
||||||
@@ -344,8 +345,6 @@ public class MainViewModel : ViewModelBase
|
|||||||
_transition.Start(SelectedTransitionType, TransitionDurationMs, null);
|
_transition.Start(SelectedTransitionType, TransitionDurationMs, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>The Add → Webcam menu item: enabled when the active scene doesn't show the webcam yet.</summary>
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Right-click-on-preview → "Show Webcam": offered when the active scene has no
|
/// Right-click-on-preview → "Show Webcam": offered when the active scene has no
|
||||||
/// webcam config (add one) or hides it (unhide — keeps the config row).
|
/// webcam config (add one) or hides it (unhide — keeps the config row).
|
||||||
@@ -1815,7 +1814,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
_ = _cameraManager.ReleaseAllAsync(previousDevice);
|
_ = _cameraManager.ReleaseAllAsync(previousDevice);
|
||||||
|
|
||||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||||
OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
|
|
||||||
if (_webcam == null || string.IsNullOrWhiteSpace(newDevice)) return;
|
if (_webcam == null || string.IsNullOrWhiteSpace(newDevice)) return;
|
||||||
if (previousDevice != newDevice)
|
if (previousDevice != newDevice)
|
||||||
@@ -2264,7 +2263,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
if (e.OldItems != null)
|
if (e.OldItems != null)
|
||||||
foreach (SceneElement element in e.OldItems)
|
foreach (SceneElement element in e.OldItems)
|
||||||
element.PropertyChanged -= OnElementPropertyChanged;
|
element.PropertyChanged -= OnElementPropertyChanged;
|
||||||
OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
ScheduleSave();
|
ScheduleSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2459,6 +2458,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
|
_webcam = new Webcam { DeviceId = device.Id, Name = device.DisplayName };
|
||||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||||
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
}
|
}
|
||||||
else if (_webcam.DeviceId != device.Id)
|
else if (_webcam.DeviceId != device.Id)
|
||||||
{
|
{
|
||||||
@@ -2690,7 +2690,7 @@ public class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_webcam = null;
|
_webcam = null;
|
||||||
OnPropertyChanged(nameof(CanChangeWebcam));
|
OnPropertyChanged(nameof(CanChangeWebcam));
|
||||||
OnPropertyChanged(nameof(CanAddWebcamToStagedScene));
|
OnPropertyChanged(nameof(CanAddWebcam));
|
||||||
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
OnPropertyChanged(nameof(CanShowWebcamInStagedScene));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,11 +313,11 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr
|
|||||||
while another scene still used it; that identity survived, so re-adding bypassed the choice).
|
while another scene still used it; that identity survived, so re-adding bypassed the choice).
|
||||||
Picking a different camera than the current app-wide one swaps it everywhere via
|
Picking a different camera than the current app-wide one swaps it everywhere via
|
||||||
`SwapWebcamIdentityAsync` (the same path "Change Webcam…" uses), keeping the singleton honest;
|
`SwapWebcamIdentityAsync` (the same path "Change Webcam…" uses), keeping the singleton honest;
|
||||||
picking the same one just places the config. The Add Webcam menu greys out when the **active**
|
picking the same one just places the config. The Add Webcam menu item greys out whenever a webcam
|
||||||
scene already has a config (`CanAddWebcamToActiveScene`); showing a hidden webcam reuses the
|
exists **anywhere** — one camera identity app-wide (`CanAddWebcam` = `StagedScene != null && _webcam == null`,
|
||||||
existing config (`CanShowWebcamInActiveScene` / empty-canvas right-click "Show Webcam" — which, on
|
TASK 26; the old per-staged-scene gate let a second picker run from a scene that lacked the config).
|
||||||
a config-less canvas, delegates to Add Webcam and so also picks). **Removing the last webcam
|
**Removing the last webcam config anywhere clears the identity** (`_webcam = null`, raising
|
||||||
config anywhere clears the identity** (`_webcam = null`), which also drops "Change Webcam…".
|
`CanAddWebcam`/`CanChangeWebcam`), which also drops "Change Webcam…" and re-enables Add.
|
||||||
- **Round→rect restores the aspect (persisted, schema v4):** `SceneElement.ToggleClipShape()`
|
- **Round→rect restores the aspect (persisted, schema v4):** `SceneElement.ToggleClipShape()`
|
||||||
snapshots the rectangular Width/Height into public `RectWidth`/`RectHeight` before going Round and
|
snapshots the rectangular Width/Height into public `RectWidth`/`RectHeight` before going Round and
|
||||||
restores them when switching back — otherwise the Round resize lock (square) would leave a square
|
restores them when switching back — otherwise the Round resize lock (square) would leave a square
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Xunit;
|
||||||
|
using ytLive.Models;
|
||||||
|
using ytLive.ViewModels;
|
||||||
|
|
||||||
|
namespace ytLive.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TASK 26 integration: the (+) menu's Webcam row gates on the app-wide webcam
|
||||||
|
/// identity — one camera per app, so the row must stay disabled while ANY scene
|
||||||
|
/// shows the webcam (not just the staged one), and re-enable only after the
|
||||||
|
/// last config anywhere is removed (which also drops the identity).
|
||||||
|
/// Real-App + temp-DB pattern (see BackgroundHealIntegrationTests).
|
||||||
|
/// </summary>
|
||||||
|
[Collection("RealApp")]
|
||||||
|
public sealed class WebcamMenuGateTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanAddWebcam_Gates_On_The_AppWide_Webcam_Identity()
|
||||||
|
{
|
||||||
|
_app.Run(Run);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly RealAppHost _app;
|
||||||
|
|
||||||
|
public WebcamMenuGateTests(RealAppHost app) => _app = app;
|
||||||
|
|
||||||
|
private void Run()
|
||||||
|
{
|
||||||
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-wcgate-{Guid.NewGuid():N}.db");
|
||||||
|
MainViewModel.LayoutPathOverride = tempDb;
|
||||||
|
var window = default(MainWindow);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var schema = new ytLive.Services.LayoutStore(tempDb)) { }
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
SeedDatabaseWithWebcam(tempDb);
|
||||||
|
|
||||||
|
window = new MainWindow();
|
||||||
|
var vm = (MainViewModel)window.DataContext;
|
||||||
|
|
||||||
|
var starting = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Starting));
|
||||||
|
var live = vm.Scenes.Single(s => SceneCatalog.Is(s.Name, SceneCatalog.Live));
|
||||||
|
|
||||||
|
vm.StagedScene = live;
|
||||||
|
Assert.True(live.WebcamConfig == null, "Live must not carry the webcam config");
|
||||||
|
Assert.False(vm.CanAddWebcam,
|
||||||
|
"webcam exists in another scene — Add Webcam must be greyed out app-wide");
|
||||||
|
|
||||||
|
vm.StagedScene = starting;
|
||||||
|
Assert.False(vm.CanAddWebcam, "staged scene itself has the webcam");
|
||||||
|
|
||||||
|
var config = starting.WebcamConfig!;
|
||||||
|
vm.RemoveSourceCommand.Execute(config);
|
||||||
|
|
||||||
|
Assert.True(vm.CanAddWebcam,
|
||||||
|
"last webcam config anywhere was removed — identity cleared, picker may run again");
|
||||||
|
|
||||||
|
vm.SaveLayoutNow();
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
using var webcams = connection.CreateCommand();
|
||||||
|
webcams.CommandText = "SELECT COUNT(*) FROM Webcam;";
|
||||||
|
Assert.Equal(0L, (long)webcams.ExecuteScalar()!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
window?.Close();
|
||||||
|
MainViewModel.LayoutPathOverride = null;
|
||||||
|
SqliteConnection.ClearAllPools();
|
||||||
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Five canonical scenes + a webcam identity used by exactly one
|
||||||
|
/// non-staged-by-default scene (Starting), so staging any other screen
|
||||||
|
/// reproduces the old per-scene gate's false "available".</summary>
|
||||||
|
private static void SeedDatabaseWithWebcam(string path)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={path}");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
var scenes = new (string Id, string Name)[]
|
||||||
|
{
|
||||||
|
("scene-starting", "Starting"),
|
||||||
|
("scene-live", "Live"),
|
||||||
|
("scene-brb", "BRB"),
|
||||||
|
("scene-chat", "Chat"),
|
||||||
|
("scene-ending", "Ending"),
|
||||||
|
};
|
||||||
|
foreach (var (id, name) in scenes)
|
||||||
|
{
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText =
|
||||||
|
"INSERT INTO Scene (Id, Name, HasBackground, SortOrder) VALUES ($id, $name, 1, $order);";
|
||||||
|
cmd.Parameters.AddWithValue("$id", id);
|
||||||
|
cmd.Parameters.AddWithValue("$name", name);
|
||||||
|
cmd.Parameters.AddWithValue("$order", Array.IndexOf(scenes, (id, name)));
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var webcam = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
webcam.CommandText =
|
||||||
|
"INSERT INTO Webcam (Id, DeviceId, Name) VALUES ('wc-1', 'test-camera', 'Test Cam');";
|
||||||
|
webcam.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var config = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
config.CommandText =
|
||||||
|
@"INSERT INTO WebcamSceneConfig
|
||||||
|
(SceneId, WebcamId, IsVisible, X, Y, Width, Height, Opacity, SortOrder)
|
||||||
|
VALUES ('scene-starting', 'wc-1', 1, 100, 100, 480, 270, 1, 0);";
|
||||||
|
config.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user