New AI actions panel. Replaced new command bar.
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
<PackageVersion Include="Microsoft.Xaml.Behaviors.WinUI.Managed" Version="3.0.1" />
|
||||||
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.13" />
|
<PackageVersion Include="Wino.Mail.Contracts" Version="1.0.14" />
|
||||||
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
<PackageVersion Include="MimeKit" Version="4.15.1" />
|
||||||
<PackageVersion Include="morelinq" Version="4.4.0" />
|
<PackageVersion Include="morelinq" Version="4.4.0" />
|
||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ public interface IMenuOperation
|
|||||||
{
|
{
|
||||||
bool IsEnabled { get; }
|
bool IsEnabled { get; }
|
||||||
string Identifier { get; }
|
string Identifier { get; }
|
||||||
|
bool IsSecondaryMenuPreferred { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,16 @@ public interface IMimeFileService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default);
|
Task SaveTranslatedHtmlAsync(Guid accountId, Guid fileId, string targetLanguage, string html, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns cached summary text for the given mime resource if it exists.
|
||||||
|
/// </summary>
|
||||||
|
Task<string> GetSummaryTextAsync(Guid accountId, Guid fileId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves summary text for the given mime resource.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveSummaryTextAsync(Guid accountId, Guid fileId, string summary, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares the final model containing rendering details.
|
/// Prepares the final model containing rendering details.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Menus;
|
using Wino.Core.Domain.Models.Menus;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Folders;
|
namespace Wino.Core.Domain.Models.Folders;
|
||||||
|
|
||||||
public class FolderOperationMenuItem : MenuOperationItemBase<FolderOperation>, IMenuOperation
|
public class FolderOperationMenuItem : MenuOperationItemBase<FolderOperation>
|
||||||
{
|
{
|
||||||
protected FolderOperationMenuItem(FolderOperation operation, bool isEnabled) : base(operation, isEnabled) { }
|
protected FolderOperationMenuItem(FolderOperation operation, bool isEnabled, bool isSecondaryMenuItem = false) : base(operation, isEnabled)
|
||||||
|
{
|
||||||
public static FolderOperationMenuItem Create(FolderOperation operation, bool isEnabled = true)
|
IsSecondaryMenuPreferred = isSecondaryMenuItem;
|
||||||
=> new FolderOperationMenuItem(operation, isEnabled);
|
}
|
||||||
|
|
||||||
|
public static FolderOperationMenuItem Create(FolderOperation operation, bool isEnabled = true, bool isSecondaryMenuItem = false)
|
||||||
|
=> new FolderOperationMenuItem(operation, isEnabled, isSecondaryMenuItem);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Menus;
|
namespace Wino.Core.Domain.Models.Menus;
|
||||||
|
|
||||||
public class MailOperationMenuItem : MenuOperationItemBase<MailOperation>, IMenuOperation
|
public class MailOperationMenuItem : MenuOperationItemBase<MailOperation>
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether this menu item should be placed in SecondaryCommands if used in CommandBar.
|
|
||||||
/// </summary>
|
|
||||||
public bool IsSecondaryMenuPreferred { get; set; }
|
|
||||||
|
|
||||||
protected MailOperationMenuItem(MailOperation operation, bool isEnabled, bool isSecondaryMenuItem = false) : base(operation, isEnabled)
|
protected MailOperationMenuItem(MailOperation operation, bool isEnabled, bool isSecondaryMenuItem = false) : base(operation, isEnabled)
|
||||||
{
|
{
|
||||||
IsSecondaryMenuPreferred = isSecondaryMenuItem;
|
IsSecondaryMenuPreferred = isSecondaryMenuItem;
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Menus;
|
namespace Wino.Core.Domain.Models.Menus;
|
||||||
|
|
||||||
public class MenuOperationItemBase<TOperation> where TOperation : Enum
|
public class MenuOperationItemBase<TOperation> : ObservableObject, IMenuOperation where TOperation : Enum
|
||||||
{
|
{
|
||||||
|
private TOperation _operation;
|
||||||
|
private string _identifier = string.Empty;
|
||||||
|
private bool _isEnabled;
|
||||||
|
private bool _isSecondaryMenuPreferred;
|
||||||
|
|
||||||
public MenuOperationItemBase(TOperation operation, bool isEnabled)
|
public MenuOperationItemBase(TOperation operation, bool isEnabled)
|
||||||
{
|
{
|
||||||
Operation = operation;
|
Operation = operation;
|
||||||
@@ -11,7 +18,33 @@ public class MenuOperationItemBase<TOperation> where TOperation : Enum
|
|||||||
Identifier = operation.ToString();
|
Identifier = operation.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public TOperation Operation { get; set; }
|
public TOperation Operation
|
||||||
public string Identifier { get; set; }
|
{
|
||||||
public bool IsEnabled { get; set; }
|
get => _operation;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _operation, value))
|
||||||
|
{
|
||||||
|
Identifier = value.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Identifier
|
||||||
|
{
|
||||||
|
get => _identifier;
|
||||||
|
protected set => SetProperty(ref _identifier, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get => _isEnabled;
|
||||||
|
set => SetProperty(ref _isEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSecondaryMenuPreferred
|
||||||
|
{
|
||||||
|
get => _isSecondaryMenuPreferred;
|
||||||
|
set => SetProperty(ref _isSecondaryMenuPreferred, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1062,6 +1062,7 @@
|
|||||||
"Composer_SmimeEncryption": "S/MIME Encryption",
|
"Composer_SmimeEncryption": "S/MIME Encryption",
|
||||||
"Composer_EmailTemplatesPlaceholder": "E-mail templates",
|
"Composer_EmailTemplatesPlaceholder": "E-mail templates",
|
||||||
"Composer_AiSummarize": "Summarize with AI",
|
"Composer_AiSummarize": "Summarize with AI",
|
||||||
|
"Composer_AiSummarizeDescription": "Extract key points, action items, and decisions from this email.",
|
||||||
"Composer_AiTranslate": "Translate with AI",
|
"Composer_AiTranslate": "Translate with AI",
|
||||||
"Composer_AiActions": "AI Actions",
|
"Composer_AiActions": "AI Actions",
|
||||||
"Composer_AiRewrite": "Rewrite with AI",
|
"Composer_AiRewrite": "Rewrite with AI",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
|
public WinoMailCollection MailCollection { get; set; } = new WinoMailCollection();
|
||||||
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
|
public ObservableCollection<FolderPivotViewModel> PivotFolders { get; set; } = [];
|
||||||
public ObservableCollection<MailOperationMenuItem> ActionItems { get; set; } = [];
|
public ObservableCollection<IMenuOperation> ActionItems { get; set; } = [];
|
||||||
|
|
||||||
private readonly SemaphoreSlim listManipulationSemepahore = new SemaphoreSlim(1);
|
private readonly SemaphoreSlim listManipulationSemepahore = new SemaphoreSlim(1);
|
||||||
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
|
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
|
||||||
@@ -439,11 +439,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
|
|||||||
public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request);
|
public Task ExecuteHoverAction(MailOperationPreperationRequest request) => ExecuteMailOperationAsync(request);
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ExecuteTopBarAction(MailOperationMenuItem menuItem)
|
private async Task ExecuteTopBarAction(IMenuOperation menuItem)
|
||||||
{
|
{
|
||||||
if (menuItem == null || MailCollection.SelectedItemsCount == 0) return;
|
if (menuItem is not MailOperationMenuItem mailOperationMenuItem || MailCollection.SelectedItemsCount == 0) return;
|
||||||
|
|
||||||
await HandleMailOperation(menuItem.Operation, MailCollection.SelectedItems);
|
await HandleMailOperation(mailOperationMenuItem.Operation, MailCollection.SelectedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
|||||||
public ObservableCollection<AccountContactViewModel> CcItems { get; set; } = [];
|
public ObservableCollection<AccountContactViewModel> CcItems { get; set; } = [];
|
||||||
public ObservableCollection<AccountContactViewModel> BccItems { get; set; } = [];
|
public ObservableCollection<AccountContactViewModel> BccItems { get; set; } = [];
|
||||||
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = [];
|
public ObservableCollection<MailAttachmentViewModel> Attachments { get; set; } = [];
|
||||||
public ObservableCollection<MailOperationMenuItem> MenuItems { get; set; } = [];
|
public ObservableCollection<IMenuOperation> MenuItems { get; set; } = [];
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -255,21 +255,18 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task OperationClicked(MailOperationMenuItem menuItem)
|
private async Task OperationClicked(IMenuOperation menuItem)
|
||||||
{
|
{
|
||||||
if (menuItem == null) return;
|
if (menuItem is not MailOperationMenuItem mailOperationMenuItem) return;
|
||||||
|
|
||||||
await HandleMailOperationAsync(menuItem.Operation);
|
await HandleMailOperationAsync(mailOperationMenuItem.Operation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleMailOperationAsync(MailOperation operation)
|
private async Task HandleMailOperationAsync(MailOperation operation)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Toggle theme
|
if (operation == MailOperation.SaveAs)
|
||||||
if (operation == MailOperation.DarkEditor || operation == MailOperation.LightEditor)
|
|
||||||
IsDarkWebviewRenderer = !IsDarkWebviewRenderer;
|
|
||||||
else if (operation == MailOperation.SaveAs)
|
|
||||||
{
|
{
|
||||||
await SaveAsAsync();
|
await SaveAsAsync();
|
||||||
}
|
}
|
||||||
@@ -589,12 +586,6 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
|
|||||||
{
|
{
|
||||||
MenuItems.Clear();
|
MenuItems.Clear();
|
||||||
|
|
||||||
// Add light/dark editor theme switch.
|
|
||||||
if (IsDarkWebviewRenderer)
|
|
||||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.LightEditor));
|
|
||||||
else
|
|
||||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.DarkEditor));
|
|
||||||
|
|
||||||
// Save As PDF
|
// Save As PDF
|
||||||
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true));
|
MenuItems.Add(MailOperationMenuItem.Create(MailOperation.SaveAs, true, true));
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
<ResourceDictionary Source="/Styles/WinoInfoBar.xaml" />
|
<ResourceDictionary Source="/Styles/WinoInfoBar.xaml" />
|
||||||
<ResourceDictionary Source="/Styles/SharedStyles.xaml" />
|
<ResourceDictionary Source="/Styles/SharedStyles.xaml" />
|
||||||
<ResourceDictionary Source="/Styles/IconTemplates.xaml" />
|
<ResourceDictionary Source="/Styles/IconTemplates.xaml" />
|
||||||
|
<ResourceDictionary Source="/Styles/OperationCommandBar.xaml" />
|
||||||
|
|
||||||
|
|
||||||
<coreStyles:CustomMessageDialogStyles />
|
<coreStyles:CustomMessageDialogStyles />
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using CommunityToolkit.WinUI;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
|
||||||
using Microsoft.Xaml.Interactivity;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Menus;
|
|
||||||
using Wino.Helpers;
|
|
||||||
using Wino.Mail.WinUI;
|
|
||||||
using Wino.Mail.WinUI.Controls;
|
|
||||||
|
|
||||||
namespace Wino.Behaviors;
|
|
||||||
|
|
||||||
public partial class BindableCommandBarBehavior : Behavior<CommandBar>
|
|
||||||
{
|
|
||||||
private readonly IPreferencesService? _preferencesService = App.Current.Services.GetService<IPreferencesService>();
|
|
||||||
public static readonly DependencyProperty PrimaryCommandsProperty = DependencyProperty.Register(
|
|
||||||
"PrimaryCommands", typeof(object), typeof(BindableCommandBarBehavior),
|
|
||||||
new PropertyMetadata(null, UpdateCommands));
|
|
||||||
|
|
||||||
[GeneratedDependencyProperty]
|
|
||||||
public partial ICommand? ItemClickedCommand { get; set; }
|
|
||||||
|
|
||||||
public object PrimaryCommands
|
|
||||||
{
|
|
||||||
get { return GetValue(PrimaryCommandsProperty); }
|
|
||||||
set { SetValue(PrimaryCommandsProperty, value); }
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnDetaching()
|
|
||||||
{
|
|
||||||
base.OnDetaching();
|
|
||||||
|
|
||||||
AttachChanges(false);
|
|
||||||
|
|
||||||
if (PrimaryCommands is IEnumerable enumerable)
|
|
||||||
{
|
|
||||||
foreach (var item in enumerable)
|
|
||||||
{
|
|
||||||
DetachCommandElement(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DetachCommandElement(object item)
|
|
||||||
{
|
|
||||||
if (item is ButtonBase button)
|
|
||||||
{
|
|
||||||
button.Click -= Button_Click;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is AppBarElementContainer container && container.Content is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePrimaryCommands()
|
|
||||||
{
|
|
||||||
if (AssociatedObject == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (PrimaryCommands == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (AssociatedObject.PrimaryCommands is IEnumerable enumerableObjects)
|
|
||||||
{
|
|
||||||
foreach (var item in enumerableObjects)
|
|
||||||
{
|
|
||||||
DetachCommandElement(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AssociatedObject.SecondaryCommands is IEnumerable secondaryObject)
|
|
||||||
{
|
|
||||||
foreach (var item in secondaryObject)
|
|
||||||
{
|
|
||||||
DetachCommandElement(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AssociatedObject.PrimaryCommands.Clear();
|
|
||||||
AssociatedObject.SecondaryCommands.Clear();
|
|
||||||
|
|
||||||
if (PrimaryCommands is not IEnumerable enumerable) return;
|
|
||||||
|
|
||||||
foreach (var command in enumerable)
|
|
||||||
{
|
|
||||||
if (command is MailOperationMenuItem mailOperationMenuItem)
|
|
||||||
{
|
|
||||||
ICommandBarElement? menuItem = null;
|
|
||||||
|
|
||||||
if (mailOperationMenuItem.Operation == Core.Domain.Enums.MailOperation.Seperator)
|
|
||||||
{
|
|
||||||
menuItem = new AppBarSeparator();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var label = XamlHelpers.GetOperationString(mailOperationMenuItem.Operation);
|
|
||||||
var labelPosition = string.IsNullOrWhiteSpace(label) || _preferencesService == null || !_preferencesService.IsShowActionLabelsEnabled ?
|
|
||||||
CommandBarLabelPosition.Collapsed : CommandBarLabelPosition.Default;
|
|
||||||
|
|
||||||
var iconGlyph = XamlHelpers.GetWinoIconGlyph(mailOperationMenuItem.Operation);
|
|
||||||
var glyphValue = ControlConstants.WinoIconFontDictionary.TryGetValue(iconGlyph, out var glyph) ? glyph : string.Empty;
|
|
||||||
|
|
||||||
menuItem = new AppBarButton
|
|
||||||
{
|
|
||||||
Width = double.NaN,
|
|
||||||
MinWidth = 40,
|
|
||||||
Icon = new WinoFontIcon() { Glyph = glyphValue },
|
|
||||||
Label = label,
|
|
||||||
LabelPosition = labelPosition,
|
|
||||||
DataContext = mailOperationMenuItem,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(label))
|
|
||||||
{
|
|
||||||
var toolTip = new ToolTip
|
|
||||||
{
|
|
||||||
Content = label
|
|
||||||
};
|
|
||||||
ToolTipService.SetToolTip((DependencyObject)menuItem, toolTip);
|
|
||||||
}
|
|
||||||
|
|
||||||
((AppBarButton)menuItem).Click -= Button_Click;
|
|
||||||
((AppBarButton)menuItem).Click += Button_Click;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mailOperationMenuItem.IsSecondaryMenuPreferred)
|
|
||||||
{
|
|
||||||
AssociatedObject.SecondaryCommands.Add(menuItem);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
AssociatedObject.PrimaryCommands.Add(menuItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//if (dependencyObject is ICommandBarElement icommandBarElement)
|
|
||||||
//{
|
|
||||||
// if (dependencyObject is ButtonBase button)
|
|
||||||
// {
|
|
||||||
// button.Click -= Button_Click;
|
|
||||||
// button.Click += Button_Click;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (command is MailOperationMenuItem mailOperationMenuItem)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Button_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
ItemClickedCommand?.Execute(((ButtonBase)sender).DataContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnAttached()
|
|
||||||
{
|
|
||||||
base.OnAttached();
|
|
||||||
|
|
||||||
AttachChanges(true);
|
|
||||||
|
|
||||||
UpdatePrimaryCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PrimaryCommandsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
UpdatePrimaryCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void UpdateCommands(DependencyObject dependencyObject,
|
|
||||||
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
|
|
||||||
{
|
|
||||||
if (dependencyObject is not BindableCommandBarBehavior behavior) return;
|
|
||||||
|
|
||||||
if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged oldList)
|
|
||||||
{
|
|
||||||
oldList.CollectionChanged -= behavior.PrimaryCommandsCollectionChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
behavior.AttachChanges(true);
|
|
||||||
behavior.UpdatePrimaryCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AttachChanges(bool register)
|
|
||||||
{
|
|
||||||
if (PrimaryCommands is null) return;
|
|
||||||
|
|
||||||
if (PrimaryCommands is INotifyCollectionChanged collectionChanged)
|
|
||||||
{
|
|
||||||
if (register)
|
|
||||||
{
|
|
||||||
collectionChanged.CollectionChanged -= PrimaryCommandsCollectionChanged;
|
|
||||||
collectionChanged.CollectionChanged += PrimaryCommandsCollectionChanged;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
collectionChanged.CollectionChanged -= PrimaryCommandsCollectionChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
xmlns:models="using:Wino.Core.Domain.Models.Ai"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:models="using:Wino.Core.Domain.Models.Ai"
|
||||||
x:Name="root"
|
x:Name="root"
|
||||||
Loaded="OnLoaded"
|
Loaded="OnLoaded"
|
||||||
Unloaded="OnUnloaded"
|
Unloaded="OnUnloaded"
|
||||||
@@ -20,11 +20,17 @@
|
|||||||
CornerRadius="10">
|
CornerRadius="10">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<ProgressBar x:Name="BusyProgressBar" IsIndeterminate="True" Visibility="Collapsed" />
|
<ProgressBar
|
||||||
|
x:Name="BusyProgressBar"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
|
||||||
<StackPanel x:Name="LoadingPanel" Spacing="12">
|
<StackPanel x:Name="LoadingPanel" Spacing="12">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
<ProgressRing Width="20" Height="20" IsActive="True" />
|
<ProgressRing
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
IsActive="True" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
@@ -32,8 +38,14 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel x:Name="SignedOutPanel" Spacing="14" Visibility="Collapsed">
|
<StackPanel
|
||||||
<Border Height="120" Padding="18" CornerRadius="12">
|
x:Name="SignedOutPanel"
|
||||||
|
Spacing="14"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<Border
|
||||||
|
Height="120"
|
||||||
|
Padding="18"
|
||||||
|
CornerRadius="12">
|
||||||
<Border.Background>
|
<Border.Background>
|
||||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||||
<GradientStop Offset="0" Color="#1A6EE7B7" />
|
<GradientStop Offset="0" Color="#1A6EE7B7" />
|
||||||
@@ -47,7 +59,12 @@
|
|||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Border Width="56" Height="56" VerticalAlignment="Top" Background="#22FFFFFF" CornerRadius="28">
|
<Border
|
||||||
|
Width="56"
|
||||||
|
Height="56"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Background="#22FFFFFF"
|
||||||
|
CornerRadius="28">
|
||||||
<FontIcon
|
<FontIcon
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@@ -87,22 +104,32 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel x:Name="PurchasePanel" Spacing="14" Visibility="Collapsed">
|
<StackPanel
|
||||||
<Border Padding="16" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="12">
|
x:Name="PurchasePanel"
|
||||||
|
Spacing="14"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<Border
|
||||||
|
Padding="16"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}"
|
||||||
|
CornerRadius="12">
|
||||||
<StackPanel Spacing="8">
|
<StackPanel Spacing="8">
|
||||||
<TextBlock
|
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.AiActions_NoPackTitle, Mode=OneWay}" />
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
|
||||||
Text="{x:Bind domain:Translator.AiActions_NoPackTitle, Mode=OneWay}" />
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Style="{StaticResource BodyTextBlockStyle}"
|
Style="{StaticResource BodyTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.AiActions_NoPackDescription, Mode=OneWay}"
|
Text="{x:Bind domain:Translator.AiActions_NoPackDescription, Mode=OneWay}"
|
||||||
TextWrapping="WrapWholeWords" />
|
TextWrapping="WrapWholeWords" />
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<Border Padding="8,2" Background="{ThemeResource AccentFillColorDefaultBrush}" CornerRadius="8">
|
<Border
|
||||||
|
Padding="8,2"
|
||||||
|
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||||
|
CornerRadius="8">
|
||||||
<TextBlock Foreground="White" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoPrice, Mode=OneWay}" />
|
<TextBlock Foreground="White" Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoPrice, Mode=OneWay}" />
|
||||||
</Border>
|
</Border>
|
||||||
<Border Padding="8,2" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" CornerRadius="8">
|
<Border
|
||||||
|
Padding="8,2"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
CornerRadius="8">
|
||||||
<TextBlock Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoRequests, Mode=OneWay}" />
|
<TextBlock Text="{x:Bind domain:Translator.WinoAccount_Management_AiPackPromoRequests, Mode=OneWay}" />
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -110,76 +137,96 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
HorizontalAlignment="Left"
|
||||||
Click="PurchaseButton_Click"
|
Click="PurchaseButton_Click"
|
||||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackGetButton, Mode=OneWay}"
|
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackGetButton, Mode=OneWay}"
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Style="{StaticResource AccentButtonStyle}" />
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel x:Name="ReadyPanel" Spacing="12" Visibility="Collapsed">
|
<StackPanel
|
||||||
<Border Padding="12" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="10">
|
x:Name="ReadyPanel"
|
||||||
<Grid ColumnSpacing="10">
|
Spacing="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<!-- Row 1: Action tabs + usage -->
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<Border
|
<controls:Segmented
|
||||||
Width="36"
|
x:Name="ActionSelector"
|
||||||
Height="36"
|
Height="30"
|
||||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
|
||||||
CornerRadius="18"
|
|
||||||
Opacity="0.18" />
|
|
||||||
<FontIcon
|
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
SelectionChanged="ActionSelector_SelectionChanged"
|
||||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
Style="{StaticResource ButtonSegmentedStyle}">
|
||||||
Glyph="" />
|
<controls:SegmentedItem
|
||||||
|
x:Name="TranslateSegment"
|
||||||
<StackPanel Grid.Column="1" Spacing="3">
|
Padding="12,6"
|
||||||
<TextBlock
|
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureTranslate, Mode=OneWay}">
|
||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
|
||||||
Text="{x:Bind domain:Translator.Composer_AiActions, Mode=OneWay}" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="UsageSummaryTextBlock"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
|
||||||
TextWrapping="WrapWholeWords" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="UsageResetTextBlock"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
|
||||||
TextWrapping="WrapWholeWords" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<controls:Segmented x:Name="ActionSelector" SelectionChanged="ActionSelector_SelectionChanged">
|
|
||||||
<controls:SegmentedItem x:Name="TranslateSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureTranslate, Mode=OneWay}">
|
|
||||||
<controls:SegmentedItem.Icon>
|
<controls:SegmentedItem.Icon>
|
||||||
<SymbolIcon Symbol="Switch" />
|
<SymbolIcon Symbol="Switch" />
|
||||||
</controls:SegmentedItem.Icon>
|
</controls:SegmentedItem.Icon>
|
||||||
</controls:SegmentedItem>
|
</controls:SegmentedItem>
|
||||||
<controls:SegmentedItem x:Name="RewriteSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite, Mode=OneWay}">
|
<controls:SegmentedItem
|
||||||
|
x:Name="RewriteSegment"
|
||||||
|
Padding="12,6"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite, Mode=OneWay}">
|
||||||
<controls:SegmentedItem.Icon>
|
<controls:SegmentedItem.Icon>
|
||||||
<SymbolIcon Symbol="Edit" />
|
<SymbolIcon Symbol="Edit" />
|
||||||
</controls:SegmentedItem.Icon>
|
</controls:SegmentedItem.Icon>
|
||||||
</controls:SegmentedItem>
|
</controls:SegmentedItem>
|
||||||
<controls:SegmentedItem x:Name="SummarizeSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}">
|
<controls:SegmentedItem
|
||||||
|
x:Name="SummarizeSegment"
|
||||||
|
Padding="12,6"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}">
|
||||||
<controls:SegmentedItem.Icon>
|
<controls:SegmentedItem.Icon>
|
||||||
<SymbolIcon Symbol="Bullets" />
|
<SymbolIcon Symbol="Bullets" />
|
||||||
</controls:SegmentedItem.Icon>
|
</controls:SegmentedItem.Icon>
|
||||||
</controls:SegmentedItem>
|
</controls:SegmentedItem>
|
||||||
</controls:Segmented>
|
</controls:Segmented>
|
||||||
|
|
||||||
<StackPanel x:Name="TranslateOptionsPanel" Spacing="8" Visibility="Collapsed">
|
<StackPanel
|
||||||
|
Grid.Column="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8">
|
||||||
|
<ProgressBar
|
||||||
|
x:Name="UsageProgressBar"
|
||||||
|
Width="100"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Maximum="1000"
|
||||||
|
Value="0" />
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
x:Name="UsageSummaryTextBlock"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Row 2: Action-specific options -->
|
||||||
|
<Grid ColumnSpacing="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Translate options -->
|
||||||
|
<StackPanel
|
||||||
|
x:Name="TranslateOptionsPanel"
|
||||||
|
Orientation="Horizontal"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.Composer_AiTranslateLanguage, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.Composer_AiTranslateLanguage, Mode=OneWay}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
x:Name="TranslateLanguageComboBox"
|
x:Name="TranslateLanguageComboBox"
|
||||||
SelectedItem="{x:Bind SelectedTranslateLanguageOption, Mode=TwoWay}"
|
MinWidth="120"
|
||||||
SelectionChanged="TranslateLanguageComboBox_SelectionChanged">
|
SelectionChanged="TranslateLanguageComboBox_SelectionChanged">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="models:AiTranslateLanguageOption">
|
<DataTemplate x:DataType="models:AiTranslateLanguageOption">
|
||||||
@@ -191,17 +238,23 @@
|
|||||||
x:Name="RunTranslateButton"
|
x:Name="RunTranslateButton"
|
||||||
Click="RunTranslateButton_Click"
|
Click="RunTranslateButton_Click"
|
||||||
Content="{x:Bind domain:Translator.Composer_AiTranslateApply, Mode=OneWay}"
|
Content="{x:Bind domain:Translator.Composer_AiTranslateApply, Mode=OneWay}"
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Style="{StaticResource AccentButtonStyle}" />
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel x:Name="RewriteOptionsPanel" Spacing="8" Visibility="Collapsed">
|
<!-- Rewrite options -->
|
||||||
|
<StackPanel
|
||||||
|
x:Name="RewriteOptionsPanel"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
Text="{x:Bind domain:Translator.Composer_AiRewriteMode, Mode=OneWay}" />
|
Text="{x:Bind domain:Translator.Composer_AiRewriteMode, Mode=OneWay}" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
x:Name="RewriteModeComboBox"
|
x:Name="RewriteModeComboBox"
|
||||||
SelectedItem="{x:Bind SelectedRewriteModeOption, Mode=TwoWay}"
|
MinWidth="140"
|
||||||
SelectionChanged="RewriteModeComboBox_SelectionChanged">
|
SelectionChanged="RewriteModeComboBox_SelectionChanged">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="models:AiRewriteModeOption">
|
<DataTemplate x:DataType="models:AiRewriteModeOption">
|
||||||
@@ -209,6 +262,12 @@
|
|||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ComboBox.ItemTemplate>
|
</ComboBox.ItemTemplate>
|
||||||
</ComboBox>
|
</ComboBox>
|
||||||
|
<Button
|
||||||
|
x:Name="RunRewriteButton"
|
||||||
|
Click="RunRewriteButton_Click"
|
||||||
|
Content="{x:Bind domain:Translator.Composer_AiRewriteApply, Mode=OneWay}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="RewriteDescriptionTextBlock"
|
x:Name="RewriteDescriptionTextBlock"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
@@ -218,13 +277,38 @@
|
|||||||
x:Name="CustomRewriteTextBox"
|
x:Name="CustomRewriteTextBox"
|
||||||
PlaceholderText="{x:Bind domain:Translator.Composer_AiRewriteCustomPlaceholder, Mode=OneWay}"
|
PlaceholderText="{x:Bind domain:Translator.Composer_AiRewriteCustomPlaceholder, Mode=OneWay}"
|
||||||
Visibility="Collapsed" />
|
Visibility="Collapsed" />
|
||||||
<Button
|
|
||||||
x:Name="RunRewriteButton"
|
|
||||||
Click="RunRewriteButton_Click"
|
|
||||||
Content="{x:Bind domain:Translator.Composer_AiRewriteApply, Mode=OneWay}"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Style="{StaticResource AccentButtonStyle}" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Summarize options -->
|
||||||
|
<StackPanel
|
||||||
|
x:Name="SummarizeOptionsPanel"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.Composer_AiSummarizeDescription, Mode=OneWay}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||||
|
<Button
|
||||||
|
x:Name="RunSummarizeButton"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Click="RunSummarizeButton_Click"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}"
|
||||||
|
Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<FontIcon
|
||||||
|
x:Name="SummarizeCachedIndicator"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||||
|
FontSize="16"
|
||||||
|
Foreground="#2AA84A"
|
||||||
|
Glyph=""
|
||||||
|
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiSummarize, Mode=OneWay}"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
@@ -18,6 +20,7 @@ namespace Wino.Mail.WinUI.Controls;
|
|||||||
|
|
||||||
public sealed partial class AiActionsPanel : UserControl, IDisposable
|
public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||||
{
|
{
|
||||||
|
public event EventHandler? CloseRequested;
|
||||||
private readonly IWinoAccountProfileService _profileService = App.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
private readonly IWinoAccountProfileService _profileService = App.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
||||||
private readonly IStoreManagementService _storeManagementService = App.Current.Services.GetRequiredService<IStoreManagementService>();
|
private readonly IStoreManagementService _storeManagementService = App.Current.Services.GetRequiredService<IStoreManagementService>();
|
||||||
private readonly IMailDialogService _dialogService = App.Current.Services.GetRequiredService<IMailDialogService>();
|
private readonly IMailDialogService _dialogService = App.Current.Services.GetRequiredService<IMailDialogService>();
|
||||||
@@ -27,6 +30,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
private bool _isBusy;
|
private bool _isBusy;
|
||||||
private AiActionType _lastConfigurableAction = AiActionType.Translate;
|
private AiActionType _lastConfigurableAction = AiActionType.Translate;
|
||||||
|
private bool _hasCachedSummary;
|
||||||
private CancellationTokenSource? _actionCancellationTokenSource;
|
private CancellationTokenSource? _actionCancellationTokenSource;
|
||||||
private IReadOnlyList<AiTranslateLanguageOption> _translateOptions = Array.Empty<AiTranslateLanguageOption>();
|
private IReadOnlyList<AiTranslateLanguageOption> _translateOptions = Array.Empty<AiTranslateLanguageOption>();
|
||||||
private IReadOnlyList<AiRewriteModeOption> _rewriteOptions = Array.Empty<AiRewriteModeOption>();
|
private IReadOnlyList<AiRewriteModeOption> _rewriteOptions = Array.Empty<AiRewriteModeOption>();
|
||||||
@@ -66,6 +70,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
{
|
{
|
||||||
UpdateActionAvailability();
|
UpdateActionAvailability();
|
||||||
ApplySelectedAction(SelectDefaultAction());
|
ApplySelectedAction(SelectDefaultAction());
|
||||||
|
_ = RefreshCachedSummaryStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
@@ -83,25 +88,44 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
|
|
||||||
private void LoadOptions()
|
private void LoadOptions()
|
||||||
{
|
{
|
||||||
|
// Save current selections before replacing ItemsSource (which clears SelectedItem).
|
||||||
|
var previousTranslateCode = SelectedTranslateLanguageOption?.Code;
|
||||||
|
var previousRewriteMode = SelectedRewriteModeOption?.Mode;
|
||||||
|
|
||||||
_translateOptions = _optionsService.GetTranslateLanguageOptions();
|
_translateOptions = _optionsService.GetTranslateLanguageOptions();
|
||||||
_rewriteOptions = _optionsService.GetRewriteModeOptions();
|
_rewriteOptions = _optionsService.GetRewriteModeOptions();
|
||||||
|
|
||||||
TranslateLanguageComboBox.ItemsSource = _translateOptions;
|
TranslateLanguageComboBox.ItemsSource = _translateOptions;
|
||||||
RewriteModeComboBox.ItemsSource = _rewriteOptions;
|
RewriteModeComboBox.ItemsSource = _rewriteOptions;
|
||||||
|
|
||||||
SelectedTranslateLanguageOption ??= _translateOptions.Count > 0 ? _translateOptions[0] : null;
|
// Restore selection by matching on value, falling back to first item.
|
||||||
SelectedRewriteModeOption ??= _rewriteOptions.Count > 0 ? _rewriteOptions[0] : null;
|
SelectedTranslateLanguageOption = FindOption(_translateOptions, o => o.Code == previousTranslateCode) ?? (_translateOptions.Count > 0 ? _translateOptions[0] : null);
|
||||||
|
SelectedRewriteModeOption = FindOption(_rewriteOptions, o => o.Mode == previousRewriteMode) ?? (_rewriteOptions.Count > 0 ? _rewriteOptions[0] : null);
|
||||||
|
|
||||||
TranslateLanguageComboBox.SelectedItem = SelectedTranslateLanguageOption;
|
TranslateLanguageComboBox.SelectedItem = SelectedTranslateLanguageOption;
|
||||||
RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption;
|
RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption;
|
||||||
UpdateRewriteOptionState();
|
UpdateRewriteOptionState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static T? FindOption<T>(IReadOnlyList<T> options, Func<T, bool> predicate) where T : class
|
||||||
|
{
|
||||||
|
foreach (var option in options)
|
||||||
|
{
|
||||||
|
if (predicate(option))
|
||||||
|
{
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateActionAvailability()
|
private void UpdateActionAvailability()
|
||||||
{
|
{
|
||||||
TranslateSegment.Visibility = HasAction(AiActionType.Translate) ? Visibility.Visible : Visibility.Collapsed;
|
TranslateSegment.Visibility = HasAction(AiActionType.Translate) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
RewriteSegment.Visibility = HasAction(AiActionType.Rewrite) ? Visibility.Visible : Visibility.Collapsed;
|
RewriteSegment.Visibility = HasAction(AiActionType.Rewrite) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
SummarizeSegment.Visibility = HasAction(AiActionType.Summarize) ? Visibility.Visible : Visibility.Collapsed;
|
SummarizeSegment.Visibility = HasAction(AiActionType.Summarize) ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
SummarizeCachedIndicator.Visibility = HasAction(AiActionType.Summarize) && _hasCachedSummary ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool HasAction(AiActionType action) => (AvailableActions & action) == action;
|
private bool HasAction(AiActionType action) => (AvailableActions & action) == action;
|
||||||
@@ -143,6 +167,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
|
|
||||||
TranslateOptionsPanel.Visibility = action == AiActionType.Translate ? Visibility.Visible : Visibility.Collapsed;
|
TranslateOptionsPanel.Visibility = action == AiActionType.Translate ? Visibility.Visible : Visibility.Collapsed;
|
||||||
RewriteOptionsPanel.Visibility = action == AiActionType.Rewrite ? Visibility.Visible : Visibility.Collapsed;
|
RewriteOptionsPanel.Visibility = action == AiActionType.Rewrite ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
SummarizeOptionsPanel.Visibility = action == AiActionType.Summarize ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshAvailabilityAsync()
|
public async Task RefreshAvailabilityAsync()
|
||||||
@@ -160,7 +185,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(true);
|
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(true);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
{
|
{
|
||||||
UpdateUsageSummary(string.Empty, string.Empty);
|
UpdateUsageSummary(string.Empty);
|
||||||
UpdatePanelState(showSignedOut: true);
|
UpdatePanelState(showSignedOut: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -168,7 +193,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
|
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
|
||||||
if (!hasAiPack)
|
if (!hasAiPack)
|
||||||
{
|
{
|
||||||
UpdateUsageSummary(string.Empty, string.Empty);
|
UpdateUsageSummary(string.Empty);
|
||||||
UpdatePanelState(showPurchase: true);
|
UpdatePanelState(showPurchase: true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -176,13 +201,17 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true);
|
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true);
|
||||||
if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null)
|
if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null)
|
||||||
{
|
{
|
||||||
UpdateUsageSummary(CreateUsageSummary(aiStatusResponse.Result), CreateUsageResetText(aiStatusResponse.Result));
|
UpdateUsageSummary(
|
||||||
|
CreateUsageSummary(aiStatusResponse.Result),
|
||||||
|
GetUsedCount(aiStatusResponse.Result),
|
||||||
|
GetUsageLimit(aiStatusResponse.Result));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
|
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await RefreshCachedSummaryStateAsync().ConfigureAwait(true);
|
||||||
ApplySelectedAction(SelectDefaultAction());
|
ApplySelectedAction(SelectDefaultAction());
|
||||||
UpdatePanelState(showReady: true);
|
UpdatePanelState(showReady: true);
|
||||||
}
|
}
|
||||||
@@ -191,7 +220,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
|
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
||||||
UpdatePanelState(showReady: true);
|
UpdatePanelState(showReady: true);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -211,13 +240,29 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
return Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
return Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CreateUsageResetText(AiStatusResultDto aiStatus)
|
private static int GetUsedCount(AiStatusResultDto aiStatus)
|
||||||
|
=> aiStatus.Used is int used ? used : 0;
|
||||||
|
|
||||||
|
private static string CreateUsageSummary(QuotaInfoDto quotaInfo)
|
||||||
{
|
{
|
||||||
return aiStatus.CurrentPeriodEndUtc is DateTimeOffset resetDateUtc
|
if (quotaInfo.Used is int used && quotaInfo.MonthlyLimit is int limit && limit > 0)
|
||||||
? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime)
|
{
|
||||||
: string.Empty;
|
return string.Format(Translator.AiActions_UsageSummary, used, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Translator.WinoAccount_Management_AiPackUsageLoadFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetUsedCount(QuotaInfoDto quotaInfo)
|
||||||
|
=> quotaInfo.Used is int used ? used : 0;
|
||||||
|
|
||||||
|
private static int GetUsageLimit(QuotaInfoDto quotaInfo)
|
||||||
|
=> quotaInfo.MonthlyLimit is int limit && limit > 0 ? limit : 1000;
|
||||||
|
|
||||||
|
private static int GetUsageLimit(AiStatusResultDto aiStatus)
|
||||||
|
=> aiStatus.MonthlyLimit is int limit && limit > 0 ? limit : 1000;
|
||||||
|
|
||||||
|
|
||||||
private void UpdatePanelState(bool showLoading = false, bool showSignedOut = false, bool showPurchase = false, bool showReady = false)
|
private void UpdatePanelState(bool showLoading = false, bool showSignedOut = false, bool showPurchase = false, bool showReady = false)
|
||||||
{
|
{
|
||||||
LoadingPanel.Visibility = showLoading ? Visibility.Visible : Visibility.Collapsed;
|
LoadingPanel.Visibility = showLoading ? Visibility.Visible : Visibility.Collapsed;
|
||||||
@@ -226,11 +271,19 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
ReadyPanel.Visibility = showReady ? Visibility.Visible : Visibility.Collapsed;
|
ReadyPanel.Visibility = showReady ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateUsageSummary(string usageText, string resetText)
|
private void UpdateUsageSummary(string usageText, int usedCount = 0)
|
||||||
{
|
{
|
||||||
UsageSummaryTextBlock.Text = usageText;
|
UsageSummaryTextBlock.Text = usageText;
|
||||||
UsageResetTextBlock.Text = resetText;
|
UsageProgressBar.Maximum = 1000;
|
||||||
UsageResetTextBlock.Visibility = string.IsNullOrWhiteSpace(resetText) ? Visibility.Collapsed : Visibility.Visible;
|
UsageProgressBar.Value = Math.Min(usedCount, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateUsageSummary(string usageText, int usedCount, int usageLimit)
|
||||||
|
{
|
||||||
|
var normalizedLimit = usageLimit > 0 ? usageLimit : 1000;
|
||||||
|
UsageSummaryTextBlock.Text = usageText;
|
||||||
|
UsageProgressBar.Maximum = normalizedLimit;
|
||||||
|
UsageProgressBar.Value = Math.Min(usedCount, normalizedLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetBusyUi(bool isBusy, bool showLoading)
|
private void SetBusyUi(bool isBusy, bool showLoading)
|
||||||
@@ -244,6 +297,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
CustomRewriteTextBox.IsEnabled = !isBusy;
|
CustomRewriteTextBox.IsEnabled = !isBusy;
|
||||||
RunTranslateButton.IsEnabled = !isBusy;
|
RunTranslateButton.IsEnabled = !isBusy;
|
||||||
RunRewriteButton.IsEnabled = !isBusy;
|
RunRewriteButton.IsEnabled = !isBusy;
|
||||||
|
RunSummarizeButton.IsEnabled = !isBusy;
|
||||||
SignedOutPanel.IsHitTestVisible = !isBusy;
|
SignedOutPanel.IsHitTestVisible = !isBusy;
|
||||||
PurchasePanel.IsHitTestVisible = !isBusy;
|
PurchasePanel.IsHitTestVisible = !isBusy;
|
||||||
|
|
||||||
@@ -363,7 +417,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
if (ReferenceEquals(ActionSelector.SelectedItem, SummarizeSegment))
|
if (ReferenceEquals(ActionSelector.SelectedItem, SummarizeSegment))
|
||||||
{
|
{
|
||||||
ApplySelectedAction(AiActionType.Summarize);
|
ApplySelectedAction(AiActionType.Summarize);
|
||||||
_ = ExecuteAiActionAsync(AiActionType.Summarize);
|
_ = RefreshCachedSummaryStateAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +456,17 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
await ExecuteAiActionAsync(AiActionType.Rewrite);
|
await ExecuteAiActionAsync(AiActionType.Rewrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void RunSummarizeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await ExecuteAiActionAsync(AiActionType.Summarize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
CancelPendingOperation();
|
||||||
|
CloseRequested?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ExecuteAiActionAsync(AiActionType action)
|
private async Task ExecuteAiActionAsync(AiActionType action)
|
||||||
{
|
{
|
||||||
if (_isBusy)
|
if (_isBusy)
|
||||||
@@ -423,15 +488,6 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var html = await HtmlHost.GetCurrentHtmlAsync(cancellationToken).ConfigureAwait(true);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(html))
|
|
||||||
{
|
|
||||||
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_AiHtmlEmpty, InfoBarMessageType.Error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action == AiActionType.Translate && SelectedTranslateLanguageOption == null)
|
if (action == AiActionType.Translate && SelectedTranslateLanguageOption == null)
|
||||||
{
|
{
|
||||||
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
||||||
@@ -444,6 +500,41 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action == AiActionType.Translate)
|
||||||
|
{
|
||||||
|
var cachedTranslation = await HtmlHost.TryGetCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(cachedTranslation))
|
||||||
|
{
|
||||||
|
await HtmlHost.ApplyHtmlResultAsync(cachedTranslation, cancellationToken).ConfigureAwait(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == AiActionType.Summarize)
|
||||||
|
{
|
||||||
|
var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(cachedSummary))
|
||||||
|
{
|
||||||
|
_hasCachedSummary = true;
|
||||||
|
UpdateActionAvailability();
|
||||||
|
await ShowSummaryDialogAsync(cachedSummary).ConfigureAwait(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = await HtmlHost.GetCurrentHtmlAsync(cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(html))
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_AiHtmlEmpty, InfoBarMessageType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var response = action switch
|
var response = action switch
|
||||||
{
|
{
|
||||||
AiActionType.Translate => await _profileService.TranslateAsync(html, SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true),
|
AiActionType.Translate => await _profileService.TranslateAsync(html, SelectedTranslateLanguageOption?.Code ?? string.Empty, cancellationToken).ConfigureAwait(true),
|
||||||
@@ -460,6 +551,37 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (response.Quota != null)
|
||||||
|
{
|
||||||
|
UpdateUsageSummary(
|
||||||
|
CreateUsageSummary(response.Quota),
|
||||||
|
GetUsedCount(response.Quota),
|
||||||
|
GetUsageLimit(response.Quota));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == AiActionType.Translate)
|
||||||
|
{
|
||||||
|
await HtmlHost.SaveCachedTranslationHtmlAsync(SelectedTranslateLanguageOption?.Code ?? string.Empty, response.Result.Html, cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action == AiActionType.Summarize)
|
||||||
|
{
|
||||||
|
await HtmlHost.SaveCachedSummaryTextAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
_hasCachedSummary = true;
|
||||||
|
UpdateActionAvailability();
|
||||||
|
|
||||||
|
var savedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(cancellationToken).ConfigureAwait(true);
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
await ShowSummaryDialogAsync(string.IsNullOrWhiteSpace(savedSummary) ? response.Result.Html : savedSummary).ConfigureAwait(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
await HtmlHost.ApplyHtmlResultAsync(response.Result.Html, cancellationToken).ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@@ -479,14 +601,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
_actionCancellationTokenSource = null;
|
_actionCancellationTokenSource = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action == AiActionType.Summarize)
|
// Summarize no longer auto-switches back; the user explicitly selected the tab.
|
||||||
{
|
|
||||||
var fallbackAction = _lastConfigurableAction != AiActionType.None && HasAction(_lastConfigurableAction)
|
|
||||||
? _lastConfigurableAction
|
|
||||||
: SelectDefaultAction();
|
|
||||||
|
|
||||||
ApplySelectedAction(fallbackAction);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,6 +620,94 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
|||||||
return CustomRewriteTextBox.Text?.Trim() ?? string.Empty;
|
return CustomRewriteTextBox.Text?.Trim() ?? string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RefreshCachedSummaryStateAsync()
|
||||||
|
{
|
||||||
|
if (HtmlHost == null || !HasAction(AiActionType.Summarize))
|
||||||
|
{
|
||||||
|
_hasCachedSummary = false;
|
||||||
|
UpdateActionAvailability();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cachedSummary = await HtmlHost.TryGetCachedSummaryTextAsync(CancellationToken.None).ConfigureAwait(true);
|
||||||
|
_hasCachedSummary = !string.IsNullOrWhiteSpace(cachedSummary);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_hasCachedSummary = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateActionAvailability();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ShowSummaryDialogAsync(string summary)
|
||||||
|
{
|
||||||
|
if (HtmlHost == null)
|
||||||
|
{
|
||||||
|
await _dialogService.ShowMessageAsync(summary, Translator.Composer_AiSummarize, WinoCustomMessageDialogIcon.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summaryTextBox = new TextBox
|
||||||
|
{
|
||||||
|
Text = summary,
|
||||||
|
IsReadOnly = true,
|
||||||
|
AcceptsReturn = true,
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
MinHeight = 240,
|
||||||
|
MaxHeight = 420,
|
||||||
|
BorderThickness = new Thickness(0),
|
||||||
|
Background = new SolidColorBrush(Windows.UI.Color.FromArgb(0, 0, 0, 0))
|
||||||
|
};
|
||||||
|
|
||||||
|
var dialog = new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = XamlRoot,
|
||||||
|
RequestedTheme = ActualTheme,
|
||||||
|
Title = Translator.Composer_AiSummarize,
|
||||||
|
PrimaryButtonText = Translator.Buttons_Save,
|
||||||
|
SecondaryButtonText = Translator.Buttons_Close,
|
||||||
|
DefaultButton = ContentDialogButton.Secondary,
|
||||||
|
Content = new ScrollViewer
|
||||||
|
{
|
||||||
|
Content = summaryTextBox,
|
||||||
|
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||||
|
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
dialog.PrimaryButtonClick += async (sender, args) =>
|
||||||
|
{
|
||||||
|
var deferral = args.GetDeferral();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var path = await _dialogService.PickFilePathAsync(HtmlHost.GetSuggestedSummaryFileName());
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(path, summary);
|
||||||
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Info, string.Format(Translator.ClipboardTextCopied_Message, Path.GetFileName(path)), InfoBarMessageType.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
deferral.Complete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelAndDisposeActionCancellationToken()
|
private void CancelAndDisposeActionCancellationToken()
|
||||||
{
|
{
|
||||||
if (_actionCancellationTokenSource == null)
|
if (_actionCancellationTokenSource == null)
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ public interface IAiHtmlActionHost
|
|||||||
Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken);
|
Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken);
|
||||||
Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken);
|
Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken);
|
||||||
Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken);
|
Task SaveCachedTranslationHtmlAsync(string languageCode, string html, CancellationToken cancellationToken);
|
||||||
|
Task<string?> TryGetCachedSummaryTextAsync(CancellationToken cancellationToken);
|
||||||
|
Task SaveCachedSummaryTextAsync(string summary, CancellationToken cancellationToken);
|
||||||
|
string GetSuggestedSummaryFileName();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,477 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using CommunityToolkit.WinUI;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Folders;
|
||||||
|
using Wino.Core.Domain.Models.Menus;
|
||||||
|
using Wino.Helpers;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls;
|
||||||
|
|
||||||
|
public sealed partial class OperationCommandBar : CommandBar
|
||||||
|
{
|
||||||
|
private const string MailOperationTemplateKey = "OperationCommandBarMailOperationTemplate";
|
||||||
|
private const string FolderOperationTemplateKey = "OperationCommandBarFolderOperationTemplate";
|
||||||
|
private const string AIActionsTemplateKey = "OperationCommandBarAIActionsTemplate";
|
||||||
|
private const string ThemeToggleTemplateKey = "OperationCommandBarThemeToggleTemplate";
|
||||||
|
private const string SeparatorTemplateKey = "OperationCommandBarSeparatorTemplate";
|
||||||
|
|
||||||
|
private readonly IPreferencesService? _preferencesService;
|
||||||
|
private readonly HashSet<INotifyPropertyChanged> _trackedMenuItems = [];
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial ObservableCollection<IMenuOperation>? MenuItems { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial ICommand? ItemInvokedCommand { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial bool IsAIActionsEnabled { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial bool IsAIActionsPaneToggleVisible { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial bool IsEditorThemeDark { get; set; }
|
||||||
|
|
||||||
|
[GeneratedDependencyProperty]
|
||||||
|
public partial bool IsEditorThemeToggleVisible { get; set; }
|
||||||
|
|
||||||
|
public event EventHandler<bool>? AIActionsEnabledChanged;
|
||||||
|
|
||||||
|
public OperationCommandBar()
|
||||||
|
{
|
||||||
|
_preferencesService = App.Current.Services.GetService<IPreferencesService>();
|
||||||
|
|
||||||
|
DefaultLabelPosition = CommandBarDefaultLabelPosition.Right;
|
||||||
|
IsDynamicOverflowEnabled = true;
|
||||||
|
OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Auto;
|
||||||
|
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
Unloaded += OnUnloaded;
|
||||||
|
DynamicOverflowItemsChanging += OperationCommandBar_DynamicOverflowItemsChanging;
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnMenuItemsPropertyChanged(DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.OldValue is INotifyCollectionChanged oldCollection)
|
||||||
|
{
|
||||||
|
oldCollection.CollectionChanged -= MenuItems_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetachTrackedMenuItemHandlers();
|
||||||
|
|
||||||
|
if (e.NewValue is ObservableCollection<IMenuOperation> newItems)
|
||||||
|
{
|
||||||
|
newItems.CollectionChanged += MenuItems_CollectionChanged;
|
||||||
|
TrackMenuItemHandlers((IEnumerable<IMenuOperation>)newItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsAIActionsEnabledChanged(bool newValue)
|
||||||
|
{
|
||||||
|
AIActionsEnabledChanged?.Invoke(this, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsAIActionsPaneToggleVisibleChanged(bool newValue)
|
||||||
|
{
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsEditorThemeDarkChanged(bool newValue)
|
||||||
|
{
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsEditorThemeToggleVisibleChanged(bool newValue)
|
||||||
|
{
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ClearGeneratedCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OperationCommandBar_DynamicOverflowItemsChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Action == CommandBarDynamicOverflowAction.AddingToOverflow || sender.SecondaryCommands.Count > 0)
|
||||||
|
{
|
||||||
|
sender.OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Visible;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sender.OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuItems_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Action == NotifyCollectionChangedAction.Reset)
|
||||||
|
{
|
||||||
|
DetachTrackedMenuItemHandlers();
|
||||||
|
|
||||||
|
if (sender is IEnumerable<IMenuOperation> refreshedItems)
|
||||||
|
{
|
||||||
|
TrackMenuItemHandlers(refreshedItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UntrackMenuItemHandlers(e.OldItems);
|
||||||
|
TrackMenuItemHandlers(e.NewItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuItem_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(e.PropertyName)
|
||||||
|
|| e.PropertyName == nameof(IMenuOperation.IsEnabled)
|
||||||
|
|| e.PropertyName == nameof(IMenuOperation.IsSecondaryMenuPreferred)
|
||||||
|
|| e.PropertyName == nameof(MenuOperationItemBase<MailOperation>.Operation)
|
||||||
|
|| e.PropertyName == nameof(MenuOperationItemBase<MailOperation>.Identifier))
|
||||||
|
{
|
||||||
|
RefreshCommands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackMenuItemHandlers(IEnumerable<IMenuOperation> items)
|
||||||
|
{
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is INotifyPropertyChanged propertyChanged && _trackedMenuItems.Add(propertyChanged))
|
||||||
|
{
|
||||||
|
propertyChanged.PropertyChanged += MenuItem_PropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TrackMenuItemHandlers(System.Collections.IList? items)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is IMenuOperation menuItem)
|
||||||
|
{
|
||||||
|
TrackMenuItemHandlers([menuItem]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UntrackMenuItemHandlers(System.Collections.IList? items)
|
||||||
|
{
|
||||||
|
if (items == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item is INotifyPropertyChanged propertyChanged && _trackedMenuItems.Remove(propertyChanged))
|
||||||
|
{
|
||||||
|
propertyChanged.PropertyChanged -= MenuItem_PropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachTrackedMenuItemHandlers()
|
||||||
|
{
|
||||||
|
foreach (var item in _trackedMenuItems)
|
||||||
|
{
|
||||||
|
item.PropertyChanged -= MenuItem_PropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
_trackedMenuItems.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshCommands()
|
||||||
|
{
|
||||||
|
ClearGeneratedCommands();
|
||||||
|
|
||||||
|
if (IsAIActionsPaneToggleVisible)
|
||||||
|
{
|
||||||
|
PrimaryCommands.Add(CreateAIActionsToggleButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEditorThemeToggleVisible)
|
||||||
|
{
|
||||||
|
PrimaryCommands.Add(CreateThemeToggleButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MenuItems == null)
|
||||||
|
{
|
||||||
|
UpdateOverflowButtonVisibility();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in MenuItems)
|
||||||
|
{
|
||||||
|
var element = CreateCommandElement(item);
|
||||||
|
if (element == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.IsSecondaryMenuPreferred)
|
||||||
|
{
|
||||||
|
SecondaryCommands.Add(element);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PrimaryCommands.Add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateOverflowButtonVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearGeneratedCommands()
|
||||||
|
{
|
||||||
|
DetachCommandHandlers(PrimaryCommands);
|
||||||
|
DetachCommandHandlers(SecondaryCommands);
|
||||||
|
|
||||||
|
PrimaryCommands.Clear();
|
||||||
|
SecondaryCommands.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DetachCommandHandlers(IEnumerable<ICommandBarElement> commands)
|
||||||
|
{
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case AppBarButton button:
|
||||||
|
button.Click -= OperationButton_Click;
|
||||||
|
button.Click -= ThemeButton_Click;
|
||||||
|
break;
|
||||||
|
case AppBarToggleButton toggleButton:
|
||||||
|
toggleButton.ClearValue(AppBarToggleButton.IsCheckedProperty);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICommandBarElement? CreateCommandElement(IMenuOperation item)
|
||||||
|
{
|
||||||
|
if (item is MailOperationMenuItem mailOperation && mailOperation.Operation == MailOperation.Seperator)
|
||||||
|
{
|
||||||
|
return LoadCommandBarElementTemplate(SeparatorTemplateKey, new SeparatorCommandBarItemViewModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is MailOperationMenuItem mailOperationItem)
|
||||||
|
{
|
||||||
|
var button = LoadCommandBarElementTemplate(
|
||||||
|
MailOperationTemplateKey,
|
||||||
|
new MenuOperationCommandBarItemViewModel(
|
||||||
|
mailOperationItem,
|
||||||
|
XamlHelpers.GetOperationString(mailOperationItem.Operation),
|
||||||
|
XamlHelpers.GetWinoIconGlyph(mailOperationItem.Operation),
|
||||||
|
GetOperationLabelPosition(XamlHelpers.GetOperationString(mailOperationItem.Operation))))
|
||||||
|
as AppBarButton;
|
||||||
|
|
||||||
|
if (button == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.Tag = mailOperationItem;
|
||||||
|
button.Click += OperationButton_Click;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item is FolderOperationMenuItem folderOperationItem)
|
||||||
|
{
|
||||||
|
var label = XamlHelpers.GetOperationString(folderOperationItem.Operation);
|
||||||
|
var button = LoadCommandBarElementTemplate(
|
||||||
|
FolderOperationTemplateKey,
|
||||||
|
new MenuOperationCommandBarItemViewModel(
|
||||||
|
folderOperationItem,
|
||||||
|
label,
|
||||||
|
XamlHelpers.GetPathGeometry(folderOperationItem.Operation),
|
||||||
|
GetOperationLabelPosition(label)))
|
||||||
|
as AppBarButton;
|
||||||
|
|
||||||
|
if (button == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.Tag = folderOperationItem;
|
||||||
|
button.Click += OperationButton_Click;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppBarToggleButton CreateAIActionsToggleButton()
|
||||||
|
{
|
||||||
|
var button = (AppBarToggleButton)LoadCommandBarElementTemplate(
|
||||||
|
AIActionsTemplateKey,
|
||||||
|
new AIActionsCommandBarItemViewModel(Translator.Composer_AiActions, "\uE945"));
|
||||||
|
|
||||||
|
button.SetBinding(AppBarToggleButton.IsCheckedProperty, new Binding
|
||||||
|
{
|
||||||
|
Mode = BindingMode.TwoWay,
|
||||||
|
Path = new PropertyPath(nameof(IsAIActionsEnabled)),
|
||||||
|
Source = this
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AppBarButton CreateThemeToggleButton()
|
||||||
|
{
|
||||||
|
var label = IsEditorThemeDark ? Translator.Composer_LightTheme : Translator.Composer_DarkTheme;
|
||||||
|
var icon = IsEditorThemeDark ? WinoIconGlyph.LightEditor : WinoIconGlyph.DarkEditor;
|
||||||
|
|
||||||
|
var button = (AppBarButton)LoadCommandBarElementTemplate(
|
||||||
|
ThemeToggleTemplateKey,
|
||||||
|
new ThemeCommandBarItemViewModel(label, icon));
|
||||||
|
|
||||||
|
button.Click += ThemeButton_Click;
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OperationButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is AppBarButton button && button.Tag is IMenuOperation operation)
|
||||||
|
{
|
||||||
|
ItemInvokedCommand?.Execute(operation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThemeButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
IsEditorThemeDark = !IsEditorThemeDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
private object? FindTemplateResource(string key)
|
||||||
|
{
|
||||||
|
if (TryGetResourceRecursive(Resources, key, out var resource))
|
||||||
|
{
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryGetResourceRecursive(Application.Current.Resources, key, out resource) ? resource : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetResourceRecursive(ResourceDictionary dictionary, string key, out object? resource)
|
||||||
|
{
|
||||||
|
if (dictionary.TryGetValue(key, out resource))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var mergedDictionary in dictionary.MergedDictionaries)
|
||||||
|
{
|
||||||
|
if (TryGetResourceRecursive(mergedDictionary, key, out resource))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ICommandBarElement LoadCommandBarElementTemplate(string resourceKey, object dataContext)
|
||||||
|
{
|
||||||
|
var template = FindTemplateResource(resourceKey) as DataTemplate
|
||||||
|
?? throw new InvalidOperationException($"Unable to resolve resource '{resourceKey}'.");
|
||||||
|
|
||||||
|
if (template.LoadContent() is not ICommandBarElement element)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Resource '{resourceKey}' did not create an ICommandBarElement.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element is FrameworkElement frameworkElement)
|
||||||
|
{
|
||||||
|
frameworkElement.DataContext = dataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CommandBarLabelPosition GetOperationLabelPosition(string label)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(label) || _preferencesService == null || !_preferencesService.IsShowActionLabelsEnabled
|
||||||
|
? CommandBarLabelPosition.Collapsed
|
||||||
|
: CommandBarLabelPosition.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateOverflowButtonVisibility()
|
||||||
|
{
|
||||||
|
OverflowButtonVisibility = SecondaryCommands.Count > 0
|
||||||
|
? CommandBarOverflowButtonVisibility.Visible
|
||||||
|
: CommandBarOverflowButtonVisibility.Auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MenuOperationCommandBarItemViewModel
|
||||||
|
{
|
||||||
|
public MenuOperationCommandBarItemViewModel(IMenuOperation operation, string label, WinoIconGlyph icon, CommandBarLabelPosition labelPosition)
|
||||||
|
{
|
||||||
|
Operation = operation;
|
||||||
|
Label = label;
|
||||||
|
Icon = icon;
|
||||||
|
ToolTip = label;
|
||||||
|
LabelPosition = labelPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMenuOperation Operation { get; }
|
||||||
|
public string Label { get; }
|
||||||
|
public WinoIconGlyph Icon { get; }
|
||||||
|
public string ToolTip { get; }
|
||||||
|
public bool IsEnabled => Operation.IsEnabled;
|
||||||
|
public CommandBarLabelPosition LabelPosition { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class AIActionsCommandBarItemViewModel
|
||||||
|
{
|
||||||
|
public AIActionsCommandBarItemViewModel(string toolTip, string glyph)
|
||||||
|
{
|
||||||
|
ToolTip = toolTip;
|
||||||
|
Glyph = glyph;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToolTip { get; }
|
||||||
|
public string Glyph { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ThemeCommandBarItemViewModel
|
||||||
|
{
|
||||||
|
public ThemeCommandBarItemViewModel(string toolTip, WinoIconGlyph icon)
|
||||||
|
{
|
||||||
|
ToolTip = toolTip;
|
||||||
|
Icon = icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToolTip { get; }
|
||||||
|
public WinoIconGlyph Icon { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SeparatorCommandBarItemViewModel;
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:controls="using:Wino.Mail.WinUI.Controls">
|
||||||
|
|
||||||
|
<Style
|
||||||
|
BasedOn="{StaticResource DefaultCommandBarStyle}"
|
||||||
|
TargetType="controls:OperationCommandBar">
|
||||||
|
<Setter Property="DefaultLabelPosition" Value="Right" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||||
|
<Setter Property="IsDynamicOverflowEnabled" Value="True" />
|
||||||
|
<Setter Property="OverflowButtonVisibility" Value="Auto" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="OperationCommandBarMailOperationTemplate">
|
||||||
|
<AppBarButton
|
||||||
|
MinWidth="40"
|
||||||
|
IsEnabled="{Binding IsEnabled}"
|
||||||
|
Label="{Binding Label}"
|
||||||
|
LabelPosition="{Binding LabelPosition}"
|
||||||
|
ToolTipService.ToolTip="{Binding ToolTip}">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<controls:WinoFontIcon Icon="{Binding Icon}" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="OperationCommandBarFolderOperationTemplate">
|
||||||
|
<AppBarButton
|
||||||
|
MinWidth="40"
|
||||||
|
IsEnabled="{Binding IsEnabled}"
|
||||||
|
Label="{Binding Label}"
|
||||||
|
LabelPosition="{Binding LabelPosition}"
|
||||||
|
ToolTipService.ToolTip="{Binding ToolTip}">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<controls:WinoFontIcon Icon="{Binding Icon}" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="OperationCommandBarAIActionsTemplate">
|
||||||
|
<AppBarToggleButton
|
||||||
|
MinWidth="40"
|
||||||
|
LabelPosition="Collapsed"
|
||||||
|
ToolTipService.ToolTip="{Binding ToolTip}">
|
||||||
|
<AppBarToggleButton.Icon>
|
||||||
|
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="{Binding Glyph}" />
|
||||||
|
</AppBarToggleButton.Icon>
|
||||||
|
</AppBarToggleButton>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="OperationCommandBarThemeToggleTemplate">
|
||||||
|
<AppBarButton
|
||||||
|
MinWidth="40"
|
||||||
|
LabelPosition="Collapsed"
|
||||||
|
ToolTipService.ToolTip="{Binding ToolTip}">
|
||||||
|
<AppBarButton.Icon>
|
||||||
|
<controls:WinoFontIcon Icon="{Binding Icon}" />
|
||||||
|
</AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="OperationCommandBarSeparatorTemplate">
|
||||||
|
<AppBarSeparator />
|
||||||
|
</DataTemplate>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -451,6 +451,20 @@ public sealed partial class ComposePage : ComposePageAbstract,
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<string?> TryGetCachedSummaryTextAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return Task.FromResult<string?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveCachedSummaryTextAsync(string summary, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetSuggestedSummaryFileName() => "email-summary.txt";
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -15,10 +15,7 @@
|
|||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
xmlns:enums="using:Wino.Core.Domain.Enums"
|
xmlns:enums="using:Wino.Core.Domain.Enums"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:i="using:Microsoft.Xaml.Interactivity"
|
|
||||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
|
||||||
xmlns:listview="using:Wino.Mail.WinUI.Controls.ListView"
|
xmlns:listview="using:Wino.Mail.WinUI.Controls.ListView"
|
||||||
xmlns:local="using:Wino.Behaviors"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:menuflyouts="using:Wino.MenuFlyouts"
|
xmlns:menuflyouts="using:Wino.MenuFlyouts"
|
||||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
@@ -176,15 +173,15 @@
|
|||||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||||
CornerRadius="8"
|
CornerRadius="8"
|
||||||
Visibility="{x:Bind ViewModel.PreferencesService.IsMailListActionBarEnabled}">
|
Visibility="{x:Bind ViewModel.PreferencesService.IsMailListActionBarEnabled}">
|
||||||
<CommandBar
|
<coreControls:OperationCommandBar
|
||||||
HorizontalAlignment="Left"
|
HorizontalAlignment="Left"
|
||||||
DefaultLabelPosition="Collapsed"
|
DefaultLabelPosition="Collapsed"
|
||||||
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedItemsCount), Mode=OneWay}"
|
IsEnabled="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.MailCollection.SelectedItemsCount), Mode=OneWay}"
|
||||||
|
IsAIActionsPaneToggleVisible="False"
|
||||||
|
ItemInvokedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}"
|
||||||
|
MenuItems="{x:Bind ViewModel.ActionItems, Mode=OneWay}"
|
||||||
OverflowButtonVisibility="Auto">
|
OverflowButtonVisibility="Auto">
|
||||||
<interactivity:Interaction.Behaviors>
|
</coreControls:OperationCommandBar>
|
||||||
<local:BindableCommandBarBehavior ItemClickedCommand="{x:Bind ViewModel.ExecuteTopBarActionCommand}" PrimaryCommands="{x:Bind ViewModel.ActionItems, Mode=OneWay}" />
|
|
||||||
</interactivity:Interaction.Behaviors>
|
|
||||||
</CommandBar>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Pivot + Sync + Multi Select -->
|
<!-- Pivot + Sync + Multi Select -->
|
||||||
|
|||||||
@@ -4,16 +4,14 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:abstract="using:Wino.Views.Abstract"
|
xmlns:abstract="using:Wino.Views.Abstract"
|
||||||
xmlns:controls="using:Wino.Controls"
|
xmlns:controls="using:Wino.Controls"
|
||||||
|
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
||||||
xmlns:customcontrols="using:Wino.Mail.WinUI.Controls.CustomControls"
|
xmlns:customcontrols="using:Wino.Mail.WinUI.Controls.CustomControls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
xmlns:entities="using:Wino.Core.Domain.Entities.Shared"
|
xmlns:entities="using:Wino.Core.Domain.Entities.Shared"
|
||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
|
||||||
xmlns:local="using:Wino.Behaviors"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
xmlns:coreControls="using:Wino.Mail.WinUI.Controls"
|
|
||||||
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
|
||||||
x:Name="root"
|
x:Name="root"
|
||||||
@@ -266,20 +264,17 @@
|
|||||||
<PathIcon Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
|
<PathIcon Data="M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Grid>
|
</Grid>
|
||||||
<CommandBar
|
<coreControls:OperationCommandBar
|
||||||
x:Name="RendererBar"
|
x:Name="RendererCommandBar"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
DefaultLabelPosition="Right"
|
DefaultLabelPosition="Right"
|
||||||
DynamicOverflowItemsChanging="BarDynamicOverflowChanging"
|
IsAIActionsPaneToggleVisible="True"
|
||||||
IsDynamicOverflowEnabled="True"
|
IsEditorThemeDark="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=TwoWay}"
|
||||||
OverflowButtonVisibility="Auto">
|
IsEditorThemeToggleVisible="True"
|
||||||
<interactivity:Interaction.Behaviors>
|
ItemInvokedCommand="{x:Bind ViewModel.OperationClickedCommand}"
|
||||||
<local:BindableCommandBarBehavior
|
MenuItems="{x:Bind ViewModel.MenuItems, Mode=OneWay}">
|
||||||
ItemClickedCommand="{x:Bind ViewModel.OperationClickedCommand}"
|
<coreControls:OperationCommandBar.Content>
|
||||||
PrimaryCommands="{x:Bind ViewModel.MenuItems, Mode=OneWay}" />
|
|
||||||
</interactivity:Interaction.Behaviors>
|
|
||||||
<CommandBar.Content>
|
|
||||||
<Grid Padding="0,5">
|
<Grid Padding="0,5">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
@@ -322,22 +317,9 @@
|
|||||||
<TextBlock FontSize="12" Text="{x:Bind helpers:XamlHelpers.GetCreationDateString(ViewModel.CreationDate, ViewModel.PreferencesService.Prefer24HourTimeFormat), Mode=OneWay}" />
|
<TextBlock FontSize="12" Text="{x:Bind helpers:XamlHelpers.GetCreationDateString(ViewModel.CreationDate, ViewModel.PreferencesService.Prefer24HourTimeFormat), Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<AppBarToggleButton
|
|
||||||
x:Name="ReaderAiActionsToggleButton"
|
|
||||||
Grid.Column="2"
|
|
||||||
Checked="ReaderAiActionsToggleButton_Checked"
|
|
||||||
MinWidth="40"
|
|
||||||
HorizontalContentAlignment="Center"
|
|
||||||
LabelPosition="Collapsed"
|
|
||||||
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_AiActions}">
|
|
||||||
<AppBarToggleButton.Icon>
|
|
||||||
<FontIcon FontFamily="{StaticResource SymbolThemeFontFamily}" Glyph="" />
|
|
||||||
</AppBarToggleButton.Icon>
|
|
||||||
</AppBarToggleButton>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</CommandBar.Content>
|
</coreControls:OperationCommandBar.Content>
|
||||||
</CommandBar>
|
</coreControls:OperationCommandBar>
|
||||||
|
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
@@ -433,7 +415,7 @@
|
|||||||
Margin="0,8,0,0"
|
Margin="0,8,0,0"
|
||||||
AvailableActions="Translate, Summarize"
|
AvailableActions="Translate, Summarize"
|
||||||
HtmlHost="{x:Bind}"
|
HtmlHost="{x:Bind}"
|
||||||
Visibility="{x:Bind GetAiActionsPanelVisibility(ReaderAiActionsToggleButton.IsChecked), Mode=OneWay}" />
|
Visibility="{x:Bind helpers:XamlHelpers.BoolToVisibilityConverter(RendererCommandBar.IsAIActionsEnabled), Mode=OneWay}" />
|
||||||
|
|
||||||
<!-- Attachments -->
|
<!-- Attachments -->
|
||||||
<Grid Grid.Row="4">
|
<Grid Grid.Row="4">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -42,8 +42,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
|
|
||||||
public WebView2 GetWebView() => Chromium;
|
public WebView2 GetWebView() => Chromium;
|
||||||
|
|
||||||
public Visibility GetAiActionsPanelVisibility(bool? isChecked) => isChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
|
||||||
|
|
||||||
public MailRenderingPage()
|
public MailRenderingPage()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
@@ -56,6 +54,7 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
{
|
{
|
||||||
return Chromium.CoreWebView2.PrintToPdfAsync(path, null).AsTask();
|
return Chromium.CoreWebView2.PrintToPdfAsync(path, null).AsTask();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PrintingResult> DirectPrintAsync(WebView2PrintSettingsModel settings)
|
private async Task<PrintingResult> DirectPrintAsync(WebView2PrintSettingsModel settings)
|
||||||
@@ -150,6 +149,8 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
ViewModel.SaveHTMLasPDFFunc = null;
|
ViewModel.SaveHTMLasPDFFunc = null;
|
||||||
ViewModel.DirectPrintFuncAsync = null;
|
ViewModel.DirectPrintFuncAsync = null;
|
||||||
_currentRenderedHtml = string.Empty;
|
_currentRenderedHtml = string.Empty;
|
||||||
|
RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged;
|
||||||
|
RendererCommandBar.IsAIActionsEnabled = false;
|
||||||
ReaderAiActionsPanel.CancelPendingOperation();
|
ReaderAiActionsPanel.CancelPendingOperation();
|
||||||
|
|
||||||
DisposeWebView2();
|
DisposeWebView2();
|
||||||
@@ -168,10 +169,13 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ReaderAiActionsToggleButton_Checked(object sender, RoutedEventArgs e)
|
private async void RendererCommandBar_AIActionsEnabledChanged(object? sender, bool isEnabled)
|
||||||
|
{
|
||||||
|
if (isEnabled)
|
||||||
{
|
{
|
||||||
await ReaderAiActionsPanel.RefreshAvailabilityAsync();
|
await ReaderAiActionsPanel.RefreshAvailabilityAsync();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken)
|
public async Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -206,6 +210,43 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> TryGetCachedSummaryTextAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!ViewModel.CurrentMailAccountId.HasValue || !ViewModel.CurrentMailFileId.HasValue)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _mimeFileService.GetSummaryTextAsync(
|
||||||
|
ViewModel.CurrentMailAccountId.Value,
|
||||||
|
ViewModel.CurrentMailFileId.Value,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveCachedSummaryTextAsync(string summary, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
if (!ViewModel.CurrentMailAccountId.HasValue || !ViewModel.CurrentMailFileId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _mimeFileService.SaveSummaryTextAsync(
|
||||||
|
ViewModel.CurrentMailAccountId.Value,
|
||||||
|
ViewModel.CurrentMailFileId.Value,
|
||||||
|
summary,
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetSuggestedSummaryFileName()
|
||||||
|
{
|
||||||
|
var subject = string.IsNullOrWhiteSpace(ViewModel.Subject) ? "email-summary" : ViewModel.Subject;
|
||||||
|
return $"{SanitizeFileNamePart(subject)}.txt";
|
||||||
|
}
|
||||||
|
|
||||||
private void DisposeWebView2()
|
private void DisposeWebView2()
|
||||||
{
|
{
|
||||||
if (Chromium == null) return;
|
if (Chromium == null) return;
|
||||||
@@ -228,6 +269,9 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
// Initialize WebView2 wiring before base navigation invokes ViewModel rendering.
|
// Initialize WebView2 wiring before base navigation invokes ViewModel rendering.
|
||||||
// Base.OnNavigatedTo triggers VM.OnNavigatedTo, which can send HtmlRenderingRequested.
|
// Base.OnNavigatedTo triggers VM.OnNavigatedTo, which can send HtmlRenderingRequested.
|
||||||
DOMLoadedTask = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
DOMLoadedTask = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
RendererCommandBar.AIActionsEnabledChanged -= RendererCommandBar_AIActionsEnabledChanged;
|
||||||
|
RendererCommandBar.AIActionsEnabledChanged += RendererCommandBar_AIActionsEnabledChanged;
|
||||||
|
RendererCommandBar.IsAIActionsEnabled = false;
|
||||||
Chromium.CoreWebView2Initialized -= CoreWebViewInitialized;
|
Chromium.CoreWebView2Initialized -= CoreWebViewInitialized;
|
||||||
Chromium.CoreWebView2Initialized += CoreWebViewInitialized;
|
Chromium.CoreWebView2Initialized += CoreWebViewInitialized;
|
||||||
_ = Chromium.EnsureCoreWebView2Async();
|
_ = Chromium.EnsureCoreWebView2Async();
|
||||||
@@ -300,14 +344,6 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BarDynamicOverflowChanging(CommandBar sender, DynamicOverflowItemsChangingEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.Action == CommandBarDynamicOverflowAction.AddingToOverflow || sender.SecondaryCommands.Any())
|
|
||||||
sender.OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Visible;
|
|
||||||
else
|
|
||||||
sender.OverflowButtonVisibility = CommandBarOverflowButtonVisibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task UpdateEditorThemeAsync()
|
private async Task UpdateEditorThemeAsync()
|
||||||
{
|
{
|
||||||
await DOMLoadedTask.Task;
|
await DOMLoadedTask.Task;
|
||||||
@@ -404,4 +440,21 @@ public sealed partial class MailRenderingPage : MailRenderingPageAbstract,
|
|||||||
{
|
{
|
||||||
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
|
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SanitizeFileNamePart(string value)
|
||||||
|
{
|
||||||
|
var invalidCharacters = Path.GetInvalidFileNameChars();
|
||||||
|
var sanitizedChars = value.Trim().ToCharArray();
|
||||||
|
|
||||||
|
for (var i = 0; i < sanitizedChars.Length; i++)
|
||||||
|
{
|
||||||
|
if (Array.IndexOf(invalidCharacters, sanitizedChars[i]) >= 0)
|
||||||
|
{
|
||||||
|
sanitizedChars[i] = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sanitized = new string(sanitizedChars).Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? "email-summary" : sanitized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using HtmlAgilityPack;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
using MimeKit.Cryptography;
|
using MimeKit.Cryptography;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@@ -162,6 +163,43 @@ public class MimeFileService : IMimeFileService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetSummaryTextAsync(Guid accountId, Guid fileId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var summaryPath = await GetSummaryTextPathAsync(accountId, fileId).ConfigureAwait(false);
|
||||||
|
if (!File.Exists(summaryPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await File.ReadAllTextAsync(summaryPath, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Could not read summary cache for FileId: {FileId}", fileId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveSummaryTextAsync(Guid accountId, Guid fileId, string summary, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var summaryPath = await GetSummaryTextPathAsync(accountId, fileId).ConfigureAwait(false);
|
||||||
|
await File.WriteAllTextAsync(summaryPath, NormalizeSummaryText(summary), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Could not save summary cache for FileId: {FileId}", fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
public MailRenderModel GetMailRenderModel(MimeMessage message, string mimeLocalPath, MailRenderingOptions options = null)
|
||||||
{
|
{
|
||||||
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
var visitor = CreateHTMLPreviewVisitor(message, mimeLocalPath);
|
||||||
@@ -258,6 +296,12 @@ public class MimeFileService : IMimeFileService
|
|||||||
return Path.Combine(resourcePath, $"translated-{SanitizeFileNamePart(targetLanguage)}.html");
|
return Path.Combine(resourcePath, $"translated-{SanitizeFileNamePart(targetLanguage)}.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> GetSummaryTextPathAsync(Guid accountId, Guid fileId)
|
||||||
|
{
|
||||||
|
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
|
||||||
|
return Path.Combine(resourcePath, "summary.txt");
|
||||||
|
}
|
||||||
|
|
||||||
private static string SanitizeFileNamePart(string value)
|
private static string SanitizeFileNamePart(string value)
|
||||||
{
|
{
|
||||||
var invalidCharacters = Path.GetInvalidFileNameChars();
|
var invalidCharacters = Path.GetInvalidFileNameChars();
|
||||||
@@ -268,4 +312,44 @@ public class MimeFileService : IMimeFileService
|
|||||||
|
|
||||||
return sanitizedChars.Length == 0 ? "default" : new string(sanitizedChars);
|
return sanitizedChars.Length == 0 ? "default" : new string(sanitizedChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NormalizeSummaryText(string summary)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(summary))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!summary.Contains('<'))
|
||||||
|
{
|
||||||
|
return summary.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
var document = new HtmlDocument();
|
||||||
|
document.LoadHtml(summary);
|
||||||
|
|
||||||
|
var lineBreakNodes = document.DocumentNode.SelectNodes("//br|//p|//div|//li");
|
||||||
|
if (lineBreakNodes != null)
|
||||||
|
{
|
||||||
|
foreach (var node in lineBreakNodes)
|
||||||
|
{
|
||||||
|
if (node.Name.Equals("li", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
node.ParentNode?.InsertBefore(document.CreateTextNode(Environment.NewLine + "- "), node);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
node.ParentNode?.InsertBefore(document.CreateTextNode(Environment.NewLine), node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var plainText = HtmlEntity.DeEntitize(document.DocumentNode.InnerText ?? string.Empty);
|
||||||
|
return string.Join(
|
||||||
|
Environment.NewLine,
|
||||||
|
plainText
|
||||||
|
.Split([Environment.NewLine], StringSplitOptions.None)
|
||||||
|
.Select(line => line.Trim())
|
||||||
|
.Where(line => !string.IsNullOrWhiteSpace(line)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user