The blur is universal, so it would swallow the logo along with everything else. The compositor now defers every visible brand logo: skipped in both element loops, blitted immediately AFTER the blur group, before the flash/social/ticker overlays. One logo per scene, so it is cheap. Preview mirrors it: the staged scene's brand logo is drawn above the blur host (StagedBrandLogo), so what you see in the editor is what the encoder gets. Test: a blur covering the logo's area leaves the logo's pixels exactly white while the coloured background beneath it blends.
131 lines
4.6 KiB
C#
131 lines
4.6 KiB
C#
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;
|
|
|
|
/// <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 ImageSource? _brandLogoPreviewImage;
|
|
private bool _brandLogoPreviewLoaded;
|
|
|
|
/// <summary>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.</summary>
|
|
public ImageSource? BrandLogoPreviewImage
|
|
{
|
|
get
|
|
{
|
|
if (!_brandLogoPreviewLoaded) RefreshBrandLogoPreview();
|
|
return _brandLogoPreviewImage;
|
|
}
|
|
}
|
|
|
|
internal void RefreshBrandLogoPreview()
|
|
{
|
|
_brandLogoPreviewLoaded = true;
|
|
_brandLogoPreviewImage = LoadBrandLogoImage(_layoutStore.GetBrandLogoAssetId());
|
|
OnPropertyChanged(nameof(BrandLogoPreviewImage));
|
|
}
|
|
|
|
/// <summary>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.</summary>
|
|
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,
|
|
};
|
|
|
|
/// <summary>The staged scene's brand logo, exposed so the preview can draw it ABOVE the universal
|
|
/// blur (the compositor blits it after the blur group — the logo is never obscured).</summary>
|
|
public Source? StagedBrandLogo =>
|
|
StagedScene?.Elements.OfType<Source>().FirstOrDefault(s => s.IsBrandLogo);
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|