using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows.Media; using System.Windows.Media.Imaging; using ytLive.Helpers; using ytLive.Models; using ytLive.ViewModels; namespace ytLive.ViewModels; public partial class MainViewModel { internal static System.Windows.Media.Imaging.BitmapSource? LoadBackgroundImage(Scene scene) { var bg = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground); if (bg != null && !bg.UseDefaultBackground && !string.IsNullOrWhiteSpace(bg.CustomBackgroundPath) && System.IO.File.Exists(bg.CustomBackgroundPath)) { try { var bitmap = new System.Windows.Media.Imaging.BitmapImage(); bitmap.BeginInit(); bitmap.UriSource = new Uri(bg.CustomBackgroundPath); bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad; bitmap.EndInit(); bitmap.Freeze(); return bitmap; } catch { /* fall through to AssetId */ } } if (bg != null && !string.IsNullOrWhiteSpace(bg.AssetId)) return ImageCache.Get(bg.AssetId); return null; } private static BitmapSource CreatePlaceholderSnapshot(string sceneName) { var colors = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Starting"] = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(233, 69, 96)), ["Live"] = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94)), ["BRB"] = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(234, 179, 8)), ["Chat"] = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(59, 130, 246)), ["Ending"] = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(168, 85, 247)), }; var brush = colors.TryGetValue(sceneName, out var c) ? c : new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(60, 60, 80)); var dv = new System.Windows.Media.DrawingVisual(); using (var ctx = dv.RenderOpen()) { ctx.DrawRectangle(brush, null, new System.Windows.Rect(0, 0, 320, 180)); var ft = new System.Windows.Media.FormattedText( sceneName, System.Globalization.CultureInfo.InvariantCulture, System.Windows.FlowDirection.LeftToRight, new System.Windows.Media.Typeface("Segoe UI"), 18, System.Windows.Media.Brushes.White, 96); ctx.DrawText(ft, new System.Windows.Point(160 - ft.Width / 2, 90 - ft.Height / 2)); } var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap( 320, 180, 96, 96, System.Windows.Media.PixelFormats.Pbgra32); rtb.Render(dv); rtb.Freeze(); return rtb; } // ─── Screen background capture (live desktop/game) ─── private const string MonitorKeyPrefix = "monitor:"; /// Every background-enabled scene has exactly one background, kept at index 0 (bottom layer). /// Returns null for a scene with = false. internal static Source? EnsureBackground(Scene scene) { if (!scene.HasBackground) return null; var background = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground); if (background != null) return background; background = CreateBackground(scene.Name); scene.Elements.Insert(0, background); return background; } /// The one Background row for a scene. Live gets the capture flavor /// (Show Desktop / monitor switch / fullscreen-game detect) falling back to /// live-background.jpg; every other screen is a static default-art layer. /// Both are named "Background" — the user never sees capture jargon here. internal static Source CreateBackground(string sceneName) { var isLive = SceneCatalog.Is(sceneName, SceneCatalog.Live); return new Source { Name = "Background", Type = isLive ? SourceType.DisplayCapture : SourceType.Background, IsBackground = true, IsEnabled = true, X = 0, Y = 0, Width = MasterFrameWidth, Height = MasterFrameHeight, }; } /// Heals any loaded layout to the locked Background model: exactly /// one Background per canonical scene, correct flavor for the scene (static /// art everywhere, DisplayCapture on Live only), at index 0, named /// "Background". Duplicates and stale rows are dropped; missing ones seeded; /// non-canonical scenes get HasBackground=false and lose any backgrounds. internal static void NormalizeBackgrounds(IEnumerable scenes) { foreach (var scene in scenes) { if (!SceneCatalog.HasBackground(scene.Name)) { scene.HasBackground = false; foreach (var stale in scene.Elements.OfType().Where(s => s.IsBackground).ToList()) scene.Elements.Remove(stale); continue; } scene.HasBackground = true; var isLive = SceneCatalog.Is(scene.Name, SceneCatalog.Live); var wantedType = isLive ? SourceType.DisplayCapture : SourceType.Background; var candidates = scene.Elements.OfType().Where(s => s.IsBackground).ToList(); var keep = candidates.FirstOrDefault(b => b.Type == wantedType) ?? candidates.FirstOrDefault(); foreach (var extra in candidates.Where(b => !ReferenceEquals(b, keep))) scene.Elements.Remove(extra); if (keep == null) { keep = CreateBackground(scene.Name); scene.Elements.Insert(0, keep); } // Source.Type's setter derives IsBackground from the enum, so a // flavor correction must re-assert the flag afterwards. if (keep.Type != wantedType) { keep.Type = wantedType; keep.IsBackground = true; } keep.Name = "Background"; keep.IsEnabled = true; var index = scene.Elements.IndexOf(keep); if (index > 0) scene.Elements.Move(index, 0); } } // Only the Live screen's background is a real-time surface. Every other // lookup below (auto-capture, focus hooks, refresh) must ignore the static // default-art rows so they can never start a desktop capture session. private IEnumerable AllBackgrounds() => Scenes.Where(s => SceneCatalog.Is(s.Name, SceneCatalog.Live)) .SelectMany(s => s.Elements.OfType().Where(x => x.IsBackground)); }