diff --git a/Controls/OverlayHost.xaml b/Controls/OverlayHost.xaml
index a41e0dc..ec8a2bb 100644
--- a/Controls/OverlayHost.xaml
+++ b/Controls/OverlayHost.xaml
@@ -57,9 +57,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Controls/OverlayHost.xaml.cs b/Controls/OverlayHost.xaml.cs
index 6400916..6752088 100644
--- a/Controls/OverlayHost.xaml.cs
+++ b/Controls/OverlayHost.xaml.cs
@@ -11,6 +11,36 @@ public partial class OverlayHost : UserControl
InitializeComponent();
}
+ /// Is the enlarged brand-logo preview showing? Test seam — a Popup is a separate
+ /// window, so nothing else can observe it.
+ internal bool IsBrandLogoPreviewOpen => BrandLogoPreviewPopup.IsOpen;
+
+ /// Hover opens the enlargement. Nothing to show when no image is set, so the
+ /// thumbnail simply does nothing in that case.
+ private void BrandLogoThumb_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
+ => OpenBrandLogoPreview();
+
+ /// Leaving the thumbnail closes it — unless the pointer walked onto the popup
+ /// itself (which sits beside it), where it stays put so the creator can look closely.
+ private void BrandLogoThumb_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
+ {
+ if (BrandLogoPreviewPopup.Child?.IsMouseOver == true) return;
+ CloseBrandLogoPreview();
+ }
+
+ /// A click collapses the enlargement — the explicit "enough, thanks" gesture.
+ private void BrandLogoThumb_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ => CloseBrandLogoPreview();
+
+ private void BrandLogoPreviewPopup_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
+ => CloseBrandLogoPreview();
+
+ /// The thumbnail is hidden when no image is set (see the XAML), so there is nothing
+ /// to open in that case — no empty frame can appear.
+ internal void OpenBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = true;
+
+ internal void CloseBrandLogoPreview() => BrandLogoPreviewPopup.IsOpen = false;
+
private void BackToAbout_Click(object sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel vm)
diff --git a/ViewModels/MainViewModel.BrandLogo.cs b/ViewModels/MainViewModel.BrandLogo.cs
index b20a2fe..b9e86fe 100644
--- a/ViewModels/MainViewModel.BrandLogo.cs
+++ b/ViewModels/MainViewModel.BrandLogo.cs
@@ -1,4 +1,6 @@
using System.IO;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
using System.Windows.Input;
using ytLive.Helpers;
using ytLive.Models;
@@ -22,6 +24,51 @@ public partial class MainViewModel
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
@@ -32,6 +79,7 @@ public partial class MainViewModel
if (dialog.ShowDialog() != true) return;
_layoutStore.SetBrandLogo(File.ReadAllBytes(dialog.FileName), Path.GetFileName(dialog.FileName));
EnsureBrandLogos();
+ RefreshBrandLogoPreview();
OnPropertyChanged(nameof(BrandLogoFileDisplay));
ScheduleSave();
}
diff --git a/ytLive.Tests/BrandLogoPreviewTests.cs b/ytLive.Tests/BrandLogoPreviewTests.cs
new file mode 100644
index 0000000..37cbe20
--- /dev/null
+++ b/ytLive.Tests/BrandLogoPreviewTests.cs
@@ -0,0 +1,60 @@
+using Xunit;
+
+namespace ytLive.Tests;
+
+/// The creator (2026-10-30): in App Settings the brand-logo field is only a file NAME,
+/// which says nothing about the picture, so "myopic users like myself" cannot confirm what is
+/// set. The field now carries a thumbnail — hover enlarges it, a click collapses it.
+///
+/// The ONE test covers both halves: the XAML wiring that routes hover/leave/click, and the
+/// control's own open/close behaviour (a Popup is a separate window, so only this seam can
+/// observe it). Runs inside the real App because the control resolves theme resources at load.
+[Collection("RealApp")]
+public class BrandLogoPreviewTests
+{
+ private readonly RealAppHost _app;
+
+ public BrandLogoPreviewTests(RealAppHost app) => _app = app;
+
+ [Fact]
+ public void Hovering_the_thumbnail_opens_the_enlarged_preview_and_a_click_collapses_it()
+ {
+ // 1. Wiring: the thumbnail is image-bound and hidden when no image is set, the enlarged
+ // popup shows the same source at a readable size, and every gesture reaches a handler.
+ var xaml = SourceAssert.Text("Controls", "OverlayHost.xaml");
+ Assert.Contains("x:Name=\"BrandLogoThumbImage\"", xaml);
+ Assert.Contains("{Binding BrandLogoPreviewImage}", xaml);
+ Assert.Contains("NotNullToVis", xaml);
+ Assert.Contains("x:Name=\"BrandLogoPreviewPopup\"", xaml);
+ Assert.Contains("MaxWidth=\"420\"", xaml);
+ foreach (var handler in new[]
+ {
+ "MouseEnter=\"BrandLogoThumb_MouseEnter\"",
+ "MouseLeave=\"BrandLogoThumb_MouseLeave\"",
+ "MouseLeftButtonUp=\"BrandLogoThumb_Click\"",
+ "MouseLeave=\"BrandLogoPreviewPopup_MouseLeave\"",
+ })
+ Assert.Contains(handler, xaml);
+
+ // 2. Behaviour on the real control.
+ _app.Run(() =>
+ {
+ // A Popup refuses to open until its placement target is in a live visual tree, so the
+ // control is hosted in a real window — the same condition the app provides.
+ var host = new ytLive.Controls.OverlayHost();
+ var window = new System.Windows.Window { Content = host, Width = 420, Height = 420 };
+ window.Show();
+ try
+ {
+ Assert.False(host.IsBrandLogoPreviewOpen);
+
+ host.OpenBrandLogoPreview();
+ Assert.True(host.IsBrandLogoPreviewOpen, "hovering the thumbnail must open the enlargement");
+
+ host.CloseBrandLogoPreview();
+ Assert.False(host.IsBrandLogoPreviewOpen, "a click must collapse it again");
+ }
+ finally { window.Close(); }
+ });
+ }
+}