feat(settings): brand-logo thumbnail with a hover enlargement
The creator (2026-10-30): the brand-logo field in App Settings showed only a file NAME, so a myopic user could not confirm which picture was set. Now a 28x28 thumbnail sits left of the field showing the actual image, and hovering it opens an enlarged copy (up to 420x320) beside the panel with the file name underneath. A click collapses it; moving the pointer away closes it, unless the pointer walked onto the popup itself, where it stays so the creator can look closely. Mechanics: - `MainViewModel.BrandLogoPreviewImage` decodes the stored asset ONCE and freezes it, so the thumbnail and the enlargement share one bitmap and no dispatcher hop is needed. A corrupt image returns null instead of taking the dialog down. - The thumbnail is hidden when no image is set (NotNullToVis), so there is nothing to hover and no empty frame can appear. - The popup is a separate window: WPF refuses to open one until its placement target is in a live visual tree, which is why the test hosts the control in a real window rather than asserting on a detached control. Guard: BrandLogoPreviewTests — the wiring (image binding, the no-image gate, the four gesture handlers, the readable enlargement size) and the control's open/close behaviour.
This commit is contained in:
@@ -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;
|
||||
|
||||
/// <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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user