Implemented initial version for popping out window for rendering and compose pages

This commit is contained in:
Burak Kaan Köse
2026-04-11 01:04:59 +02:00
parent d5c121ce24
commit 4cb08f0a98
22 changed files with 628 additions and 79 deletions
@@ -58,6 +58,7 @@
"Buttons_Allow": "Allow", "Buttons_Allow": "Allow",
"Buttons_Apply": "Apply", "Buttons_Apply": "Apply",
"Buttons_ApplyTheme": "Apply Theme", "Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Pop out",
"Buttons_Browse": "Browse", "Buttons_Browse": "Browse",
"Buttons_Cancel": "Cancel", "Buttons_Cancel": "Cancel",
"Buttons_Close": "Close", "Buttons_Close": "Close",
+24 -9
View File
@@ -23,21 +23,22 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions; using Wino.Core.Extensions;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.UI; using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public partial class ComposePageViewModel : MailBaseViewModel, public partial class ComposePageViewModel : MailBaseViewModel,
IRecipient<ReaderItemRefreshRequestedEvent>,
IRecipient<SynchronizationActionsAdded>, IRecipient<SynchronizationActionsAdded>,
IRecipient<SynchronizationActionsCompleted>, IRecipient<SynchronizationActionsCompleted>,
IRecipient<AccountSynchronizerStateChanged> IRecipient<AccountSynchronizerStateChanged>
{ {
public event EventHandler CloseRequested;
private static readonly TimeSpan LocalDraftRetryGracePeriod = TimeSpan.FromSeconds(15); private static readonly TimeSpan LocalDraftRetryGracePeriod = TimeSpan.FromSeconds(15);
public Func<Task<string>> GetHTMLBodyFunction; public Func<Task<string>> GetHTMLBodyFunction;
public Func<string, Task> RenderHtmlBodyAsyncFunc { get; set; }
public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args)
{ {
@@ -529,9 +530,9 @@ public partial class ComposePageViewModel : MailBaseViewModel,
} }
} }
public async void Receive(ReaderItemRefreshRequestedEvent message) public async Task RefreshDraftAsync(MailItemViewModel draftMailItemViewModel)
{ {
if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return; if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return;
// Save current draft before switching. // Save current draft before switching.
await UpdateMimeChangesAsync(); await UpdateMimeChangesAsync();
@@ -542,7 +543,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
IncludedAttachments.Clear(); IncludedAttachments.Clear();
// Set the new draft item and prepare it. // Set the new draft item and prepare it.
CurrentMailDraftItem = message.MailItemViewModel; CurrentMailDraftItem = draftMailItemViewModel;
await UpdatePendingOperationStateAsync(); await UpdatePendingOperationStateAsync();
await LoadEmailTemplatesAsync(); await LoadEmailTemplatesAsync();
await TryPrepareComposeAsync(true); await TryPrepareComposeAsync(true);
@@ -591,7 +592,6 @@ public partial class ComposePageViewModel : MailBaseViewModel,
{ {
base.RegisterRecipients(); base.RegisterRecipients();
Messenger.Register<ReaderItemRefreshRequestedEvent>(this);
Messenger.Register<SynchronizationActionsAdded>(this); Messenger.Register<SynchronizationActionsAdded>(this);
Messenger.Register<SynchronizationActionsCompleted>(this); Messenger.Register<SynchronizationActionsCompleted>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this); Messenger.Register<AccountSynchronizerStateChanged>(this);
@@ -601,7 +601,6 @@ public partial class ComposePageViewModel : MailBaseViewModel,
{ {
base.UnregisterRecipients(); base.UnregisterRecipients();
Messenger.Unregister<ReaderItemRefreshRequestedEvent>(this);
Messenger.Unregister<SynchronizationActionsAdded>(this); Messenger.Unregister<SynchronizationActionsAdded>(this);
Messenger.Unregister<SynchronizationActionsCompleted>(this); Messenger.Unregister<SynchronizationActionsCompleted>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this); Messenger.Unregister<AccountSynchronizerStateChanged>(this);
@@ -754,9 +753,12 @@ public partial class ComposePageViewModel : MailBaseViewModel,
IsCCBCCVisible = true; IsCCBCCVisible = true;
Subject = replyingMime.Subject; Subject = replyingMime.Subject;
Messenger.Send(new CreateNewComposeMailRequested(renderModel));
}); });
if (RenderHtmlBodyAsyncFunc != null)
{
await ExecuteUIThread(async () => await RenderHtmlBodyAsyncFunc(renderModel.RenderHtml));
}
} }
private void LoadAttachments() private void LoadAttachments()
@@ -891,6 +893,19 @@ public partial class ComposePageViewModel : MailBaseViewModel,
} }
} }
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
{
base.OnMailRemoved(removedMail, source);
if (CurrentMailDraftItem?.MailCopy == null)
return;
if (CurrentMailDraftItem.MailCopy.UniqueId != removedMail.UniqueId)
return;
await ExecuteUIThread(() => CloseRequested?.Invoke(this, EventArgs.Empty));
}
private void NotifyComposeActionStateChanged() private void NotifyComposeActionStateChanged()
{ {
OnPropertyChanged(nameof(IsLocalDraft)); OnPropertyChanged(nameof(IsLocalDraft));
@@ -26,7 +26,7 @@ using Wino.Core.Domain.Models.Printing;
using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Reader;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Models;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.UI; using Wino.Messaging.UI;
using IMailService = Wino.Core.Domain.Interfaces.IMailService; using IMailService = Wino.Core.Domain.Interfaces.IMailService;
@@ -34,10 +34,12 @@ using IMailService = Wino.Core.Domain.Interfaces.IMailService;
namespace Wino.Mail.ViewModels; namespace Wino.Mail.ViewModels;
public partial class MailRenderingPageViewModel : MailBaseViewModel, public partial class MailRenderingPageViewModel : MailBaseViewModel,
IRecipient<ReaderItemRefreshRequestedEvent>,
IRecipient<ThumbnailAdded>, IRecipient<ThumbnailAdded>,
ITransferProgress // For listening IMAP message download progress. ITransferProgress // For listening IMAP message download progress.
{ {
public event EventHandler CloseRequested;
public event EventHandler<ComposeDraftRequestedEventArgs> ComposeRequested;
private readonly IMailDialogService _dialogService; private readonly IMailDialogService _dialogService;
private readonly IUnderlyingThemeService _underlyingThemeService; private readonly IUnderlyingThemeService _underlyingThemeService;
@@ -58,8 +60,9 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// Func to get WebView2 to save current HTML as PDF to given location. // Func to get WebView2 to save current HTML as PDF to given location.
// Used in 'Save as' and 'Print' functionality. // Used in 'Save as' and 'Print' functionality.
public Func<string, Task<bool>> SaveHTMLasPDFFunc { get; set; } public Func<string, Task<bool>> SaveHTMLasPDFFunc { get; set; }
public Func<WebView2PrintSettingsModel, Task<PrintingResult>> DirectPrintFuncAsync { get; set; } public Func<WebView2PrintSettingsModel, Task<PrintingResult>> DirectPrintFuncAsync { get; set; }
public Func<string, Task> RenderHtmlAsyncFunc { get; set; }
public Func<Task> ClearRenderedHtmlAsyncFunc { get; set; }
#region Properties #region Properties
@@ -319,6 +322,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy); var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.MailCopy.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest); await _requestDelegator.ExecuteAsync(draftPreparationRequest);
ComposeRequested?.Invoke(this, new ComposeDraftRequestedEventArgs(draftMailCopy.UniqueId));
} }
else if (initializedMailItemViewModel != null) else if (initializedMailItemViewModel != null)
@@ -362,8 +366,10 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
initializedMimeMessageInformation = null; initializedMimeMessageInformation = null;
CurrentMailItemDisplayInformation = null; CurrentMailItemDisplayInformation = null;
// Dispose existing content first. if (ClearRenderedHtmlAsyncFunc != null)
Messenger.Send(new CancelRenderingContentRequested()); {
await ExecuteUIThread(async () => await ClearRenderedHtmlAsyncFunc());
}
// This page can be accessed for 2 purposes. // This page can be accessed for 2 purposes.
// 1. Rendering a mail item when the user selects. // 1. Rendering a mail item when the user selects.
@@ -509,8 +515,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
CurrentRenderModel = _mimeFileService.GetMailRenderModel(message, messagePath, renderingOptions); CurrentRenderModel = _mimeFileService.GetMailRenderModel(message, messagePath, renderingOptions);
Messenger.Send(new HtmlRenderingRequested(CurrentRenderModel.RenderHtml));
foreach (var attachment in CurrentRenderModel.Attachments) foreach (var attachment in CurrentRenderModel.Attachments)
{ {
Attachments.Add(new MailAttachmentViewModel(attachment)); Attachments.Add(new MailAttachmentViewModel(attachment));
@@ -520,6 +524,11 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
StatePersistenceService.IsReadingMail = true; StatePersistenceService.IsReadingMail = true;
}); });
if (RenderHtmlAsyncFunc != null)
{
await ExecuteUIThread(async () => await RenderHtmlAsyncFunc(CurrentRenderModel.RenderHtml));
}
} }
private async Task<List<AccountContactViewModel>> GetAccountContacts(InternetAddressList internetAddresses) private async Task<List<AccountContactViewModel>> GetAccountContacts(InternetAddressList internetAddresses)
@@ -658,6 +667,19 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => { InitializeCommandBarItems(); }); await ExecuteUIThread(() => { InitializeCommandBarItems(); });
} }
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
{
base.OnMailRemoved(removedMail, source);
if (initializedMailItemViewModel?.MailCopy == null)
return;
if (initializedMailItemViewModel.MailCopy.UniqueId != removedMail.UniqueId)
return;
await ExecuteUIThread(() => CloseRequested?.Invoke(this, EventArgs.Empty));
}
[RelayCommand] [RelayCommand]
private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel) private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel)
{ {
@@ -794,13 +816,13 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
// For upload. // For upload.
void ITransferProgress.Report(long bytesTransferred) { } void ITransferProgress.Report(long bytesTransferred) { }
public async void Receive(ReaderItemRefreshRequestedEvent message) public async Task RefreshMailItemAsync(MailItemViewModel mailItemViewModel)
{ {
if (message.MailItemViewModel == null || message.MailItemViewModel.IsDraft) return; if (mailItemViewModel == null || mailItemViewModel.IsDraft) return;
try try
{ {
await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token); await RenderAsync(mailItemViewModel, renderCancellationTokenSource.Token);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -907,7 +929,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
{ {
base.RegisterRecipients(); base.RegisterRecipients();
Messenger.Register<ReaderItemRefreshRequestedEvent>(this);
Messenger.Register<ThumbnailAdded>(this); Messenger.Register<ThumbnailAdded>(this);
} }
@@ -915,7 +936,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
{ {
base.UnregisterRecipients(); base.UnregisterRecipients();
Messenger.Unregister<ReaderItemRefreshRequestedEvent>(this);
Messenger.Unregister<ThumbnailAdded>(this); Messenger.Unregister<ThumbnailAdded>(this);
} }
} }
@@ -0,0 +1,13 @@
using System;
namespace Wino.Mail.ViewModels.Models;
public sealed class ComposeDraftRequestedEventArgs : EventArgs
{
public ComposeDraftRequestedEventArgs(Guid draftUniqueId)
{
DraftUniqueId = draftUniqueId;
}
public Guid DraftUniqueId { get; }
}
@@ -23,6 +23,7 @@ public sealed partial class OperationCommandBar : CommandBar
private const string MailOperationTemplateKey = "OperationCommandBarMailOperationTemplate"; private const string MailOperationTemplateKey = "OperationCommandBarMailOperationTemplate";
private const string FolderOperationTemplateKey = "OperationCommandBarFolderOperationTemplate"; private const string FolderOperationTemplateKey = "OperationCommandBarFolderOperationTemplate";
private const string AIActionsTemplateKey = "OperationCommandBarAIActionsTemplate"; private const string AIActionsTemplateKey = "OperationCommandBarAIActionsTemplate";
private const string PopOutTemplateKey = "OperationCommandBarThemeToggleTemplate";
private const string ThemeToggleTemplateKey = "OperationCommandBarThemeToggleTemplate"; private const string ThemeToggleTemplateKey = "OperationCommandBarThemeToggleTemplate";
private const string SeparatorTemplateKey = "OperationCommandBarSeparatorTemplate"; private const string SeparatorTemplateKey = "OperationCommandBarSeparatorTemplate";
@@ -47,7 +48,11 @@ public sealed partial class OperationCommandBar : CommandBar
[GeneratedDependencyProperty] [GeneratedDependencyProperty]
public partial bool IsEditorThemeToggleVisible { get; set; } public partial bool IsEditorThemeToggleVisible { get; set; }
[GeneratedDependencyProperty]
public partial bool IsPopOutButtonVisible { get; set; }
public event EventHandler<bool>? AIActionsEnabledChanged; public event EventHandler<bool>? AIActionsEnabledChanged;
public event EventHandler? PopOutClicked;
public OperationCommandBar() public OperationCommandBar()
{ {
@@ -58,7 +63,6 @@ public sealed partial class OperationCommandBar : CommandBar
OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Auto; OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Auto;
Loaded += OnLoaded; Loaded += OnLoaded;
Unloaded += OnUnloaded;
DynamicOverflowItemsChanging += OperationCommandBar_DynamicOverflowItemsChanging; DynamicOverflowItemsChanging += OperationCommandBar_DynamicOverflowItemsChanging;
} }
@@ -100,14 +104,14 @@ public sealed partial class OperationCommandBar : CommandBar
RefreshCommands(); RefreshCommands();
} }
private void OnLoaded(object sender, RoutedEventArgs e) partial void OnIsPopOutButtonVisibleChanged(bool newValue)
{ {
RefreshCommands(); RefreshCommands();
} }
private void OnUnloaded(object sender, RoutedEventArgs e) private void OnLoaded(object sender, RoutedEventArgs e)
{ {
ClearGeneratedCommands(); RefreshCommands();
} }
private void OperationCommandBar_DynamicOverflowItemsChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args) private void OperationCommandBar_DynamicOverflowItemsChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args)
@@ -216,6 +220,11 @@ public sealed partial class OperationCommandBar : CommandBar
PrimaryCommands.Add(CreateAIActionsToggleButton()); PrimaryCommands.Add(CreateAIActionsToggleButton());
} }
if (IsPopOutButtonVisible)
{
PrimaryCommands.Add(CreatePopOutButton());
}
if (IsEditorThemeToggleVisible) if (IsEditorThemeToggleVisible)
{ {
PrimaryCommands.Add(CreateThemeToggleButton()); PrimaryCommands.Add(CreateThemeToggleButton());
@@ -266,6 +275,7 @@ public sealed partial class OperationCommandBar : CommandBar
case AppBarButton button: case AppBarButton button:
button.Click -= OperationButton_Click; button.Click -= OperationButton_Click;
button.Click -= ThemeButton_Click; button.Click -= ThemeButton_Click;
button.Click -= PopOutButton_Click;
break; break;
case AppBarToggleButton toggleButton: case AppBarToggleButton toggleButton:
toggleButton.ClearValue(AppBarToggleButton.IsCheckedProperty); toggleButton.ClearValue(AppBarToggleButton.IsCheckedProperty);
@@ -356,6 +366,16 @@ public sealed partial class OperationCommandBar : CommandBar
return button; return button;
} }
private AppBarButton CreatePopOutButton()
{
var button = (AppBarButton)LoadCommandBarElementTemplate(
PopOutTemplateKey,
new OperationCommandBarThemeItemViewModel(Translator.Buttons_PopOut, WinoIconGlyph.OpenInNewWindow));
button.Click += PopOutButton_Click;
return button;
}
private void OperationButton_Click(object sender, RoutedEventArgs e) private void OperationButton_Click(object sender, RoutedEventArgs e)
{ {
if (sender is AppBarButton button && button.Tag is IMenuOperation operation) if (sender is AppBarButton button && button.Tag is IMenuOperation operation)
@@ -369,6 +389,11 @@ public sealed partial class OperationCommandBar : CommandBar
IsEditorThemeDark = !IsEditorThemeDark; IsEditorThemeDark = !IsEditorThemeDark;
} }
private void PopOutButton_Click(object sender, RoutedEventArgs e)
{
PopOutClicked?.Invoke(this, EventArgs.Empty);
}
private object? FindTemplateResource(string key) private object? FindTemplateResource(string key)
{ {
if (TryGetResourceRecursive(Resources, key, out var resource)) if (TryGetResourceRecursive(Resources, key, out var resource))
@@ -430,6 +455,11 @@ public sealed partial class OperationCommandBar : CommandBar
: CommandBarOverflowButtonVisibility.Auto; : CommandBarOverflowButtonVisibility.Auto;
} }
public void InvalidateCommands()
{
RefreshCommands();
}
private sealed class SeparatorCommandBarItemViewModel; private sealed class SeparatorCommandBarItemViewModel;
} }
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8" ?>
<winuiex:WindowEx
x:Class="Wino.Mail.WinUI.HostedContentPopoutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:winuiex="using:WinUIEx"
mc:Ignorable="d">
<Grid
x:Name="RootGrid"
Padding="4,32,4,4"
Background="{ThemeResource WinoApplicationBackgroundColor}">
<Grid x:Name="ContentHost">
<Grid.ChildrenTransitions>
<TransitionCollection>
<PopupThemeTransition />
</TransitionCollection>
</Grid.ChildrenTransitions>
</Grid>
</Grid>
</winuiex:WindowEx>
@@ -0,0 +1,46 @@
using System;
using Microsoft.UI.Xaml;
using Wino.Mail.WinUI.Models;
using WinUIEx;
namespace Wino.Mail.WinUI;
public sealed partial class HostedContentPopoutWindow : WindowEx
{
private readonly Action _closedCallback;
public HostedPopoutDescriptor Descriptor { get; }
public HostedContentPopoutWindow(HostedPopoutDescriptor descriptor, Action closedCallback)
{
Descriptor = descriptor;
_closedCallback = closedCallback;
InitializeComponent();
Title = descriptor.Title;
Width = descriptor.Width;
Height = descriptor.Height;
MinWidth = descriptor.MinWidth;
MinHeight = descriptor.MinHeight;
ExtendsContentIntoTitleBar = true;
this.SetIcon("Assets/Wino_Icon.ico");
this.CenterOnScreen();
Closed += OnClosed;
}
public void SetHostedContent(FrameworkElement content)
{
ContentHost.Children.Clear();
ContentHost.Children.Add(content);
}
private void OnClosed(object sender, WindowEventArgs args)
{
Closed -= OnClosed;
_closedCallback();
}
}
@@ -0,0 +1,15 @@
using Microsoft.UI.Xaml;
using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Models;
namespace Wino.Mail.WinUI.Interfaces;
public interface IHostedPopoutSource
{
bool CanPopOutCurrentContent();
FrameworkElement? GetCurrentHostedContent();
HostedPopoutDescriptor CreatePopoutDescriptor(IPopoutClient client);
FrameworkElement DetachHostedContent();
void OnHostedContentPoppedOut(FrameworkElement content, HostedContentPopoutWindow window, HostedPopoutDescriptor descriptor);
void OnHostedPopoutClosed(FrameworkElement content, HostedPopoutDescriptor descriptor);
}
@@ -0,0 +1,13 @@
using System;
using Wino.Mail.WinUI.Models;
namespace Wino.Mail.WinUI.Interfaces;
public interface IPopoutClient
{
bool SupportsPopOut { get; }
event EventHandler<PopOutRequestedEventArgs> PopOutRequested;
event EventHandler<PopoutHostActionRequestedEventArgs> HostActionRequested;
HostedPopoutDescriptor GetPopoutDescriptor();
void OnPopoutStateChanged(bool isPoppedOut);
}
@@ -0,0 +1,10 @@
namespace Wino.Mail.WinUI.Models;
public sealed record HostedPopoutDescriptor(
string WindowName,
string Title,
double Width,
double Height,
double MinWidth,
double MinHeight,
string ContentKind);
@@ -0,0 +1,8 @@
using System;
namespace Wino.Mail.WinUI.Models;
public sealed class PopOutRequestedEventArgs : EventArgs
{
public static PopOutRequestedEventArgs Default { get; } = new();
}
@@ -0,0 +1,9 @@
using System;
namespace Wino.Mail.WinUI.Models;
public enum PopoutHostActionKind
{
CloseHostedInstance,
PopOutNextNavigation
}
@@ -0,0 +1,17 @@
using System;
namespace Wino.Mail.WinUI.Models;
public sealed class PopoutHostActionRequestedEventArgs : EventArgs
{
public PopoutHostActionRequestedEventArgs(PopoutHostActionKind actionKind, Type? targetPageType = null, Guid? targetMailUniqueId = null)
{
ActionKind = actionKind;
TargetPageType = targetPageType;
TargetMailUniqueId = targetMailUniqueId;
}
public PopoutHostActionKind ActionKind { get; }
public Type? TargetPageType { get; }
public Guid? TargetMailUniqueId { get; }
}
+2 -1
View File
@@ -3,5 +3,6 @@ namespace Wino.Mail.WinUI.Models;
public enum WinoWindowKind public enum WinoWindowKind
{ {
Shell, Shell,
Welcome Welcome,
HostedPopout
} }
@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
namespace Wino.Mail.WinUI.Services;
public static class HostedContentPopoutCoordinator
{
public static async Task<bool> PopOutCurrentContentAsync(IHostedPopoutSource source)
{
if (!source.CanPopOutCurrentContent())
return false;
var content = source.GetCurrentHostedContent();
if (content is not FrameworkElement frameworkElement || frameworkElement is not IPopoutClient client || !client.SupportsPopOut)
return false;
var descriptor = source.CreatePopoutDescriptor(client);
var windowManager = WinoApplication.Current.Services.GetRequiredService<IWinoWindowManager>();
if (windowManager.GetWindow(WinoWindowKind.HostedPopout, descriptor.WindowName) is HostedContentPopoutWindow existingWindow)
{
windowManager.ActivateWindow(existingWindow);
return false;
}
var detachedContent = source.DetachHostedContent();
if (detachedContent is IPopoutClient detachedClient)
{
detachedClient.OnPopoutStateChanged(true);
}
HostedContentPopoutWindow? popoutWindow = null;
popoutWindow = (HostedContentPopoutWindow)windowManager.CreateWindow(
WinoWindowKind.HostedPopout,
() => new HostedContentPopoutWindow(descriptor, () =>
{
source.OnHostedPopoutClosed(detachedContent, descriptor);
}),
descriptor.WindowName);
popoutWindow.SetHostedContent(detachedContent);
source.OnHostedContentPoppedOut(detachedContent, popoutWindow, descriptor);
windowManager.ActivateWindow(popoutWindow);
var themeService = WinoApplication.Current.Services.GetService<INewThemeService>();
if (themeService != null)
{
await themeService.ApplyThemeToActiveWindowAsync();
}
return true;
}
}
@@ -381,7 +381,10 @@ public class NavigationService : NavigationServiceBase, INavigationService
&& parameter is MailItemViewModel mailItemViewModel && parameter is MailItemViewModel mailItemViewModel
&& page != WinoPage.ComposePage) && page != WinoPage.ComposePage)
{ {
WeakReferenceMessenger.Default.Send(new ReaderItemRefreshRequestedEvent(mailItemViewModel)); if (listingFrame.Content is MailRenderingPage renderingPage)
{
_ = renderingPage.RefreshMailItemAsync(mailItemViewModel);
}
} }
else if (listingFrame.Content != null else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.ComposePage) && listingFrame.Content.GetType() == GetPageType(WinoPage.ComposePage)
@@ -390,7 +393,10 @@ public class NavigationService : NavigationServiceBase, INavigationService
{ {
// ComposePage is already active and we're switching to another draft. // ComposePage is already active and we're switching to another draft.
// Reuse existing ComposePage and WebView2 instead of navigating. // Reuse existing ComposePage and WebView2 instead of navigating.
WeakReferenceMessenger.Default.Send(new ReaderItemRefreshRequestedEvent(composeDraftViewModel)); if (listingFrame.Content is ComposePage composePage)
{
_ = composePage.RefreshDraftAsync(composeDraftViewModel);
}
} }
else if (listingFrame.Content != null else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage) && listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
@@ -172,6 +172,14 @@
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE945;" /> <FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="&#xE945;" />
</AppBarToggleButton.Icon> </AppBarToggleButton.Icon>
</AppBarToggleButton> </AppBarToggleButton>
<AppBarButton
Click="PopOutButton_Click"
ToolTipService.ToolTip="{x:Bind domain:Translator.Buttons_PopOut}"
Visibility="{x:Bind GetPopOutButtonVisibility(), Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="OpenInNewWindow" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Click="ToggleEditorThemeClicked" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(WebViewEditor.IsEditorDarkMode), Mode=OneWay}"> <AppBarButton Click="ToggleEditorThemeClicked" ToolTipService.ToolTip="{x:Bind GetEditorThemeToolTip(WebViewEditor.IsEditorDarkMode), Mode=OneWay}">
<AppBarButton.Icon> <AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(WebViewEditor.IsEditorDarkMode), Mode=OneWay}" /> <coreControls:WinoFontIcon Icon="{x:Bind GetEditorThemeIcon(WebViewEditor.IsEditorDarkMode), Mode=OneWay}" />
+48 -14
View File
@@ -25,6 +25,8 @@ using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages; using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI.Controls; using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
using Wino.Views.Abstract; using Wino.Views.Abstract;
@@ -33,13 +35,19 @@ namespace Wino.Views.Mail;
public sealed partial class ComposePage : ComposePageAbstract, public sealed partial class ComposePage : ComposePageAbstract,
IAiHtmlActionHost, IAiHtmlActionHost,
IRecipient<CreateNewComposeMailRequested>, IPopoutClient,
IRecipient<ApplicationThemeChanged>, IRecipient<ApplicationThemeChanged>
IRecipient<ReaderItemRefreshRequestedEvent>
{ {
private bool _isPoppedOut;
public bool SupportsPopOut => !_isPoppedOut;
public event EventHandler<PopOutRequestedEventArgs>? PopOutRequested;
public event EventHandler<PopoutHostActionRequestedEventArgs>? HostActionRequested;
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView(); public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
public Visibility GetAiActionsToggleVisibility(bool isHidden) => isHidden ? Visibility.Collapsed : Visibility.Visible; public Visibility GetAiActionsToggleVisibility(bool isHidden) => isHidden ? Visibility.Collapsed : Visibility.Visible;
public Visibility GetPopOutButtonVisibility() => SupportsPopOut ? Visibility.Visible : Visibility.Collapsed;
public Visibility GetAiActionsPanelVisibility(bool? isChecked, bool isHidden) public Visibility GetAiActionsPanelVisibility(bool? isChecked, bool isHidden)
=> !isHidden && isChecked == true ? Visibility.Visible : Visibility.Collapsed; => !isHidden && isChecked == true ? Visibility.Visible : Visibility.Collapsed;
@@ -49,6 +57,28 @@ public sealed partial class ComposePage : ComposePageAbstract,
public ComposePage() public ComposePage()
{ {
InitializeComponent(); InitializeComponent();
ViewModel.CloseRequested += ViewModel_CloseRequested;
}
public HostedPopoutDescriptor GetPopoutDescriptor()
{
var title = string.IsNullOrWhiteSpace(ViewModel.Subject) ? Translator.Draft : ViewModel.Subject;
var draftId = ViewModel.CurrentMailDraftItem?.MailCopy?.UniqueId.ToString("N") ?? title;
return new HostedPopoutDescriptor(
$"compose-{draftId}",
title,
1180,
860,
760,
600,
nameof(ComposePage));
}
public void OnPopoutStateChanged(bool isPoppedOut)
{
_isPoppedOut = isPoppedOut;
Bindings.Update();
} }
public WinoIconGlyph GetEditorThemeIcon(bool isDarkMode) => isDarkMode ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor; public WinoIconGlyph GetEditorThemeIcon(bool isDarkMode) => isDarkMode ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor;
@@ -277,11 +307,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
_disposables.Add(WebViewEditor); _disposables.Add(WebViewEditor);
ViewModel.GetHTMLBodyFunction = WebViewEditor.GetHtmlBodyAsync; ViewModel.GetHTMLBodyFunction = WebViewEditor.GetHtmlBodyAsync;
} ViewModel.RenderHtmlBodyAsyncFunc = WebViewEditor.RenderHtmlAsync;
async void IRecipient<CreateNewComposeMailRequested>.Receive(CreateNewComposeMailRequested message)
{
await WebViewEditor.RenderHtmlAsync(message.RenderModel.RenderHtml);
} }
private void ShowCCBCCClicked(object sender, RoutedEventArgs e) private void ShowCCBCCClicked(object sender, RoutedEventArgs e)
@@ -289,6 +315,16 @@ public sealed partial class ComposePage : ComposePageAbstract,
ViewModel.IsCCBCCVisible = true; ViewModel.IsCCBCCVisible = true;
} }
private void PopOutButton_Click(object sender, RoutedEventArgs e)
{
PopOutRequested?.Invoke(this, PopOutRequestedEventArgs.Default);
}
private void ViewModel_CloseRequested(object? sender, EventArgs e)
{
HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.CloseHostedInstance));
}
private async void ComposeAiActionsToggleButton_Checked(object sender, RoutedEventArgs e) private async void ComposeAiActionsToggleButton_Checked(object sender, RoutedEventArgs e)
{ {
await ComposeAiActionsPanel.RefreshAvailabilityAsync(); await ComposeAiActionsPanel.RefreshAvailabilityAsync();
@@ -333,12 +369,13 @@ public sealed partial class ComposePage : ComposePageAbstract,
WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark; WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark;
} }
void IRecipient<ReaderItemRefreshRequestedEvent>.Receive(ReaderItemRefreshRequestedEvent message) public async Task RefreshDraftAsync(MailItemViewModel draftMailItemViewModel)
{ {
if (message.MailItemViewModel == null || !message.MailItemViewModel.IsDraft) return; if (draftMailItemViewModel == null || !draftMailItemViewModel.IsDraft) return;
// Reset the initial focus flag so ToBox gets focus for the new draft. // Reset the initial focus flag so ToBox gets focus for the new draft.
isInitialFocusHandled = false; isInitialFocusHandled = false;
await ViewModel.RefreshDraftAsync(draftMailItemViewModel);
} }
private void ImportanceClicked(object sender, RoutedEventArgs e) private void ImportanceClicked(object sender, RoutedEventArgs e)
@@ -423,6 +460,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
FocusManager.GotFocus -= GlobalFocusManagerGotFocus; FocusManager.GotFocus -= GlobalFocusManagerGotFocus;
ComposeAiActionsPanel.CancelPendingOperation(); ComposeAiActionsPanel.CancelPendingOperation();
await ViewModel.UpdateMimeChangesAsync(); await ViewModel.UpdateMimeChangesAsync();
ViewModel.RenderHtmlBodyAsyncFunc = null;
DisposeDisposables(); DisposeDisposables();
} }
@@ -496,18 +534,14 @@ public sealed partial class ComposePage : ComposePageAbstract,
{ {
base.RegisterRecipients(); base.RegisterRecipients();
WeakReferenceMessenger.Default.Register<CreateNewComposeMailRequested>(this);
WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this);
WeakReferenceMessenger.Default.Register<ReaderItemRefreshRequestedEvent>(this);
} }
protected override void UnregisterRecipients() protected override void UnregisterRecipients()
{ {
base.UnregisterRecipients(); base.UnregisterRecipients();
WeakReferenceMessenger.Default.Unregister<CreateNewComposeMailRequested>(this);
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this);
WeakReferenceMessenger.Default.Unregister<ReaderItemRefreshRequestedEvent>(this);
} }
// TODO: Save mime on closing the app. // TODO: Save mime on closing the app.
+5 -6
View File
@@ -176,12 +176,11 @@
<coreControls:OperationCommandBar <coreControls:OperationCommandBar
HorizontalAlignment="Left" HorizontalAlignment="Left"
DefaultLabelPosition="Collapsed" DefaultLabelPosition="Collapsed"
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedItemsCount), Mode=OneWay}"
IsAIActionsPaneToggleVisible="False" IsAIActionsPaneToggleVisible="False"
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedItemsCount), Mode=OneWay}"
ItemInvokedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}" ItemInvokedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}"
MenuItems="{x:Bind ViewModel.ActionItems, Mode=OneWay}" MenuItems="{x:Bind ViewModel.ActionItems, Mode=OneWay}"
OverflowButtonVisibility="Auto"> OverflowButtonVisibility="Auto" />
</coreControls:OperationCommandBar>
</Grid> </Grid>
<!-- Pivot + Sync + Multi Select --> <!-- Pivot + Sync + Multi Select -->
@@ -359,7 +358,7 @@
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal" toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="0" toolkitExt:ScrollViewerExtensions.VerticalScrollBarMargin="-6"
CanDragItems="True" CanDragItems="True"
ChoosingItemContainer="WinoListViewChoosingItemContainer" ChoosingItemContainer="WinoListViewChoosingItemContainer"
IsItemClickEnabled="True" IsItemClickEnabled="True"
@@ -377,7 +376,7 @@
</listview:WinoListView.ItemContainerTransitions> </listview:WinoListView.ItemContainerTransitions>
<listview:WinoListView.ItemsPanel> <listview:WinoListView.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<ItemsStackPanel AreStickyGroupHeadersEnabled="True" GroupPadding="10,10,10,0" /> <ItemsStackPanel AreStickyGroupHeadersEnabled="True" GroupPadding="4,0,4,4" />
</ItemsPanelTemplate> </ItemsPanelTemplate>
</listview:WinoListView.ItemsPanel> </listview:WinoListView.ItemsPanel>
<listview:WinoListView.Resources> <listview:WinoListView.Resources>
@@ -488,7 +487,7 @@
<Grid <Grid
x:Name="RenderingGrid" x:Name="RenderingGrid"
Grid.Column="1" Grid.Column="1"
Margin="7,0,0,0"> Margin="0,3,0,0">
<!-- Mail Rendering Frame --> <!-- Mail Rendering Frame -->
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" /> <Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
+186 -2
View File
@@ -28,8 +28,10 @@ using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI; using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Controls.ListView; using Wino.Mail.WinUI.Controls.ListView;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Mail.WinUI.Helpers;
using Wino.Mail.WinUI.Interfaces; using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models; using Wino.Mail.WinUI.Models;
using Wino.Mail.WinUI.Services;
using Wino.MenuFlyouts.Context; using Wino.MenuFlyouts.Context;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Views.Abstract; using Wino.Views.Abstract;
@@ -48,12 +50,16 @@ public sealed partial class MailListPage : MailListPageAbstract,
IRecipient<ActiveMailItemChangedEvent>, IRecipient<ActiveMailItemChangedEvent>,
IRecipient<SelectMailItemContainerEvent>, IRecipient<SelectMailItemContainerEvent>,
IRecipient<DisposeRenderingFrameRequested>, IRecipient<DisposeRenderingFrameRequested>,
IHostedPopoutSource,
ITitleBarSearchHost ITitleBarSearchHost
{ {
private const double RENDERING_COLUMN_MIN_WIDTH = 375; private const double RENDERING_COLUMN_MIN_WIDTH = 375;
private const int SELECTION_SETTLE_DELAY_MS = 120; private const int SELECTION_SETTLE_DELAY_MS = 120;
private const int RENDERING_FRAME_RELEASE_DELAY_MS = 2000; private const int RENDERING_FRAME_RELEASE_DELAY_MS = 2000;
private int _idleNavigationRequestVersion = 0; private int _idleNavigationRequestVersion = 0;
private IPopoutClient? _activePopoutClient;
private readonly Dictionary<FrameworkElement, HostedContentPopoutWindow> _hostedPopoutWindows = [];
private PendingHostedPopoutNavigation? _pendingHostedPopoutNavigation;
private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); private IStatePersistanceService StatePersistenceService { get; } = WinoApplication.Current.Services.GetService<IStatePersistanceService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService<IKeyPressService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}"); private IKeyPressService KeyPressService { get; } = WinoApplication.Current.Services.GetService<IKeyPressService>() ?? throw new Exception($"Can't resolve {nameof(KeyPressService)}");
@@ -69,6 +75,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
public MailListPage() public MailListPage()
{ {
InitializeComponent(); InitializeComponent();
RenderingFrame.Navigated += RenderingFrame_Navigated;
} }
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
@@ -101,6 +108,7 @@ public sealed partial class MailListPage : MailListPageAbstract,
base.OnNavigatedFrom(e); base.OnNavigatedFrom(e);
InvalidatePendingIdleNavigation(); InvalidatePendingIdleNavigation();
DetachPopoutClient();
this.Bindings.StopTracking(); this.Bindings.StopTracking();
@@ -325,7 +333,10 @@ public sealed partial class MailListPage : MailListPageAbstract,
PrepareRenderingPageWebViewTransition(); PrepareRenderingPageWebViewTransition();
// Dispose existing HTML content from rendering page webview. // Dispose existing HTML content from rendering page webview.
WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested()); if (RenderingFrame.Content is MailRenderingPage renderingPage)
{
_ = renderingPage.ClearRenderedContentAsync();
}
} }
else if (IsComposingPageActive()) else if (IsComposingPageActive())
{ {
@@ -359,6 +370,55 @@ public sealed partial class MailListPage : MailListPageAbstract,
private bool IsRenderingPageActive() => RenderingFrame.Content is MailRenderingPage; private bool IsRenderingPageActive() => RenderingFrame.Content is MailRenderingPage;
private bool IsComposingPageActive() => RenderingFrame.Content is ComposePage; private bool IsComposingPageActive() => RenderingFrame.Content is ComposePage;
private void RenderingFrame_Navigated(object sender, NavigationEventArgs e)
{
AttachPopoutClient(RenderingFrame.Content as IPopoutClient);
if (_pendingHostedPopoutNavigation != null
&& TryGetPendingHostedPopoutTarget(RenderingFrame.Content, _pendingHostedPopoutNavigation, out var hostedContent))
{
_ = ContinuePendingHostedPopoutNavigationAsync(hostedContent, _pendingHostedPopoutNavigation);
}
}
private void AttachPopoutClient(IPopoutClient? client)
{
if (ReferenceEquals(_activePopoutClient, client))
return;
DetachPopoutClient();
_activePopoutClient = client;
if (_activePopoutClient != null)
{
_activePopoutClient.PopOutRequested += ActivePopoutClient_PopOutRequested;
_activePopoutClient.HostActionRequested += ActivePopoutClient_HostActionRequested;
}
}
private void DetachPopoutClient()
{
if (_activePopoutClient != null)
{
_activePopoutClient.PopOutRequested -= ActivePopoutClient_PopOutRequested;
_activePopoutClient.HostActionRequested -= ActivePopoutClient_HostActionRequested;
_activePopoutClient = null;
}
}
private async void ActivePopoutClient_PopOutRequested(object? sender, PopOutRequestedEventArgs e)
{
await HostedContentPopoutCoordinator.PopOutCurrentContentAsync(this);
}
private void ActivePopoutClient_HostActionRequested(object? sender, PopoutHostActionRequestedEventArgs e)
{
if (sender is FrameworkElement content)
{
HandleHostedClientAction(content, e);
}
}
private void InvalidatePendingIdleNavigation() private void InvalidatePendingIdleNavigation()
{ {
unchecked unchecked
@@ -378,7 +438,10 @@ public sealed partial class MailListPage : MailListPageAbstract,
if (IsRenderingPageActive()) if (IsRenderingPageActive())
{ {
WeakReferenceMessenger.Default.Send(new CancelRenderingContentRequested()); if (RenderingFrame.Content is MailRenderingPage renderingPage)
{
_ = renderingPage.ClearRenderedContentAsync();
}
} }
await Task.Delay(RENDERING_FRAME_RELEASE_DELAY_MS); await Task.Delay(RENDERING_FRAME_RELEASE_DELAY_MS);
@@ -926,4 +989,125 @@ public sealed partial class MailListPage : MailListPageAbstract,
return Task.CompletedTask; return Task.CompletedTask;
} }
public bool CanPopOutCurrentContent()
{
return RenderingFrame.Content is FrameworkElement
&& RenderingFrame.Content is IPopoutClient client
&& client.SupportsPopOut;
}
public FrameworkElement? GetCurrentHostedContent()
{
return RenderingFrame.Content as FrameworkElement;
}
public HostedPopoutDescriptor CreatePopoutDescriptor(IPopoutClient client)
{
return client.GetPopoutDescriptor();
}
public FrameworkElement DetachHostedContent()
{
if (RenderingFrame.Content is not FrameworkElement content)
throw new InvalidOperationException("RenderingFrame does not host detachable content.");
InvalidatePendingIdleNavigation();
DetachPopoutClient();
RenderingFrame.Content = null;
ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.None);
return content;
}
public void OnHostedContentPoppedOut(FrameworkElement content, HostedContentPopoutWindow window, HostedPopoutDescriptor descriptor)
{
if (content is IPopoutClient client)
{
client.HostActionRequested -= ActivePopoutClient_HostActionRequested;
client.HostActionRequested += ActivePopoutClient_HostActionRequested;
}
_hostedPopoutWindows[content] = window;
_ = ViewModel.MailCollection.UnselectAllAsync();
UpdateAdaptiveness();
}
public void OnHostedPopoutClosed(FrameworkElement content, HostedPopoutDescriptor descriptor)
{
if (_hostedPopoutWindows.Remove(content) && content is IPopoutClient hostedClient)
{
hostedClient.HostActionRequested -= ActivePopoutClient_HostActionRequested;
}
if (_pendingHostedPopoutNavigation?.SourceContent == content)
{
_pendingHostedPopoutNavigation = null;
}
DispatcherQueue.TryEnqueue(() =>
{
if (content is IPopoutClient client)
{
client.OnPopoutStateChanged(false);
}
WindowCleanupHelper.CleanupObject(content);
});
}
private void HandleHostedClientAction(FrameworkElement content, PopoutHostActionRequestedEventArgs args)
{
if (!_hostedPopoutWindows.TryGetValue(content, out var hostedWindow))
return;
switch (args.ActionKind)
{
case PopoutHostActionKind.CloseHostedInstance:
hostedWindow.Close();
break;
case PopoutHostActionKind.PopOutNextNavigation when args.TargetPageType != null:
_pendingHostedPopoutNavigation = new PendingHostedPopoutNavigation(content, hostedWindow, args.TargetPageType, args.TargetMailUniqueId);
break;
}
}
private static bool TryGetPendingHostedPopoutTarget(object? currentContent, PendingHostedPopoutNavigation pendingHostedNavigation, out FrameworkElement hostedContent)
{
hostedContent = null!;
if (currentContent is not FrameworkElement currentFrameworkElement || currentFrameworkElement.GetType() != pendingHostedNavigation.TargetPageType)
return false;
if (pendingHostedNavigation.TargetMailUniqueId.HasValue
&& currentFrameworkElement is ComposePage composePage
&& composePage.ViewModel.CurrentMailDraftItem?.MailCopy?.UniqueId != pendingHostedNavigation.TargetMailUniqueId.Value)
{
return false;
}
hostedContent = currentFrameworkElement;
return true;
}
private async Task ContinuePendingHostedPopoutNavigationAsync(FrameworkElement content, PendingHostedPopoutNavigation pendingHostedNavigation)
{
if (!ReferenceEquals(_pendingHostedPopoutNavigation, pendingHostedNavigation))
return;
_pendingHostedPopoutNavigation = null;
var didPopOut = await HostedContentPopoutCoordinator.PopOutCurrentContentAsync(this);
if (didPopOut)
{
pendingHostedNavigation.SourceWindow.Close();
}
}
private sealed record PendingHostedPopoutNavigation(
FrameworkElement SourceContent,
HostedContentPopoutWindow SourceWindow,
Type TargetPageType,
Guid? TargetMailUniqueId);
} }
@@ -272,8 +272,10 @@
IsAIActionsPaneToggleVisible="{x:Bind GetAiActionsToggleVisible(ViewModel.PreferencesService.IsAiActionsPanelHidden), Mode=OneWay}" IsAIActionsPaneToggleVisible="{x:Bind GetAiActionsToggleVisible(ViewModel.PreferencesService.IsAiActionsPanelHidden), Mode=OneWay}"
IsEditorThemeDark="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=TwoWay}" IsEditorThemeDark="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=TwoWay}"
IsEditorThemeToggleVisible="True" IsEditorThemeToggleVisible="True"
IsPopOutButtonVisible="{x:Bind SupportsPopOut, Mode=OneWay}"
ItemInvokedCommand="{x:Bind ViewModel.OperationClickedCommand}" ItemInvokedCommand="{x:Bind ViewModel.OperationClickedCommand}"
MenuItems="{x:Bind ViewModel.MenuItems, Mode=OneWay}"> MenuItems="{x:Bind ViewModel.MenuItems, Mode=OneWay}"
PopOutClicked="RendererCommandBar_PopOutClicked">
<coreControls:OperationCommandBar.Content> <coreControls:OperationCommandBar.Content>
<Grid Padding="0,5"> <Grid Padding="0,5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -16,9 +16,12 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Printing; using Wino.Core.Domain.Models.Printing;
using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Models;
using Wino.Mail.WinUI; using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Controls; using Wino.Mail.WinUI.Controls;
using Wino.Mail.WinUI.Extensions; using Wino.Mail.WinUI.Extensions;
using Wino.Mail.WinUI.Interfaces;
using Wino.Mail.WinUI.Models;
using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
using Wino.Views.Abstract; using Wino.Views.Abstract;
@@ -27,8 +30,7 @@ namespace Wino.Views.Mail;
public sealed partial class MailRenderingPage : MailRenderingPageAbstract, public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
IAiHtmlActionHost, IAiHtmlActionHost,
IRecipient<HtmlRenderingRequested>, IPopoutClient,
IRecipient<CancelRenderingContentRequested>,
IRecipient<ApplicationThemeChanged> IRecipient<ApplicationThemeChanged>
{ {
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!; private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!;
@@ -39,6 +41,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
private bool? _lastAppliedDarkTheme; private bool? _lastAppliedDarkTheme;
private TaskCompletionSource<bool> DOMLoadedTask = new TaskCompletionSource<bool>(); private TaskCompletionSource<bool> DOMLoadedTask = new TaskCompletionSource<bool>();
private string _currentRenderedHtml = string.Empty; private string _currentRenderedHtml = string.Empty;
private bool _isPoppedOut;
public bool SupportsPopOut => !_isPoppedOut;
public event EventHandler<PopOutRequestedEventArgs>? PopOutRequested;
public event EventHandler<PopoutHostActionRequestedEventArgs>? HostActionRequested;
public WebView2 GetWebView() => Chromium; public WebView2 GetWebView() => Chromium;
public bool GetAiActionsToggleVisible(bool isHidden) => !isHidden; public bool GetAiActionsToggleVisible(bool isHidden) => !isHidden;
@@ -57,9 +64,34 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
{ {
return Chromium.CoreWebView2.PrintToPdfAsync(path, null).AsTask(); return Chromium.CoreWebView2.PrintToPdfAsync(path, null).AsTask();
}); });
ViewModel.RenderHtmlAsyncFunc = RenderInternalAsync;
ViewModel.ClearRenderedHtmlAsyncFunc = ClearRenderedContentAsync;
ViewModel.CloseRequested += ViewModel_CloseRequested;
ViewModel.ComposeRequested += ViewModel_ComposeRequested;
} }
public HostedPopoutDescriptor GetPopoutDescriptor()
{
var title = string.IsNullOrWhiteSpace(ViewModel.Subject) ? Translator.MailItemNoSubject : ViewModel.Subject;
var uniquePart = ViewModel.CurrentMailFileId?.ToString("N") ?? title;
return new HostedPopoutDescriptor(
$"mail-rendering-{uniquePart}",
title,
1080,
780,
640,
480,
nameof(MailRenderingPage));
}
public void OnPopoutStateChanged(bool isPoppedOut)
{
_isPoppedOut = isPoppedOut;
Bindings.Update();
RendererCommandBar.InvalidateCommands();
}
private async Task<PrintingResult> DirectPrintAsync(WebView2PrintSettingsModel settings) private async Task<PrintingResult> DirectPrintAsync(WebView2PrintSettingsModel settings)
{ {
if (Chromium.CoreWebView2 == null) return PrintingResult.Failed; if (Chromium.CoreWebView2 == null) return PrintingResult.Failed;
@@ -132,20 +164,14 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
private void DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) => DOMLoadedTask.TrySetResult(true); private void DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) => DOMLoadedTask.TrySetResult(true);
async void IRecipient<HtmlRenderingRequested>.Receive(HtmlRenderingRequested message) public async Task ClearRenderedContentAsync()
{ {
// Ensure WebView2 is fully initialized before first render.
// OnNavigatedTo starts initialization fire-and-forget; this await
// guarantees the core is ready before we invoke any script.
await EnsureChromiumInitializedAsync(); await EnsureChromiumInitializedAsync();
if (message == null || string.IsNullOrEmpty(message.HtmlBody)) if (!isRenderingInProgress)
{ {
await RenderInternalAsync(string.Empty); await RenderInternalAsync(string.Empty);
return;
} }
await RenderInternalAsync(message.HtmlBody);
} }
protected override void OnNavigatedFrom(NavigationEventArgs e) protected override void OnNavigatedFrom(NavigationEventArgs e)
@@ -157,8 +183,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
ViewModel.SaveHTMLasPDFFunc = null; ViewModel.SaveHTMLasPDFFunc = null;
ViewModel.DirectPrintFuncAsync = null; ViewModel.DirectPrintFuncAsync = null;
ViewModel.RenderHtmlAsyncFunc = null;
ViewModel.ClearRenderedHtmlAsyncFunc = null;
_currentRenderedHtml = string.Empty; _currentRenderedHtml = string.Empty;
RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged; RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged;
RendererCommandBar.PopOutClicked -= RendererCommandBar_PopOutClicked;
RendererCommandBar.IsAIActionsEnabled = false; RendererCommandBar.IsAIActionsEnabled = false;
ReaderAiActionsPanel.CancelPendingOperation(); ReaderAiActionsPanel.CancelPendingOperation();
@@ -178,6 +207,11 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
} }
public Task RefreshMailItemAsync(MailItemViewModel mailItemViewModel)
{
return ViewModel.RefreshMailItemAsync(mailItemViewModel);
}
private async void RendererCommandBar_AIActionsEnabledChanged(object? sender, bool isEnabled) private async void RendererCommandBar_AIActionsEnabledChanged(object? sender, bool isEnabled)
{ {
if (isEnabled) if (isEnabled)
@@ -275,11 +309,13 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
protected override void OnNavigatedTo(NavigationEventArgs e) protected override void OnNavigatedTo(NavigationEventArgs e)
{ {
// Initialize WebView2 wiring before base navigation invokes ViewModel rendering.
// Base.OnNavigatedTo triggers VM.OnNavigatedTo, which can send HtmlRenderingRequested.
DOMLoadedTask = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); DOMLoadedTask = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
ViewModel.RenderHtmlAsyncFunc = RenderInternalAsync;
ViewModel.ClearRenderedHtmlAsyncFunc = ClearRenderedContentAsync;
RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged; RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged;
RendererCommandBar.AIActionsEnabledChanged += RendererCommandBar_AIActionsEnabledChanged; RendererCommandBar.AIActionsEnabledChanged += RendererCommandBar_AIActionsEnabledChanged;
RendererCommandBar.PopOutClicked -= RendererCommandBar_PopOutClicked;
RendererCommandBar.PopOutClicked += RendererCommandBar_PopOutClicked;
RendererCommandBar.IsAIActionsEnabled = false; RendererCommandBar.IsAIActionsEnabled = false;
Chromium.CoreWebView2Initialized -= CoreWebViewInitialized; Chromium.CoreWebView2Initialized -= CoreWebViewInitialized;
Chromium.CoreWebView2Initialized += CoreWebViewInitialized; Chromium.CoreWebView2Initialized += CoreWebViewInitialized;
@@ -316,17 +352,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
Chromium.Source = new Uri("https://wino.mail/reader.html"); Chromium.Source = new Uri("https://wino.mail/reader.html");
} }
async void IRecipient<CancelRenderingContentRequested>.Receive(CancelRenderingContentRequested message)
{
await EnsureChromiumInitializedAsync();
if (!isRenderingInProgress)
{
await RenderInternalAsync(string.Empty);
}
}
private async void WebViewNavigationStarting(WebView2 sender, CoreWebView2NavigationStartingEventArgs args) private async void WebViewNavigationStarting(WebView2 sender, CoreWebView2NavigationStartingEventArgs args)
{ {
// This is our reader. // This is our reader.
@@ -397,7 +422,8 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
private void InternetAddressClicked(object sender, RoutedEventArgs e) private void InternetAddressClicked(object sender, RoutedEventArgs e)
{ {
if (sender is HyperlinkButton hyperlinkButton) // TODO: Popped out windows don't have xaml root assigned properly, therefore ShowAt will fail.
if (sender is HyperlinkButton hyperlinkButton && !_isPoppedOut)
{ {
hyperlinkButton.ContextFlyout.ShowAt(hyperlinkButton); hyperlinkButton.ContextFlyout.ShowAt(hyperlinkButton);
} }
@@ -411,6 +437,21 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
} }
} }
private void RendererCommandBar_PopOutClicked(object? sender, EventArgs e)
{
PopOutRequested?.Invoke(this, PopOutRequestedEventArgs.Default);
}
private void ViewModel_CloseRequested(object? sender, EventArgs e)
{
HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.CloseHostedInstance));
}
private void ViewModel_ComposeRequested(object? sender, ComposeDraftRequestedEventArgs e)
{
HostActionRequested?.Invoke(this, new PopoutHostActionRequestedEventArgs(PopoutHostActionKind.PopOutNextNavigation, typeof(ComposePage), e.DraftUniqueId));
}
private void OpenAttachment_Click(object sender, RoutedEventArgs e) private void OpenAttachment_Click(object sender, RoutedEventArgs e)
{ {
if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment) if (sender is MenuFlyoutItem item && item.CommandParameter is MailAttachmentViewModel attachment)
@@ -431,8 +472,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
{ {
base.RegisterRecipients(); base.RegisterRecipients();
WeakReferenceMessenger.Default.Register<HtmlRenderingRequested>(this);
WeakReferenceMessenger.Default.Register<CancelRenderingContentRequested>(this);
WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Register<ApplicationThemeChanged>(this);
} }
@@ -440,8 +479,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
{ {
base.UnregisterRecipients(); base.UnregisterRecipients();
WeakReferenceMessenger.Default.Unregister<HtmlRenderingRequested>(this);
WeakReferenceMessenger.Default.Unregister<CancelRenderingContentRequested>(this);
WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this); WeakReferenceMessenger.Default.Unregister<ApplicationThemeChanged>(this);
} }