From 1da34080d1ab91571a02c78c47cb81f53349390b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 7 Mar 2026 23:33:25 +0100 Subject: [PATCH] Refactoring the html editor toolbar. --- AGENTS.md | 10 + .../CalendarEventComposePageViewModel.cs | 27 +- .../Data/CalendarComposeAttendeeViewModel.cs | 4 +- .../Entities/Shared/AccountContact.cs | 5 +- .../Entities/Shared/IContactDisplayItem.cs | 8 + .../Translations/en_US/resources.json | 1 - Wino.Mail.WinUI/Controls/EditorCommanding.cs | 94 ++++- .../EditorTabbedCommandBarControl.xaml | 357 +++++++++++++----- .../EditorTabbedCommandBarControl.xaml.cs | 147 +++++--- .../Controls/WebViewEditorControl.cs | 5 +- Wino.Mail.WinUI/JS/editor.js | 99 ++++- Wino.Mail.WinUI/MailAppShell.xaml.cs | 34 +- .../MailAuthenticatorConfiguration.cs | 1 + Wino.Mail.WinUI/ShellWindow.xaml | 11 + Wino.Mail.WinUI/ShellWindow.xaml.cs | 28 ++ Wino.Mail.WinUI/Styles/DataTemplates.xaml | 49 +++ .../Calendar/CalendarEventComposePage.xaml | 46 ++- .../Calendar/CalendarEventComposePage.xaml.cs | 39 ++ Wino.Mail.WinUI/Views/Mail/ComposePage.xaml | 69 +--- 19 files changed, 754 insertions(+), 280 deletions(-) create mode 100644 Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs diff --git a/AGENTS.md b/AGENTS.md index f42d0134..0d33bd8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,15 @@ dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config **Platforms:** x86, x64, ARM64 +## Efficient Workflow + +- Start with targeted symbol or file search before reading full files +- Prefer one focused task per thread; use a new thread for unrelated follow-up work +- Keep verification narrow: build only the affected project, not the full solution, unless cross-project changes require it +- After the first restore, prefer `--no-restore` builds unless package or project references changed +- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context +- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository + ## Architecture ### Solution Structure @@ -130,6 +139,7 @@ private string searchQuery = string.Empty; - String interpolation over string.Format - Wrap async operations in try-catch - Log errors via IWinoLogger +- For dependency properties in WinUI code, always prefer `[GeneratedDependencyProperty]` from CommunityToolkit over manual `DependencyProperty.Register(...)` declarations. - In ViewModels, update all UI-bound properties/collections via `ExecuteUIThread(...)` (especially after awaited calls and any use of `ConfigureAwait(false)`). - In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`. diff --git a/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs index 63da2502..fd3a8458 100644 --- a/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarEventComposePageViewModel.cs @@ -242,14 +242,21 @@ public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel { foreach (var file in pickedFiles) { - if (Attachments.Any(existing => existing.FilePath.Equals(file.FullFilePath, StringComparison.OrdinalIgnoreCase))) - continue; - - Attachments.Add(new CalendarComposeAttachmentViewModel(file.FileName, file.FullFilePath, file.FileExtension, file.Size)); + TryAddAttachment(file.FileName, file.FullFilePath, file.FileExtension, file.Size); } }); } + public bool TryAddAttachment(string filePath, long size) + { + if (string.IsNullOrWhiteSpace(filePath)) + return false; + + var fileName = Path.GetFileName(filePath); + var fileExtension = Path.GetExtension(filePath); + return TryAddAttachment(fileName, filePath, fileExtension, size); + } + [RelayCommand] private void RemoveAttachment(CalendarComposeAttachmentViewModel attachment) { @@ -640,6 +647,18 @@ public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel OnPropertyChanged(nameof(HasAttachments)); } + private bool TryAddAttachment(string fileName, string filePath, string fileExtension, long size) + { + if (string.IsNullOrWhiteSpace(filePath) || + Attachments.Any(existing => existing.FilePath.Equals(filePath, StringComparison.OrdinalIgnoreCase))) + { + return false; + } + + Attachments.Add(new CalendarComposeAttachmentViewModel(fileName, filePath, fileExtension, size)); + return true; + } + } public partial class CalendarComposeFrequencyOption : ObservableObject diff --git a/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs index a0cec4f4..f6231563 100644 --- a/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs +++ b/Wino.Calendar.ViewModels/Data/CalendarComposeAttendeeViewModel.cs @@ -2,11 +2,13 @@ using Wino.Core.Domain.Entities.Shared; namespace Wino.Calendar.ViewModels.Data; -public class CalendarComposeAttendeeViewModel +public class CalendarComposeAttendeeViewModel : IContactDisplayItem { public string DisplayName { get; } public string Email { get; } public AccountContact ResolvedContact { get; } + public string Address => Email; + public AccountContact PreviewContact => ResolvedContact; public bool HasDistinctDisplayName => !string.IsNullOrWhiteSpace(DisplayName) && !DisplayName.Equals(Email, System.StringComparison.OrdinalIgnoreCase); public CalendarComposeAttendeeViewModel(string displayName, string email, AccountContact resolvedContact = null) diff --git a/Wino.Core.Domain/Entities/Shared/AccountContact.cs b/Wino.Core.Domain/Entities/Shared/AccountContact.cs index d6ecb591..8ca24de4 100644 --- a/Wino.Core.Domain/Entities/Shared/AccountContact.cs +++ b/Wino.Core.Domain/Entities/Shared/AccountContact.cs @@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Entities.Shared; // TODO: This can easily evolve to Contact store, just like People app in Windows 10/11. // Do it. -public class AccountContact : IEquatable +public class AccountContact : IEquatable, IContactDisplayItem { /// /// E-mail address of the contact. @@ -43,6 +43,9 @@ public class AccountContact : IEquatable /// public bool IsOverridden { get; set; } = false; + public string DisplayName => string.IsNullOrWhiteSpace(Name) ? Address : Name; + AccountContact IContactDisplayItem.PreviewContact => this; + public override bool Equals(object obj) { return Equals(obj as AccountContact); diff --git a/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs new file mode 100644 index 00000000..4938f8d4 --- /dev/null +++ b/Wino.Core.Domain/Entities/Shared/IContactDisplayItem.cs @@ -0,0 +1,8 @@ +namespace Wino.Core.Domain.Entities.Shared; + +public interface IContactDisplayItem +{ + string DisplayName { get; } + string Address { get; } + AccountContact PreviewContact { get; } +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 76c6732d..450d873a 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -149,7 +149,6 @@ "CalendarEventCompose_PickCalendarTitle": "Pick a calendar", "CalendarEventCompose_Recurring": "Recurring", "CalendarEventCompose_RecurringSummary": "Occurs every {0} {1}{2} {3} effective {4}{5}", - "CalendarEventCompose_RecurringSummarySmart": "Occurs {0}{1} {2} starting {3}{4}", "CalendarEventCompose_RepeatEvery": "Repeat every", "CalendarEventCompose_SelectCalendar": "Select calendar", "CalendarEventCompose_SingleOccurrenceSummary": "Occurs on {0} {1}", diff --git a/Wino.Mail.WinUI/Controls/EditorCommanding.cs b/Wino.Mail.WinUI/Controls/EditorCommanding.cs index 063c512c..0be5479d 100644 --- a/Wino.Mail.WinUI/Controls/EditorCommanding.cs +++ b/Wino.Mail.WinUI/Controls/EditorCommanding.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -97,16 +98,33 @@ public sealed record class EditorTableCommandArgs( public sealed record class EditorColorOption(string Name, string Value) { - public SolidColorBrush Brush => new(ParseColor(Value)); + public SolidColorBrush Brush => new(ParseColorValue(Value)); - private static Color ParseColor(string? value) + public static Color ParseColorValue(string? value) { if (string.IsNullOrWhiteSpace(value)) { return Colors.Transparent; } - var hex = value.Trim().TrimStart('#'); + var normalizedValue = value.Trim(); + + if (string.Equals(normalizedValue, "transparent", StringComparison.OrdinalIgnoreCase)) + { + return Colors.Transparent; + } + + if (TryParseRgbColor(normalizedValue, out var rgbColor)) + { + return rgbColor; + } + + if (TryParseNamedColor(normalizedValue, out var namedColor)) + { + return namedColor; + } + + var hex = normalizedValue.TrimStart('#'); if (hex.Length == 6) { hex = $"FF{hex}"; @@ -123,6 +141,76 @@ public sealed record class EditorColorOption(string Name, string Value) (byte)((argb >> 8) & 0xFF), (byte)(argb & 0xFF)); } + + private static bool TryParseRgbColor(string value, out Color color) + { + color = Colors.Transparent; + + var isRgba = value.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase); + var isRgb = value.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase); + if (!isRgb && !isRgba) + { + return false; + } + + var startIndex = value.IndexOf('('); + var endIndex = value.LastIndexOf(')'); + if (startIndex < 0 || endIndex <= startIndex) + { + return false; + } + + var segments = value[(startIndex + 1)..endIndex] + .Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if ((isRgb && segments.Length != 3) || (isRgba && segments.Length != 4)) + { + return false; + } + + if (!byte.TryParse(segments[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var red) || + !byte.TryParse(segments[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var green) || + !byte.TryParse(segments[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var blue)) + { + return false; + } + + byte alpha = 255; + if (isRgba) + { + if (!double.TryParse(segments[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var alphaValue)) + { + return false; + } + + alpha = alphaValue <= 1d + ? (byte)Math.Clamp(Math.Round(alphaValue * 255d), 0d, 255d) + : (byte)Math.Clamp(Math.Round(alphaValue), 0d, 255d); + } + + color = Color.FromArgb(alpha, red, green, blue); + return true; + } + + private static bool TryParseNamedColor(string value, out Color color) + { + color = value.ToLowerInvariant() switch + { + "black" => Colors.Black, + "white" => Colors.White, + "gray" or "grey" => Colors.Gray, + "red" => Colors.Red, + "orange" => Colors.Orange, + "yellow" => Colors.Yellow, + "green" => Colors.Green, + "blue" => Colors.Blue, + "purple" => Colors.Purple, + "pink" => Colors.Pink, + _ => Colors.Transparent + }; + + return !color.Equals(Colors.Transparent) || string.Equals(value, "transparent", StringComparison.OrdinalIgnoreCase); + } } public sealed record class EditorParagraphStyleOption(string Name, string Tag); diff --git a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml index ca4f91a7..cee76dd9 100644 --- a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml +++ b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml @@ -11,29 +11,47 @@ mc:Ignorable="d"> - + + + @@ -41,6 +59,12 @@ 0 + + @@ -49,25 +73,41 @@ - + - + - + - + @@ -75,139 +115,254 @@ - + - + - + - + - - - - + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + + + + + - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + - - - - + + + + + + + + + + - + - + - + @@ -224,7 +379,11 @@ - + @@ -236,13 +395,21 @@ - + - + diff --git a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs index ccc57de4..befdcb61 100644 --- a/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/EditorTabbedCommandBarControl.xaml.cs @@ -2,63 +2,42 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Windows.UI; namespace Wino.Mail.Controls; public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditorCommandControl { - public static readonly DependencyProperty CommandTargetProperty = DependencyProperty.Register( - nameof(CommandTarget), - typeof(IEditorCommandTarget), - typeof(EditorTabbedCommandBarControl), - new PropertyMetadata(null, OnCommandTargetChanged)); + [GeneratedDependencyProperty] + public partial IEditorCommandTarget? CommandTarget { get; set; } - public static readonly DependencyProperty PaneCustomContentProperty = DependencyProperty.Register( - nameof(PaneCustomContent), - typeof(object), - typeof(EditorTabbedCommandBarControl), - new PropertyMetadata(null)); + [GeneratedDependencyProperty] + public partial object? PaneCustomContent { get; set; } - public static readonly DependencyProperty InsertCustomContentProperty = DependencyProperty.Register( - nameof(InsertCustomContent), - typeof(object), - typeof(EditorTabbedCommandBarControl), - new PropertyMetadata(null)); + [GeneratedDependencyProperty] + public partial object? InsertCustomContent { get; set; } - public static readonly DependencyProperty OptionsCustomContentProperty = DependencyProperty.Register( - nameof(OptionsCustomContent), - typeof(object), - typeof(EditorTabbedCommandBarControl), - new PropertyMetadata(null)); + [GeneratedDependencyProperty] + public partial object? OptionsCustomContent { get; set; } + + [GeneratedDependencyProperty] + public partial EditorColorOption? SelectedTextColorOption { get; set; } + + [GeneratedDependencyProperty] + public partial EditorColorOption? SelectedHighlightColorOption { get; set; } private bool _isApplyingState; private IEditorCommandTarget? _subscribedTarget; + private static readonly SolidColorBrush TransparentBrush = new(EditorColorOption.ParseColorValue(null)); + private IReadOnlyList _textColorOptions = Array.Empty(); + private IReadOnlyList _highlightColorOptions = Array.Empty(); - public IEditorCommandTarget? CommandTarget - { - get => (IEditorCommandTarget?)GetValue(CommandTargetProperty); - set => SetValue(CommandTargetProperty, value); - } - - public object? PaneCustomContent - { - get => GetValue(PaneCustomContentProperty); - set => SetValue(PaneCustomContentProperty, value); - } - - public object? InsertCustomContent - { - get => GetValue(InsertCustomContentProperty); - set => SetValue(InsertCustomContentProperty, value); - } - - public object? OptionsCustomContent - { - get => GetValue(OptionsCustomContentProperty); - set => SetValue(OptionsCustomContentProperty, value); - } + public Brush SelectedTextColorBrush => SelectedTextColorOption?.Brush ?? TransparentBrush; + public Brush SelectedHighlightColorBrush => SelectedHighlightColorOption?.Brush ?? TransparentBrush; public EditorTabbedCommandBarControl() { @@ -104,12 +83,15 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor _subscribedTarget = null; } - private static void OnCommandTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + partial void OnCommandTargetChanged(IEditorCommandTarget? newValue) { - var control = (EditorTabbedCommandBarControl)d; - control.AttachCommandTarget((IEditorCommandTarget?)e.NewValue); + AttachCommandTarget(newValue); } + partial void OnSelectedTextColorOptionChanged(EditorColorOption? newValue) => Bindings.Update(); + + partial void OnSelectedHighlightColorOptionChanged(EditorColorOption? newValue) => Bindings.Update(); + private void OnLoaded(object sender, RoutedEventArgs e) { AttachCommandTarget(CommandTarget); @@ -136,8 +118,10 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor FontSizeComboBox.ItemsSource = capabilities.FontSizes; AlignmentComboBox.ItemsSource = capabilities.Alignments; ParagraphStyleComboBox.ItemsSource = capabilities.ParagraphStyles; - TextColorComboBox.ItemsSource = capabilities.TextColors; - HighlightColorComboBox.ItemsSource = capabilities.HighlightColors; + _textColorOptions = capabilities.TextColors; + _highlightColorOptions = capabilities.HighlightColors; + TextColorComboBox.ItemsSource = _textColorOptions; + HighlightColorComboBox.ItemsSource = _highlightColorOptions; LineHeightComboBox.ItemsSource = capabilities.LineHeights; } @@ -163,8 +147,10 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor FontSizeComboBox.SelectedItem = MatchValueItem(FontSizeComboBox.ItemsSource, state.FontSize); LineHeightComboBox.SelectedItem = MatchStringItem(LineHeightComboBox.ItemsSource, state.LineHeight); ParagraphStyleComboBox.SelectedItem = MatchParagraphItem(state.ParagraphStyle); - TextColorComboBox.SelectedItem = MatchColorItem(TextColorComboBox.ItemsSource, state.TextColor); - HighlightColorComboBox.SelectedItem = MatchColorItem(HighlightColorComboBox.ItemsSource, state.HighlightColor); + SelectedTextColorOption = ResolveColorOption(_textColorOptions, state.TextColor); + SelectedHighlightColorOption = ResolveColorOption(_highlightColorOptions, state.HighlightColor); + TextColorComboBox.SelectedItem = SelectedTextColorOption; + HighlightColorComboBox.SelectedItem = SelectedHighlightColorOption; _isApplyingState = false; } @@ -207,14 +193,54 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor return styles.FirstOrDefault(item => string.Equals(item.Tag, tag, StringComparison.OrdinalIgnoreCase)); } - private static object? MatchColorItem(object? itemsSource, string? value) + private static EditorColorOption? MatchColorItem(IEnumerable colors, string? value) { - if (itemsSource is not IEnumerable colors) + var normalizedValue = value ?? string.Empty; + var matchedByValue = colors.FirstOrDefault(item => string.Equals(item.Value, normalizedValue, StringComparison.OrdinalIgnoreCase)); + if (matchedByValue != null) { - return null; + return matchedByValue; } - return colors.FirstOrDefault(item => string.Equals(item.Value, value ?? string.Empty, StringComparison.OrdinalIgnoreCase)); + var targetColor = EditorColorOption.ParseColorValue(value); + return colors.FirstOrDefault(item => item.Brush.Color.Equals(targetColor)); + } + + private static EditorColorOption? ResolveColorOption(IEnumerable colors, string? value) + { + var colorOptions = colors.ToList(); + var matchedColor = MatchColorItem(colors, value); + if (matchedColor != null) + { + return matchedColor; + } + + if (string.IsNullOrWhiteSpace(value)) + { + return colorOptions.FirstOrDefault(item => string.IsNullOrWhiteSpace(item.Value)); + } + + var targetColor = EditorColorOption.ParseColorValue(value); + var selectableColors = colorOptions + .Where(item => !string.IsNullOrWhiteSpace(item.Value)) + .ToList(); + + if (selectableColors.Count == 0) + { + return colorOptions.FirstOrDefault(); + } + + return selectableColors + .OrderBy(item => GetColorDistance(item.Brush.Color, targetColor)) + .First(); + } + + private static int GetColorDistance(Color left, Color right) + { + var redDiff = left.R - right.R; + var greenDiff = left.G - right.G; + var blueDiff = left.B - right.B; + return (redDiff * redDiff) + (greenDiff * greenDiff) + (blueDiff * blueDiff); } private async Task ExecuteAsync(EditorCommand command) @@ -281,22 +307,26 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor private async void TextColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (_isApplyingState || TextColorComboBox.SelectedItem is not EditorColorOption color) + SelectedTextColorOption = TextColorComboBox.SelectedItem as EditorColorOption; + + if (_isApplyingState || SelectedTextColorOption == null) { return; } - await ExecuteAsync(EditorCommand.SetTextColor(color.Value)); + await ExecuteAsync(EditorCommand.SetTextColor(SelectedTextColorOption.Value)); } private async void HighlightColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { - if (_isApplyingState || HighlightColorComboBox.SelectedItem is not EditorColorOption color) + SelectedHighlightColorOption = HighlightColorComboBox.SelectedItem as EditorColorOption; + + if (_isApplyingState || SelectedHighlightColorOption == null) { return; } - await ExecuteAsync(EditorCommand.SetHighlightColor(color.Value)); + await ExecuteAsync(EditorCommand.SetHighlightColor(SelectedHighlightColorOption.Value)); } private async void LineHeightComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) @@ -412,6 +442,7 @@ public sealed partial class EditorTabbedCommandBarControl : UserControl, IEditor await ExecuteAsync(EditorCommand.InsertTable(new EditorTableCommandArgs((int)Math.Max(1, rowsBox.Value), (int)Math.Max(1, columnsBox.Value)))); } } + } diff --git a/Wino.Mail.WinUI/Controls/WebViewEditorControl.cs b/Wino.Mail.WinUI/Controls/WebViewEditorControl.cs index ab7d0bcc..84206270 100644 --- a/Wino.Mail.WinUI/Controls/WebViewEditorControl.cs +++ b/Wino.Mail.WinUI/Controls/WebViewEditorControl.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -392,7 +393,9 @@ public sealed partial class WebViewEditorControl : Control, IDisposable, IEditor JsonSerializer.Serialize(_preferencesService.ComposerFont, BasicTypesJsonContext.Default.String), JsonSerializer.Serialize(_preferencesService.ComposerFontSize, BasicTypesJsonContext.Default.Int32), JsonSerializer.Serialize(_preferencesService.ReaderFont, BasicTypesJsonContext.Default.String), - JsonSerializer.Serialize(_preferencesService.ReaderFontSize, BasicTypesJsonContext.Default.Int32)); + JsonSerializer.Serialize(_preferencesService.ReaderFontSize, BasicTypesJsonContext.Default.Int32), + JsonSerializer.Serialize(DefaultTextColors.Select(option => option.Value).Where(value => !string.IsNullOrWhiteSpace(value)).ToList(), BasicTypesJsonContext.Default.ListString), + JsonSerializer.Serialize(DefaultHighlightColors.Select(option => option.Value).Where(value => !string.IsNullOrWhiteSpace(value)).ToList(), BasicTypesJsonContext.Default.ListString)); UpdateCapabilities(BuildCapabilities(fonts)); _editorReadyTask.TrySetResult(true); diff --git a/Wino.Mail.WinUI/JS/editor.js b/Wino.Mail.WinUI/JS/editor.js index 76d62ca2..b8c4a031 100644 --- a/Wino.Mail.WinUI/JS/editor.js +++ b/Wino.Mail.WinUI/JS/editor.js @@ -27,13 +27,18 @@ let stateSyncQueued = false; let imageInputBound = false; let inlineFontsPluginRegistered = false; let lastKnownRange = null; +let availableTextColors = []; +let availableHighlightColors = []; -function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, defaultReaderFont, defaultReaderFontSize) { +function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, defaultReaderFont, defaultReaderFontSize, textColors, highlightColors) { if (editor) { scheduleStateSync(); return true; } + availableTextColors = Array.isArray(textColors) ? textColors.map(normalizeColor).filter(Boolean) : []; + availableHighlightColors = Array.isArray(highlightColors) ? highlightColors.map(normalizeColor).filter(Boolean) : []; + const fontsWithFallbackObject = fonts.reduce((acc, font) => { acc[`'${font}',Arial,sans-serif`] = font; return acc; @@ -445,8 +450,8 @@ function buildEditorState() { fontFamily: normalizeFontFamily(style.fontFamily), fontSize: fontSize, paragraphStyle: normalizeParagraphTag(blockElement), - textColor: normalizeColor(style.color), - highlightColor: normalizeColor(style.backgroundColor), + textColor: snapColorToPalette(resolveEditorColorValue(selectionElement, 'color', style.color), availableTextColors), + highlightColor: snapColorToPalette(resolveEditorColorValue(selectionElement, 'backgroundColor', style.backgroundColor), availableHighlightColors), lineHeight: normalizeLineHeight(blockStyle.lineHeight, fontSize), linkUrl: linkElement ? linkElement.getAttribute('href') || '' : '', selectedText: selection && isSelectionInsideEditor() ? selection.toString() : '' @@ -634,6 +639,94 @@ function normalizeColor(value) { return `#${toHex(red)}${toHex(green)}${toHex(blue)}`; } +function snapColorToPalette(value, palette) { + const normalizedColor = normalizeColor(value); + if (!normalizedColor) { + return ''; + } + + if (!Array.isArray(palette) || palette.length === 0) { + return normalizedColor; + } + + if (palette.includes(normalizedColor)) { + return normalizedColor; + } + + const targetRgb = hexToRgb(normalizedColor); + if (!targetRgb) { + return normalizedColor; + } + + let nearestColor = palette[0]; + let nearestDistance = Number.MAX_SAFE_INTEGER; + + palette.forEach(candidate => { + const candidateRgb = hexToRgb(candidate); + if (!candidateRgb) { + return; + } + + const distance = getColorDistance(targetRgb, candidateRgb); + if (distance < nearestDistance) { + nearestColor = candidate; + nearestDistance = distance; + } + }); + + return nearestColor; +} + +function hexToRgb(value) { + const normalized = normalizeColor(value); + if (!normalized || !normalized.startsWith('#') || normalized.length !== 7) { + return null; + } + + return { + red: parseInt(normalized.slice(1, 3), 16), + green: parseInt(normalized.slice(3, 5), 16), + blue: parseInt(normalized.slice(5, 7), 16) + }; +} + +function getColorDistance(left, right) { + const redDiff = left.red - right.red; + const greenDiff = left.green - right.green; + const blueDiff = left.blue - right.blue; + return (redDiff * redDiff) + (greenDiff * greenDiff) + (blueDiff * blueDiff); +} + +function resolveEditorColorValue(selectionElement, propertyName, computedValue) { + if (!editor || !editor.editor) { + return ''; + } + + const darkReaderAttributeName = propertyName === 'backgroundColor' + ? 'data-darkreader-inline-bgcolor' + : 'data-darkreader-inline-color'; + + let currentElement = selectionElement; + while (currentElement) { + if (currentElement.style && currentElement.style[propertyName]) { + return currentElement.style[propertyName]; + } + + const darkReaderValue = currentElement.getAttribute && currentElement.getAttribute(darkReaderAttributeName); + if (darkReaderValue) { + return darkReaderValue; + } + + if (currentElement === editor.editor) { + break; + } + + currentElement = currentElement.parentElement; + } + + return ''; +} + function toHex(value) { return Number(value).toString(16).padStart(2, '0'); } diff --git a/Wino.Mail.WinUI/MailAppShell.xaml.cs b/Wino.Mail.WinUI/MailAppShell.xaml.cs index 0c826a5a..13fe2ce7 100644 --- a/Wino.Mail.WinUI/MailAppShell.xaml.cs +++ b/Wino.Mail.WinUI/MailAppShell.xaml.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; @@ -18,7 +17,6 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Navigation; -using Wino.Extensions; using Wino.Mail.ViewModels.Data; using Wino.Mail.WinUI; using Wino.Mail.WinUI.Controls; @@ -35,8 +33,7 @@ namespace Wino.Views; public sealed partial class MailAppShell : MailAppShellAbstract, IRecipient, IRecipient, - IRecipient, - IRecipient + IRecipient { public Frame GetShellFrame() => InnerShellFrame; @@ -306,33 +303,6 @@ public sealed partial class MailAppShell : MailAppShellAbstract, } } - /// - /// InfoBar message is requested. - /// - public async void Receive(InfoBarMessageRequested message) - { - await DispatcherQueue.EnqueueAsync(async () => - { - if (string.IsNullOrEmpty(message.ActionButtonTitle) || message.Action == null) - { - ShellInfoBar.ActionButton = null; - } - else - { - ShellInfoBar.ActionButton = new Button() - { - Content = message.ActionButtonTitle, - Command = new RelayCommand(message.Action) - }; - } - - ShellInfoBar.Message = message.Message; - ShellInfoBar.Title = message.Title; - ShellInfoBar.Severity = message.Severity.AsMUXCInfoBarSeverity(); - ShellInfoBar.IsOpen = true; - }); - } - private void NavigationViewDisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args) { if (args.DisplayMode == NavigationViewDisplayMode.Minimal) @@ -349,7 +319,6 @@ public sealed partial class MailAppShell : MailAppShellAbstract, { base.RegisterRecipients(); - WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); @@ -359,7 +328,6 @@ public sealed partial class MailAppShell : MailAppShellAbstract, { base.UnregisterRecipients(); - WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); diff --git a/Wino.Mail.WinUI/Services/MailAuthenticatorConfiguration.cs b/Wino.Mail.WinUI/Services/MailAuthenticatorConfiguration.cs index a1a6c186..4d82b854 100644 --- a/Wino.Mail.WinUI/Services/MailAuthenticatorConfiguration.cs +++ b/Wino.Mail.WinUI/Services/MailAuthenticatorConfiguration.cs @@ -33,6 +33,7 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig "https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/calendar.events", "https://www.googleapis.com/auth/calendar.settings.readonly", + "https://www.googleapis.com/auth/drive.file", ]; public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore"; diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index b8d94b46..3dc761f2 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -4,6 +4,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:coreControls="using:Wino.Mail.WinUI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:domain="using:Wino.Core.Domain" xmlns:local="using:Wino.Mail.WinUI" @@ -115,6 +116,16 @@ CacheSize="2" Navigated="MainFrameNavigated" /> + + , + IRecipient, IRecipient, IRecipient, IRecipient @@ -186,6 +188,30 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, UpdateTitleBarColors(message.IsUnderlyingThemeDark); } + public void Receive(InfoBarMessageRequested message) + { + DispatcherQueue.TryEnqueue(() => + { + if (string.IsNullOrEmpty(message.ActionButtonTitle) || message.Action == null) + { + ShellInfoBar.ActionButton = null; + } + else + { + ShellInfoBar.ActionButton = new Button() + { + Content = message.ActionButtonTitle, + Command = new RelayCommand(message.Action) + }; + } + + ShellInfoBar.Message = message.Message; + ShellInfoBar.Title = message.Title; + ShellInfoBar.Severity = message.Severity.AsMUXCInfoBarSeverity(); + ShellInfoBar.IsOpen = true; + }); + } + public void Receive(SynchronizationActionsAdded message) { DispatcherQueue.TryEnqueue(() => @@ -316,6 +342,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, { WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); } @@ -324,6 +351,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow, { WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); + WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); WeakReferenceMessenger.Default.Unregister(this); } diff --git a/Wino.Mail.WinUI/Styles/DataTemplates.xaml b/Wino.Mail.WinUI/Styles/DataTemplates.xaml index c3d19828..c39736c2 100644 --- a/Wino.Mail.WinUI/Styles/DataTemplates.xaml +++ b/Wino.Mail.WinUI/Styles/DataTemplates.xaml @@ -4,6 +4,7 @@ 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.Mail.WinUI.Controls" xmlns:coreSelectors="using:Wino.Mail.WinUI.Selectors" xmlns:coreViewModelData="using:Wino.Core.ViewModels.Data" @@ -13,6 +14,7 @@ xmlns:menu="using:Wino.Core.Domain.MenuItems" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:personalization="using:Wino.Mail.WinUI.Models.Personalization" + xmlns:shared="using:Wino.Core.Domain.Entities.Shared" xmlns:viewModelData="using:Wino.Mail.ViewModels.Data" xmlns:winuiControls="using:CommunityToolkit.WinUI.Controls"> @@ -219,6 +221,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml index 3bde1b94..75977043 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml @@ -29,16 +29,6 @@ - - - - - - - - - - @@ -60,9 +50,7 @@ - + + TokenItemTemplate="{StaticResource ContactTokenTemplate}" /> - - - - - + --> + @@ -536,12 +529,15 @@ Click="RemoveAttachmentClicked" Style="{StaticResource TransparentActionButtonStyle}" Tag="{x:Bind}"> - + - - + + @@ -560,7 +556,7 @@ diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml.cs index d4d3ab0e..79d1ed28 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarEventComposePage.xaml.cs @@ -9,7 +9,10 @@ using EmailValidation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; +using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; +using Windows.Storage; +using Wino.Core.Domain; using Wino.Messaging.Client.Shell; using Wino.Calendar.ViewModels.Data; using Wino.Mail.WinUI.Views.Abstract; @@ -145,6 +148,42 @@ public sealed partial class CalendarEventComposePage : CalendarEventComposePageA } } + private void AttachmentsPane_DragOver(object sender, DragEventArgs e) + { + e.AcceptedOperation = e.DataView.Contains(StandardDataFormats.StorageItems) + ? DataPackageOperation.Copy + : DataPackageOperation.None; + + if (e.AcceptedOperation == DataPackageOperation.Copy) + { + e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message; + e.DragUIOverride.IsCaptionVisible = true; + e.DragUIOverride.IsGlyphVisible = true; + e.DragUIOverride.IsContentVisible = true; + } + } + + private void AttachmentsPane_DragLeave(object sender, DragEventArgs e) + { + } + + private async void AttachmentsPane_Drop(object sender, DragEventArgs e) + { + if (!e.DataView.Contains(StandardDataFormats.StorageItems)) + { + return; + } + + var storageItems = await e.DataView.GetStorageItemsAsync(); + var files = storageItems.OfType(); + + foreach (var file in files) + { + var basicProperties = await file.GetBasicPropertiesAsync(); + await ViewModel.ExecuteUIThread(() => ViewModel.TryAddAttachment(file.Path, (long)basicProperties.Size)); + } + } + public void Receive(ApplicationThemeChanged message) { ViewModel.IsDarkWebviewRenderer = message.IsUnderlyingThemeDark; diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml index 00cef9a5..8a7c1a30 100644 --- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml +++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml @@ -25,47 +25,6 @@ mc:Ignorable="d"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -186,13 +145,7 @@ Visibility="{x:Bind ViewModel.IsDraftBusy, Mode=OneWay}"> - - - - - - - + + + + + + + LabelPosition="Collapsed"> @@ -388,11 +347,11 @@ ItemsSource="{x:Bind ViewModel.ToItems, Mode=OneTime}" LostFocus="AddressBoxLostFocus" PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}" - SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}" + SuggestedItemTemplate="{StaticResource ContactSuggestionTemplate}" Tag="ToBox" TokenDelimiter=";" TokenItemAdding="TokenItemAdding" - TokenItemTemplate="{StaticResource TokenBoxTemplate}" /> + TokenItemTemplate="{StaticResource ContactTokenTemplate}" />