Control Surface UX: pill toggle, background rename, per-scene properties

- Sliding pill toggle (green ON / red OFF) replaces checkbox
- Geometry mismatch error for custom backgrounds
- Backdrop -> Background: C# rename, SQL column rename with migration
- Source.IsBackground: new -> override (fixes WPF binding)
- Legacy 'Backdrop' display name migrated to 'Background'
- Per-scene UseDefaultBackground + CustomBackgroundPath
- Asset files renamed backdrop -> background
- StagedScene/LiveScene split, thumbnail strip, edit gating
- TransitionService (Cut/Fade/Move)
- Context menu: Capture Desktop, Refresh Desktop
- Resolution badge removed, ON-AIR status light added
This commit is contained in:
2026-08-22 18:10:59 -07:00
parent e06bfa42a8
commit 4b9a8e515f
35 changed files with 1406 additions and 717 deletions
+31 -31
View File
@@ -78,21 +78,21 @@ public class LayoutStorePersistenceTests
}
}
// The backdrop (schema v5): permanent bottom layer holding live desktop/game
// capture. IsBackdrop + CaptureKey must survive a save + reload so a
// The background (schema v5): permanent bottom layer holding live desktop/game
// capture. IsBackground + CaptureKey must survive a save + reload so a
// re-designated target isn't lost on restart.
[Fact]
public void Backdrop_IsBackdrop_And_CaptureKey_Survive_Save_And_Reload()
public void Background_IsBackground_And_CaptureKey_Survive_Save_And_Reload()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
using var store = new LayoutStore(path);
var backdrop = new Source
var background = new Source
{
Name = "Game Capture",
Type = SourceType.DisplayCapture,
IsBackdrop = true,
IsBackground = true,
IsEnabled = true,
X = 0,
Y = 0,
@@ -102,13 +102,13 @@ public class LayoutStorePersistenceTests
CaptureKey = "monitor:1",
};
var scene = new Scene { Name = "Starting" };
scene.Elements.Add(backdrop);
scene.Elements.Add(background);
store.Save(new[] { scene }, null, null);
var reloaded = store.Load();
var restored = Assert.IsType<Source>(Assert.Single(reloaded[0].Elements));
Assert.True(restored.IsBackdrop);
Assert.True(restored.IsBackground);
Assert.Equal("monitor:1", restored.CaptureKey);
Assert.Equal(1, restored.MonitorIndex);
Assert.Equal(SourceType.DisplayCapture, restored.Type);
@@ -120,23 +120,23 @@ public class LayoutStorePersistenceTests
}
}
// Per-scene backdrop switch (schema v6): a scene whose backdrop was turned
// Per-scene background switch (schema v6): a scene whose background was turned
// off must stay off across a save + reload.
[Fact]
public void Scene_HasBackdrop_Survives_Save_And_Reload()
public void Scene_HasBackground_Survives_Save_And_Reload()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
{
using var store = new LayoutStore(path);
var on = new Scene { Name = "Live", HasBackdrop = true };
var off = new Scene { Name = "Ending", HasBackdrop = false };
var on = new Scene { Name = "Live", HasBackground = true };
var off = new Scene { Name = "Ending", HasBackground = false };
store.Save(new[] { on, off }, null, null);
var reloaded = store.Load();
Assert.True(reloaded.First(s => s.Name == "Live").HasBackdrop);
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackdrop);
Assert.True(reloaded.First(s => s.Name == "Live").HasBackground);
Assert.False(reloaded.First(s => s.Name == "Ending").HasBackground);
}
finally
{
@@ -145,12 +145,12 @@ public class LayoutStorePersistenceTests
}
}
// v5 → v6 one-time backfill: pre-v6 DBs already have backdrops sitting in the
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackdrop
// column turns those scenes off and drops their backdrop sources; Live keeps
// its backdrop.
// v5 → v6 one-time backfill: pre-v6 DBs already have backgrounds sitting in the
// non-Live canonical scenes (Starting/BRB/Chat/Ending). Adding the HasBackground
// column turns those scenes off and drops their background sources; Live keeps
// its background.
[Fact]
public void V5_Database_Backfill_Removes_NonLive_Backdrops()
public void V5_Database_Backfill_Removes_NonLive_Backgrounds()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-{Guid.NewGuid():N}.db");
try
@@ -183,19 +183,19 @@ public class LayoutStorePersistenceTests
DeviceId TEXT,
ClipShape TEXT NOT NULL DEFAULT 'Traditional',
IsMirrored INTEGER NOT NULL DEFAULT 0,
IsBackdrop INTEGER NOT NULL DEFAULT 0,
IsBackground INTEGER NOT NULL DEFAULT 0,
CaptureKey TEXT,
SortOrder INTEGER NOT NULL DEFAULT 0
);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-starting', 'Starting', 0);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-live', 'Live', 1);
INSERT INTO Scene (Id, Name, SortOrder) VALUES ('s-chat', 'Chat', 2);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-live', 's-live', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackdrop, CaptureKey, SortOrder)
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Backdrop', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-starting', 's-starting', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-live', 's-live', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
INSERT INTO Source (Id, SceneId, Type, Name, IsBackground, CaptureKey, SortOrder)
VALUES ('b-chat', 's-chat', 'DisplayCapture', 'Background', 1, 'monitor:0', 0);
PRAGMA user_version = 5;
""";
cmd.ExecuteNonQuery();
@@ -205,16 +205,16 @@ public class LayoutStorePersistenceTests
var scenes = store.Load();
var starting = Assert.Single(scenes, s => s.Name == "Starting");
Assert.False(starting.HasBackdrop);
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackdrop: true });
Assert.False(starting.HasBackground);
Assert.DoesNotContain(starting.Elements, e => e is Source { IsBackground: true });
var chat = Assert.Single(scenes, s => s.Name == "Chat");
Assert.False(chat.HasBackdrop);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackdrop: true });
Assert.False(chat.HasBackground);
Assert.DoesNotContain(chat.Elements, e => e is Source { IsBackground: true });
var live = Assert.Single(scenes, s => s.Name == "Live");
Assert.True(live.HasBackdrop);
Assert.Single(live.Elements, e => e is Source { IsBackdrop: true });
Assert.True(live.HasBackground);
Assert.Single(live.Elements, e => e is Source { IsBackground: true });
}
finally
{