Webcam menu gates on the app-wide identity (TASK 26)

This commit is contained in:
2026-08-24 08:56:17 -07:00
parent d774799138
commit ef710c5537
6 changed files with 163 additions and 49 deletions
+25 -35
View File
@@ -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
View File
@@ -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}"/>
+1
View File
@@ -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)
+8 -8
View File
@@ -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));
} }
+5 -5
View File
@@ -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
+123
View File
@@ -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();
}
}
}