using System.IO; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Input; using ytLive.Helpers; using ytLive.Models; namespace ytLive.ViewModels; /// /// The permanent brand logo: one image layer in every scene, directly above the background. /// Its image comes from App Settings; each scene keeps its own visibility (the eye toggle). /// public partial class MainViewModel { internal const double BrandLogoX = 32; internal const double BrandLogoY = 32; internal const double BrandLogoWidth = 240; internal const double BrandLogoHeight = 120; private ICommand? _browseBrandLogo; public ICommand BrowseBrandLogoCommand => _browseBrandLogo ??= new RelayCommand(_ => BrowseBrandLogo()); public string BrandLogoFileDisplay => _layoutStore.GetBrandLogoFileName() ?? "None"; private ImageSource? _brandLogoPreviewImage; private bool _brandLogoPreviewLoaded; /// The current brand-logo image, for the thumbnail beside the file field in App /// Settings (the creator, 2026-10-30: a myopic user cannot tell what a file *name* looks /// like). Null until an image is set. public ImageSource? BrandLogoPreviewImage { get { if (!_brandLogoPreviewLoaded) RefreshBrandLogoPreview(); return _brandLogoPreviewImage; } } internal void RefreshBrandLogoPreview() { _brandLogoPreviewLoaded = true; _brandLogoPreviewImage = LoadBrandLogoImage(_layoutStore.GetBrandLogoAssetId()); OnPropertyChanged(nameof(BrandLogoPreviewImage)); } /// Decoded once and frozen: the thumbnail and the enlarged preview share it, and a /// frozen bitmap can be handed to any thread without a dispatcher round-trip. private ImageSource? LoadBrandLogoImage(string? assetId) { if (string.IsNullOrWhiteSpace(assetId)) return null; var bytes = _layoutStore.GetAssetBytes(assetId); if (bytes is not { Length: > 0 }) return null; try { var image = new BitmapImage(); image.BeginInit(); image.CacheOption = BitmapCacheOption.OnLoad; image.StreamSource = new MemoryStream(bytes); image.EndInit(); image.Freeze(); return image; } catch (Exception) { return null; // a corrupt image must not take the dialog down with it } } private void BrowseBrandLogo() { var dialog = new Microsoft.Win32.OpenFileDialog { Title = "Choose brand logo", Filter = "Images|*.png;*.jpg;*.jpeg;*.bmp;*.gif", }; if (dialog.ShowDialog() != true) return; _layoutStore.SetBrandLogo(File.ReadAllBytes(dialog.FileName), Path.GetFileName(dialog.FileName)); EnsureBrandLogos(); RefreshBrandLogoPreview(); OnPropertyChanged(nameof(BrandLogoFileDisplay)); ScheduleSave(); } internal static Source CreateBrandLogo(string? assetId) => new() { Name = "Brand logo", Type = SourceType.Image, IsBrandLogo = true, IsEnabled = true, AssetId = assetId, X = BrandLogoX, Y = BrandLogoY, Width = BrandLogoWidth, Height = BrandLogoHeight, }; /// Exactly one brand logo per scene, placed directly above the background, carrying /// the current App Settings image. internal void EnsureBrandLogos() { var assetId = _layoutStore.GetBrandLogoAssetId(); foreach (var scene in Scenes) { var logos = scene.Elements.OfType().Where(s => s.IsBrandLogo).ToList(); foreach (var extra in logos.Skip(1)) scene.Elements.Remove(extra); var logo = logos.FirstOrDefault(); if (logo == null) { var background = scene.Elements.OfType().FirstOrDefault(s => s.IsBackground); var index = background == null ? 0 : scene.Elements.IndexOf(background) + 1; logo = CreateBrandLogo(assetId); scene.Elements.Insert(index, logo); } else { logo.AssetId = assetId; } _sceneGraph.InvalidateBake(scene); } } }