diff --git a/Controls/LeftPanel.xaml b/Controls/LeftPanel.xaml index 2416809..fe43b72 100644 --- a/Controls/LeftPanel.xaml +++ b/Controls/LeftPanel.xaml @@ -49,12 +49,19 @@ - + - + + + + + - - + + @@ -171,7 +180,40 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + - + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + @@ -60,4 +84,4 @@ - \ No newline at end of file + diff --git a/Controls/SceneThumbnailStrip.xaml.cs b/Controls/SceneThumbnailStrip.xaml.cs index bf67dfc..092fffa 100644 --- a/Controls/SceneThumbnailStrip.xaml.cs +++ b/Controls/SceneThumbnailStrip.xaml.cs @@ -14,6 +14,16 @@ public partial class SceneThumbnailStrip : UserControl { InitializeComponent(); SizeChanged += (_, _) => Height = ActualWidth * DesignAspect; + Loaded += (_, _) => + { + if (DataContext is MainViewModel vm) + foreach (var scene in vm.Scenes) + { + var staged = scene.IsStaged; + scene.IsStaged = !staged; + scene.IsStaged = staged; + } + }; } private void Thumbnail_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) diff --git a/DESIGN.md b/DESIGN.md index 60dc5c3..8c956dc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -51,6 +51,13 @@ the first Stitch draft of the live screen. Icons: Material Symbols Outlined. +## Color roles (decided 2026-10-26) + +- **Red `#ff5451`:** YouTube references, on-air/GO LIVE, and record only. +- **Attention orange `#ff8c3a`:** dropped packets, the live screen indicator, and blinking warning lights. +- **Amber `#ffb95f`:** game audio and Super Chat only. +- **`error` `#ef4444`:** unchanged for now; it sits close to the on-air red and may need revisiting. + ## Open questions for the next pass - These tokens have not been reconciled against the app's current WPF theme diff --git a/Models/SceneElement.cs b/Models/SceneElement.cs index 935eb01..e7443bc 100644 --- a/Models/SceneElement.cs +++ b/Models/SceneElement.cs @@ -84,6 +84,9 @@ public abstract class SceneElement : INotifyPropertyChanged /// True only for AlertBox-type sources. Gates stream alerts properties panel. public virtual bool IsAlertBox => false; + public virtual bool IsTextOverlay => false; + public virtual bool IsMediaSource => false; + public virtual bool IsLiveCapture => false; public virtual bool IsBackground { get; set; } private bool _useDefaultBackground = true; diff --git a/Models/Source.cs b/Models/Source.cs index 1709a1f..b178583 100644 --- a/Models/Source.cs +++ b/Models/Source.cs @@ -73,6 +73,7 @@ public class Source : SceneElement Raise(nameof(IsLiveCapture)); Raise(nameof(DisplaySource)); Raise(nameof(IsWebSource)); + Raise(nameof(IsTextOverlay)); } } } @@ -85,6 +86,7 @@ public class Source : SceneElement ? ElementKind.Dynamic : ElementKind.Static; public override bool IsWebSource => Type == SourceType.WebSource; + public override bool IsTextOverlay => Type == SourceType.TextOverlay; /// The permanent scene background (live desktop/game capture). Never removable/reorderable. private bool _isBackground; @@ -110,7 +112,7 @@ public class Source : SceneElement private string? _captureKey; public string? CaptureKey { get => _captureKey; set => Set(ref _captureKey, value); } - public bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture; + public override bool IsLiveCapture => Type is SourceType.DisplayCapture or SourceType.WindowCapture; // Image (asset stored in the layout database) private string? _assetId; @@ -190,7 +192,7 @@ public class Source : SceneElement set => Set(ref _mediaPlaybackState, value); } - public bool IsMediaSource => Type == SourceType.MediaSource; + public override bool IsMediaSource => Type == SourceType.MediaSource; // Alert video config (TASK 47): the shipped default clip lives in the Asset // table (seeded like image assets); the creator's replacement is a FILE PATH diff --git a/ViewModels/MainViewModel.Background.cs b/ViewModels/MainViewModel.Background.cs index 0ce53d7..50bce24 100644 --- a/ViewModels/MainViewModel.Background.cs +++ b/ViewModels/MainViewModel.Background.cs @@ -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) ─── diff --git a/ViewModels/MainViewModel.Scenes.cs b/ViewModels/MainViewModel.Scenes.cs index 114ff1a..7256b3e 100644 --- a/ViewModels/MainViewModel.Scenes.cs +++ b/ViewModels/MainViewModel.Scenes.cs @@ -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); + } } /// @@ -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 diff --git a/ViewModels/MainViewModel.Sources.cs b/ViewModels/MainViewModel.Sources.cs index 761846a..a4f2ad5 100644 --- a/ViewModels/MainViewModel.Sources.cs +++ b/ViewModels/MainViewModel.Sources.cs @@ -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(); } } diff --git a/ViewModels/MainViewModel.Streaming.Operations.cs b/ViewModels/MainViewModel.Streaming.Operations.cs index 3791f98..bf32176 100644 --- a/ViewModels/MainViewModel.Streaming.Operations.cs +++ b/ViewModels/MainViewModel.Streaming.Operations.cs @@ -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(); diff --git a/ViewModels/MainViewModel.Webcam.cs b/ViewModels/MainViewModel.Webcam.cs index d2d485c..ed6cb3a 100644 --- a/ViewModels/MainViewModel.Webcam.cs +++ b/ViewModels/MainViewModel.Webcam.cs @@ -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 diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index af76267..8dcfa34 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -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();