Layers panel and scene strip: scene layers header with icons, per-type layer icons, PROGRAM badge and selected/live orange borders, scene name and hotkey overlays, Starting as default scene

- LeftPanel: SCENE LAYERS header with layer count; per-type blue icons; properties header gear; layers list min/max height; properties placeholder; list and properties indent
- SceneThumbnailStrip: names/hotkeys over the tile; PROGRAM badge with blinking red dot on the selected tile; orange border on selected and live tiles; fixed the element border override that hid the style border; reflag tiles on load
- LiveScene is now set on go-live, record start, and live transitions (it was never assigned, so IsLive never fired)
- Startup stages the scene named Starting
- DESIGN.md: color roles (red = YouTube/on-air/record, orange #ff8c3a = attention, amber = game audio/Super Chat)
- Staged live tile uses background art instead of the green LIVE placeholder

Not pushed.
This commit is contained in:
2026-10-26 14:00:23 -07:00
parent 24aca7d04a
commit 3239808a73
12 changed files with 146 additions and 45 deletions
+3 -6
View File
@@ -155,14 +155,10 @@ public partial class MainViewModel
? OnAirImage
: SceneThumbnailImage(scene);
if (img == null)
{
var stagedLive = isStaged
&& SceneCatalog.Is(scene.Name, SceneCatalog.Live);
img = stagedLive ? null : LoadBackgroundImage(scene);
}
img = LoadBackgroundImage(scene);
if (img != null)
scene.Snapshot = img;
else if (scene.Snapshot == null || (isStaged && OnAirImage == null))
else if (scene.Snapshot == null)
scene.Snapshot = CreatePlaceholderSnapshot(scene.Name);
}
}
@@ -443,6 +439,7 @@ public partial class MainViewModel
}
SeedOnAirImage();
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
}
// ─── Scene strip tile artwork (creator ruling 2026-10-10) ───
+5
View File
@@ -45,6 +45,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowPreviewPlaceholder));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
OnPropertyChanged(nameof(CanChangeBackground));
OnPropertyChanged(nameof(CanAddWebcam));
OnPropertyChanged(nameof(CanAddYouTubeChat));
@@ -102,7 +103,10 @@ public partial class MainViewModel
if (scene.IsHidden || scene == StagedScene) return;
StagedScene = scene;
if (IsLive)
{
LiveScene = scene;
_transition.Start(SelectedTransitionType, TransitionDurationMs, null);
}
}
/// <summary>
@@ -186,6 +190,7 @@ public partial class MainViewModel
}
public bool ShowEmptySceneHint => StagedScene != null && StagedScene.Elements.Count == 0;
public int LayerCount => StagedScene?.Elements.Count(e => !e.IsZoomLayer) ?? 0;
public bool ShowSourcesEmptyHint => StagedScene == null || StagedScene.Elements.Count == 0;
public bool IsCountdownAvailable => StagedScene != null
+3
View File
@@ -85,6 +85,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
@@ -227,6 +228,7 @@ public partial class MainViewModel
SelectedElement = source;
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
@@ -260,6 +262,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
}
}
@@ -65,6 +65,7 @@ public partial class MainViewModel : ViewModelBase
ResetHealth(StreamStatus.Offline);
_audioMixer.StartLive(EncoderOptions.DefaultAudioPipeName);
IsRecording = true;
LiveScene = StagedScene;
await _framePump.StartAsync();
}
@@ -222,6 +223,7 @@ public partial class MainViewModel : ViewModelBase
// only once frames flow. The liveChatId (snippet.liveChatId) does not exist
// before that (2026-09-25 diagnosis), so chat resolution runs AFTER the pump
// starts. Chat is non-fatal — if it fails, the stream continues without chat.
LiveScene = StagedScene;
await _framePump.StartAsync(); // never throws; failures log + surface via Failed
string? liveChatId = null;
@@ -324,6 +326,7 @@ public partial class MainViewModel : ViewModelBase
StreamStatus = StreamStatus.Offline;
IsRecording = false;
LiveScene = null;
// TASK 41: a stopped test broadcast forgets its drawer session (idempotent)
// and drops the TEST face — a test never outlives its run.
TestSession.EndTestSilently();
+1
View File
@@ -375,6 +375,7 @@ public partial class MainViewModel
OnPropertyChanged(nameof(ShowEmptySceneHint));
OnPropertyChanged(nameof(ShowSourcesEmptyHint));
OnPropertyChanged(nameof(LayerCount));
UpdateActiveBackground();
// PreviewBitmapChanged fires only on the camera's first frame, so a config
+1 -1
View File
@@ -438,7 +438,7 @@ public partial class MainViewModel : ViewModelBase
{
_isLoading = false;
}
StagedScene = Scenes.FirstOrDefault();
StagedScene = Scenes.FirstOrDefault(s => SceneCatalog.Is(s.Name, SceneCatalog.Starting)) ?? Scenes.FirstOrDefault();
UpdateActiveBackground();
UpdateActiveChatBoxPreviews();
UpdateActiveAlertBoxPreviews();