Initial WinUI switch.

This commit is contained in:
Burak Kaan Köse
2025-09-29 11:16:14 +02:00
parent f9c53ca2c9
commit e67b893ae4
345 changed files with 22458 additions and 746 deletions
@@ -0,0 +1,62 @@
using System.Numerics;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
using Wino.Core.Domain.Interfaces;
using Wino.Core.UWP.Controls;
namespace Wino.Controls;
public partial class AccountNavigationItem : WinoNavigationViewItem
{
public static readonly DependencyProperty IsActiveAccountProperty = DependencyProperty.Register(nameof(IsActiveAccount), typeof(bool), typeof(AccountNavigationItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsActiveAccountChanged)));
public static readonly DependencyProperty BindingDataProperty = DependencyProperty.Register(nameof(BindingData), typeof(IAccountMenuItem), typeof(AccountNavigationItem), new PropertyMetadata(null));
public bool IsActiveAccount
{
get { return (bool)GetValue(IsActiveAccountProperty); }
set { SetValue(IsActiveAccountProperty, value); }
}
public IAccountMenuItem BindingData
{
get { return (IAccountMenuItem)GetValue(BindingDataProperty); }
set { SetValue(BindingDataProperty, value); }
}
private const string PART_NavigationViewItemMenuItemsHost = "NavigationViewItemMenuItemsHost";
private const string PART_SelectionIndicator = "CustomSelectionIndicator";
private ItemsRepeater _itemsRepeater;
private Microsoft.UI.Xaml.Shapes.Rectangle _selectionIndicator;
public AccountNavigationItem()
{
DefaultStyleKey = typeof(AccountNavigationItem);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsRepeater = GetTemplateChild(PART_NavigationViewItemMenuItemsHost) as ItemsRepeater;
_selectionIndicator = GetTemplateChild(PART_SelectionIndicator) as Microsoft.UI.Xaml.Shapes.Rectangle;
UpdateSelectionBorder();
}
private static void OnIsActiveAccountChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is AccountNavigationItem control)
control.UpdateSelectionBorder();
}
private void UpdateSelectionBorder()
{
if (_selectionIndicator == null) return;
_selectionIndicator.Scale = IsActiveAccount ? new Vector3(1, 1, 1) : new Vector3(0, 0, 0);
_selectionIndicator.Visibility = IsActiveAccount ? Visibility.Visible : Visibility.Collapsed;
}
}
@@ -0,0 +1,418 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using Serilog;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
namespace Wino.Controls.Advanced;
/// <summary>
/// Custom ListView control that handles multiple selection with Extended/Multiple selection mode
/// and supports threads.
/// </summary>
public partial class WinoListView : ListView, IDisposable
{
private ILogger logger = Log.ForContext<WinoListView>();
private const string PART_ScrollViewer = "ScrollViewer";
private ScrollViewer internalScrollviewer;
/// <summary>
/// Gets or sets whether this ListView belongs to thread items.
/// This is important for detecting selected items etc.
/// </summary>
public bool IsThreadListView
{
get { return (bool)GetValue(IsThreadListViewProperty); }
set { SetValue(IsThreadListViewProperty, value); }
}
public ICommand ItemDeletedCommand
{
get { return (ICommand)GetValue(ItemDeletedCommandProperty); }
set { SetValue(ItemDeletedCommandProperty, value); }
}
public ICommand LoadMoreCommand
{
get { return (ICommand)GetValue(LoadMoreCommandProperty); }
set { SetValue(LoadMoreCommandProperty, value); }
}
public bool IsThreadScrollingEnabled
{
get { return (bool)GetValue(IsThreadScrollingEnabledProperty); }
set { SetValue(IsThreadScrollingEnabledProperty, value); }
}
public static readonly DependencyProperty IsThreadScrollingEnabledProperty = DependencyProperty.Register(nameof(IsThreadScrollingEnabled), typeof(bool), typeof(WinoListView), new PropertyMetadata(false));
public static readonly DependencyProperty LoadMoreCommandProperty = DependencyProperty.Register(nameof(LoadMoreCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
public static readonly DependencyProperty IsThreadListViewProperty = DependencyProperty.Register(nameof(IsThreadListView), typeof(bool), typeof(WinoListView), new PropertyMetadata(false, new PropertyChangedCallback(OnIsThreadViewChanged)));
public static readonly DependencyProperty ItemDeletedCommandProperty = DependencyProperty.Register(nameof(ItemDeletedCommand), typeof(ICommand), typeof(WinoListView), new PropertyMetadata(null));
public WinoListView()
{
CanDragItems = true;
IsItemClickEnabled = true;
IsMultiSelectCheckBoxEnabled = true;
IsRightTapEnabled = true;
SelectionMode = ListViewSelectionMode.Extended;
ShowsScrollingPlaceholders = false;
SingleSelectionFollowsFocus = true;
DragItemsCompleted += ItemDragCompleted;
DragItemsStarting += ItemDragStarting;
SelectionChanged += SelectedItemsChanged;
ProcessKeyboardAccelerators += ProcessDelKey;
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
internalScrollviewer = GetTemplateChild(PART_ScrollViewer) as ScrollViewer;
if (internalScrollviewer == null)
{
logger.Warning("WinoListView does not have an internal ScrollViewer. Infinite scrolling behavior might be effected.");
return;
}
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
internalScrollviewer.ViewChanged += InternalScrollVeiwerViewChanged;
}
private static void OnIsThreadViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is WinoListView winoListView)
{
winoListView.AdjustThreadViewContainerVisuals();
}
}
private void AdjustThreadViewContainerVisuals()
{
if (IsThreadListView)
{
ItemContainerTransitions.Clear();
}
}
private double lastestRaisedOffset = 0;
private int lastItemSize = 0;
// TODO: This is buggy. Does not work all the time. Debug.
private void InternalScrollVeiwerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (internalScrollviewer == null) return;
// No need to raise init request if there are no items in the list.
if (Items.Count == 0) return;
// If the scrolling is finished, check the current viewport height.
if (e.IsIntermediate)
{
var currentOffset = internalScrollviewer.VerticalOffset;
var maxOffset = internalScrollviewer.ScrollableHeight;
if (currentOffset + 10 >= maxOffset && lastestRaisedOffset != maxOffset && Items.Count != lastItemSize)
{
// We must load more.
lastestRaisedOffset = maxOffset;
lastItemSize = Items.Count;
LoadMoreCommand?.Execute(null);
}
}
}
private void ProcessDelKey(UIElement sender, Microsoft.UI.Xaml.Input.ProcessKeyboardAcceleratorEventArgs args)
{
if (args.Key == Windows.System.VirtualKey.Delete)
{
args.Handled = true;
ItemDeletedCommand?.Execute(MailOperation.SoftDelete);
}
}
private void ItemDragCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
{
if (args.Items.Any(a => a is MailItemViewModel))
{
args.Items.Cast<MailItemViewModel>().ForEach(a => a.IsCustomFocused = false);
}
}
private void ItemDragStarting(object sender, DragItemsStartingEventArgs args)
{
// Dragging multiple mails from different accounts/folders are supported with the condition below:
// All mails belongs to the drag will be matched on the dropped folder's account.
// Meaning that if users drag 1 mail from Account A/Inbox and 1 mail from Account B/Inbox,
// and drop to Account A/Inbox, the mail from Account B/Inbox will NOT be moved.
if (IsThreadListView)
{
var allItems = args.Items.Cast<MailItemViewModel>();
// Highlight all items
allItems.ForEach(a => a.IsCustomFocused = true);
// Set native drag arg properties.
var dragPackage = new MailDragPackage(allItems.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
else
{
var dragPackage = new MailDragPackage(args.Items.Cast<IMailItem>());
args.Data.Properties.Add(nameof(MailDragPackage), dragPackage);
}
}
public void ChangeSelectionMode(ListViewSelectionMode selectionMode)
{
SelectionMode = selectionMode;
if (!IsThreadListView)
{
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
{
threadListView.SelectionMode = selectionMode;
}
});
}
}
/// <summary>
/// Finds the container for given mail item and adds it to selected items.
/// </summary>
/// <param name="mailItemViewModel">Mail to be added to selected items.</param>
/// <returns>Whether selection was successful or not.</returns>
public bool SelectMailItemContainer(MailItemViewModel mailItemViewModel)
{
var itemContainer = ContainerFromItem(mailItemViewModel);
// This item might be in thread container.
if (itemContainer == null)
{
bool found = false;
Items.OfType<ThreadMailItemViewModel>().ForEach(c =>
{
if (!found)
{
var threadListView = GetThreadInternalListView(c);
if (threadListView != null)
found = threadListView.SelectMailItemContainer(mailItemViewModel);
}
});
return found;
}
SelectedItems.Add(mailItemViewModel);
return true;
}
/// <summary>
/// Recursively clears all selections except the given mail.
/// </summary>
/// <param name="exceptViewModel">Exceptional mail item to be not unselected.</param>
/// <param name="preserveThreadExpanding">Whether expansion states of thread containers should stay as it is or not.</param>
public void ClearSelections(MailItemViewModel exceptViewModel = null, bool preserveThreadExpanding = false)
{
SelectedItems.Clear();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
var threadListView = GetThreadInternalListView(c);
if (threadListView == null)
return;
if (exceptViewModel != null)
{
if (!threadListView.SelectedItems.Contains(exceptViewModel))
{
if (!preserveThreadExpanding)
{
c.IsThreadExpanded = false;
}
threadListView.SelectedItems.Clear();
}
}
else
{
if (!preserveThreadExpanding)
{
c.IsThreadExpanded = false;
}
threadListView.SelectedItems.Clear();
}
});
}
/// <summary>
/// Recursively selects all mails, including thread items.
/// </summary>
public void SelectAllWino()
{
SelectAll();
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
c.IsThreadExpanded = true;
var threadListView = GetThreadInternalListView(c);
threadListView?.SelectAll();
});
}
// SelectedItems changed.
private void SelectedItemsChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems != null)
{
foreach (var removedItem in e.RemovedItems)
{
if (removedItem is MailItemViewModel removedMailItemViewModel)
{
// Mail item un-selected.
removedMailItemViewModel.IsSelected = false;
WeakReferenceMessenger.Default.Send(new MailItemSelectionRemovedEvent(removedMailItemViewModel));
}
else if (removedItem is ThreadMailItemViewModel removedThreadItemViewModel)
{
removedThreadItemViewModel.IsThreadExpanded = false;
}
}
}
if (e.AddedItems != null)
{
foreach (var addedItem in e.AddedItems)
{
if (addedItem is MailItemViewModel addedMailItemViewModel)
{
// Mail item selected.
addedMailItemViewModel.IsSelected = true;
WeakReferenceMessenger.Default.Send(new MailItemSelectedEvent(addedMailItemViewModel));
}
else if (addedItem is ThreadMailItemViewModel threadMailItemViewModel)
{
if (IsThreadScrollingEnabled)
{
if (internalScrollviewer != null && ContainerFromItem(threadMailItemViewModel) is FrameworkElement threadFrameworkElement)
{
internalScrollviewer.ScrollToElement(threadFrameworkElement, true, true, bringToTopOrLeft: true);
}
}
// Try to select first item.
if (GetThreadInternalListView(threadMailItemViewModel) is WinoListView internalListView)
{
internalListView.SelectFirstItem();
}
}
}
}
if (!IsThreadListView)
{
if (SelectionMode == ListViewSelectionMode.Extended && SelectedItems.Count == 1)
{
// Only 1 single item is selected in extended mode for main list view.
// We should un-select all thread items.
Items.Where(a => a is ThreadMailItemViewModel).Cast<ThreadMailItemViewModel>().ForEach(c =>
{
// c.IsThreadExpanded = false;
var threadListView = GetThreadInternalListView(c);
threadListView?.SelectedItems.Clear();
});
}
}
}
public async void SelectFirstItem()
{
if (Items.Count > 0)
{
if (Items[0] is MailItemViewModel firstMailItemViewModel)
{
// Make sure the invisible container is realized.
await Task.Delay(250);
if (ContainerFromItem(firstMailItemViewModel) is ListViewItem firstItemContainer)
{
firstItemContainer.IsSelected = true;
}
firstMailItemViewModel.IsSelected = true;
}
}
}
private WinoListView GetThreadInternalListView(ThreadMailItemViewModel threadMailItemViewModel)
{
var itemContainer = ContainerFromItem(threadMailItemViewModel);
if (itemContainer is ListViewItem listItem)
{
var expander = listItem.GetChildByName<WinoExpander>("ThreadExpander");
if (expander != null)
return expander.Content as WinoListView;
}
return null;
}
public void Dispose()
{
DragItemsCompleted -= ItemDragCompleted;
DragItemsStarting -= ItemDragStarting;
SelectionChanged -= SelectedItemsChanged;
ProcessKeyboardAccelerators -= ProcessDelKey;
if (internalScrollviewer != null)
{
internalScrollviewer.ViewChanged -= InternalScrollVeiwerViewChanged;
}
foreach (var item in Items)
{
if (item is ThreadMailItemViewModel threadMailItemViewModel)
{
var threadListView = GetThreadInternalListView(threadMailItemViewModel);
threadListView?.Dispose();
}
}
}
}
@@ -0,0 +1,229 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Fernandezja.ColorHashSharp;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Microsoft.UI.Xaml.Shapes;
using Windows.UI;
using Wino.Mail.WinUI;
namespace Wino.Controls;
public partial class ImagePreviewControl : Control
{
private const string PART_EllipseInitialsGrid = "EllipseInitialsGrid";
private const string PART_InitialsTextBlock = "InitialsTextBlock";
private const string PART_KnownHostImage = "KnownHostImage";
private const string PART_Ellipse = "Ellipse";
private const string PART_FaviconSquircle = "FaviconSquircle";
private const string PART_FaviconImage = "FaviconImage";
#region Dependency Properties
public static readonly DependencyProperty FromNameProperty = DependencyProperty.Register(nameof(FromName), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged));
public static readonly DependencyProperty FromAddressProperty = DependencyProperty.Register(nameof(FromAddress), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, OnInformationChanged));
public static readonly DependencyProperty SenderContactPictureProperty = DependencyProperty.Register(nameof(SenderContactPicture), typeof(string), typeof(ImagePreviewControl), new PropertyMetadata(string.Empty, new PropertyChangedCallback(OnInformationChanged)));
public static readonly DependencyProperty ThumbnailUpdatedEventProperty = DependencyProperty.Register(nameof(ThumbnailUpdatedEvent), typeof(bool), typeof(ImagePreviewControl), new PropertyMetadata(false, new PropertyChangedCallback(OnInformationChanged)));
public bool ThumbnailUpdatedEvent
{
get { return (bool)GetValue(ThumbnailUpdatedEventProperty); }
set { SetValue(ThumbnailUpdatedEventProperty, value); }
}
/// <summary>
/// Gets or sets base64 string of the sender contact picture.
/// </summary>
public string SenderContactPicture
{
get { return (string)GetValue(SenderContactPictureProperty); }
set { SetValue(SenderContactPictureProperty, value); }
}
public string FromName
{
get { return (string)GetValue(FromNameProperty); }
set { SetValue(FromNameProperty, value); }
}
public string FromAddress
{
get { return (string)GetValue(FromAddressProperty); }
set { SetValue(FromAddressProperty, value); }
}
#endregion
private Ellipse Ellipse;
private Grid InitialsGrid;
private TextBlock InitialsTextblock;
private Image KnownHostImage;
private Border FaviconSquircle;
private Image FaviconImage;
private CancellationTokenSource contactPictureLoadingCancellationTokenSource;
public ImagePreviewControl()
{
DefaultStyleKey = nameof(ImagePreviewControl);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
InitialsGrid = GetTemplateChild(PART_EllipseInitialsGrid) as Grid;
InitialsTextblock = GetTemplateChild(PART_InitialsTextBlock) as TextBlock;
KnownHostImage = GetTemplateChild(PART_KnownHostImage) as Image;
Ellipse = GetTemplateChild(PART_Ellipse) as Ellipse;
FaviconSquircle = GetTemplateChild(PART_FaviconSquircle) as Border;
FaviconImage = GetTemplateChild(PART_FaviconImage) as Image;
UpdateInformation();
}
private static void OnInformationChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is ImagePreviewControl control)
control.UpdateInformation();
}
private async void UpdateInformation()
{
if ((KnownHostImage == null && FaviconSquircle == null) || InitialsGrid == null || InitialsTextblock == null || (string.IsNullOrEmpty(FromName) && string.IsNullOrEmpty(FromAddress)))
return;
// Cancel active image loading if exists.
if (!contactPictureLoadingCancellationTokenSource?.IsCancellationRequested ?? false)
{
contactPictureLoadingCancellationTokenSource.Cancel();
}
string contactPicture = SenderContactPicture;
var isAvatarThumbnail = false;
if (string.IsNullOrEmpty(contactPicture) && !string.IsNullOrEmpty(FromAddress))
{
contactPicture = await App.Current.ThumbnailService.GetThumbnailAsync(FromAddress);
isAvatarThumbnail = true;
}
if (!string.IsNullOrEmpty(contactPicture))
{
if (isAvatarThumbnail && FaviconSquircle != null && FaviconImage != null)
{
// Show favicon in squircle
FaviconSquircle.Visibility = Visibility.Visible;
InitialsGrid.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
var bitmapImage = await GetBitmapImageAsync(contactPicture);
if (bitmapImage != null)
{
FaviconImage.Source = bitmapImage;
}
}
else
{
// Show normal avatar (tondo)
FaviconSquircle.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
InitialsGrid.Visibility = Visibility.Visible;
contactPictureLoadingCancellationTokenSource = new CancellationTokenSource();
try
{
var brush = await GetContactImageBrushAsync(contactPicture);
if (brush != null)
{
if (!contactPictureLoadingCancellationTokenSource?.Token.IsCancellationRequested ?? false)
{
Ellipse.Fill = brush;
InitialsTextblock.Text = string.Empty;
}
}
}
catch (Exception)
{
Debugger.Break();
}
}
}
else
{
FaviconSquircle.Visibility = Visibility.Collapsed;
KnownHostImage.Visibility = Visibility.Collapsed;
InitialsGrid.Visibility = Visibility.Visible;
var colorHash = new ColorHash();
var rgb = colorHash.Rgb(FromAddress);
Ellipse.Fill = new SolidColorBrush(Color.FromArgb(rgb.A, rgb.R, rgb.G, rgb.B));
InitialsTextblock.Text = ExtractInitialsFromName(FromName);
}
}
private static async Task<ImageBrush> GetContactImageBrushAsync(string base64)
{
// Load the image from base64 string.
var bitmapImage = await GetBitmapImageAsync(base64);
if (bitmapImage == null) return null;
return new ImageBrush() { ImageSource = bitmapImage };
}
private static async Task<BitmapImage> GetBitmapImageAsync(string base64)
{
try
{
var bitmapImage = new BitmapImage();
var imageArray = Convert.FromBase64String(base64);
var imageStream = new MemoryStream(imageArray);
var randomAccessImageStream = imageStream.AsRandomAccessStream();
randomAccessImageStream.Seek(0);
await bitmapImage.SetSourceAsync(randomAccessImageStream);
return bitmapImage;
}
catch (Exception) { }
return null;
}
public string ExtractInitialsFromName(string name)
{
// Change from name to from address in case of name doesn't exists.
if (string.IsNullOrEmpty(name))
{
name = FromAddress;
}
// first remove all: punctuation, separator chars, control chars, and numbers (unicode style regexes)
string initials = Regex.Replace(name, @"[\p{P}\p{S}\p{C}\p{N}]+", "");
// Replacing all possible whitespace/separator characters (unicode style), with a single, regular ascii space.
initials = Regex.Replace(initials, @"\p{Z}+", " ");
// Remove all Sr, Jr, I, II, III, IV, V, VI, VII, VIII, IX at the end of names
initials = Regex.Replace(initials.Trim(), @"\s+(?:[JS]R|I{1,3}|I[VX]|VI{0,3})$", "", RegexOptions.IgnoreCase);
// Extract up to 2 initials from the remaining cleaned name.
initials = Regex.Replace(initials, @"^(\p{L})[^\s]*(?:\s+(?:\p{L}+\s+(?=\p{L}))?(?:(\p{L})\p{L}*)?)?$", "$1$2").Trim();
if (initials.Length > 2)
{
// Worst case scenario, everything failed, just grab the first two letters of what we have left.
initials = initials.Substring(0, 2);
}
return initials.ToUpperInvariant();
}
}
@@ -0,0 +1,336 @@
<UserControl
x:Class="Wino.Controls.MailItemDisplayInformationControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:controls="using:Wino.Controls"
xmlns:coreControls="using:Wino.Core.UWP.Controls"
xmlns:domain="using:Wino.Core.Domain"
xmlns:enums="using:Wino.Core.Domain.Enums"
xmlns:helpers="using:Wino.Helpers"
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
FocusVisualMargin="8"
FocusVisualPrimaryBrush="{StaticResource SystemControlRevealFocusVisualBrush}"
FocusVisualPrimaryThickness="2"
FocusVisualSecondaryBrush="{StaticResource SystemControlFocusVisualSecondaryBrush}"
FocusVisualSecondaryThickness="1"
PointerEntered="ControlPointerEntered"
PointerExited="ControlPointerExited">
<UserControl.Resources>
<Style
x:Key="HoverActionButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
</UserControl.Resources>
<Grid>
<Grid
x:Name="RootContainer"
Padding="0,1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
x:DefaultBindMode="OneWay">
<Grid x:Name="UnreadContainer">
<Ellipse
Width="8"
Height="8"
Margin="0,12,8,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Canvas.ZIndex="0"
Fill="{ThemeResource SystemAccentColor}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(MailItem.IsRead), Mode=OneWay}" />
</Grid>
<Border
x:Name="RootContainerVisualWrapper"
Margin="0,4"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0.5"
CornerRadius="4" />
<Grid x:Name="MainContentContainer">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<controls:ImagePreviewControl
x:Name="ContactImage"
Width="35"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="14"
FromAddress="{x:Bind MailItem.FromAddress, Mode=OneWay}"
FromName="{x:Bind MailItem.FromName, Mode=OneWay}"
SenderContactPicture="{x:Bind MailItem.SenderContact.Base64ContactPicture}"
ThumbnailUpdatedEvent="{x:Bind IsThumbnailUpdated, Mode=OneWay}"
Visibility="{x:Bind IsAvatarVisible, Mode=OneWay}" />
<Grid
x:Name="ContentGrid"
Grid.Column="1"
Canvas.ZIndex="2">
<!-- Sender + Title -->
<Grid x:Name="ContentStackpanel" VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Sender + IsDraft + Hover Buttons -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- IsDraft Tag -->
<TextBlock
x:Name="DraftTitle"
Margin="0,0,4,0"
x:Load="{x:Bind MailItem.IsDraft, Mode=OneWay}"
Foreground="{StaticResource DeleteBrush}">
<Run Text="[" /><Run Text="{x:Bind domain:Translator.Draft}" /><Run Text="]" /> <Run Text=" " />
</TextBlock>
<!-- Sender -->
<TextBlock
x:Name="SenderTextFromName"
Grid.Column="1"
Text="{x:Bind MailItem.FromName}"
TextTrimming="WordEllipsis"
Visibility="{x:Bind helpers:XamlHelpers.StringToVisibilityConverter(MailItem.FromName)}" />
<!-- Sender -->
<TextBlock
x:Name="SenderTextFromAddress"
Grid.Column="1"
Text="{x:Bind MailItem.FromAddress}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind helpers:XamlHelpers.StringToVisibilityReversedConverter(MailItem.FromName)}" />
<!-- Hover button -->
<StackPanel
x:Name="HoverActionButtons"
Grid.Column="2"
HorizontalAlignment="Right"
Background="Transparent"
Canvas.ZIndex="999"
ChildrenTransitions="{x:Null}"
Orientation="Horizontal"
Spacing="12"
Visibility="Collapsed">
<Button Click="FirstActionClicked" Style="{StaticResource HoverActionButtonStyle}">
<Button.Content>
<coreControls:WinoFontIcon FontSize="16" Icon="{x:Bind helpers:XamlHelpers.GetWinoIconGlyph(LeftHoverAction), Mode=OneWay}" />
</Button.Content>
</Button>
<Button Click="SecondActionClicked" Style="{StaticResource HoverActionButtonStyle}">
<Button.Content>
<coreControls:WinoFontIcon FontSize="16" Icon="{x:Bind helpers:XamlHelpers.GetWinoIconGlyph(CenterHoverAction), Mode=OneWay}" />
</Button.Content>
</Button>
<Button Click="ThirdActionClicked" Style="{StaticResource HoverActionButtonStyle}">
<Button.Content>
<coreControls:WinoFontIcon FontSize="16" Icon="{x:Bind helpers:XamlHelpers.GetWinoIconGlyph(RightHoverAction), Mode=OneWay}" />
</Button.Content>
</Button>
</StackPanel>
</Grid>
<!-- Subject + IsDraft -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<local:AnimatedIcon
xmlns:local="using:Microsoft.UI.Xaml.Controls"
x:Name="ExpandCollapseChevron"
Width="14"
Height="14"
Margin="-4,0,2,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
local:AnimatedIcon.State="NormalOff"
AutomationProperties.AccessibilityView="Raw"
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
RenderTransformOrigin="0.5, 0.5"
Visibility="{x:Bind IsThreadExpanderVisible, Mode=OneWay}">
<animatedvisuals:AnimatedChevronRightDownSmallVisualSource />
<local:AnimatedIcon.FallbackIconSource>
<local:FontIconSource
FontFamily="{StaticResource SymbolThemeFontFamily}"
FontSize="12"
Glyph="&#xE76C;"
IsTextScaleFactorEnabled="False" />
</local:AnimatedIcon.FallbackIconSource>
<local:AnimatedIcon.RenderTransform />
</local:AnimatedIcon>
<!-- Subject is bound in the code behind. -->
<TextBlock
x:Name="TitleText"
Grid.Column="1"
MaxLines="1"
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Column="2"
Margin="4,0,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="11"
Opacity="0.7"
Text="{x:Bind helpers:XamlHelpers.GetMailItemDisplaySummaryForListing(MailItem.IsDraft, MailItem.CreationDate, Prefer24HourTimeFormat)}" />
</Grid>
<!-- Message -->
<Grid
x:Name="PreviewTextContainerRoot"
Grid.Row="2"
VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="PreviewTextContainer">
<TextBlock
x:Name="PreviewTextblock"
x:Load="{x:Bind helpers:XamlHelpers.ShouldDisplayPreview(MailItem.PreviewText), Mode=OneWay}"
MaxLines="1"
Opacity="0.7"
Text="{x:Bind MailItem.PreviewText}"
TextTrimming="CharacterEllipsis" />
</Grid>
<!-- Right Icons Container -->
<StackPanel
x:Name="IconsContainer"
Grid.Column="1"
Margin="4,4,1,4"
Orientation="Horizontal"
Spacing="2">
<ContentPresenter
x:Name="HasAttachmentContent"
x:Load="{x:Bind MailItem.HasAttachments, Mode=OneWay}"
ContentTemplate="{StaticResource AttachmentSymbolControlTemplate}" />
<ContentPresenter
x:Name="IsFlaggedContent"
x:Load="{x:Bind MailItem.IsFlagged, Mode=OneWay}"
ContentTemplate="{StaticResource FlaggedSymbolControlTemplate}" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<VisualStateManager.VisualStateGroups>
<!-- Read States -->
<VisualStateGroup x:Name="ReadStates">
<VisualState x:Name="Unread">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind MailItem.IsRead, Converter={StaticResource ReverseBooleanConverter}, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="TitleText.Foreground" Value="{ThemeResource SystemAccentColor}" />
<Setter Target="TitleText.FontWeight" Value="Semibold" />
<Setter Target="SenderTextFromName.FontWeight" Value="Bold" />
<Setter Target="SenderTextFromAddress.FontWeight" Value="Bold" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Read" />
</VisualStateGroup>
<!-- Sizing States -->
<VisualStateGroup x:Name="SizingStates">
<VisualState x:Name="Compact">
<VisualState.Setters>
<Setter Target="RootContainer.Height" Value="60" />
<Setter Target="ContentGrid.Padding" Value="8,0" />
<Setter Target="PreviewTextContainer.Visibility" Value="Collapsed" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.ObjectEquals(DisplayMode, enums:MailListDisplayMode.Compact), Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<!-- Medium -->
<VisualState x:Name="Medium">
<VisualState.Setters>
<Setter Target="RootContainer.Height" Value="80" />
<Setter Target="ContentGrid.Padding" Value="6,0" />
<Setter Target="PreviewTextContainer.Visibility" Value="Visible" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.ObjectEquals(DisplayMode, enums:MailListDisplayMode.Medium), Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<!-- Spacious -->
<VisualState x:Name="Spacious">
<VisualState.Setters>
<Setter Target="RootContainer.Height" Value="Auto" />
<Setter Target="ContentGrid.Padding" Value="12,12,6,12" />
<Setter Target="PreviewTextContainer.Visibility" Value="Visible" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind helpers:XamlHelpers.ObjectEquals(DisplayMode, enums:MailListDisplayMode.Spacious), Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<!-- Preview Text States -->
<VisualStateGroup x:Name="PreviewTextStates">
<VisualState x:Name="ShowText" />
<VisualState x:Name="HideText">
<VisualState.Setters>
<Setter Target="PreviewTextContainerRoot.Visibility" Value="Collapsed" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ShowPreviewText, Mode=OneWay, Converter={StaticResource ReverseBooleanConverter}}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<!-- Thread Expanding States -->
<VisualStateGroup x:Name="ExpanderStates">
<VisualState x:Name="NotExpanded" />
<VisualState x:Name="ExpandedState">
<VisualState.Setters>
<Setter Target="ExpandCollapseChevron.(controls:AnimatedIcon.State)" Value="NormalOn" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind IsThreadExpanded, Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</UserControl>
@@ -0,0 +1,218 @@
using System.Linq;
using System.Numerics;
using System.Windows.Input;
using CommunityToolkit.WinUI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
using Wino.Extensions;
using Wino.Mail.ViewModels.Data;
namespace Wino.Controls;
public sealed partial class MailItemDisplayInformationControl : UserControl
{
public ImagePreviewControl GetImagePreviewControl() => ContactImage;
public bool IsRunningHoverAction { get; set; }
public static readonly DependencyProperty DisplayModeProperty = DependencyProperty.Register(nameof(DisplayMode), typeof(MailListDisplayMode), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailListDisplayMode.Spacious));
public static readonly DependencyProperty ShowPreviewTextProperty = DependencyProperty.Register(nameof(ShowPreviewText), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
public static readonly DependencyProperty IsCustomFocusedProperty = DependencyProperty.Register(nameof(IsCustomFocused), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsAvatarVisibleProperty = DependencyProperty.Register(nameof(IsAvatarVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
public static readonly DependencyProperty IsSubjectVisibleProperty = DependencyProperty.Register(nameof(IsSubjectVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
public static readonly DependencyProperty ConnectedExpanderProperty = DependencyProperty.Register(nameof(ConnectedExpander), typeof(WinoExpander), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null));
public static readonly DependencyProperty LeftHoverActionProperty = DependencyProperty.Register(nameof(LeftHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None));
public static readonly DependencyProperty CenterHoverActionProperty = DependencyProperty.Register(nameof(CenterHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None));
public static readonly DependencyProperty RightHoverActionProperty = DependencyProperty.Register(nameof(RightHoverAction), typeof(MailOperation), typeof(MailItemDisplayInformationControl), new PropertyMetadata(MailOperation.None));
public static readonly DependencyProperty HoverActionExecutedCommandProperty = DependencyProperty.Register(nameof(HoverActionExecutedCommand), typeof(ICommand), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null));
public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(MailItemDisplayInformationControl), new PropertyMetadata(null, new PropertyChangedCallback(OnMailItemChanged)));
public static readonly DependencyProperty IsHoverActionsEnabledProperty = DependencyProperty.Register(nameof(IsHoverActionsEnabled), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(true));
public static readonly DependencyProperty Prefer24HourTimeFormatProperty = DependencyProperty.Register(nameof(Prefer24HourTimeFormat), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpanderVisibleProperty = DependencyProperty.Register(nameof(IsThreadExpanderVisible), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThreadExpandedProperty = DependencyProperty.Register(nameof(IsThreadExpanded), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public static readonly DependencyProperty IsThumbnailUpdatedProperty = DependencyProperty.Register(nameof(IsThumbnailUpdated), typeof(bool), typeof(MailItemDisplayInformationControl), new PropertyMetadata(false));
public bool IsThumbnailUpdated
{
get { return (bool)GetValue(IsThumbnailUpdatedProperty); }
set { SetValue(IsThumbnailUpdatedProperty, value); }
}
public bool IsThreadExpanded
{
get { return (bool)GetValue(IsThreadExpandedProperty); }
set { SetValue(IsThreadExpandedProperty, value); }
}
public bool IsThreadExpanderVisible
{
get { return (bool)GetValue(IsThreadExpanderVisibleProperty); }
set { SetValue(IsThreadExpanderVisibleProperty, value); }
}
public bool Prefer24HourTimeFormat
{
get { return (bool)GetValue(Prefer24HourTimeFormatProperty); }
set { SetValue(Prefer24HourTimeFormatProperty, value); }
}
public bool IsHoverActionsEnabled
{
get { return (bool)GetValue(IsHoverActionsEnabledProperty); }
set { SetValue(IsHoverActionsEnabledProperty, value); }
}
public IMailItem MailItem
{
get { return (IMailItem)GetValue(MailItemProperty); }
set { SetValue(MailItemProperty, value); }
}
public ICommand HoverActionExecutedCommand
{
get { return (ICommand)GetValue(HoverActionExecutedCommandProperty); }
set { SetValue(HoverActionExecutedCommandProperty, value); }
}
public MailOperation LeftHoverAction
{
get { return (MailOperation)GetValue(LeftHoverActionProperty); }
set { SetValue(LeftHoverActionProperty, value); }
}
public MailOperation CenterHoverAction
{
get { return (MailOperation)GetValue(CenterHoverActionProperty); }
set { SetValue(CenterHoverActionProperty, value); }
}
public MailOperation RightHoverAction
{
get { return (MailOperation)GetValue(RightHoverActionProperty); }
set { SetValue(RightHoverActionProperty, value); }
}
public WinoExpander ConnectedExpander
{
get { return (WinoExpander)GetValue(ConnectedExpanderProperty); }
set { SetValue(ConnectedExpanderProperty, value); }
}
public bool IsSubjectVisible
{
get { return (bool)GetValue(IsSubjectVisibleProperty); }
set { SetValue(IsSubjectVisibleProperty, value); }
}
public bool IsAvatarVisible
{
get { return (bool)GetValue(IsAvatarVisibleProperty); }
set { SetValue(IsAvatarVisibleProperty, value); }
}
public bool IsCustomFocused
{
get { return (bool)GetValue(IsCustomFocusedProperty); }
set { SetValue(IsCustomFocusedProperty, value); }
}
public bool ShowPreviewText
{
get { return (bool)GetValue(ShowPreviewTextProperty); }
set { SetValue(ShowPreviewTextProperty, value); }
}
public MailListDisplayMode DisplayMode
{
get { return (MailListDisplayMode)GetValue(DisplayModeProperty); }
set { SetValue(DisplayModeProperty, value); }
}
public MailItemDisplayInformationControl()
{
this.InitializeComponent();
var compositor = this.Visual().Compositor;
var leftBackgroundVisual = compositor.CreateSpriteVisual();
RootContainerVisualWrapper.SetChildVisual(leftBackgroundVisual);
MainContentContainer.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
RootContainer.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
ContentGrid.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
ContentStackpanel.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
IconsContainer.EnableImplicitAnimation(VisualPropertyType.Offset, 400);
RootContainerVisualWrapper.SizeChanged += (s, e) => leftBackgroundVisual.Size = e.NewSize.ToVector2();
}
private static void OnMailItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is MailItemDisplayInformationControl control)
{
control.UpdateInformation();
}
}
private void UpdateInformation()
{
if (MailItem == null) return;
TitleText.Text = string.IsNullOrWhiteSpace(MailItem.Subject) ? Translator.MailItemNoSubject : MailItem.Subject;
}
private void ControlPointerEntered(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (IsHoverActionsEnabled)
{
HoverActionButtons.Visibility = Visibility.Visible;
UnreadContainer.Visibility = Visibility.Collapsed;
}
}
private void ControlPointerExited(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
{
if (IsHoverActionsEnabled)
{
HoverActionButtons.Visibility = Visibility.Collapsed;
UnreadContainer.Visibility = Visibility.Visible;
}
}
private void ExecuteHoverAction(MailOperation operation)
{
IsRunningHoverAction = true;
MailOperationPreperationRequest package = null;
if (MailItem is MailCopy mailCopy)
package = new MailOperationPreperationRequest(operation, mailCopy, toggleExecution: true);
else if (MailItem is ThreadMailItemViewModel threadMailItemViewModel)
package = new MailOperationPreperationRequest(operation, threadMailItemViewModel.GetMailCopies(), toggleExecution: true);
else if (MailItem is ThreadMailItem threadMailItem)
package = new MailOperationPreperationRequest(operation, threadMailItem.ThreadItems.Cast<MailItemViewModel>().Select(a => a.MailCopy), toggleExecution: true);
if (package == null) return;
HoverActionExecutedCommand?.Execute(package);
}
private void FirstActionClicked(object sender, RoutedEventArgs e)
{
ExecuteHoverAction(LeftHoverAction);
}
private void SecondActionClicked(object sender, RoutedEventArgs e)
{
ExecuteHoverAction(CenterHoverAction);
}
private void ThirdActionClicked(object sender, RoutedEventArgs e)
{
ExecuteHoverAction(RightHoverAction);
}
}
@@ -0,0 +1,72 @@
using System;
using System.Windows.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Wino.Controls;
/// <summary>
/// Templated button for each setting in Settings Dialog.
/// </summary>
public partial class SettingsMenuItemControl : Control
{
public string Title
{
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
}
public FrameworkElement Icon
{
get { return (FrameworkElement)GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return (object)GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public bool IsClickable
{
get { return (bool)GetValue(IsClickableProperty); }
set { SetValue(IsClickableProperty, value); }
}
public bool IsNavigateIconVisible
{
get { return (bool)GetValue(IsNavigateIconVisibleProperty); }
set { SetValue(IsNavigateIconVisibleProperty, value); }
}
public FrameworkElement SideContent
{
get { return (FrameworkElement)GetValue(SideContentProperty); }
set { SetValue(SideContentProperty, value); }
}
public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(nameof(CommandParameter), typeof(object), typeof(SettingsMenuItemControl), new PropertyMetadata(null));
public static readonly DependencyProperty SideContentProperty = DependencyProperty.Register(nameof(SideContent), typeof(FrameworkElement), typeof(SettingsMenuItemControl), new PropertyMetadata(null));
public static readonly DependencyProperty IsClickableProperty = DependencyProperty.Register(nameof(IsClickable), typeof(bool), typeof(SettingsMenuItemControl), new PropertyMetadata(true));
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(nameof(Command), typeof(ICommand), typeof(SettingsMenuItemControl), new PropertyMetadata(null));
public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(FrameworkElement), typeof(SettingsMenuItemControl), new PropertyMetadata(null));
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(nameof(Description), typeof(string), typeof(SettingsMenuItemControl), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(nameof(Title), typeof(string), typeof(SettingsMenuItemControl), new PropertyMetadata(string.Empty));
public static readonly DependencyProperty IsNavigateIconVisibleProperty = DependencyProperty.Register(nameof(IsNavigateIconVisible), typeof(bool), typeof(SettingsMenuItemControl), new PropertyMetadata(true));
}
@@ -0,0 +1,358 @@
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.UI.ViewManagement.Core;
using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Reader;
using Wino.Core.UWP;
using Wino.Core.UWP.Extensions;
using Wino.Mail.WinUI;
namespace Wino.Mail.Controls;
public sealed partial class WebViewEditorControl : Control, IDisposable
{
private readonly INativeAppService _nativeAppService = App.Current.Services.GetService<INativeAppService>();
private readonly IFontService _fontService = App.Current.Services.GetService<IFontService>();
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>();
[GeneratedDependencyProperty]
public partial bool IsEditorDarkMode { get; set; }
async partial void OnIsEditorDarkModeChanged(bool newValue)
{
await UpdateEditorThemeAsync();
}
[GeneratedDependencyProperty]
public partial bool IsEditorBold { get; set; }
private bool _isEditorBoldInternal;
async partial void OnIsEditorBoldChanged(bool newValue)
{
if (newValue != _isEditorBoldInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("bold", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorItalic { get; set; }
private bool _isEditorItalicInternal;
async partial void OnIsEditorItalicChanged(bool newValue)
{
if (newValue != _isEditorItalicInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("italic", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorUnderline { get; set; }
private bool _isEditorUnderlineInternal;
async partial void OnIsEditorUnderlineChanged(bool newValue)
{
if (newValue != _isEditorUnderlineInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("underline", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorStrikethrough { get; set; }
private bool _isEditorStrikethroughInternal;
async partial void OnIsEditorStrikethroughChanged(bool newValue)
{
if (newValue != _isEditorStrikethroughInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("strikethrough", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorOl { get; set; }
private bool _isEditorOlInternal;
async partial void OnIsEditorOlChanged(bool newValue)
{
if (newValue != _isEditorOlInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("insertorderedlist", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorUl { get; set; }
private bool _isEditorUlInternal;
async partial void OnIsEditorUlChanged(bool newValue)
{
if (newValue != _isEditorUlInternal)
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("insertunorderedlist", BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty(DefaultValue = true)]
public partial bool IsEditorIndentEnabled { get; private set; }
[GeneratedDependencyProperty]
public partial bool IsEditorOutdentEnabled { get; private set; }
[GeneratedDependencyProperty]
public partial int EditorAlignmentSelectedIndex { get; set; }
private int _editorAlignmentSelectedIndexInternal;
async partial void OnEditorAlignmentSelectedIndexChanged(int newValue)
{
if (newValue != _editorAlignmentSelectedIndexInternal)
{
var alignmentAction = newValue switch
{
0 => "justifyleft",
1 => "justifycenter",
2 => "justifyright",
3 => "justifyfull",
_ => throw new ArgumentOutOfRangeException(nameof(newValue))
};
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize(alignmentAction, BasicTypesJsonContext.Default.String));
}
}
[GeneratedDependencyProperty]
public partial bool IsEditorWebViewEditor { get; set; }
async partial void OnIsEditorWebViewEditorChanged(bool newValue)
{
await _chromium.ExecuteScriptFunctionSafeAsync("toggleToolbar", JsonSerializer.Serialize(newValue, BasicTypesJsonContext.Default.Boolean));
}
private const string PART_WebView = "WebView";
private WebView2 _chromium;
private bool _disposedValue;
private readonly TaskCompletionSource<bool> _domLoadedTask = new();
public WebViewEditorControl()
{
this.DefaultStyleKey = typeof(WebViewEditorControl);
IsEditorDarkMode = WinoApplication.Current.UnderlyingThemeService.IsUnderlyingThemeDark();
}
protected override async void OnApplyTemplate()
{
base.OnApplyTemplate();
_chromium = GetTemplateChild(PART_WebView) as WebView2;
await InitializeComponent();
}
private async Task InitializeComponent()
{
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00FFFFFF");
Environment.SetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS", "--enable-features=OverlayScrollbar,msOverlayScrollbarWinStyle,msOverlayScrollbarWinStyleAnimation");
_chromium.CoreWebView2Initialized += ChromiumInitialized;
await _chromium.EnsureCoreWebView2Async();
}
public async void EditorIndentAsync()
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("indent", BasicTypesJsonContext.Default.String));
}
public async void EditorOutdentAsync()
{
await _chromium.ExecuteScriptFunctionSafeAsync("editor.execCommand", JsonSerializer.Serialize("outdent", BasicTypesJsonContext.Default.String));
}
public void ToggleEditorTheme()
{
IsEditorDarkMode = !IsEditorDarkMode;
}
public async Task<string> GetHtmlBodyAsync()
{
var editorContent = await _chromium.ExecuteScriptFunctionSafeAsync("GetHTMLContent");
return JsonSerializer.Deserialize(editorContent, BasicTypesJsonContext.Default.String);
}
public async void ShowImagePicker()
{
await _chromium.ExecuteScriptFunctionSafeAsync("imageInput.click");
}
public async Task InsertImagesAsync(List<ImageInfo> images)
{
await _chromium.ExecuteScriptFunctionSafeAsync("insertImages", JsonSerializer.Serialize(images, DomainModelsJsonContext.Default.ListImageInfo));
}
public async void ShowEmojiPicker()
{
CoreInputView.GetForCurrentView().TryShow(CoreInputViewKind.Emoji);
await FocusEditorAsync(focusControlAsWell: true);
}
public WebView2 GetUnderlyingWebView() => _chromium;
public async Task RenderHtmlAsync(string htmlBody)
{
await _domLoadedTask.Task;
await UpdateEditorThemeAsync();
await InitializeEditorAsync();
await _chromium.ExecuteScriptFunctionAsync("RenderHTML", parameters: JsonSerializer.Serialize(string.IsNullOrEmpty(htmlBody) ? " " : htmlBody, BasicTypesJsonContext.Default.String));
}
private async Task<string> InitializeEditorAsync()
{
var fonts = _fontService.GetFonts();
var composerFont = _preferencesService.ComposerFont;
int composerFontSize = _preferencesService.ComposerFontSize;
var readerFont = _preferencesService.ReaderFont;
int readerFontSize = _preferencesService.ReaderFontSize;
return await _chromium.ExecuteScriptFunctionAsync("initializeJodit", false,
JsonSerializer.Serialize(fonts, BasicTypesJsonContext.Default.ListString),
JsonSerializer.Serialize(composerFont, BasicTypesJsonContext.Default.String),
JsonSerializer.Serialize(composerFontSize, BasicTypesJsonContext.Default.Int32),
JsonSerializer.Serialize(readerFont, BasicTypesJsonContext.Default.String),
JsonSerializer.Serialize(readerFontSize, BasicTypesJsonContext.Default.Int32));
}
private async void ChromiumInitialized(WebView2 sender, CoreWebView2InitializedEventArgs args)
{
var editorBundlePath = (await _nativeAppService.GetEditorBundlePathAsync()).Replace("editor.html", string.Empty);
_chromium.CoreWebView2.SetVirtualHostNameToFolderMapping("app.editor", editorBundlePath, CoreWebView2HostResourceAccessKind.Allow);
_chromium.Source = new Uri("https://app.editor/editor.html");
_chromium.CoreWebView2.DOMContentLoaded += DomLoaded;
_chromium.CoreWebView2.WebMessageReceived += ScriptMessageReceived;
}
public async Task UpdateEditorThemeAsync()
{
await _domLoadedTask.Task;
if (IsEditorDarkMode)
{
_chromium.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark;
await _chromium.ExecuteScriptFunctionSafeAsync("SetDarkEditor");
}
else
{
_chromium.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Light;
await _chromium.ExecuteScriptFunctionSafeAsync("SetLightEditor");
}
}
/// <summary>
/// Places the cursor in the composer.
/// </summary>
/// <param name="focusControlAsWell">Whether control itself should be focused as well or not.</param>
public async Task FocusEditorAsync(bool focusControlAsWell)
{
await _chromium.ExecuteScriptSafeAsync("editor.selection.setCursorIn(editor.editor.firstChild, true)");
if (focusControlAsWell)
{
_chromium.Focus(FocusState.Keyboard);
_chromium.Focus(FocusState.Programmatic);
}
}
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
var change = JsonSerializer.Deserialize(args.WebMessageAsJson, DomainModelsJsonContext.Default.WebViewMessage);
if (change.Type == "bold")
{
_isEditorBoldInternal = change.Value == "true";
IsEditorBold = _isEditorBoldInternal;
}
else if (change.Type == "italic")
{
_isEditorItalicInternal = change.Value == "true";
IsEditorItalic = _isEditorItalicInternal;
}
else if (change.Type == "underline")
{
_isEditorUnderlineInternal = change.Value == "true";
IsEditorUnderline = _isEditorUnderlineInternal;
}
else if (change.Type == "strikethrough")
{
_isEditorStrikethroughInternal = change.Value == "true";
IsEditorStrikethrough = _isEditorStrikethroughInternal;
}
else if (change.Type == "ol")
{
_isEditorOlInternal = change.Value == "true";
IsEditorOl = _isEditorOlInternal;
}
else if (change.Type == "ul")
{
_isEditorUlInternal = change.Value == "true";
IsEditorUl = _isEditorUlInternal;
}
else if (change.Type == "indent")
{
IsEditorIndentEnabled = change.Value != "disabled";
}
else if (change.Type == "outdent")
{
IsEditorOutdentEnabled = change.Value != "disabled";
}
else if (change.Type == "alignment")
{
var parsedValue = change.Value switch
{
"jodit-icon_left" => 0,
"jodit-icon_center" => 1,
"jodit-icon_right" => 2,
"jodit-icon_justify" => 3,
_ => 0
};
_editorAlignmentSelectedIndexInternal = parsedValue;
EditorAlignmentSelectedIndex = _editorAlignmentSelectedIndexInternal;
}
}
private void DomLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args) => _domLoadedTask.TrySetResult(true);
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing && _chromium != null)
{
_chromium.CoreWebView2Initialized -= ChromiumInitialized;
if (_chromium.CoreWebView2 != null)
{
_chromium.CoreWebView2.DOMContentLoaded -= DomLoaded;
_chromium.CoreWebView2.WebMessageReceived -= ScriptMessageReceived;
}
_chromium.Close();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
+128
View File
@@ -0,0 +1,128 @@
using CommunityToolkit.Diagnostics;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Markup;
namespace Wino.Controls;
[ContentProperty(Name = nameof(Content))]
public partial class WinoExpander : Control
{
private const string PART_HeaderGrid = "HeaderGrid";
private const string PART_ContentAreaWrapper = "ContentAreaWrapper";
private const string PART_ContentArea = "ContentArea";
private ContentControl HeaderGrid;
private ContentControl ContentArea;
private Grid ContentAreaWrapper;
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(nameof(Header), typeof(UIElement), typeof(WinoExpander), new PropertyMetadata(null));
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(UIElement), typeof(WinoExpander), new PropertyMetadata(null));
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(nameof(IsExpanded), typeof(bool), typeof(WinoExpander), new PropertyMetadata(false, new PropertyChangedCallback(OnIsExpandedChanged)));
public static readonly DependencyProperty TemplateSettingsProperty = DependencyProperty.Register(nameof(TemplateSettings), typeof(WinoExpanderTemplateSettings), typeof(WinoExpander), new PropertyMetadata(new WinoExpanderTemplateSettings()));
public UIElement Content
{
get { return (UIElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public WinoExpanderTemplateSettings TemplateSettings
{
get { return (WinoExpanderTemplateSettings)GetValue(TemplateSettingsProperty); }
set { SetValue(TemplateSettingsProperty, value); }
}
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
public UIElement Header
{
get { return (UIElement)GetValue(HeaderProperty); }
set { SetValue(HeaderProperty, value); }
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
HeaderGrid = GetTemplateChild(PART_HeaderGrid) as ContentControl;
ContentAreaWrapper = GetTemplateChild(PART_ContentAreaWrapper) as Grid;
ContentArea = GetTemplateChild(PART_ContentArea) as ContentControl;
Guard.IsNotNull(HeaderGrid, nameof(HeaderGrid));
Guard.IsNotNull(ContentAreaWrapper, nameof(ContentAreaWrapper));
Guard.IsNotNull(ContentArea, nameof(ContentArea));
var clipComposition = ElementCompositionPreview.GetElementVisual(ContentAreaWrapper);
clipComposition.Clip = clipComposition.Compositor.CreateInsetClip();
ContentAreaWrapper.SizeChanged += ContentSizeChanged;
HeaderGrid.Tapped += HeaderTapped;
}
private void ContentSizeChanged(object sender, SizeChangedEventArgs e)
{
TemplateSettings.ContentHeight = e.NewSize.Height;
TemplateSettings.NegativeContentHeight = -1 * (double)e.NewSize.Height;
}
private void HeaderTapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
// Tapped is delegated from executing hover action like flag or delete.
// No need to toggle the expander.
if (Header is MailItemDisplayInformationControl itemDisplayInformationControl &&
itemDisplayInformationControl.IsRunningHoverAction)
{
itemDisplayInformationControl.IsRunningHoverAction = false;
return;
}
IsExpanded = !IsExpanded;
}
private static void OnIsExpandedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is WinoExpander control)
control.UpdateVisualStates();
}
private void UpdateVisualStates()
{
VisualStateManager.GoToState(this, IsExpanded ? "Expanded" : "Collapsed", true);
}
}
#region Settings
public class WinoExpanderTemplateSettings : DependencyObject
{
public static readonly DependencyProperty HeaderHeightProperty = DependencyProperty.Register(nameof(HeaderHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0));
public static readonly DependencyProperty ContentHeightProperty = DependencyProperty.Register(nameof(ContentHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0));
public static readonly DependencyProperty NegativeContentHeightProperty = DependencyProperty.Register(nameof(NegativeContentHeight), typeof(double), typeof(WinoExpanderTemplateSettings), new PropertyMetadata(0.0));
public double NegativeContentHeight
{
get { return (double)GetValue(NegativeContentHeightProperty); }
set { SetValue(NegativeContentHeightProperty, value); }
}
public double HeaderHeight
{
get { return (double)GetValue(HeaderHeightProperty); }
set { SetValue(HeaderHeightProperty, value); }
}
public double ContentHeight
{
get { return (double)GetValue(ContentHeightProperty); }
set { SetValue(ContentHeightProperty, value); }
}
}
#endregion
@@ -0,0 +1,81 @@
using System.Linq;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.MailItem;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
namespace Wino.Controls;
public partial class WinoSwipeControlItems : SwipeItems
{
public static readonly DependencyProperty SwipeOperationProperty = DependencyProperty.Register(nameof(SwipeOperation), typeof(MailOperation), typeof(WinoSwipeControlItems), new PropertyMetadata(default(MailOperation), new PropertyChangedCallback(OnItemsChanged)));
public static readonly DependencyProperty MailItemProperty = DependencyProperty.Register(nameof(MailItem), typeof(IMailItem), typeof(WinoSwipeControlItems), new PropertyMetadata(null));
public IMailItem MailItem
{
get { return (IMailItem)GetValue(MailItemProperty); }
set { SetValue(MailItemProperty, value); }
}
public MailOperation SwipeOperation
{
get { return (MailOperation)GetValue(SwipeOperationProperty); }
set { SetValue(SwipeOperationProperty, value); }
}
private static void OnItemsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (obj is WinoSwipeControlItems control)
{
control.BuildSwipeItems();
}
}
private void BuildSwipeItems()
{
this.Clear();
var swipeItem = GetSwipeItem(SwipeOperation);
this.Add(swipeItem);
}
private SwipeItem GetSwipeItem(MailOperation operation)
{
if (MailItem == null) return null;
var finalOperation = operation;
bool isSingleItem = MailItem is MailItemViewModel;
if (isSingleItem)
{
var singleItem = MailItem as MailItemViewModel;
if (operation == MailOperation.MarkAsRead && singleItem.IsRead)
finalOperation = MailOperation.MarkAsUnread;
else if (operation == MailOperation.MarkAsUnread && !singleItem.IsRead)
finalOperation = MailOperation.MarkAsRead;
}
else
{
var threadItem = MailItem as ThreadMailItemViewModel;
if (operation == MailOperation.MarkAsRead && threadItem.ThreadItems.All(a => a.IsRead))
finalOperation = MailOperation.MarkAsUnread;
else if (operation == MailOperation.MarkAsUnread && threadItem.ThreadItems.All(a => !a.IsRead))
finalOperation = MailOperation.MarkAsRead;
}
var item = new SwipeItem()
{
IconSource = new WinoFontIconSource() { Icon = XamlHelpers.GetWinoIconGlyph(finalOperation) },
Text = XamlHelpers.GetOperationString(finalOperation),
};
return item;
}
}