Initial WinUI switch.
This commit is contained in:
@@ -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=""
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user