Web editor refactoring and some calendar occurrence summary stuff.

This commit is contained in:
Burak Kaan Köse
2026-03-07 15:37:02 +01:00
parent 09f1cee3a5
commit d1f8163d72
17 changed files with 2649 additions and 1068 deletions
+5 -3
View File
@@ -93,10 +93,11 @@ private string searchQuery = string.Empty;
## Localization
1. Add English strings ONLY to `Wino.Core.Domain/Translations/en_US/resources.json`
1. Add English strings ONLY to Wino.Core.Domain/Translations/en_US/resources.json
2. Build project - source generators create Translator properties
3. Use `Translator.{PropertyName}` in code/XAML
4. **NEVER** edit other language files - Crowdin manages translations
3. Use Translator.{PropertyName} in code/XAML
4. NEVER edit any resources.json file outside Wino.Core.Domain/Translations/en_US/resources.json
5. Treat all non-en_US translation files as managed externally and leave them untouched, even when adding new localization keys
## Storage
@@ -133,3 +134,4 @@ private string searchQuery = string.Empty;
- In `EventDetailsPageViewModel.LoadAttendeesAsync`, never mutate `CurrentEvent.Attendees` outside `ExecuteUIThread(...)`.
@@ -507,55 +507,21 @@ public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
{
var effectiveStart = GetEffectiveStartDateTime();
var effectiveEnd = GetEffectiveEndDateTime();
var timeSummary = IsAllDay
? Translator.CalendarItemAllDay
: string.Format(
CultureInfo.CurrentCulture,
Translator.CalendarEventCompose_TimeRangeSummary,
effectiveStart.ToString(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", CultureInfo.CurrentCulture),
effectiveEnd.ToString(CurrentSettings.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", CultureInfo.CurrentCulture));
if (!IsRecurring)
{
RecurrenceSummary = string.Format(
CultureInfo.CurrentCulture,
Translator.CalendarEventCompose_SingleOccurrenceSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", CultureInfo.CurrentCulture),
timeSummary);
return;
}
var frequencyLabel = SelectedRecurrenceFrequencyOption?.PluralLabel(SelectedRecurrenceInterval)
?? Translator.CalendarEventCompose_FrequencyWeekPlural;
var selectedDays = WeekdayOptions
.Where(option => option.IsSelected)
.Select(option => option.FullDayName)
.Select(option => option.DayOfWeek)
.ToList();
var weekdaySummary = selectedDays.Count == 0
? string.Empty
: string.Format(
CultureInfo.CurrentCulture,
Translator.CalendarEventCompose_WeekdaySummary,
string.Join(", ", selectedDays));
var untilSummary = RecurrenceEndDate.HasValue
? string.Format(
CultureInfo.CurrentCulture,
Translator.CalendarEventCompose_UntilSummary,
RecurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", CultureInfo.CurrentCulture))
: string.Empty;
RecurrenceSummary = string.Format(
CultureInfo.CurrentCulture,
Translator.CalendarEventCompose_RecurringSummary,
RecurrenceSummary = CalendarRecurrenceSummaryFormatter.BuildSummary(
IsRecurring,
effectiveStart,
effectiveEnd,
IsAllDay,
CurrentSettings,
SelectedRecurrenceInterval,
frequencyLabel,
weekdaySummary,
timeSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", CultureInfo.CurrentCulture),
untilSummary).Trim();
SelectedRecurrenceFrequencyOption?.Frequency ?? CalendarItemRecurrenceFrequency.Weekly,
selectedDays,
RecurrenceEndDate);
}
private string BuildRecurrenceRule()
@@ -707,3 +673,5 @@ public partial class CalendarComposeWeekdayOption : ObservableObject
Label = label;
}
}
@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Calendar;
namespace Wino.Core.Domain;
public static class CalendarRecurrenceSummaryFormatter
{
private static readonly DayOfWeek[] OrderedDays =
[
DayOfWeek.Monday,
DayOfWeek.Tuesday,
DayOfWeek.Wednesday,
DayOfWeek.Thursday,
DayOfWeek.Friday,
DayOfWeek.Saturday,
DayOfWeek.Sunday
];
public static string BuildSummary(
bool isRecurring,
DateTimeOffset effectiveStart,
DateTimeOffset effectiveEnd,
bool isAllDay,
CalendarSettings settings,
int interval,
CalendarItemRecurrenceFrequency frequency,
IReadOnlyCollection<DayOfWeek> daysOfWeek,
DateTimeOffset? recurrenceEndDate)
{
var culture = settings?.CultureInfo ?? CultureInfo.CurrentCulture;
var timeSummary = isAllDay
? Translator.CalendarItemAllDay
: string.Format(
culture,
Translator.CalendarEventCompose_TimeRangeSummary,
effectiveStart.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture),
effectiveEnd.ToString(settings?.DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour ? "HH:mm" : "h:mm tt", culture));
if (!isRecurring)
{
return string.Format(
culture,
Translator.CalendarEventCompose_SingleOccurrenceSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
timeSummary);
}
var normalizedDays = NormalizeDays(daysOfWeek);
var isEveryDay = (frequency == CalendarItemRecurrenceFrequency.Daily && interval == 1) ||
(frequency == CalendarItemRecurrenceFrequency.Weekly && interval == 1 && normalizedDays.Count == 7);
var cadenceSummary = isEveryDay
? $"{Translator.CalendarEventCompose_Every} {Translator.CalendarEventCompose_FrequencyDay}"
: interval == 1
? $"{Translator.CalendarEventCompose_Every} {GetSingularFrequencyLabel(frequency)}"
: $"{Translator.CalendarEventCompose_Every} {interval.ToString(culture)} {GetPluralFrequencyLabel(frequency)}";
var weekdaySummary = string.Empty;
if (frequency == CalendarItemRecurrenceFrequency.Weekly && normalizedDays.Count > 0 && normalizedDays.Count < 7)
{
weekdaySummary = string.Format(
culture,
Translator.CalendarEventCompose_WeekdaySummary,
string.Join(", ", normalizedDays.Select(day => culture.DateTimeFormat.GetDayName(day))));
}
var untilSummary = recurrenceEndDate.HasValue
? string.Format(
culture,
Translator.CalendarEventCompose_UntilSummary,
recurrenceEndDate.Value.ToString("ddd yyyy-MM-dd", culture))
: string.Empty;
return string.Format(
culture,
Translator.GetTranslatedString("CalendarEventCompose_RecurringSummarySmart"),
cadenceSummary,
weekdaySummary,
timeSummary,
effectiveStart.ToString("dddd yyyy-MM-dd", culture),
untilSummary).Trim();
}
private static IReadOnlyList<DayOfWeek> NormalizeDays(IReadOnlyCollection<DayOfWeek> daysOfWeek)
{
if (daysOfWeek == null || daysOfWeek.Count == 0)
{
return [];
}
return daysOfWeek
.Distinct()
.OrderBy(day => Array.IndexOf(OrderedDays, day))
.ToList();
}
private static string GetSingularFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDay,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeek,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonth,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYear,
_ => Translator.CalendarEventCompose_FrequencyWeek
};
}
private static string GetPluralFrequencyLabel(CalendarItemRecurrenceFrequency frequency)
{
return frequency switch
{
CalendarItemRecurrenceFrequency.Daily => Translator.CalendarEventCompose_FrequencyDayPlural,
CalendarItemRecurrenceFrequency.Weekly => Translator.CalendarEventCompose_FrequencyWeekPlural,
CalendarItemRecurrenceFrequency.Monthly => Translator.CalendarEventCompose_FrequencyMonthPlural,
CalendarItemRecurrenceFrequency.Yearly => Translator.CalendarEventCompose_FrequencyYearPlural,
_ => Translator.CalendarEventCompose_FrequencyWeekPlural
};
}
}
@@ -1,4 +1,4 @@
{
{
"AccountAlias_Column_Alias": "Alias",
"AccountAlias_Column_IsPrimaryAlias": "Primary",
"AccountAlias_Column_Verified": "Verified",
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.UI;
using Microsoft.UI.Xaml.Media;
using Windows.UI;
namespace Wino.Mail.Controls;
public interface IEditorCommandTarget
{
EditorState CurrentState { get; }
EditorCapabilities Capabilities { get; }
event EventHandler<EditorState>? StateChanged;
event EventHandler<EditorCapabilities>? CapabilitiesChanged;
Task ExecuteCommandAsync(EditorCommand command);
}
public interface IEditorCommandControl
{
IEditorCommandTarget? CommandTarget { get; set; }
void AttachCommandTarget(IEditorCommandTarget? target);
void DetachCommandTarget();
}
public enum EditorCommandKind
{
ToggleBold,
ToggleItalic,
ToggleUnderline,
ToggleStrikethrough,
ToggleOrderedList,
ToggleUnorderedList,
Indent,
Outdent,
SetAlignment,
SetFontFamily,
SetFontSize,
SetParagraphStyle,
SetTextColor,
SetHighlightColor,
SetLineHeight,
InsertImage,
InsertLink,
RemoveLink,
InsertEmoji,
InsertTable,
ToggleBuiltInToolbar,
ToggleTheme,
ToggleSpellCheck
}
public enum EditorTextAlignment
{
Left,
Center,
Right,
Justify
}
public sealed record class EditorCommand(EditorCommandKind Kind, object? Value = null)
{
public static EditorCommand ToggleBold() => new(EditorCommandKind.ToggleBold);
public static EditorCommand ToggleItalic() => new(EditorCommandKind.ToggleItalic);
public static EditorCommand ToggleUnderline() => new(EditorCommandKind.ToggleUnderline);
public static EditorCommand ToggleStrikethrough() => new(EditorCommandKind.ToggleStrikethrough);
public static EditorCommand ToggleOrderedList() => new(EditorCommandKind.ToggleOrderedList);
public static EditorCommand ToggleUnorderedList() => new(EditorCommandKind.ToggleUnorderedList);
public static EditorCommand Indent() => new(EditorCommandKind.Indent);
public static EditorCommand Outdent() => new(EditorCommandKind.Outdent);
public static EditorCommand SetAlignment(EditorTextAlignment alignment) => new(EditorCommandKind.SetAlignment, alignment);
public static EditorCommand SetFontFamily(string fontFamily) => new(EditorCommandKind.SetFontFamily, fontFamily);
public static EditorCommand SetFontSize(int fontSize) => new(EditorCommandKind.SetFontSize, fontSize);
public static EditorCommand SetParagraphStyle(string tagName) => new(EditorCommandKind.SetParagraphStyle, tagName);
public static EditorCommand SetTextColor(string color) => new(EditorCommandKind.SetTextColor, color);
public static EditorCommand SetHighlightColor(string color) => new(EditorCommandKind.SetHighlightColor, color);
public static EditorCommand SetLineHeight(string lineHeight) => new(EditorCommandKind.SetLineHeight, lineHeight);
public static EditorCommand InsertImage() => new(EditorCommandKind.InsertImage);
public static EditorCommand InsertEmoji() => new(EditorCommandKind.InsertEmoji);
public static EditorCommand InsertLink(EditorLinkCommandArgs args) => new(EditorCommandKind.InsertLink, args);
public static EditorCommand RemoveLink() => new(EditorCommandKind.RemoveLink);
public static EditorCommand InsertTable(EditorTableCommandArgs args) => new(EditorCommandKind.InsertTable, args);
public static EditorCommand ToggleBuiltInToolbar(bool isVisible) => new(EditorCommandKind.ToggleBuiltInToolbar, isVisible);
public static EditorCommand ToggleTheme(bool isDarkMode) => new(EditorCommandKind.ToggleTheme, isDarkMode);
public static EditorCommand ToggleSpellCheck(bool isEnabled) => new(EditorCommandKind.ToggleSpellCheck, isEnabled);
}
public sealed record class EditorLinkCommandArgs(
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("text")] string? Text = null,
[property: JsonPropertyName("openInNewWindow")] bool OpenInNewWindow = true);
public sealed record class EditorTableCommandArgs(
[property: JsonPropertyName("rows")] int Rows,
[property: JsonPropertyName("columns")] int Columns);
public sealed record class EditorColorOption(string Name, string Value)
{
public SolidColorBrush Brush => new(ParseColor(Value));
private static Color ParseColor(string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return Colors.Transparent;
}
var hex = value.Trim().TrimStart('#');
if (hex.Length == 6)
{
hex = $"FF{hex}";
}
if (hex.Length != 8 || !uint.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out var argb))
{
return Colors.Transparent;
}
return Color.FromArgb(
(byte)((argb >> 24) & 0xFF),
(byte)((argb >> 16) & 0xFF),
(byte)((argb >> 8) & 0xFF),
(byte)(argb & 0xFF));
}
}
public sealed record class EditorParagraphStyleOption(string Name, string Tag);
public sealed record class EditorCapabilities
{
public IReadOnlyList<string> Fonts { get; init; } = Array.Empty<string>();
public IReadOnlyList<int> FontSizes { get; init; } = Array.Empty<int>();
public IReadOnlyList<EditorColorOption> TextColors { get; init; } = Array.Empty<EditorColorOption>();
public IReadOnlyList<EditorColorOption> HighlightColors { get; init; } = Array.Empty<EditorColorOption>();
public IReadOnlyList<EditorParagraphStyleOption> ParagraphStyles { get; init; } = Array.Empty<EditorParagraphStyleOption>();
public IReadOnlyList<string> LineHeights { get; init; } = Array.Empty<string>();
public IReadOnlyList<EditorTextAlignment> Alignments { get; init; } = Array.Empty<EditorTextAlignment>();
}
public sealed record class EditorState
{
public bool IsBold { get; init; }
public bool IsItalic { get; init; }
public bool IsUnderline { get; init; }
public bool IsStrikethrough { get; init; }
public bool IsOrderedList { get; init; }
public bool IsUnorderedList { get; init; }
public bool CanIndent { get; init; } = true;
public bool CanOutdent { get; init; }
public bool HasSelection { get; init; }
public bool IsDarkMode { get; init; }
public bool IsBuiltInToolbarVisible { get; init; }
public bool IsSpellCheckEnabled { get; init; } = true;
public EditorTextAlignment Alignment { get; init; } = EditorTextAlignment.Left;
public string? FontFamily { get; init; }
public int? FontSize { get; init; }
public string? ParagraphStyle { get; init; }
public string? TextColor { get; init; }
public string? HighlightColor { get; init; }
public string? LineHeight { get; init; }
public string? LinkUrl { get; init; }
public string? SelectedText { get; init; }
}
@@ -0,0 +1,259 @@
<UserControl
x:Class="Wino.Mail.Controls.EditorTabbedCommandBarControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Mail.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mail="using:Wino.Mail.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
x:Name="Root"
mc:Ignorable="d">
<UserControl.Resources>
<Style x:Key="CompactComboBoxStyle" TargetType="ComboBox">
<Setter Property="MinWidth" Value="88" />
<Setter Property="MaxWidth" Value="136" />
</Style>
<Style x:Key="CompactPickerContainerStyle" TargetType="AppBarElementContainer">
<Setter Property="MinWidth" Value="0" />
<Setter Property="Margin" Value="0" />
</Style>
<DataTemplate x:Key="ColorOptionTemplate" x:DataType="mail:EditorColorOption">
<StackPanel Orientation="Horizontal" Spacing="8">
<Grid Width="14" Height="14">
<Rectangle
RadiusX="3"
RadiusY="3"
Fill="{x:Bind Brush}"
Stroke="{ThemeResource TextFillColorSecondaryBrush}"
StrokeThickness="1" />
</Grid>
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name}" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<toolkit:TabbedCommandBar>
<toolkit:TabbedCommandBar.Resources>
<SolidColorBrush x:Key="TabContentContentBorderBackground" Color="Transparent" />
<SolidColorBrush x:Key="TabContentContentBorderBorderBrush" Color="Transparent" />
<Thickness x:Key="TabContentBorderBorderThickness">0</Thickness>
</toolkit:TabbedCommandBar.Resources>
<toolkit:TabbedCommandBar.PaneCustomContent>
<ContentPresenter Content="{x:Bind PaneCustomContent, Mode=OneWay}" />
</toolkit:TabbedCommandBar.PaneCustomContent>
<toolkit:TabbedCommandBar.MenuItems>
<toolkit:TabbedCommandBarItem DefaultLabelPosition="Collapsed" Header="Format">
<AppBarToggleButton x:Name="BoldButton" Click="BoldButton_Click" Label="Bold" ToolTipService.ToolTip="Bold (Ctrl+B)">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BoldPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton x:Name="ItalicButton" Click="ItalicButton_Click" Label="Italic" ToolTipService.ToolTip="Italic (Ctrl+I)">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource ItalicPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton x:Name="UnderlineButton" Click="UnderlineButton_Click" Label="Underline" ToolTipService.ToolTip="Underline (Ctrl+U)">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource UnderlinePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton x:Name="StrikeButton" Click="StrikeButton_Click" Label="Strikethrough" ToolTipService.ToolTip="Strikethrough">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource StrikePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarSeparator />
<AppBarToggleButton x:Name="BulletListButton" Click="BulletListButton_Click" Label="Bullets" ToolTipService.ToolTip="Bulleted list">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BulletedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton x:Name="OrderedListButton" Click="OrderedListButton_Click" Label="Numbered list" ToolTipService.ToolTip="Numbered list">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource OrderedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarButton x:Name="OutdentButton" Click="OutdentButton_Click" Label="Outdent" ToolTipService.ToolTip="Outdent">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource DecreaseIndentPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="IndentButton" Click="IndentButton_Click" Label="Indent" ToolTipService.ToolTip="Indent">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource IncreaseIndentPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Text alignment">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon
Width="14"
Height="14"
VerticalAlignment="Center"
Data="{StaticResource AlignLeftPathIcon}" />
<ComboBox
x:Name="AlignmentComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="108"
SelectionChanged="AlignmentComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Font family">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon VerticalAlignment="Center" FontSize="14" Glyph="&#xE8D2;" />
<ComboBox
x:Name="FontFamilyComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="120"
PlaceholderText="Font"
SelectionChanged="FontFamilyComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Font size">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="12" />
<ComboBox
x:Name="FontSizeComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="80"
PlaceholderText="Size"
SelectionChanged="FontSizeComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Paragraph style">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon
Width="14"
Height="14"
VerticalAlignment="Center"
Data="{StaticResource ParagraphPathIcon}" />
<ComboBox
x:Name="ParagraphStyleComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="110"
DisplayMemberPath="Name"
PlaceholderText="Paragraph"
SelectionChanged="ParagraphStyleComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Text color">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock VerticalAlignment="Center" FontWeight="SemiBold" Text="A" />
<ComboBox
x:Name="TextColorComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="116"
ItemTemplate="{StaticResource ColorOptionTemplate}"
PlaceholderText="Text"
SelectionChanged="TextColorComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Highlight color">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon VerticalAlignment="Center" FontSize="14" Glyph="&#xE7C3;" />
<ComboBox
x:Name="HighlightColorComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="122"
ItemTemplate="{StaticResource ColorOptionTemplate}"
PlaceholderText="Highlight"
SelectionChanged="HighlightColorComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
<AppBarElementContainer Style="{StaticResource CompactPickerContainerStyle}" ToolTipService.ToolTip="Line height">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon VerticalAlignment="Center" FontSize="14" Glyph="&#xE8C8;" />
<ComboBox
x:Name="LineHeightComboBox"
Style="{StaticResource CompactComboBoxStyle}"
MinWidth="86"
PlaceholderText="Line"
SelectionChanged="LineHeightComboBox_SelectionChanged" />
</StackPanel>
</AppBarElementContainer>
</toolkit:TabbedCommandBarItem>
<toolkit:TabbedCommandBarItem DefaultLabelPosition="Collapsed" Header="Insert">
<AppBarButton x:Name="ImageButton" Click="ImageButton_Click" Label="Image" ToolTipService.ToolTip="Insert image">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource AddPhotoPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="EmojiButton" Click="EmojiButton_Click" Label="Emoji" ToolTipService.ToolTip="Insert emoji">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource EmojiPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="LinkButton" Click="LinkButton_Click" Label="Link" ToolTipService.ToolTip="Insert or edit link">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource AddLinkPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
x:Name="RemoveLinkButton"
Click="RemoveLinkButton_Click"
Label="Remove link"
ToolTipService.ToolTip="Remove link"
Visibility="Collapsed">
<AppBarButton.Icon>
<SymbolIcon Symbol="Remove" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton x:Name="TableButton" Click="TableButton_Click" Label="Table" ToolTipService.ToolTip="Insert table">
<AppBarButton.Icon>
<FontIcon Glyph="&#xE14C;" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarElementContainer ToolTipService.ToolTip="Insert actions">
<ContentPresenter Content="{x:Bind InsertCustomContent, Mode=OneWay}" />
</AppBarElementContainer>
</toolkit:TabbedCommandBarItem>
<toolkit:TabbedCommandBarItem DefaultLabelPosition="Collapsed" Header="Options">
<AppBarToggleButton x:Name="BuiltInToolbarButton" Click="BuiltInToolbarButton_Click" Label="Web toolbar" ToolTipService.ToolTip="Toggle built-in web toolbar">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource WebviewToolBarPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton x:Name="SpellCheckButton" Click="SpellCheckButton_Click" Label="Spell check" ToolTipService.ToolTip="Toggle spell check">
<AppBarToggleButton.Icon>
<FontIcon Glyph="&#xEA53;" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarElementContainer ToolTipService.ToolTip="Composer options">
<ContentPresenter Content="{x:Bind OptionsCustomContent, Mode=OneWay}" />
</AppBarElementContainer>
</toolkit:TabbedCommandBarItem>
</toolkit:TabbedCommandBar.MenuItems>
</toolkit:TabbedCommandBar>
</UserControl>
@@ -0,0 +1,418 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
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));
public static readonly DependencyProperty PaneCustomContentProperty = DependencyProperty.Register(
nameof(PaneCustomContent),
typeof(object),
typeof(EditorTabbedCommandBarControl),
new PropertyMetadata(null));
public static readonly DependencyProperty InsertCustomContentProperty = DependencyProperty.Register(
nameof(InsertCustomContent),
typeof(object),
typeof(EditorTabbedCommandBarControl),
new PropertyMetadata(null));
public static readonly DependencyProperty OptionsCustomContentProperty = DependencyProperty.Register(
nameof(OptionsCustomContent),
typeof(object),
typeof(EditorTabbedCommandBarControl),
new PropertyMetadata(null));
private bool _isApplyingState;
private IEditorCommandTarget? _subscribedTarget;
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 EditorTabbedCommandBarControl()
{
InitializeComponent();
Loaded += OnLoaded;
Unloaded += OnUnloaded;
}
public void AttachCommandTarget(IEditorCommandTarget? target)
{
if (_subscribedTarget == target)
{
return;
}
if (_subscribedTarget != null)
{
_subscribedTarget.StateChanged -= CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged;
}
_subscribedTarget = target;
if (_subscribedTarget != null)
{
_subscribedTarget.StateChanged += CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged += CommandTarget_CapabilitiesChanged;
ApplyCapabilities(_subscribedTarget.Capabilities);
ApplyState(_subscribedTarget.CurrentState);
}
}
public void DetachCommandTarget()
{
if (_subscribedTarget == null)
{
return;
}
_subscribedTarget.StateChanged -= CommandTarget_StateChanged;
_subscribedTarget.CapabilitiesChanged -= CommandTarget_CapabilitiesChanged;
_subscribedTarget = null;
}
private static void OnCommandTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var control = (EditorTabbedCommandBarControl)d;
control.AttachCommandTarget((IEditorCommandTarget?)e.NewValue);
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
AttachCommandTarget(CommandTarget);
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
DetachCommandTarget();
}
private void CommandTarget_StateChanged(object? sender, EditorState e)
{
ApplyState(e);
}
private void CommandTarget_CapabilitiesChanged(object? sender, EditorCapabilities e)
{
ApplyCapabilities(e);
}
private void ApplyCapabilities(EditorCapabilities capabilities)
{
FontFamilyComboBox.ItemsSource = capabilities.Fonts;
FontSizeComboBox.ItemsSource = capabilities.FontSizes;
AlignmentComboBox.ItemsSource = capabilities.Alignments;
ParagraphStyleComboBox.ItemsSource = capabilities.ParagraphStyles;
TextColorComboBox.ItemsSource = capabilities.TextColors;
HighlightColorComboBox.ItemsSource = capabilities.HighlightColors;
LineHeightComboBox.ItemsSource = capabilities.LineHeights;
}
private void ApplyState(EditorState state)
{
_isApplyingState = true;
BoldButton.IsChecked = state.IsBold;
ItalicButton.IsChecked = state.IsItalic;
UnderlineButton.IsChecked = state.IsUnderline;
StrikeButton.IsChecked = state.IsStrikethrough;
BulletListButton.IsChecked = state.IsUnorderedList;
OrderedListButton.IsChecked = state.IsOrderedList;
IndentButton.IsEnabled = state.CanIndent;
OutdentButton.IsEnabled = state.CanOutdent;
RemoveLinkButton.IsEnabled = !string.IsNullOrWhiteSpace(state.LinkUrl);
RemoveLinkButton.Visibility = RemoveLinkButton.IsEnabled ? Visibility.Visible : Visibility.Collapsed;
BuiltInToolbarButton.IsChecked = state.IsBuiltInToolbarVisible;
SpellCheckButton.IsChecked = state.IsSpellCheckEnabled;
AlignmentComboBox.SelectedItem = state.Alignment;
FontFamilyComboBox.SelectedItem = MatchStringItem(FontFamilyComboBox.ItemsSource, state.FontFamily);
FontSizeComboBox.SelectedItem = MatchValueItem<int>(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);
_isApplyingState = false;
}
private static object? MatchStringItem(object? itemsSource, string? value)
{
if (itemsSource is IEnumerable<string> strings)
{
return strings.FirstOrDefault(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase));
}
return null;
}
private static object? MatchValueItem<T>(object? itemsSource, T? value) where T : struct
{
if (!value.HasValue || itemsSource is not IEnumerable<T> values)
{
return null;
}
foreach (var item in values)
{
if (EqualityComparer<T>.Default.Equals(item, value.Value))
{
return item;
}
}
return null;
}
private object? MatchParagraphItem(string? tag)
{
if (ParagraphStyleComboBox.ItemsSource is not IEnumerable<EditorParagraphStyleOption> styles)
{
return null;
}
return styles.FirstOrDefault(item => string.Equals(item.Tag, tag, StringComparison.OrdinalIgnoreCase));
}
private static object? MatchColorItem(object? itemsSource, string? value)
{
if (itemsSource is not IEnumerable<EditorColorOption> colors)
{
return null;
}
return colors.FirstOrDefault(item => string.Equals(item.Value, value ?? string.Empty, StringComparison.OrdinalIgnoreCase));
}
private async Task ExecuteAsync(EditorCommand command)
{
if (_isApplyingState || CommandTarget == null)
{
return;
}
await CommandTarget.ExecuteCommandAsync(command);
}
private async void BoldButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleBold());
private async void ItalicButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleItalic());
private async void UnderlineButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnderline());
private async void StrikeButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleStrikethrough());
private async void BulletListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleUnorderedList());
private async void OrderedListButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.ToggleOrderedList());
private async void IndentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Indent());
private async void OutdentButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.Outdent());
private async void ImageButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertImage());
private async void EmojiButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.InsertEmoji());
private async void RemoveLinkButton_Click(object sender, RoutedEventArgs e) => await ExecuteAsync(EditorCommand.RemoveLink());
private async void AlignmentComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || AlignmentComboBox.SelectedItem is not EditorTextAlignment alignment)
{
return;
}
await ExecuteAsync(EditorCommand.SetAlignment(alignment));
}
private async void FontFamilyComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || FontFamilyComboBox.SelectedItem is not string fontFamily || string.IsNullOrWhiteSpace(fontFamily))
{
return;
}
await ExecuteAsync(EditorCommand.SetFontFamily(fontFamily));
}
private async void FontSizeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || FontSizeComboBox.SelectedItem is not int fontSize)
{
return;
}
await ExecuteAsync(EditorCommand.SetFontSize(fontSize));
}
private async void ParagraphStyleComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || ParagraphStyleComboBox.SelectedItem is not EditorParagraphStyleOption paragraphStyle)
{
return;
}
await ExecuteAsync(EditorCommand.SetParagraphStyle(paragraphStyle.Tag));
}
private async void TextColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || TextColorComboBox.SelectedItem is not EditorColorOption color)
{
return;
}
await ExecuteAsync(EditorCommand.SetTextColor(color.Value));
}
private async void HighlightColorComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || HighlightColorComboBox.SelectedItem is not EditorColorOption color)
{
return;
}
await ExecuteAsync(EditorCommand.SetHighlightColor(color.Value));
}
private async void LineHeightComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_isApplyingState || LineHeightComboBox.SelectedItem is not string lineHeight || string.IsNullOrWhiteSpace(lineHeight))
{
return;
}
await ExecuteAsync(EditorCommand.SetLineHeight(lineHeight));
}
private async void BuiltInToolbarButton_Click(object sender, RoutedEventArgs e)
{
await ExecuteAsync(EditorCommand.ToggleBuiltInToolbar(BuiltInToolbarButton.IsChecked == true));
}
private async void SpellCheckButton_Click(object sender, RoutedEventArgs e)
{
await ExecuteAsync(EditorCommand.ToggleSpellCheck(SpellCheckButton.IsChecked == true));
}
private async void LinkButton_Click(object sender, RoutedEventArgs e)
{
if (CommandTarget == null)
{
return;
}
var currentState = CommandTarget.CurrentState;
var urlTextBox = new TextBox
{
Header = "URL",
Text = currentState.LinkUrl ?? string.Empty,
PlaceholderText = "https://example.com"
};
var textTextBox = new TextBox
{
Header = "Text",
Text = currentState.SelectedText ?? string.Empty,
PlaceholderText = "Link text"
};
var openInNewWindow = new CheckBox
{
Content = "Open in new window",
IsChecked = true
};
var dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = "Insert link",
PrimaryButtonText = "Apply",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
Children =
{
urlTextBox,
textTextBox,
openInNewWindow
}
}
};
if (await dialog.ShowAsync() == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(urlTextBox.Text))
{
await ExecuteAsync(EditorCommand.InsertLink(new EditorLinkCommandArgs(urlTextBox.Text.Trim(), textTextBox.Text.Trim(), openInNewWindow.IsChecked == true)));
}
}
private async void TableButton_Click(object sender, RoutedEventArgs e)
{
var rowsBox = new NumberBox
{
Header = "Rows",
Minimum = 1,
Maximum = 10,
SmallChange = 1,
Value = 2
};
var columnsBox = new NumberBox
{
Header = "Columns",
Minimum = 1,
Maximum = 10,
SmallChange = 1,
Value = 2
};
var dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = "Insert table",
PrimaryButtonText = "Insert",
CloseButtonText = "Cancel",
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
Children =
{
rowsBox,
columnsBox
}
}
};
if (await dialog.ShowAsync() == ContentDialogResult.Primary)
{
await ExecuteAsync(EditorCommand.InsertTable(new EditorTableCommandArgs((int)Math.Max(1, rowsBox.Value), (int)Math.Max(1, columnsBox.Value))));
}
}
}
File diff suppressed because it is too large Load Diff
@@ -47,212 +47,7 @@
<RowDefinition Height="Auto" />
<RowDefinition Height="300" />
</Grid.RowDefinitions>
<CommandBar
Grid.Row="0"
HorizontalAlignment="Left"
Background="Transparent"
DefaultLabelPosition="Collapsed"
IsOpen="False">
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Light Theme"
Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="LightEditor" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="Dark Theme"
Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay, Converter={StaticResource ReverseBooleanToVisibilityConverter}}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="DarkEditor" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}"
Label="Bold"
ToolTipService.ToolTip="Bold">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BoldPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}"
Label="Italic"
ToolTipService.ToolTip="Italic">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource ItalicPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}"
Label="Underline"
ToolTipService.ToolTip="Underline">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource UnderlinePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}"
Label="Stroke"
ToolTipService.ToolTip="Stroke">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource StrikePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}"
Label="Bullet List"
ToolTipService.ToolTip="Bullet List">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BulletedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}"
Label="Ordered List"
ToolTipService.ToolTip="Ordered List">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource OrderedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorOutdentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorOutdentEnabled, Mode=OneWay}"
Label="Decrease Indent"
ToolTipService.ToolTip="Decrease Indent">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource DecreaseIndentPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorIndentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorIndentEnabled, Mode=OneWay}"
Label="Increase Indent"
ToolTipService.ToolTip="Increase Indent">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource IncreaseIndentPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarElementContainer
Width="Auto"
MinWidth="40"
VerticalAlignment="Center">
<ComboBox
Background="Transparent"
BorderBrush="Transparent"
SelectedIndex="{x:Bind WebViewEditor.EditorAlignmentSelectedIndex, Mode=TwoWay}">
<ComboBoxItem IsSelected="True" Tag="left">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignLeftPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Left}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="center">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignCenterPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Center}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="right">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignRightPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Right}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="justify">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignJustifyPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Justify}" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
</AppBarElementContainer>
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ShowImagePicker}"
Label="Add Image"
ToolTipService.ToolTip="{x:Bind domain:Translator.Photos}">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource AddPhotoPathIcon}" />
</AppBarButton.Icon>
<AppBarButton.Content>
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16" VerticalAlignment="Center">
<PathIcon Data="{StaticResource AddPhotoPathIcon}" />
</Viewbox>
<TextBlock Text="{x:Bind domain:Translator.Photos}" />
</StackPanel>
</AppBarButton.Content>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ShowEmojiPicker}"
Label="Add Emoji"
ToolTipService.ToolTip="{x:Bind domain:Translator.Emoji}">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource EmojiPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorWebViewEditor, Mode=TwoWay}"
Label="Webview ToolBar"
ToolTipService.ToolTip="Webview ToolBar">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource WebviewToolBarPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</CommandBar>
<controls2:EditorTabbedCommandBarControl CommandTarget="{x:Bind WebViewEditor}" />
<Border
Grid.Row="1"
Margin="0,8,0,0"
@@ -264,3 +59,4 @@
</Grid>
</Grid>
</ContentDialog>
+44 -40
View File
@@ -1,4 +1,5 @@
using System.Linq;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using Ical.Net.CalendarComponents;
using Ical.Net.DataTypes;
@@ -17,9 +18,6 @@ public static class CalendarXamlHelpers
public static CalendarItemViewModel GetFirstAllDayEvent(CalendarEventCollection collection)
=> collection.AllDayEvents.OfType<CalendarItemViewModel>().FirstOrDefault()!;
/// <summary>
/// Returns full date + duration info in Event Details page details title.
/// </summary>
public static string GetEventDetailsDateString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
@@ -34,18 +32,17 @@ public static class CalendarXamlHelpers
{
return $"{start.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)} - {end.ToString($"dd MMMM ddd {timeFormat}", settings.CultureInfo)}";
}
else
{
return $"{start.ToString(dateFormat, settings.CultureInfo)} - {end.ToString(timeFormat, settings.CultureInfo)}";
}
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence))
{
return string.Empty;
}
public static string GetRecurrenceString(CalendarItemViewModel calendarItemViewModel)
{
// TODO: This is incorrect.
if (calendarItemViewModel == null || string.IsNullOrEmpty(calendarItemViewModel.CalendarItem.Recurrence)) return string.Empty;
// Parse recurrence rules
var calendarEvent = new CalendarEvent
{
Start = new CalDateTime(calendarItemViewModel.StartDate),
@@ -53,45 +50,51 @@ public static class CalendarXamlHelpers
};
var recurrenceLines = Regex.Split(calendarItemViewModel.CalendarItem.Recurrence, Constants.CalendarEventRecurrenceRuleSeperator);
foreach (var line in recurrenceLines)
foreach (var line in recurrenceLines.Where(line => !string.IsNullOrWhiteSpace(line)))
{
calendarEvent.RecurrenceRules.Add(new RecurrencePattern(line));
}
if (calendarEvent.RecurrenceRules == null || !calendarEvent.RecurrenceRules.Any())
var recurrenceRule = calendarEvent.RecurrenceRules.FirstOrDefault();
if (recurrenceRule == null)
{
return "No recurrence pattern.";
return string.Empty;
}
var recurrenceRule = calendarEvent.RecurrenceRules.First();
var daysOfWeek = string.Join(", ", recurrenceRule.ByDay.Select(day => day.DayOfWeek.ToString()));
string timeZone = calendarEvent.DtStart.TzId ?? "UTC";
var frequency = MapFrequency(recurrenceRule.Frequency.ToString());
if (!frequency.HasValue)
{
return string.Empty;
}
return $"Every {daysOfWeek}, effective {calendarEvent.DtStart.Value.ToShortDateString()} " +
$"from {calendarEvent.DtStart.Value.ToShortTimeString()} to {calendarEvent.DtEnd.Value.ToShortTimeString()} " +
$"{timeZone}.";
return CalendarRecurrenceSummaryFormatter.BuildSummary(
isRecurring: true,
effectiveStart: calendarItemViewModel.Period.Start,
effectiveEnd: calendarItemViewModel.Period.End,
isAllDay: calendarItemViewModel.IsAllDayEvent,
settings: settings,
interval: recurrenceRule.Interval <= 0 ? 1 : recurrenceRule.Interval,
frequency: frequency.Value,
daysOfWeek: recurrenceRule.ByDay?.Select(day => day.DayOfWeek).ToList() ?? [],
recurrenceEndDate: recurrenceRule.Until == default ? null : new DateTimeOffset(recurrenceRule.Until));
}
public static string GetDetailsPopupDurationString(CalendarItemViewModel calendarItemViewModel, CalendarSettings settings)
{
if (calendarItemViewModel == null || settings == null) return string.Empty;
// Single event in a day.
if (!calendarItemViewModel.IsAllDayEvent && !calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} {settings.GetTimeString(calendarItemViewModel.Period.Duration)}";
}
else if (calendarItemViewModel.IsMultiDayEvent)
if (calendarItemViewModel.IsMultiDayEvent)
{
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} - {calendarItemViewModel.Period.End.ToString("d", settings.CultureInfo)}";
}
else
{
// All day event.
return $"{calendarItemViewModel.Period.Start.ToString("d", settings.CultureInfo)} ({Translator.CalendarItemAllDay})";
}
}
public static PopupPlacementMode GetDesiredPlacementModeForEventsDetailsPopup(
CalendarItemViewModel calendarItemViewModel,
@@ -99,21 +102,14 @@ public static class CalendarXamlHelpers
{
if (calendarItemViewModel == null) return PopupPlacementMode.Auto;
// All and/or multi day events always go to the top of the screen.
if (calendarItemViewModel.IsAllDayEvent || calendarItemViewModel.IsMultiDayEvent) return PopupPlacementMode.Bottom;
return XamlHelpers.GetPlaccementModeForCalendarType(calendarDisplayType);
}
/// <summary>
/// Returns true if the calendar item has an online meeting link.
/// </summary>
public static bool HasOnlineMeetingLink(CalendarItemViewModel calendarItemViewModel)
=> calendarItemViewModel != null && !string.IsNullOrEmpty(calendarItemViewModel.CalendarItem?.HtmlLink);
/// <summary>
/// Returns the text representation of an attendee's status.
/// </summary>
public static string GetAttendeeStatusText(AttendeeStatus status)
{
return status switch
@@ -126,13 +122,21 @@ public static class CalendarXamlHelpers
};
}
/// <summary>
/// Returns visibility for attendee status badge.
/// Always shows status for all attendees.
/// </summary>
public static Microsoft.UI.Xaml.Visibility GetAttendeeStatusVisibility(AttendeeStatus status)
{
// Always show status
return Microsoft.UI.Xaml.Visibility.Visible;
}
private static CalendarItemRecurrenceFrequency? MapFrequency(string frequency)
{
return frequency.ToUpperInvariant() switch
{
"DAILY" => CalendarItemRecurrenceFrequency.Daily,
"WEEKLY" => CalendarItemRecurrenceFrequency.Weekly,
"MONTHLY" => CalendarItemRecurrenceFrequency.Monthly,
"YEARLY" => CalendarItemRecurrenceFrequency.Yearly,
_ => null
};
}
}
+693 -67
View File
@@ -8,6 +8,7 @@ const joditConfig = {
"showCharsCounter": false,
"showWordsCounter": false,
"showXPathInStatusbar": false,
"spellcheck": true,
"link": {
"processVideoLink": false
},
@@ -17,92 +18,78 @@ const joditConfig = {
"insertImageAsBase64URI": true
},
"enter": "DIV"
}
};
let editor;
let editorDomObserver;
let selectionChangeHandler;
let stateSyncQueued = false;
let imageInputBound = false;
let inlineFontsPluginRegistered = false;
let lastKnownRange = null;
// This method should be called first all the time.
function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, defaultReaderFont, defaultReaderFontSize) {
const fontsWithFallabckObject = fonts.reduce((acc, font) => { acc[`'${font}',Arial,sans-serif`] = font; return acc; }, {});
if (editor) {
scheduleStateSync();
return true;
}
const fontsWithFallbackObject = fonts.reduce((acc, font) => {
acc[`'${font}',Arial,sans-serif`] = font;
return acc;
}, {});
const mergedConfig = {
...joditConfig,
controls: {
font: {
list: Jodit.atom(fontsWithFallabckObject)
list: Jodit.atom(fontsWithFallbackObject)
}
},
style: { font: `${defaultReaderFontSize}px ${defaultReaderFont}` },
style: { font: `${defaultReaderFontSize}px ${defaultReaderFont}` }
};
if (!inlineFontsPluginRegistered) {
Jodit.plugins.add('inlineFonts', jodit => {
jodit.events.on('afterEnter', () => {
const current = getSelectionElement();
if (!current) {
return;
}
Jodit.plugins.add('inlineFonts', jodit => {
jodit.events.on('afterEnter', e => {
const current = jodit.selection.current().parentNode;
current.style.fontFamily = `'${defaultComposerFont}',Arial,sans-serif`;
current.style.fontSize = `${defaultComposerFontSize}px`;
rememberSelection();
scheduleStateSync();
});
});
// Don't add const/let/var here, it should be global
editor = Jodit.make("#editor", mergedConfig);
// Handle the image input change event
imageInput.addEventListener('change', () => {
const file = imageInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (event) {
const base64Image = event.target.result;
insertImages([{ data: base64Image, name: file.name }]);
};
reader.readAsDataURL(file);
}
});
// Listeners for button events
const disabledButtons = ["indent", "outdent"];
const ariaPressedButtons = ["bold", "italic", "underline", "strikethrough", "ul", "ol"];
const alignmentButton = document.querySelector(`[ref='left']`).firstChild.firstChild;
const alignmentObserver = new MutationObserver(function () {
const value = alignmentButton.firstChild.getAttribute('class').split(' ')[0];
window.chrome.webview.postMessage({ type: 'alignment', value: value });
});
alignmentObserver.observe(alignmentButton, { childList: true, attributes: true, attributeFilter: ["class"] });
const ariaObservers = ariaPressedButtons.map(button => {
const buttonContainer = document.querySelector(`[ref='${button}']`);
const observer = new MutationObserver(function () { pressedChanged(buttonContainer) });
observer.observe(buttonContainer.firstChild, { attributes: true, attributeFilter: ["aria-pressed"] });
return observer;
});
const disabledObservers = disabledButtons.map(button => {
const buttonContainer = document.querySelector(`[ref='${button}']`);
const observer = new MutationObserver(function () { disabledButtonChanged(buttonContainer) });
observer.observe(buttonContainer.firstChild, { attributes: true, attributeFilter: ["disabled"] });
return observer;
});
function pressedChanged(buttonContainer) {
const ref = buttonContainer.getAttribute('ref');
const value = buttonContainer.firstChild.getAttribute('aria-pressed');
window.chrome.webview.postMessage({ type: ref, value: value });
inlineFontsPluginRegistered = true;
}
function disabledButtonChanged(buttonContainer) {
const ref = buttonContainer.getAttribute('ref');
const value = buttonContainer.firstChild.getAttribute('disabled');
window.chrome.webview.postMessage({ type: ref, value: value });
}
editor = Jodit.make('#editor', mergedConfig);
bindImageInput();
bindEditorStateTracking();
toggleToolbar(false);
scheduleStateSync();
return true;
}
function RenderHTML(htmlString) {
if (!editor) {
return;
}
editor.value = htmlString;
editor.synchronizeValues();
rememberSelection();
scheduleStateSync();
}
function GetHTMLContent() {
return editor.value;
return editor ? editor.value : '';
}
function SetLightEditor() {
@@ -115,16 +102,655 @@ function SetDarkEditor() {
function toggleToolbar(enable) {
const toolbar = document.querySelector('.jodit-toolbar__box');
if (enable) {
toolbar.style.display = 'flex';
if (toolbar) {
toolbar.style.display = enable ? 'flex' : 'none';
}
else {
toolbar.style.display = 'none';
scheduleStateSync();
}
function setSpellCheck(enable) {
if (!editor || !editor.editor) {
return;
}
const isEnabled = !!enable;
editor.options.spellcheck = isEnabled;
editor.editor.spellcheck = isEnabled;
editor.editor.setAttribute('spellcheck', isEnabled ? 'true' : 'false');
scheduleStateSync();
}
function insertImages(imagesInfo) {
if (!editor) {
return;
}
restoreEditorSelection();
imagesInfo.forEach(imageInfo => {
editor.selection.insertHTML(`<img src="${imageInfo.data}" alt="${imageInfo.name}">`);
editor.selection.insertHTML(`<img src="${escapeHtmlAttribute(imageInfo.data)}" alt="${escapeHtmlAttribute(imageInfo.name)}">`);
});
};
rememberSelection();
scheduleStateSync();
}
function focusEditor() {
if (!editor) {
return;
}
if (restoreEditorSelection()) {
return;
}
editor.selection.focus();
const lastChild = editor.editor.lastChild;
if (lastChild) {
editor.selection.setCursorIn(lastChild, false);
}
}
function getEditorState() {
return buildEditorState();
}
function executeEditorCommand(commandName) {
if (!editor) {
return;
}
restoreEditorSelection();
editor.execCommand(commandName);
rememberSelection();
scheduleStateSync();
}
function setFontFamily(fontFamily) {
applyInlineStyleToSelection({ fontFamily: `'${fontFamily}',Arial,sans-serif` });
}
function setFontSize(fontSize) {
applyInlineStyleToSelection({ fontSize: `${fontSize}px` });
}
function setTextColor(color) {
applyInlineStyleToSelection({ color: color || '' });
}
function setHighlightColor(color) {
applyInlineStyleToSelection({ backgroundColor: color || '' });
}
function setParagraphStyle(tagName) {
if (!editor) {
return;
}
restoreEditorSelection();
const normalizedTag = (tagName || 'div').toLowerCase();
try {
document.execCommand('formatBlock', false, normalizedTag);
}
catch {
const block = getCurrentBlockElement();
if (block && block.tagName.toLowerCase() !== normalizedTag) {
const replacement = document.createElement(normalizedTag);
while (block.firstChild) {
replacement.appendChild(block.firstChild);
}
block.parentNode.replaceChild(replacement, block);
}
}
rememberSelection();
scheduleStateSync();
}
function setLineHeight(lineHeight) {
restoreEditorSelection();
const block = getCurrentBlockElement();
if (!block) {
return;
}
block.style.lineHeight = lineHeight || '';
rememberSelection();
scheduleStateSync();
}
function upsertLink(linkArgs) {
if (!editor) {
return;
}
restoreEditorSelection();
const normalizedUrl = normalizeLinkUrl(linkArgs && linkArgs.url ? linkArgs.url : '');
if (!normalizedUrl) {
return;
}
const linkText = linkArgs && linkArgs.text ? linkArgs.text.trim() : '';
const existingLink = getSelectionElement() ? getSelectionElement().closest('a[href]') : null;
if (existingLink) {
existingLink.setAttribute('href', normalizedUrl);
if (linkArgs.openInNewWindow) {
existingLink.setAttribute('target', '_blank');
existingLink.setAttribute('rel', 'noopener noreferrer');
}
else {
existingLink.removeAttribute('target');
existingLink.removeAttribute('rel');
}
if (linkText) {
existingLink.textContent = linkText;
}
rememberSelection();
scheduleStateSync();
return;
}
const selection = window.getSelection();
if (selection && selection.rangeCount > 0 && !selection.isCollapsed && isSelectionInsideEditor()) {
try {
document.execCommand('createLink', false, normalizedUrl);
const createdLink = getSelectionElement() ? getSelectionElement().closest('a[href]') : null;
if (createdLink) {
if (linkArgs.openInNewWindow) {
createdLink.setAttribute('target', '_blank');
createdLink.setAttribute('rel', 'noopener noreferrer');
}
if (linkText) {
createdLink.textContent = linkText;
}
}
}
catch {
const selectedText = linkText || selection.toString() || normalizedUrl;
editor.selection.insertHTML(`<a href="${escapeHtmlAttribute(normalizedUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtmlText(selectedText)}</a>`);
}
rememberSelection();
scheduleStateSync();
return;
}
const text = linkText || normalizedUrl;
editor.selection.insertHTML(`<a href="${escapeHtmlAttribute(normalizedUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtmlText(text)}</a>`);
rememberSelection();
scheduleStateSync();
}
function removeLink() {
restoreEditorSelection();
const selectionElement = getSelectionElement();
const linkElement = selectionElement ? selectionElement.closest('a[href]') : null;
if (!linkElement) {
return;
}
try {
document.execCommand('unlink');
}
catch {
unwrapElement(linkElement);
}
rememberSelection();
scheduleStateSync();
}
function insertTableHtml(tableArgs) {
if (!editor) {
return;
}
restoreEditorSelection();
const rows = clampInteger(tableArgs && tableArgs.rows, 1, 10);
const columns = clampInteger(tableArgs && tableArgs.columns, 1, 10);
const htmlRows = [];
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
const cells = [];
for (let columnIndex = 0; columnIndex < columns; columnIndex += 1) {
cells.push('<td style="border:1px solid #c7c7c7;padding:6px;min-width:32px;"><br></td>');
}
htmlRows.push(`<tr>${cells.join('')}</tr>`);
}
editor.selection.insertHTML(`<table style="border-collapse:collapse;width:100%;">${htmlRows.join('')}</table><div><br></div>`);
rememberSelection();
scheduleStateSync();
}
function bindImageInput() {
if (imageInputBound) {
return;
}
imageInput.addEventListener('change', () => {
const file = imageInput.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = event => {
const base64Image = event.target.result;
insertImages([{ data: base64Image, name: file.name }]);
imageInput.value = '';
};
reader.readAsDataURL(file);
});
imageInputBound = true;
}
function bindEditorStateTracking() {
if (!editor || !editor.editor) {
return;
}
const syncHandler = () => {
rememberSelection();
scheduleStateSync();
};
['keyup', 'mouseup', 'click', 'input', 'focus', 'blur'].forEach(eventName => {
editor.editor.addEventListener(eventName, syncHandler);
});
if (editor.events && editor.events.on) {
editor.events.on('afterSetMode change afterCommand', syncHandler);
}
editorDomObserver = new MutationObserver(() => scheduleStateSync());
editorDomObserver.observe(editor.editor, {
subtree: true,
childList: true,
attributes: true,
characterData: true,
attributeFilter: ['style', 'class', 'href', 'spellcheck']
});
selectionChangeHandler = () => {
if (isSelectionInsideEditor()) {
rememberSelection();
scheduleStateSync();
}
};
document.addEventListener('selectionchange', selectionChangeHandler);
}
function scheduleStateSync() {
if (stateSyncQueued) {
return;
}
stateSyncQueued = true;
window.requestAnimationFrame(() => {
stateSyncQueued = false;
notifyState();
});
}
function notifyState() {
if (!window.chrome || !window.chrome.webview) {
return;
}
window.chrome.webview.postMessage({
type: 'state',
state: buildEditorState()
});
}
function buildEditorState() {
const selectionElement = getSelectionElement();
const contextElement = selectionElement || (editor && editor.editor ? editor.editor : document.body);
const blockElement = getCurrentBlockElement() || contextElement;
const style = window.getComputedStyle(contextElement);
const blockStyle = window.getComputedStyle(blockElement);
const selection = window.getSelection();
const listElement = selectionElement ? selectionElement.closest('ol,ul') : null;
const linkElement = selectionElement ? selectionElement.closest('a[href]') : null;
const fontSize = parsePixelSize(style.fontSize);
return {
bold: queryCommandState('bold', isBoldStyle(style)),
italic: queryCommandState('italic', style.fontStyle === 'italic'),
underline: queryCommandState('underline', (style.textDecorationLine || '').includes('underline')),
strikethrough: queryCommandState('strikeThrough', (style.textDecorationLine || '').includes('line-through')),
orderedList: !!(listElement && listElement.tagName.toLowerCase() === 'ol'),
unorderedList: !!(listElement && listElement.tagName.toLowerCase() === 'ul'),
canIndent: queryCommandEnabled('indent', true),
canOutdent: queryCommandEnabled('outdent', !!listElement || !!(selectionElement && selectionElement.closest('blockquote'))),
hasSelection: !!(selection && selection.rangeCount > 0 && !selection.isCollapsed && isSelectionInsideEditor()),
isSpellCheckEnabled: !!(editor && editor.editor && editor.editor.spellcheck),
alignment: normalizeAlignment(blockStyle.textAlign),
fontFamily: normalizeFontFamily(style.fontFamily),
fontSize: fontSize,
paragraphStyle: normalizeParagraphTag(blockElement),
textColor: normalizeColor(style.color),
highlightColor: normalizeColor(style.backgroundColor),
lineHeight: normalizeLineHeight(blockStyle.lineHeight, fontSize),
linkUrl: linkElement ? linkElement.getAttribute('href') || '' : '',
selectedText: selection && isSelectionInsideEditor() ? selection.toString() : ''
};
}
function getSelectionElement() {
if (!editor || !editor.editor) {
return null;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return editor.editor;
}
const node = selection.anchorNode;
if (!node) {
return editor.editor;
}
const element = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
if (!element || !editor.editor.contains(element)) {
return editor.editor;
}
return element;
}
function getCurrentBlockElement() {
const selectionElement = getSelectionElement();
if (!selectionElement) {
return null;
}
return selectionElement.closest('h1,h2,h3,h4,h5,h6,p,blockquote,pre,div,li,td,th') || selectionElement;
}
function rememberSelection() {
if (!editor || !editor.editor) {
return;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || !isSelectionInsideEditor()) {
return;
}
try {
lastKnownRange = selection.getRangeAt(0).cloneRange();
}
catch {
lastKnownRange = null;
}
}
function restoreEditorSelection() {
if (!editor || !editor.editor) {
return false;
}
editor.selection.focus();
if (!lastKnownRange) {
return false;
}
try {
const selection = window.getSelection();
if (!selection) {
return false;
}
const restoredRange = lastKnownRange.cloneRange();
selection.removeAllRanges();
selection.addRange(restoredRange);
return true;
}
catch {
lastKnownRange = null;
return false;
}
}
function isSelectionInsideEditor() {
if (!editor || !editor.editor) {
return false;
}
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0) {
return false;
}
const anchorNode = selection.anchorNode;
const focusNode = selection.focusNode;
return !!anchorNode && !!focusNode && editor.editor.contains(anchorNode) && editor.editor.contains(focusNode);
}
function queryCommandState(commandName, fallbackValue) {
try {
const value = document.queryCommandState(commandName);
return typeof value === 'boolean' ? value : fallbackValue;
}
catch {
return fallbackValue;
}
}
function queryCommandEnabled(commandName, fallbackValue) {
try {
const value = document.queryCommandEnabled(commandName);
return typeof value === 'boolean' ? value : fallbackValue;
}
catch {
return fallbackValue;
}
}
function isBoldStyle(style) {
const fontWeight = style.fontWeight || '400';
const numericWeight = parseInt(fontWeight, 10);
return fontWeight === 'bold' || Number.isFinite(numericWeight) && numericWeight >= 600;
}
function normalizeAlignment(value) {
const normalized = (value || '').toLowerCase();
if (normalized === 'center' || normalized === 'right' || normalized === 'justify') {
return normalized;
}
return 'left';
}
function normalizeFontFamily(value) {
if (!value) {
return '';
}
return value.split(',')[0].replace(/["']/g, '').trim();
}
function normalizeParagraphTag(element) {
return element && element.tagName ? element.tagName.toLowerCase() : 'div';
}
function normalizeLineHeight(value, fontSize) {
if (!value || value === 'normal') {
return 'normal';
}
const numericValue = parseFloat(value);
if (!Number.isFinite(numericValue)) {
return value;
}
if (value.endsWith('px') && fontSize) {
const ratio = numericValue / fontSize;
return Number.isInteger(ratio) ? `${ratio}` : ratio.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
}
return Number.isInteger(numericValue) ? `${numericValue}` : numericValue.toString();
}
function parsePixelSize(value) {
const numericValue = parseFloat(value || '');
return Number.isFinite(numericValue) ? Math.round(numericValue) : null;
}
function normalizeColor(value) {
if (!value || value === 'transparent' || value === 'rgba(0, 0, 0, 0)') {
return '';
}
if (value.startsWith('#')) {
return value.toLowerCase();
}
const rgbaMatch = value.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (!rgbaMatch) {
return value.toLowerCase();
}
const [, red, green, blue] = rgbaMatch;
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
}
function toHex(value) {
return Number(value).toString(16).padStart(2, '0');
}
function applyInlineStyleToSelection(styles) {
if (!editor || !editor.editor) {
return;
}
restoreEditorSelection();
const selection = window.getSelection();
if (!selection || selection.rangeCount === 0 || !isSelectionInsideEditor()) {
return;
}
const range = selection.getRangeAt(0);
if (selection.isCollapsed) {
const contextElement = getSelectionElement();
if (contextElement) {
Object.entries(styles).forEach(([propertyName, propertyValue]) => {
contextElement.style[propertyName] = propertyValue || '';
});
}
rememberSelection();
scheduleStateSync();
return;
}
const span = document.createElement('span');
Object.entries(styles).forEach(([propertyName, propertyValue]) => {
if (propertyValue) {
span.style[propertyName] = propertyValue;
}
});
try {
span.appendChild(range.extractContents());
range.insertNode(span);
selection.removeAllRanges();
const newRange = document.createRange();
newRange.selectNodeContents(span);
selection.addRange(newRange);
}
catch {
const css = styleObjectToCss(styles);
const selectedText = escapeHtmlText(selection.toString());
editor.selection.insertHTML(`<span style="${css}">${selectedText}</span>`);
}
rememberSelection();
scheduleStateSync();
}
function styleObjectToCss(styles) {
return Object.entries(styles)
.filter(([, value]) => value)
.map(([propertyName, propertyValue]) => `${camelToKebabCase(propertyName)}:${propertyValue}`)
.join(';');
}
function camelToKebabCase(value) {
return value.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
}
function unwrapElement(element) {
const parent = element.parentNode;
if (!parent) {
return;
}
while (element.firstChild) {
parent.insertBefore(element.firstChild, element);
}
parent.removeChild(element);
}
function clampInteger(value, min, max) {
const numericValue = parseInt(value, 10);
if (!Number.isFinite(numericValue)) {
return min;
}
return Math.min(max, Math.max(min, numericValue));
}
function normalizeLinkUrl(url) {
const trimmed = (url || '').trim();
if (!trimmed) {
return '';
}
if (/^[a-z]+:/i.test(trimmed)) {
return trimmed;
}
if (trimmed.includes('@') && !trimmed.includes('/')) {
return `mailto:${trimmed}`;
}
return `https://${trimmed}`;
}
function escapeHtmlAttribute(value) {
return `${value || ''}`
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function escapeHtmlText(value) {
return `${value || ''}`
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
@@ -187,6 +187,9 @@ public class NavigationService : NavigationServiceBase, INavigationService
// Update the application mode in state persistence service
_statePersistanceService.ApplicationMode = mode;
_statePersistanceService.CoreWindowTitle = mode == WinoApplicationMode.Calendar
? "Wino Calendar"
: "Wino Mail";
var targetPageType = mode == WinoApplicationMode.Mail ? typeof(MailAppShell) : typeof(CalendarAppShell);
var currentPageType = coreFrame.Content?.GetType();
+11
View File
@@ -4,6 +4,17 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Mail.WinUI.Styles">
<Style
x:Key="TransparentActionButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
<!-- Horizontally Stretched List View Item Container Style -->
<Style x:Key="StretchedItemContainerStyle" TargetType="ListViewItem">
<Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
@@ -272,14 +272,6 @@
</muxc:Expander>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.Footer>
<Button
Margin="14,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"
Command="{x:Bind ViewModel.SyncCommand}"
Content="Test Sync" />
</ListView.Footer>
</ListView>
<!-- Menu Items -->
@@ -19,17 +19,6 @@
mc:Ignorable="d">
<Page.Resources>
<Style
x:Key="TransparentActionButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="8" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
<Style
x:Key="FieldToggleButtonStyle"
BasedOn="{StaticResource DefaultToggleButtonStyle}"
@@ -59,41 +48,19 @@
</Grid.RowDefinitions>
<!-- Top Bar -->
<Grid
Padding="16,10"
<Border
Background="{ThemeResource WinoContentZoneBackgroud}"
CornerRadius="{StaticResource ControlCornerRadius}">
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7">
<Grid Padding="12,6" ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Left: Save + Discard -->
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Command="{x:Bind ViewModel.CreateCommand}">
<Button.Style>
<Style BasedOn="{StaticResource AccentButtonStyle}" TargetType="Button">
<Setter Property="Padding" Value="16,6" />
</Style>
</Button.Style>
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Save" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Save}" />
</StackPanel>
</Button>
<Button Command="{x:Bind ViewModel.CancelCommand}" Style="{StaticResource TransparentActionButtonStyle}">
<StackPanel Orientation="Horizontal" Spacing="8">
<coreControls:WinoFontIcon FontSize="16" Icon="Dismiss" />
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Buttons_Discard}" />
</StackPanel>
</Button>
</StackPanel>
<!-- Right: Calendar, Show As, Reminder -->
<!-- Left: Calendar, Show As, Reminder -->
<StackPanel
Grid.Column="2"
Orientation="Horizontal"
Spacing="12">
@@ -213,7 +180,24 @@
</ComboBox>
</StackPanel>
</StackPanel>
<StackPanel
Grid.Column="1"
Orientation="Horizontal"
Spacing="4">
<AppBarButton Command="{x:Bind ViewModel.CancelCommand}" Label="{x:Bind domain:Translator.Buttons_Discard}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Delete" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Command="{x:Bind ViewModel.CreateCommand}" Label="{x:Bind domain:Translator.Buttons_Save}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Save" />
</AppBarButton.Icon>
</AppBarButton>
</StackPanel>
</Grid>
</Border>
<!-- Scrollable Content -->
<ScrollViewer Grid.Row="1" MaxWidth="1200">
@@ -572,14 +556,14 @@
Icon="Draft" />
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.CalendarEventCompose_Notes}" />
</StackPanel>
<!-- Notes Editor -->
<mailControls:EditorTabbedCommandBarControl CommandTarget="{x:Bind NotesEditor}" />
<mailControls:WebViewEditorControl
x:Name="NotesEditor"
MinHeight="600"
IsEditorDarkMode="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=OneWay}"
IsEditorWebViewEditor="True" />
IsEditorDarkMode="{x:Bind ViewModel.IsDarkWebviewRenderer, Mode=OneWay}" />
</StackPanel>
</ScrollViewer>
</Grid>
</abstract:CalendarEventComposePageAbstract>
@@ -57,17 +57,6 @@
</selectors:RsvpStatusIconTemplateSelector.CancelledTemplate>
</selectors:RsvpStatusIconTemplateSelector>
<Style
x:Key="TransparentActionButtonStyle"
BasedOn="{StaticResource DefaultButtonStyle}"
TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
<Style x:Key="ActionBarElementContainerStackStyle" TargetType="StackPanel">
<Setter Property="Spacing" Value="6" />
<Setter Property="Padding" Value="10,0,4,0" />
@@ -407,7 +396,7 @@
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent), Mode=OneWay}"
Text="{x:Bind calendarHelpers:CalendarXamlHelpers.GetRecurrenceString(ViewModel.CurrentEvent, ViewModel.CurrentSettings), Mode=OneWay}"
TextWrapping="Wrap" />
</Grid>
@@ -670,3 +659,4 @@
</VisualStateManager.VisualStateGroups>
</Grid>
</abstract:EventDetailsPageAbstract>
+25 -209
View File
@@ -173,13 +173,8 @@
</Grid.RowDefinitions>
<!-- Format -->
<toolkit:TabbedCommandBar>
<toolkit:TabbedCommandBar.Resources>
<SolidColorBrush x:Key="TabContentContentBorderBackground" Color="Transparent" />
<SolidColorBrush x:Key="TabContentContentBorderBorderBrush" Color="Transparent" />
<Thickness x:Key="TabContentBorderBorderThickness">0</Thickness>
</toolkit:TabbedCommandBar.Resources>
<toolkit:TabbedCommandBar.PaneCustomContent>
<controls1:EditorTabbedCommandBarControl CommandTarget="{x:Bind WebViewEditor}">
<controls1:EditorTabbedCommandBarControl.PaneCustomContent>
<toolkit:TabbedCommandBarItem
CommandAlignment="Right"
IsDynamicOverflowEnabled="True"
@@ -192,35 +187,20 @@
<ProgressRing IsActive="True" />
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_LightTheme}"
Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="LightEditor" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.ToggleEditorTheme}"
LabelPosition="Collapsed"
ToolTipService.ToolTip="{x:Bind domain:Translator.Composer_DarkTheme}"
Visibility="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=OneWay, Converter={StaticResource ReverseBooleanToVisibilityConverter}}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="DarkEditor" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Command="{x:Bind ViewModel.DiscardCommand}" Label="{x:Bind domain:Translator.Buttons_Discard}">
<AppBarButton.Icon>
<coreControls:WinoFontIcon Icon="Delete" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarToggleButton
x:Name="EditorThemeToggleButton"
IsChecked="{x:Bind WebViewEditor.IsEditorDarkMode, Mode=TwoWay}"
Label=""
ToolTipService.ToolTip="Toggle editor dark mode">
<AppBarToggleButton.Icon>
<coreControls:WinoFontIcon Icon="DarkEditor" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarButton
Command="{x:Bind ViewModel.SendCommand}"
Label="{x:Bind domain:Translator.Buttons_Send}"
@@ -239,161 +219,10 @@
</AppBarButton.Icon>
</AppBarButton>
</toolkit:TabbedCommandBarItem>
</toolkit:TabbedCommandBar.PaneCustomContent>
<toolkit:TabbedCommandBar.MenuItems>
<toolkit:TabbedCommandBarItem DefaultLabelPosition="Collapsed" Header="{x:Bind domain:Translator.EditorToolbarOption_Format}">
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorBold, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_Bold}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BoldPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorItalic, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_Italic}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource ItalicPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUnderline, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_Underline}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource UnderlinePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorStrikethrough, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_Stroke}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource StrikePathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</controls1:EditorTabbedCommandBarControl.PaneCustomContent>
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorUl, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_BulletList}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource BulletedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorOl, Mode=TwoWay}"
Label="{x:Bind domain:Translator.Composer_OrderedList}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource OrderedListPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
<AppBarSeparator />
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorOutdentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorOutdentEnabled, Mode=OneWay}"
Label="{x:Bind domain:Translator.Composer_Outdent}">
<AppBarButton.Content>
<Viewbox Width="16">
<PathIcon Data="{StaticResource DecreaseIndentPathIcon}" />
</Viewbox>
</AppBarButton.Content>
</AppBarButton>
<AppBarButton
Width="Auto"
MinWidth="40"
Click="{x:Bind WebViewEditor.EditorIndentAsync}"
IsEnabled="{x:Bind WebViewEditor.IsEditorIndentEnabled, Mode=OneWay}"
Label="{x:Bind domain:Translator.Composer_Indent}">
<AppBarButton.Content>
<Viewbox Width="16">
<PathIcon Data="{StaticResource IncreaseIndentPathIcon}" />
</Viewbox>
</AppBarButton.Content>
</AppBarButton>
<AppBarElementContainer
Width="Auto"
MinWidth="40"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ComboBox
x:Name="AlignmentListView"
VerticalAlignment="Center"
Background="Transparent"
BorderBrush="Transparent"
SelectedIndex="{x:Bind WebViewEditor.EditorAlignmentSelectedIndex, Mode=TwoWay}">
<ComboBoxItem IsSelected="True" Tag="left">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignLeftPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Left}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="center">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignCenterPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Center}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="right">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignRightPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Right}" />
</StackPanel>
</ComboBoxItem>
<ComboBoxItem Tag="justify">
<StackPanel Orientation="Horizontal" Spacing="8">
<Viewbox Width="16">
<PathIcon Data="{StaticResource AlignJustifyPathIcon}" />
</Viewbox>
<TextBlock VerticalAlignment="Center" Text="{x:Bind domain:Translator.Justify}" />
</StackPanel>
</ComboBoxItem>
</ComboBox>
</AppBarElementContainer>
<AppBarSeparator />
<AppBarToggleButton
Width="Auto"
MinWidth="40"
IsChecked="{x:Bind WebViewEditor.IsEditorWebViewEditor, Mode=TwoWay}"
Label="{x:Bind domain:Translator.EditorTooltip_WebViewEditor}"
ToolTipService.ToolTip="{x:Bind domain:Translator.EditorTooltip_WebViewEditor}">
<AppBarToggleButton.Icon>
<PathIcon Data="{StaticResource WebviewToolBarPathIcon}" />
</AppBarToggleButton.Icon>
</AppBarToggleButton>
</toolkit:TabbedCommandBarItem>
<!-- Insert -->
<toolkit:TabbedCommandBarItem Header="{x:Bind domain:Translator.EditorToolbarOption_Insert}">
<controls1:EditorTabbedCommandBarControl.InsertCustomContent>
<StackPanel Orientation="Horizontal">
<AppBarButton
x:Name="FilesButton"
Command="{x:Bind ViewModel.AttachFilesCommand}"
@@ -402,27 +231,17 @@
<PathIcon Data="{StaticResource AttachPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
</StackPanel>
</controls1:EditorTabbedCommandBarControl.InsertCustomContent>
<AppBarButton Click="{x:Bind WebViewEditor.ShowImagePicker}" Label="{x:Bind domain:Translator.Photos}">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource AddPhotoPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
<AppBarButton Click="{x:Bind WebViewEditor.ShowEmojiPicker}" Label="{x:Bind domain:Translator.Emoji}">
<AppBarButton.Icon>
<PathIcon Data="{StaticResource EmojiPathIcon}" />
</AppBarButton.Icon>
</AppBarButton>
</toolkit:TabbedCommandBarItem>
<!-- Options -->
<toolkit:TabbedCommandBarItem Header="{x:Bind domain:Translator.EditorToolbarOption_Options}">
<AppBarElementContainer>
<controls1:EditorTabbedCommandBarControl.OptionsCustomContent>
<StackPanel Orientation="Horizontal" Spacing="8">
<ToggleSplitButton x:Name="ImportanceSplitButton" IsChecked="{x:Bind ViewModel.IsImportanceSelected, Mode=TwoWay}">
<ToggleSplitButton.Content>
<Viewbox Width="16" Height="16">
<SymbolIcon x:Name="ImportanceSplitButtonContent" Symbol="Important" />
</Viewbox>
</ToggleSplitButton.Content>
<ToggleSplitButton.Flyout>
<Flyout x:Name="ImportanceFlyout" Placement="Bottom">
<StackPanel Orientation="Horizontal">
@@ -451,8 +270,6 @@
</Flyout>
</ToggleSplitButton.Flyout>
</ToggleSplitButton>
</AppBarElementContainer>
<AppBarElementContainer>
<muxc:ToggleSplitButton
x:Name="SignatureToggleButton"
Margin="8,0,0,0"
@@ -489,8 +306,6 @@
</Flyout>
</muxc:ToggleSplitButton.Flyout>
</muxc:ToggleSplitButton>
</AppBarElementContainer>
<AppBarElementContainer>
<ToggleButton
x:Name="EncryptionToggleButton"
Margin="8,0,0,0"
@@ -505,10 +320,9 @@
</Viewbox>
</ToggleButton.Content>
</ToggleButton>
</AppBarElementContainer>
</toolkit:TabbedCommandBarItem>
</toolkit:TabbedCommandBar.MenuItems>
</toolkit:TabbedCommandBar>
</StackPanel>
</controls1:EditorTabbedCommandBarControl.OptionsCustomContent>
</controls1:EditorTabbedCommandBarControl>
<!-- Mime Info -->
<Grid
@@ -779,3 +593,5 @@
</abstract:ComposePageAbstract>