diff --git a/HANDOFF.md b/HANDOFF.md
index c1d775d..8c1683b 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -1,7 +1,7 @@
# HANDOFF — Session State
## Branch
-`main` — clean at `4b9a8e5` (Control Surface UX: pill toggle, background rename, per-scene properties).
+`main` — clean, 0 warnings.
## What shipped this session
@@ -13,21 +13,31 @@
6. **Sliding pill toggle** — green ON (#22c55e) / red OFF (#ef4444), replaces checkbox
7. **Backdrop → Background** — full C# rename, SQL column rename with migration, asset files renamed, `Source.IsBackground` `new` → `override` fix, legacy "Backdrop" display name migration
8. **Geometry mismatch error** — red text when custom background aspect ratio >5% off 16:9
-9. **Per-scene background properties** — pill toggle + custom path persist per-scene via Settings table
+9. **Per-scene background properties** — `UseDefaultBackground` + `CustomBackgroundPath` on `SceneElement` (superclass), persisted per-source via `BackgroundPath_{sourceId}` / `BackgroundUseDefault_{sourceId}` in Settings table
+10. **All five scenes have backgrounds** — `SceneCatalog.HasBackground` returns `true` for all canonical scenes. `EnforceBackgroundPolicy` no longer strips backgrounds from non-Live scenes.
+11. **Pill toggle label** — "Use default background:" TextBlock before toggle
+12. **Pill toggle wiring** — `SelectedElement.PropertyChanged` subscription triggers `UpdateBackgroundImage` + `RefreshSnapshotsAsync` + `ScheduleSave` on `UseDefaultBackground` change
-## Background properties (fully shipped)
-- `Scene.UseDefaultBackground` (bool, default true) + `Scene.CustomBackgroundPath` (string?)
-- Persisted as `BackgroundUseDefault_{sceneId}` / `BackgroundPath_{sceneId}` in Settings table
-- Pill toggle ON → loads default asset from DB; OFF → loads custom file path
-- Custom path preserved when toggled back ON (not cleared)
-- Geometry mismatch: aspect ratio check ±5% of 16:9, shows expected 1920×1080
+## Background layer rules
+- Every scene has a Background layer at position 0 — undeletable (trash icon hidden, context menu "Remove Layer" hidden, `RemoveElement` returns early)
+- `IsBackground` is derived from `SourceType.Background` via `Source.Type` setter — impossible to forget
+- All five scenes have default images stored in Asset BLOBs in SQLite
+- First run: pill ON for all five, shipped defaults load
+- Toggle OFF → browse for custom file, FQFN stored
+- On reload: try to load custom file; if missing, fall back to default asset
+- `EnsureDefaultBackground` re-seeds missing backgrounds on scene switch (covers Starting, BRB, Chat, Ending; Live covered by seed methods)
+
+## Current state
+- **0 warnings, 0 errors**
+- **210/211 tests pass** (1 pre-existing AudioPipeline gain-math failure)
+- Deleted backgrounds re-seed automatically via `EnsureDefaultBackground`
## Build
```bash
taskkill.exe /F /IM ytLive.exe # if running
"/mnt/c/Program Files/dotnet/dotnet.exe" build "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.csproj"
+"/mnt/c/Program Files/dotnet/dotnet.exe" vstest "C:\Users\gramp\Documents\Code\projects\ytLive\ytLive.Tests\bin\Debug\net8.0-windows10.0.19041.0\ytLive.Tests.dll"
```
-**0 warnings, 0 errors.**
## Polar.sh resources
| Resource | ID |
@@ -42,7 +52,7 @@ taskkill.exe /F /IM ytLive.exe # if running
| Key prefix | `LCYT-` |
## Next steps
-1. **Visual testing** — run on Windows display, verify pill toggle, background properties per-scene
+1. **Visual testing** — run on Windows display, verify backgrounds show for all 5 scenes
2. **Thumbnail drag-reorder** — reorder scenes via drag on thumbnail strip
3. **Real snapshot generation** — replace placeholder colors with compositor-rendered thumbnails
4. **Transition dropdown UI** — add XAML control to pick transition type + duration
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 5b4d2f8..d6909d7 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -608,26 +608,31 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Models/Scene.cs b/Models/Scene.cs
index 24141af..ad6d411 100644
--- a/Models/Scene.cs
+++ b/Models/Scene.cs
@@ -15,8 +15,6 @@ public class Scene : INotifyPropertyChanged
private bool _hasSocialBar;
private bool _isStaged;
private bool _isLive;
- private string? _customBackgroundPath;
- private bool _useDefaultBackground = true;
public bool IsStaged
{
@@ -30,26 +28,6 @@ public class Scene : INotifyPropertyChanged
set => Set(ref _isLive, value);
}
- ///
- /// User-selected background image file path (FQFN). Null means use the app default.
- /// Stored as a reference only — the image file is not copied into the DB.
- ///
- public string? CustomBackgroundPath
- {
- get => _customBackgroundPath;
- set => Set(ref _customBackgroundPath, value);
- }
-
- ///
- /// When true, the shipped default background image is used and CustomBackgroundPath
- /// is ignored (but preserved). Toggled by the pill-switch in the properties panel.
- ///
- public bool UseDefaultBackground
- {
- get => _useDefaultBackground;
- set => Set(ref _useDefaultBackground, value);
- }
-
public string Name
{
get => _name;
diff --git a/Models/SceneCatalog.cs b/Models/SceneCatalog.cs
index dfc1701..89799fe 100644
--- a/Models/SceneCatalog.cs
+++ b/Models/SceneCatalog.cs
@@ -47,6 +47,6 @@ public static class SceneCatalog
public static bool IsChat(string? name) => Is(name, Chat);
- /// Only the Live scene carries the live desktop/game background.
- public static bool HasBackground(string? name) => Is(name, Live);
+ /// Every scene has a background layer.
+ public static bool HasBackground(string? name) => IsCanonical(name);
}
diff --git a/Models/SceneElement.cs b/Models/SceneElement.cs
index 1f11c27..3aa641e 100644
--- a/Models/SceneElement.cs
+++ b/Models/SceneElement.cs
@@ -63,6 +63,24 @@ public abstract class SceneElement : INotifyPropertyChanged
public virtual bool IsCountdown => false;
public virtual bool IsBackground { get; set; }
+ private bool _useDefaultBackground = true;
+ /// When true, the shipped default background image is used and CustomBackgroundPath
+ /// is ignored (but preserved). Toggled by the pill-switch in the properties panel.
+ public bool UseDefaultBackground
+ {
+ get => _useDefaultBackground;
+ set => Set(ref _useDefaultBackground, value);
+ }
+
+ private string? _customBackgroundPath;
+ /// User-selected background image file path (FQFN). Null means use the app default.
+ /// Stored as a reference only — the image file is not copied into the DB.
+ public string? CustomBackgroundPath
+ {
+ get => _customBackgroundPath;
+ set => Set(ref _customBackgroundPath, value);
+ }
+
private bool _isVisible = true;
public bool IsVisible { get => _isVisible; set => Set(ref _isVisible, value); }
diff --git a/Services/LayoutStore.cs b/Services/LayoutStore.cs
index de879d3..aafaf79 100644
--- a/Services/LayoutStore.cs
+++ b/Services/LayoutStore.cs
@@ -677,13 +677,22 @@ public class LayoutStore : IDisposable
foreach (var config in configs)
scene.Elements.Add(config);
- var path = GetSetting($"BackgroundPath_{scene.Id}");
- if (!string.IsNullOrWhiteSpace(path))
- scene.CustomBackgroundPath = path;
+ var bg = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground);
+ if (bg != null)
+ {
+ // Load from source-keyed settings (new) or scene-keyed (legacy migration)
+ var path = GetSetting($"BackgroundPath_{bg.Id}");
+ if (string.IsNullOrWhiteSpace(path))
+ path = GetSetting($"BackgroundPath_{scene.Id}");
+ if (!string.IsNullOrWhiteSpace(path))
+ bg.CustomBackgroundPath = path;
- var useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
- if (useDefault == "0")
- scene.UseDefaultBackground = false;
+ var useDefault = GetSetting($"BackgroundUseDefault_{bg.Id}");
+ if (string.IsNullOrWhiteSpace(useDefault))
+ useDefault = GetSetting($"BackgroundUseDefault_{scene.Id}");
+ if (useDefault == "0")
+ bg.UseDefaultBackground = false;
+ }
}
return scenes;
@@ -1025,12 +1034,15 @@ public class LayoutStore : IDisposable
cmd.ExecuteNonQuery();
}
- // Save custom background paths and use-default flags
+ // Save custom background paths and use-default flags per background source
foreach (var scene in scenes)
{
- var pathKey = $"BackgroundPath_{scene.Id}";
- if (!string.IsNullOrWhiteSpace(scene.CustomBackgroundPath))
- UpsertSetting(pathKey, scene.CustomBackgroundPath);
+ var bg = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground);
+ if (bg == null) continue;
+
+ var pathKey = $"BackgroundPath_{bg.Id}";
+ if (!string.IsNullOrWhiteSpace(bg.CustomBackgroundPath))
+ UpsertSetting(pathKey, bg.CustomBackgroundPath);
else
{
using var del = _connection.CreateCommand();
@@ -1039,8 +1051,8 @@ public class LayoutStore : IDisposable
del.ExecuteNonQuery();
}
- var defaultKey = $"BackgroundUseDefault_{scene.Id}";
- UpsertSetting(defaultKey, scene.UseDefaultBackground ? "1" : "0");
+ var defaultKey = $"BackgroundUseDefault_{bg.Id}";
+ UpsertSetting(defaultKey, bg.UseDefaultBackground ? "1" : "0");
}
tx.Commit();
diff --git a/Themes/Controls.xaml b/Themes/Controls.xaml
index c01e1e4..6428832 100644
--- a/Themes/Controls.xaml
+++ b/Themes/Controls.xaml
@@ -555,10 +555,17 @@
+
+