2025-11-15 14:52:01 +01:00
|
|
|
using System;
|
2026-02-25 01:41:48 +01:00
|
|
|
using System.ComponentModel;
|
2026-03-06 13:43:16 +01:00
|
|
|
using System.IO;
|
2025-09-29 11:16:14 +02:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2026-02-09 22:39:30 +01:00
|
|
|
using CommunityToolkit.WinUI;
|
|
|
|
|
using EmailValidation;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Microsoft.UI.Xaml.Controls;
|
|
|
|
|
using Microsoft.UI.Xaml.Media.Imaging;
|
2026-02-09 22:39:30 +01:00
|
|
|
using Wino.Core.Domain.Interfaces;
|
2025-09-29 11:16:14 +02:00
|
|
|
using Wino.Mail.WinUI;
|
|
|
|
|
|
|
|
|
|
namespace Wino.Controls;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Contact avatar control built on top of PersonPicture.
|
|
|
|
|
/// Priority:
|
|
|
|
|
/// 1) AccountContact/Base64 picture
|
|
|
|
|
/// 2) Gravatar thumbnail (if enabled)
|
|
|
|
|
/// 3) Initials from display name fallback
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed partial class ImagePreviewControl : PersonPicture
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-03-01 21:07:10 +01:00
|
|
|
private sealed record RefreshSnapshot(string DisplayName, string Address, Guid? ContactPictureFileId, string Base64Picture);
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private static readonly TimeSpan RefreshDebounceDuration = TimeSpan.FromMilliseconds(40);
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
[GeneratedDependencyProperty]
|
|
|
|
|
public partial IMailItemDisplayInformation? MailItemInformation { get; set; }
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private readonly IThumbnailService? _thumbnailService;
|
|
|
|
|
private readonly IPreferencesService? _preferencesService;
|
2026-03-01 21:07:10 +01:00
|
|
|
private readonly IContactPictureFileService? _contactPictureFileService;
|
2026-02-25 01:41:48 +01:00
|
|
|
private INotifyPropertyChanged? _mailItemInformationPropertySource;
|
2026-02-09 22:39:30 +01:00
|
|
|
private CancellationTokenSource? _refreshCancellationTokenSource;
|
|
|
|
|
private CancellationTokenSource? _scheduledRefreshCancellationTokenSource;
|
|
|
|
|
private long _refreshVersion;
|
|
|
|
|
|
|
|
|
|
public ImagePreviewControl()
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
DefaultStyleKey = typeof(PersonPicture);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_thumbnailService = App.Current.Services.GetService<IThumbnailService>();
|
|
|
|
|
_preferencesService = App.Current.Services.GetService<IPreferencesService>();
|
2026-03-01 21:07:10 +01:00
|
|
|
_contactPictureFileService = App.Current.Services.GetService<IContactPictureFileService>();
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Keep control functional in design-time/test contexts without service provider.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Loaded += OnLoaded;
|
|
|
|
|
Unloaded += OnUnloaded;
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
partial void OnMailItemInformationPropertyChanged(DependencyPropertyChangedEventArgs e)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-25 01:41:48 +01:00
|
|
|
if (_mailItemInformationPropertySource != null)
|
|
|
|
|
{
|
|
|
|
|
_mailItemInformationPropertySource.PropertyChanged -= MailItemInformationPropertyChanged;
|
|
|
|
|
_mailItemInformationPropertySource = null;
|
|
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
|
2026-02-25 01:41:48 +01:00
|
|
|
if (e.NewValue is INotifyPropertyChanged observableMailItemInformation)
|
|
|
|
|
{
|
|
|
|
|
_mailItemInformationPropertySource = observableMailItemInformation;
|
|
|
|
|
_mailItemInformationPropertySource.PropertyChanged += MailItemInformationPropertyChanged;
|
|
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
|
2026-02-25 01:41:48 +01:00
|
|
|
RequestRefresh();
|
|
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
RequestRefresh();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-25 01:41:48 +01:00
|
|
|
if (_mailItemInformationPropertySource != null)
|
|
|
|
|
{
|
|
|
|
|
_mailItemInformationPropertySource.PropertyChanged -= MailItemInformationPropertyChanged;
|
|
|
|
|
_mailItemInformationPropertySource = null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
CancelScheduledRefresh();
|
|
|
|
|
CancelActiveRefresh();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 01:41:48 +01:00
|
|
|
private void MailItemInformationPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
// Refresh only for fields that affect avatar image or initials.
|
|
|
|
|
if (string.IsNullOrEmpty(e.PropertyName)
|
|
|
|
|
|| e.PropertyName == nameof(IMailItemDisplayInformation.Base64ContactPicture)
|
|
|
|
|
|| e.PropertyName == nameof(IMailItemDisplayInformation.SenderContact)
|
2026-03-01 12:07:15 +01:00
|
|
|
|| e.PropertyName == nameof(IMailItemDisplayInformation.FromName)
|
|
|
|
|
|| e.PropertyName == nameof(IMailItemDisplayInformation.FromAddress)
|
2026-02-25 01:41:48 +01:00
|
|
|
|| e.PropertyName == nameof(IMailItemDisplayInformation.ThumbnailUpdatedEvent))
|
|
|
|
|
{
|
|
|
|
|
RequestRefresh();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void RequestRefresh()
|
|
|
|
|
{
|
|
|
|
|
if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess)
|
|
|
|
|
{
|
|
|
|
|
QueueRefresh();
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
DispatcherQueue.TryEnqueue(QueueRefresh);
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void QueueRefresh()
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!IsLoaded)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
CancelScheduledRefresh();
|
|
|
|
|
|
|
|
|
|
var cts = new CancellationTokenSource();
|
|
|
|
|
_scheduledRefreshCancellationTokenSource = cts;
|
|
|
|
|
|
|
|
|
|
_ = DebounceAndRefreshAsync(cts.Token);
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task DebounceAndRefreshAsync(CancellationToken cancellationToken)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await Task.Delay(RefreshDebounceDuration, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
StartRefresh();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void StartRefresh()
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
CancelActiveRefresh();
|
|
|
|
|
|
|
|
|
|
var cts = new CancellationTokenSource();
|
|
|
|
|
_refreshCancellationTokenSource = cts;
|
|
|
|
|
var refreshVersion = Interlocked.Increment(ref _refreshVersion);
|
|
|
|
|
_ = RefreshAsync(refreshVersion, cts.Token);
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void CancelScheduledRefresh()
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var cts = _scheduledRefreshCancellationTokenSource;
|
|
|
|
|
_scheduledRefreshCancellationTokenSource = null;
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (cts != null && !cts.IsCancellationRequested)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
cts.Cancel();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
cts?.Dispose();
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private void CancelActiveRefresh()
|
|
|
|
|
{
|
|
|
|
|
var cts = _refreshCancellationTokenSource;
|
|
|
|
|
_refreshCancellationTokenSource = null;
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (cts != null && !cts.IsCancellationRequested)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
cts.Cancel();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
cts?.Dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RefreshAsync(long refreshVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
try
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var snapshot = await CaptureSnapshotAsync(refreshVersion, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (snapshot == null)
|
|
|
|
|
return;
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await ApplyInitialVisualStateAsync(snapshot.DisplayName, refreshVersion, cancellationToken).ConfigureAwait(false);
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-03-01 21:07:10 +01:00
|
|
|
// Skip all picture loading if the user has disabled sender pictures.
|
|
|
|
|
if (_preferencesService?.IsShowSenderPicturesEnabled == false)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// 1) File-based contact picture (preferred — native WIC decode, no base64 overhead).
|
|
|
|
|
if (snapshot.ContactPictureFileId.HasValue && _contactPictureFileService != null)
|
|
|
|
|
{
|
|
|
|
|
var filePath = _contactPictureFileService.GetContactPicturePath(snapshot.ContactPictureFileId.Value);
|
|
|
|
|
if (!string.IsNullOrEmpty(filePath))
|
|
|
|
|
{
|
|
|
|
|
var fileBitmap = await CreateBitmapFromFileAsync(filePath, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (fileBitmap != null)
|
|
|
|
|
{
|
|
|
|
|
await ApplyProfilePictureAsync(fileBitmap, refreshVersion, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2) Legacy base64 contact picture (used until migration completes or for fallback).
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(snapshot.Base64Picture))
|
|
|
|
|
{
|
|
|
|
|
var localBitmap = await CreateBitmapFromBase64Async(snapshot.Base64Picture, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (localBitmap != null)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ApplyProfilePictureAsync(localBitmap, refreshVersion, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
return;
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
|
2026-03-01 21:07:10 +01:00
|
|
|
// 3) Gravatar lookup through thumbnail service (if enabled).
|
2026-02-09 22:39:30 +01:00
|
|
|
if (_preferencesService?.IsGravatarEnabled == true &&
|
|
|
|
|
_thumbnailService != null &&
|
|
|
|
|
!string.IsNullOrWhiteSpace(snapshot.Address) &&
|
|
|
|
|
EmailValidator.Validate(snapshot.Address))
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var thumbnailBase64 = await _thumbnailService
|
|
|
|
|
.GetThumbnailAsync(snapshot.Address.Trim().ToLowerInvariant(), awaitLoad: true)
|
|
|
|
|
.ConfigureAwait(false);
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(thumbnailBase64))
|
|
|
|
|
{
|
|
|
|
|
var thumbnailBitmap = await CreateBitmapFromBase64Async(thumbnailBase64, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
if (thumbnailBitmap != null)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
await ApplyProfilePictureAsync(thumbnailBitmap, refreshVersion, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
return;
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
|
2026-03-01 21:07:10 +01:00
|
|
|
// 4) Initials fallback is already in place via DisplayName + ProfilePicture = null.
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
// Expected during virtualization/recycling.
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
catch
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
// Keep fallback initials if decoding/network fails.
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
// DependencyProperty-backed values must be read on UI thread once, then used off-thread.
|
|
|
|
|
private async Task<RefreshSnapshot?> CaptureSnapshotAsync(long refreshVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
return await ExecuteOnUiThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
if (!IsActiveRefresh(refreshVersion, cancellationToken))
|
|
|
|
|
return null;
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var address = ResolveAddress();
|
|
|
|
|
var displayName = ResolveDisplayName(address);
|
|
|
|
|
var base64Picture = ResolveBase64Picture();
|
2026-03-01 21:07:10 +01:00
|
|
|
var contactPictureFileId = MailItemInformation?.SenderContact?.ContactPictureFileId;
|
2026-02-09 22:39:30 +01:00
|
|
|
|
2026-03-01 21:07:10 +01:00
|
|
|
return new RefreshSnapshot(displayName, address, contactPictureFileId, base64Picture);
|
2026-02-09 22:39:30 +01:00
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private string ResolveAddress()
|
|
|
|
|
{
|
2026-02-25 01:41:48 +01:00
|
|
|
if (MailItemInformation == null)
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var contactAddress = MailItemInformation?.SenderContact?.Address;
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(contactAddress))
|
|
|
|
|
return contactAddress.Trim();
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(MailItemInformation?.FromAddress))
|
|
|
|
|
return MailItemInformation.FromAddress.Trim();
|
|
|
|
|
|
2026-02-25 01:41:48 +01:00
|
|
|
return string.Empty;
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private string ResolveDisplayName(string resolvedAddress)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
var contactName = MailItemInformation?.SenderContact?.Name;
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(contactName))
|
|
|
|
|
return contactName.Trim();
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(MailItemInformation?.FromName))
|
|
|
|
|
return MailItemInformation.FromName.Trim();
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
return resolvedAddress.Trim();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private string ResolveBase64Picture()
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(MailItemInformation?.SenderContact?.Base64ContactPicture))
|
|
|
|
|
return MailItemInformation.SenderContact.Base64ContactPicture;
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(MailItemInformation?.Base64ContactPicture))
|
|
|
|
|
return MailItemInformation.Base64ContactPicture;
|
|
|
|
|
|
2026-02-25 01:41:48 +01:00
|
|
|
return string.Empty;
|
2026-02-09 22:39:30 +01:00
|
|
|
}
|
|
|
|
|
private async Task ApplyInitialVisualStateAsync(string displayName, long refreshVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteOnUiThreadAsync(() =>
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!IsActiveRefresh(refreshVersion, cancellationToken))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
DisplayName = displayName;
|
2026-03-01 13:48:40 +01:00
|
|
|
Initials = null;
|
2026-02-09 22:39:30 +01:00
|
|
|
ProfilePicture = null;
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ApplyProfilePictureAsync(BitmapImage bitmapImage, long refreshVersion, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
await ExecuteOnUiThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
if (!IsActiveRefresh(refreshVersion, cancellationToken))
|
|
|
|
|
return;
|
|
|
|
|
|
2026-03-01 13:48:40 +01:00
|
|
|
Initials = string.Empty;
|
2026-02-09 22:39:30 +01:00
|
|
|
ProfilePicture = bitmapImage;
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsActiveRefresh(long refreshVersion, CancellationToken cancellationToken)
|
|
|
|
|
=> !cancellationToken.IsCancellationRequested && refreshVersion == _refreshVersion;
|
|
|
|
|
|
|
|
|
|
private async Task ExecuteOnUiThreadAsync(Action action)
|
|
|
|
|
{
|
|
|
|
|
if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess)
|
|
|
|
|
{
|
|
|
|
|
action();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var completion = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
|
|
|
|
|
|
var enqueued = DispatcherQueue.TryEnqueue(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
action();
|
|
|
|
|
completion.TrySetResult(null);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
completion.TrySetException(ex);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!enqueued)
|
|
|
|
|
{
|
|
|
|
|
completion.TrySetException(new InvalidOperationException("Failed to dispatch UI update."));
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
await completion.Task.ConfigureAwait(false);
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task<T> ExecuteOnUiThreadAsync<T>(Func<T> func)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
if (DispatcherQueue == null || DispatcherQueue.HasThreadAccess)
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
return func();
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var completion = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
var enqueued = DispatcherQueue.TryEnqueue(() =>
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
completion.TrySetResult(func());
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
completion.TrySetException(ex);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
if (!enqueued)
|
|
|
|
|
{
|
|
|
|
|
completion.TrySetException(new InvalidOperationException("Failed to dispatch UI update."));
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
return await completion.Task.ConfigureAwait(false);
|
|
|
|
|
}
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-03-01 21:07:10 +01:00
|
|
|
private async Task<BitmapImage?> CreateBitmapFromFileAsync(string filePath, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
byte[] bytes;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
bytes = await File.ReadAllBytesAsync(filePath, cancellationToken).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
return await ExecuteOnUiThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
using var memoryStream = new MemoryStream(bytes);
|
|
|
|
|
var bitmapImage = new BitmapImage();
|
|
|
|
|
bitmapImage.SetSource(memoryStream.AsRandomAccessStream());
|
|
|
|
|
return bitmapImage;
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
private async Task<BitmapImage?> CreateBitmapFromBase64Async(string base64, CancellationToken cancellationToken)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(base64))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
byte[] bytes;
|
|
|
|
|
|
|
|
|
|
try
|
2025-09-29 11:16:14 +02:00
|
|
|
{
|
2026-02-09 22:39:30 +01:00
|
|
|
bytes = await Task.Run(() => Convert.FromBase64String(base64), cancellationToken).ConfigureAwait(false);
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|
2026-02-09 22:39:30 +01:00
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
|
|
|
|
|
|
return await ExecuteOnUiThreadAsync(() =>
|
|
|
|
|
{
|
|
|
|
|
cancellationToken.ThrowIfCancellationRequested();
|
2025-09-29 11:16:14 +02:00
|
|
|
|
2026-02-09 22:39:30 +01:00
|
|
|
using var memoryStream = new MemoryStream(bytes);
|
|
|
|
|
var bitmapImage = new BitmapImage();
|
|
|
|
|
bitmapImage.SetSource(memoryStream.AsRandomAccessStream());
|
|
|
|
|
return bitmapImage;
|
|
|
|
|
}).ConfigureAwait(false);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 11:16:14 +02:00
|
|
|
}
|