Brand logo: one locked image layer per scene above the background, image from App Settings

- Source.IsBrandLogo persisted (additive migration); layer healed on load, removal blocked
- Image stored as an asset; settings keep the asset id and file name
- App Settings: Brand Logo file field with Browse
- Default placement top-left; per-scene eye uses the existing element visibility
Test: BrandLogoTests; layout/background tests pass (34). Not pushed.
This commit is contained in:
2026-10-27 09:25:49 -07:00
parent b86b6bb601
commit 841c905c06
12 changed files with 194 additions and 7 deletions
+77
View File
@@ -0,0 +1,77 @@
using System.IO;
using System.Windows.Input;
using ytLive.Helpers;
using ytLive.Models;
namespace ytLive.ViewModels;
/// <summary>
/// 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).
/// </summary>
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 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();
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,
};
/// <summary>Exactly one brand logo per scene, placed directly above the background, carrying
/// the current App Settings image.</summary>
internal void EnsureBrandLogos()
{
var assetId = _layoutStore.GetBrandLogoAssetId();
foreach (var scene in Scenes)
{
var logos = scene.Elements.OfType<Source>().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<Source>().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);
}
}
}