New AI actions panel. Replaced new command bar.
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:models="using:Wino.Core.Domain.Models.Ai"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:models="using:Wino.Core.Domain.Models.Ai"
|
||||
x:Name="root"
|
||||
Loaded="OnLoaded"
|
||||
Unloaded="OnUnloaded"
|
||||
@@ -20,11 +20,17 @@
|
||||
CornerRadius="10">
|
||||
<Grid>
|
||||
<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 Orientation="Horizontal" Spacing="10">
|
||||
<ProgressRing Width="20" Height="20" IsActive="True" />
|
||||
<ProgressRing
|
||||
Width="20"
|
||||
Height="20"
|
||||
IsActive="True" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
@@ -32,8 +38,14 @@
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="SignedOutPanel" Spacing="14" Visibility="Collapsed">
|
||||
<Border Height="120" Padding="18" CornerRadius="12">
|
||||
<StackPanel
|
||||
x:Name="SignedOutPanel"
|
||||
Spacing="14"
|
||||
Visibility="Collapsed">
|
||||
<Border
|
||||
Height="120"
|
||||
Padding="18"
|
||||
CornerRadius="12">
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
|
||||
<GradientStop Offset="0" Color="#1A6EE7B7" />
|
||||
@@ -47,7 +59,12 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</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
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
@@ -87,22 +104,32 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="PurchasePanel" Spacing="14" Visibility="Collapsed">
|
||||
<Border Padding="16" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="12">
|
||||
<StackPanel
|
||||
x:Name="PurchasePanel"
|
||||
Spacing="14"
|
||||
Visibility="Collapsed">
|
||||
<Border
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}"
|
||||
CornerRadius="12">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.AiActions_NoPackTitle, Mode=OneWay}" />
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.AiActions_NoPackTitle, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.AiActions_NoPackDescription, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<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}" />
|
||||
</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}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -110,121 +137,178 @@
|
||||
</Border>
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Click="PurchaseButton_Click"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackGetButton, Mode=OneWay}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="ReadyPanel" Spacing="12" Visibility="Collapsed">
|
||||
<Border Padding="12" Background="{ThemeResource CardBackgroundFillColorTertiaryBrush}" CornerRadius="10">
|
||||
<Grid ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel
|
||||
x:Name="ReadyPanel"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<!-- Row 1: Action tabs + usage -->
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Width="36"
|
||||
Height="36"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
CornerRadius="18"
|
||||
Opacity="0.18" />
|
||||
<FontIcon
|
||||
HorizontalAlignment="Center"
|
||||
<controls:Segmented
|
||||
x:Name="ActionSelector"
|
||||
Height="30"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="ActionSelector_SelectionChanged"
|
||||
Style="{StaticResource ButtonSegmentedStyle}">
|
||||
<controls:SegmentedItem
|
||||
x:Name="TranslateSegment"
|
||||
Padding="12,6"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureTranslate, Mode=OneWay}">
|
||||
<controls:SegmentedItem.Icon>
|
||||
<SymbolIcon Symbol="Switch" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
<controls:SegmentedItem
|
||||
x:Name="RewriteSegment"
|
||||
Padding="12,6"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite, Mode=OneWay}">
|
||||
<controls:SegmentedItem.Icon>
|
||||
<SymbolIcon Symbol="Edit" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
<controls:SegmentedItem
|
||||
x:Name="SummarizeSegment"
|
||||
Padding="12,6"
|
||||
Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}">
|
||||
<controls:SegmentedItem.Icon>
|
||||
<SymbolIcon Symbol="Bullets" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
</controls:Segmented>
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<ProgressBar
|
||||
x:Name="UsageProgressBar"
|
||||
Width="100"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource SymbolThemeFontFamily}"
|
||||
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
|
||||
Glyph="" />
|
||||
Maximum="1000"
|
||||
Value="0" />
|
||||
<TextBlock
|
||||
x:Name="UsageSummaryTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="3">
|
||||
<!-- 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}"
|
||||
Text="{x:Bind domain:Translator.Composer_AiTranslateLanguage, Mode=OneWay}" />
|
||||
<ComboBox
|
||||
x:Name="TranslateLanguageComboBox"
|
||||
MinWidth="120"
|
||||
SelectionChanged="TranslateLanguageComboBox_SelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AiTranslateLanguageOption">
|
||||
<TextBlock Text="{x:Bind Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button
|
||||
x:Name="RunTranslateButton"
|
||||
Click="RunTranslateButton_Click"
|
||||
Content="{x:Bind domain:Translator.Composer_AiTranslateApply, Mode=OneWay}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Rewrite options -->
|
||||
<StackPanel
|
||||
x:Name="RewriteOptionsPanel"
|
||||
Spacing="8"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.Composer_AiActions, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
x:Name="UsageSummaryTextBlock"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
x:Name="UsageResetTextBlock"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
Text="{x:Bind domain:Translator.Composer_AiRewriteMode, Mode=OneWay}" />
|
||||
<ComboBox
|
||||
x:Name="RewriteModeComboBox"
|
||||
MinWidth="140"
|
||||
SelectionChanged="RewriteModeComboBox_SelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AiRewriteModeOption">
|
||||
<TextBlock Text="{x:Bind Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button
|
||||
x:Name="RunRewriteButton"
|
||||
Click="RunRewriteButton_Click"
|
||||
Content="{x:Bind domain:Translator.Composer_AiRewriteApply, Mode=OneWay}"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<TextBlock
|
||||
x:Name="RewriteDescriptionTextBlock"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBox
|
||||
x:Name="CustomRewriteTextBox"
|
||||
PlaceholderText="{x:Bind domain:Translator.Composer_AiRewriteCustomPlaceholder, Mode=OneWay}"
|
||||
Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
|
||||
<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>
|
||||
<SymbolIcon Symbol="Switch" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
<controls:SegmentedItem x:Name="RewriteSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureRewrite, Mode=OneWay}">
|
||||
<controls:SegmentedItem.Icon>
|
||||
<SymbolIcon Symbol="Edit" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
<controls:SegmentedItem x:Name="SummarizeSegment" Content="{x:Bind domain:Translator.WinoAccount_Management_AiPackFeatureSummarize, Mode=OneWay}">
|
||||
<controls:SegmentedItem.Icon>
|
||||
<SymbolIcon Symbol="Bullets" />
|
||||
</controls:SegmentedItem.Icon>
|
||||
</controls:SegmentedItem>
|
||||
</controls:Segmented>
|
||||
<!-- 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>
|
||||
|
||||
<StackPanel x:Name="TranslateOptionsPanel" Spacing="8" Visibility="Collapsed">
|
||||
<TextBlock
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.Composer_AiTranslateLanguage, Mode=OneWay}" />
|
||||
<ComboBox
|
||||
x:Name="TranslateLanguageComboBox"
|
||||
SelectedItem="{x:Bind SelectedTranslateLanguageOption, Mode=TwoWay}"
|
||||
SelectionChanged="TranslateLanguageComboBox_SelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AiTranslateLanguageOption">
|
||||
<TextBlock Text="{x:Bind Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button
|
||||
x:Name="RunTranslateButton"
|
||||
Click="RunTranslateButton_Click"
|
||||
Content="{x:Bind domain:Translator.Composer_AiTranslateApply, Mode=OneWay}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel x:Name="RewriteOptionsPanel" Spacing="8" Visibility="Collapsed">
|
||||
<TextBlock
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.Composer_AiRewriteMode, Mode=OneWay}" />
|
||||
<ComboBox
|
||||
x:Name="RewriteModeComboBox"
|
||||
SelectedItem="{x:Bind SelectedRewriteModeOption, Mode=TwoWay}"
|
||||
SelectionChanged="RewriteModeComboBox_SelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:AiRewriteModeOption">
|
||||
<TextBlock Text="{x:Bind Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock
|
||||
x:Name="RewriteDescriptionTextBlock"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBox
|
||||
x:Name="CustomRewriteTextBox"
|
||||
PlaceholderText="{x:Bind domain:Translator.Composer_AiRewriteCustomPlaceholder, Mode=OneWay}"
|
||||
Visibility="Collapsed" />
|
||||
<Button
|
||||
x:Name="RunRewriteButton"
|
||||
Click="RunRewriteButton_Click"
|
||||
Content="{x:Bind domain:Translator.Composer_AiRewriteApply, Mode=OneWay}"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -18,6 +20,7 @@ namespace Wino.Mail.WinUI.Controls;
|
||||
|
||||
public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
{
|
||||
public event EventHandler? CloseRequested;
|
||||
private readonly IWinoAccountProfileService _profileService = App.Current.Services.GetRequiredService<IWinoAccountProfileService>();
|
||||
private readonly IStoreManagementService _storeManagementService = App.Current.Services.GetRequiredService<IStoreManagementService>();
|
||||
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 _isBusy;
|
||||
private AiActionType _lastConfigurableAction = AiActionType.Translate;
|
||||
private bool _hasCachedSummary;
|
||||
private CancellationTokenSource? _actionCancellationTokenSource;
|
||||
private IReadOnlyList<AiTranslateLanguageOption> _translateOptions = Array.Empty<AiTranslateLanguageOption>();
|
||||
private IReadOnlyList<AiRewriteModeOption> _rewriteOptions = Array.Empty<AiRewriteModeOption>();
|
||||
@@ -66,6 +70,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
{
|
||||
UpdateActionAvailability();
|
||||
ApplySelectedAction(SelectDefaultAction());
|
||||
_ = RefreshCachedSummaryStateAsync();
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
@@ -83,25 +88,44 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
|
||||
private void LoadOptions()
|
||||
{
|
||||
// Save current selections before replacing ItemsSource (which clears SelectedItem).
|
||||
var previousTranslateCode = SelectedTranslateLanguageOption?.Code;
|
||||
var previousRewriteMode = SelectedRewriteModeOption?.Mode;
|
||||
|
||||
_translateOptions = _optionsService.GetTranslateLanguageOptions();
|
||||
_rewriteOptions = _optionsService.GetRewriteModeOptions();
|
||||
|
||||
TranslateLanguageComboBox.ItemsSource = _translateOptions;
|
||||
RewriteModeComboBox.ItemsSource = _rewriteOptions;
|
||||
|
||||
SelectedTranslateLanguageOption ??= _translateOptions.Count > 0 ? _translateOptions[0] : null;
|
||||
SelectedRewriteModeOption ??= _rewriteOptions.Count > 0 ? _rewriteOptions[0] : null;
|
||||
// Restore selection by matching on value, falling back to first item.
|
||||
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;
|
||||
RewriteModeComboBox.SelectedItem = SelectedRewriteModeOption;
|
||||
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()
|
||||
{
|
||||
TranslateSegment.Visibility = HasAction(AiActionType.Translate) ? Visibility.Visible : Visibility.Collapsed;
|
||||
RewriteSegment.Visibility = HasAction(AiActionType.Rewrite) ? 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;
|
||||
@@ -143,6 +167,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
|
||||
TranslateOptionsPanel.Visibility = action == AiActionType.Translate ? 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()
|
||||
@@ -160,7 +185,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
var account = await _profileService.GetAuthenticatedAccountAsync().ConfigureAwait(true);
|
||||
if (account == null)
|
||||
{
|
||||
UpdateUsageSummary(string.Empty, string.Empty);
|
||||
UpdateUsageSummary(string.Empty);
|
||||
UpdatePanelState(showSignedOut: true);
|
||||
return;
|
||||
}
|
||||
@@ -168,7 +193,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
var hasAiPack = await _storeManagementService.HasProductAsync(WinoAddOnProductType.AI_PACK).ConfigureAwait(true);
|
||||
if (!hasAiPack)
|
||||
{
|
||||
UpdateUsageSummary(string.Empty, string.Empty);
|
||||
UpdateUsageSummary(string.Empty);
|
||||
UpdatePanelState(showPurchase: true);
|
||||
return;
|
||||
}
|
||||
@@ -176,13 +201,17 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
var aiStatusResponse = await _profileService.GetAiStatusAsync().ConfigureAwait(true);
|
||||
if (aiStatusResponse.IsSuccess && aiStatusResponse.Result != null)
|
||||
{
|
||||
UpdateUsageSummary(CreateUsageSummary(aiStatusResponse.Result), CreateUsageResetText(aiStatusResponse.Result));
|
||||
UpdateUsageSummary(
|
||||
CreateUsageSummary(aiStatusResponse.Result),
|
||||
GetUsedCount(aiStatusResponse.Result),
|
||||
GetUsageLimit(aiStatusResponse.Result));
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
|
||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
||||
}
|
||||
|
||||
await RefreshCachedSummaryStateAsync().ConfigureAwait(true);
|
||||
ApplySelectedAction(SelectDefaultAction());
|
||||
UpdatePanelState(showReady: true);
|
||||
}
|
||||
@@ -191,7 +220,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed, string.Empty);
|
||||
UpdateUsageSummary(Translator.WinoAccount_Management_AiPackUsageLoadFailed);
|
||||
UpdatePanelState(showReady: true);
|
||||
}
|
||||
finally
|
||||
@@ -211,13 +240,29 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
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
|
||||
? string.Format(Translator.WinoAccount_Management_AiPackResets, resetDateUtc.LocalDateTime)
|
||||
: string.Empty;
|
||||
if (quotaInfo.Used is int used && quotaInfo.MonthlyLimit is int limit && limit > 0)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private void UpdateUsageSummary(string usageText, string resetText)
|
||||
private void UpdateUsageSummary(string usageText, int usedCount = 0)
|
||||
{
|
||||
UsageSummaryTextBlock.Text = usageText;
|
||||
UsageResetTextBlock.Text = resetText;
|
||||
UsageResetTextBlock.Visibility = string.IsNullOrWhiteSpace(resetText) ? Visibility.Collapsed : Visibility.Visible;
|
||||
UsageProgressBar.Maximum = 1000;
|
||||
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)
|
||||
@@ -244,6 +297,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
CustomRewriteTextBox.IsEnabled = !isBusy;
|
||||
RunTranslateButton.IsEnabled = !isBusy;
|
||||
RunRewriteButton.IsEnabled = !isBusy;
|
||||
RunSummarizeButton.IsEnabled = !isBusy;
|
||||
SignedOutPanel.IsHitTestVisible = !isBusy;
|
||||
PurchasePanel.IsHitTestVisible = !isBusy;
|
||||
|
||||
@@ -363,7 +417,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
if (ReferenceEquals(ActionSelector.SelectedItem, SummarizeSegment))
|
||||
{
|
||||
ApplySelectedAction(AiActionType.Summarize);
|
||||
_ = ExecuteAiActionAsync(AiActionType.Summarize);
|
||||
_ = RefreshCachedSummaryStateAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +456,17 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
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)
|
||||
{
|
||||
if (_isBusy)
|
||||
@@ -423,15 +488,6 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
|
||||
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)
|
||||
{
|
||||
_dialogService.InfoBarMessage(Translator.Composer_AiErrorTitle, Translator.WinoAccount_Error_ValidationFailed, InfoBarMessageType.Error);
|
||||
@@ -444,6 +500,41 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
@@ -479,14 +601,7 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
_actionCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
if (action == AiActionType.Summarize)
|
||||
{
|
||||
var fallbackAction = _lastConfigurableAction != AiActionType.None && HasAction(_lastConfigurableAction)
|
||||
? _lastConfigurableAction
|
||||
: SelectDefaultAction();
|
||||
|
||||
ApplySelectedAction(fallbackAction);
|
||||
}
|
||||
// Summarize no longer auto-switches back; the user explicitly selected the tab.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,6 +620,94 @@ public sealed partial class AiActionsPanel : UserControl, IDisposable
|
||||
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()
|
||||
{
|
||||
if (_actionCancellationTokenSource == null)
|
||||
|
||||
@@ -9,4 +9,7 @@ public interface IAiHtmlActionHost
|
||||
Task ApplyHtmlResultAsync(string html, CancellationToken cancellationToken);
|
||||
Task<string?> TryGetCachedTranslationHtmlAsync(string languageCode, 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;
|
||||
}
|
||||
Reference in New Issue
Block a user