Files
Wino-Mail/Wino.Mail.WinUI/Controls/WebViewEditorControl.cs
T

795 lines
28 KiB
C#
Raw Normal View History

2025-11-15 14:52:01 +01:00
using System;
2025-09-29 11:16:14 +02:00
using System.Collections.Generic;
2026-03-07 23:33:25 +01:00
using System.Linq;
2025-09-29 11:16:14 +02:00
using System.Text.Json;
using System.Text.Json.Serialization;
2025-09-29 11:16:14 +02:00
using System.Threading.Tasks;
using CommunityToolkit.WinUI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core;
using Windows.UI.ViewManagement.Core;
using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.Reader;
using Wino.Mail.WinUI;
using Wino.Mail.WinUI.Extensions;
2025-09-29 11:16:14 +02:00
namespace Wino.Mail.Controls;
public sealed partial class WebViewEditorControl : Control, IDisposable, IEditorCommandTarget
2025-09-29 11:16:14 +02:00
{
private const string PART_WebView = "WebView";
private static readonly IReadOnlyList<int> DefaultFontSizes = [8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 32, 36];
private static readonly IReadOnlyList<EditorColorOption> DefaultTextColors =
[
new("Default", string.Empty),
new("Black", "#000000"),
new("Gray", "#666666"),
new("Red", "#c62828"),
new("Orange", "#ef6c00"),
new("Yellow", "#f9a825"),
new("Green", "#2e7d32"),
new("Blue", "#1565c0"),
new("Purple", "#6a1b9a")
];
private static readonly IReadOnlyList<EditorColorOption> DefaultHighlightColors =
[
new("None", string.Empty),
new("Yellow", "#fff59d"),
new("Green", "#c8e6c9"),
new("Blue", "#bbdefb"),
new("Pink", "#f8bbd0"),
new("Orange", "#ffe0b2")
];
private static readonly IReadOnlyList<EditorParagraphStyleOption> DefaultParagraphStyles =
[
new("Paragraph", "div"),
new("Heading 1", "h1"),
new("Heading 2", "h2"),
new("Heading 3", "h3"),
new("Quote", "blockquote"),
new("Code", "pre")
];
private static readonly IReadOnlyList<string> DefaultLineHeights = ["normal", "1", "1.15", "1.5", "2"];
private static readonly IReadOnlyList<EditorTextAlignment> DefaultAlignments =
[EditorTextAlignment.Left, EditorTextAlignment.Center, EditorTextAlignment.Right, EditorTextAlignment.Justify];
2025-11-14 18:51:48 +01:00
private readonly INativeAppService _nativeAppService = App.Current.Services.GetService<INativeAppService>()!;
private readonly IFontService _fontService = App.Current.Services.GetService<IFontService>()!;
private readonly IPreferencesService _preferencesService = App.Current.Services.GetService<IPreferencesService>()!;
2025-09-29 11:16:14 +02:00
[GeneratedDependencyProperty]
public partial bool IsEditorDarkMode { get; set; }
async partial void OnIsEditorDarkModeChanged(bool newValue)
{
UpdateState(GetCurrentStateOrDefault() with { IsDarkMode = newValue });
2025-09-29 11:16:14 +02:00
await UpdateEditorThemeAsync();
}
[GeneratedDependencyProperty]
public partial bool IsEditorWebViewEditor { get; set; }
async partial void OnIsEditorWebViewEditorChanged(bool newValue)
2025-09-29 11:16:14 +02:00
{
UpdateState(GetCurrentStateOrDefault() with { IsBuiltInToolbarVisible = newValue });
await ApplyBuiltInToolbarVisibilityAsync();
2025-09-29 11:16:14 +02:00
}
private WebView2? _chromium;
private bool _disposedValue;
private bool? _lastAppliedDarkTheme;
private readonly TaskCompletionSource<bool> _domLoadedTask = new();
private readonly TaskCompletionSource<bool> _editorReadyTask = new();
private readonly object _editorInitializationLock = new();
private Task? _editorInitializationTask;
public event EventHandler<EditorState>? StateChanged;
public event EventHandler<EditorCapabilities>? CapabilitiesChanged;
public EditorState CurrentState { get; private set; }
public EditorCapabilities Capabilities { get; private set; }
public WebViewEditorControl()
2025-09-29 11:16:14 +02:00
{
DefaultStyleKey = typeof(WebViewEditorControl);
IsEditorDarkMode = WinoApplication.Current.UnderlyingThemeService.IsUnderlyingThemeDark();
Capabilities = BuildCapabilities(_fontService.GetFonts());
CurrentState = CreateDefaultState();
2025-09-29 11:16:14 +02:00
}
protected override async void OnApplyTemplate()
2025-09-29 11:16:14 +02:00
{
base.OnApplyTemplate();
var newWebView = GetTemplateChild(PART_WebView) as WebView2;
if (newWebView == null)
2025-09-29 11:16:14 +02:00
{
return;
2025-09-29 11:16:14 +02:00
}
if (_chromium != null && !ReferenceEquals(_chromium, newWebView))
{
DetachChromium(_chromium, closeWebView: false);
}
_chromium = newWebView;
await InitializeWebViewAsync();
2025-09-29 11:16:14 +02:00
}
public async Task ExecuteCommandAsync(EditorCommand command)
2025-09-29 11:16:14 +02:00
{
ObjectDisposedException.ThrowIf(_disposedValue, this);
await EnsureEditorReadyAsync();
if (_chromium == null)
{
return;
}
switch (command.Kind)
2025-09-29 11:16:14 +02:00
{
case EditorCommandKind.ToggleBold:
await ExecuteEditorCommandAsync("bold");
break;
case EditorCommandKind.ToggleItalic:
await ExecuteEditorCommandAsync("italic");
break;
case EditorCommandKind.ToggleUnderline:
await ExecuteEditorCommandAsync("underline");
break;
case EditorCommandKind.ToggleStrikethrough:
await ExecuteEditorCommandAsync("strikethrough");
break;
case EditorCommandKind.ToggleOrderedList:
await ExecuteEditorCommandAsync("insertorderedlist");
break;
case EditorCommandKind.ToggleUnorderedList:
await ExecuteEditorCommandAsync("insertunorderedlist");
break;
case EditorCommandKind.Indent:
await ExecuteEditorCommandAsync("indent");
break;
case EditorCommandKind.Outdent:
await ExecuteEditorCommandAsync("outdent");
break;
case EditorCommandKind.SetAlignment when command.Value is EditorTextAlignment alignment:
await ExecuteEditorCommandAsync(alignment switch
{
EditorTextAlignment.Left => "justifyleft",
EditorTextAlignment.Center => "justifycenter",
EditorTextAlignment.Right => "justifyright",
EditorTextAlignment.Justify => "justifyfull",
_ => "justifyleft"
});
break;
case EditorCommandKind.SetFontFamily when command.Value is string fontFamily:
await _chromium.ExecuteScriptFunctionSafeAsync("setFontFamily", JsonSerializer.Serialize(fontFamily, BasicTypesJsonContext.Default.String));
break;
case EditorCommandKind.SetFontSize when command.Value is int fontSize:
await _chromium.ExecuteScriptFunctionSafeAsync("setFontSize", JsonSerializer.Serialize(fontSize, BasicTypesJsonContext.Default.Int32));
break;
case EditorCommandKind.SetParagraphStyle when command.Value is string paragraphStyle:
await _chromium.ExecuteScriptFunctionSafeAsync("setParagraphStyle", JsonSerializer.Serialize(paragraphStyle, BasicTypesJsonContext.Default.String));
break;
case EditorCommandKind.SetTextColor when command.Value is string textColor:
await _chromium.ExecuteScriptFunctionSafeAsync("setTextColor", JsonSerializer.Serialize(textColor, BasicTypesJsonContext.Default.String));
break;
case EditorCommandKind.SetHighlightColor when command.Value is string highlightColor:
await _chromium.ExecuteScriptFunctionSafeAsync("setHighlightColor", JsonSerializer.Serialize(highlightColor, BasicTypesJsonContext.Default.String));
break;
case EditorCommandKind.SetLineHeight when command.Value is string lineHeight:
await _chromium.ExecuteScriptFunctionSafeAsync("setLineHeight", JsonSerializer.Serialize(lineHeight, BasicTypesJsonContext.Default.String));
break;
case EditorCommandKind.InsertImage:
await ShowImagePickerAsync();
return;
case EditorCommandKind.InsertLink when command.Value is EditorLinkCommandArgs linkArgs:
await _chromium.ExecuteScriptFunctionSafeAsync("upsertLink", SerializeLinkArgs(linkArgs));
break;
case EditorCommandKind.RemoveLink:
await _chromium.ExecuteScriptFunctionSafeAsync("removeLink");
break;
case EditorCommandKind.InsertEmoji:
await ShowEmojiPickerAsync();
return;
case EditorCommandKind.InsertTable when command.Value is EditorTableCommandArgs tableArgs:
await _chromium.ExecuteScriptFunctionSafeAsync("insertTableHtml", SerializeTableArgs(tableArgs));
break;
case EditorCommandKind.ToggleBuiltInToolbar when command.Value is bool isToolbarVisible:
IsEditorWebViewEditor = isToolbarVisible;
return;
case EditorCommandKind.ToggleTheme when command.Value is bool isDarkMode:
IsEditorDarkMode = isDarkMode;
return;
case EditorCommandKind.ToggleSpellCheck when command.Value is bool isSpellCheckEnabled:
await _chromium.ExecuteScriptFunctionSafeAsync("setSpellCheck", JsonSerializer.Serialize(isSpellCheckEnabled, BasicTypesJsonContext.Default.Boolean));
break;
default:
throw new InvalidOperationException($"Unsupported editor command: {command.Kind}");
2025-09-29 11:16:14 +02:00
}
await RefreshStateAsync();
2025-09-29 11:16:14 +02:00
}
public async Task EditorIndentAsync() => await ExecuteCommandAsync(EditorCommand.Indent());
public async Task EditorOutdentAsync() => await ExecuteCommandAsync(EditorCommand.Outdent());
public void ToggleEditorTheme()
2025-09-29 11:16:14 +02:00
{
IsEditorDarkMode = !IsEditorDarkMode;
}
public async Task<string?> GetHtmlBodyAsync()
{
await EnsureEditorReadyAsync();
if (_chromium == null)
2025-09-29 11:16:14 +02:00
{
return null;
2025-09-29 11:16:14 +02:00
}
var editorContent = await _chromium.ExecuteScriptFunctionSafeAsync("GetHTMLContent");
return JsonSerializer.Deserialize(editorContent, BasicTypesJsonContext.Default.String);
2025-09-29 11:16:14 +02:00
}
public async Task ShowImagePickerAsync()
2025-09-29 11:16:14 +02:00
{
await EnsureEditorReadyAsync();
if (_chromium != null)
2025-09-29 11:16:14 +02:00
{
await _chromium.ExecuteScriptFunctionSafeAsync("imageInput.click");
2025-09-29 11:16:14 +02:00
}
}
public async void ShowImagePicker()
{
await ShowImagePickerAsync();
}
2025-09-29 11:16:14 +02:00
public async Task InsertImagesAsync(List<ImageInfo> images)
2025-09-29 11:16:14 +02:00
{
await EnsureEditorReadyAsync();
if (_chromium != null)
2025-09-29 11:16:14 +02:00
{
await _chromium.ExecuteScriptFunctionSafeAsync("insertImages", JsonSerializer.Serialize(images, DomainModelsJsonContext.Default.ListImageInfo));
await RefreshStateAsync();
2025-09-29 11:16:14 +02:00
}
}
public async Task ShowEmojiPickerAsync()
{
CoreInputView.GetForCurrentView().TryShow(CoreInputViewKind.Emoji);
await FocusEditorAsync(focusControlAsWell: true);
}
2025-09-29 11:16:14 +02:00
public async void ShowEmojiPicker()
2025-09-29 11:16:14 +02:00
{
await ShowEmojiPickerAsync();
2025-09-29 11:16:14 +02:00
}
public WebView2 GetUnderlyingWebView() => _chromium!;
2025-09-29 11:16:14 +02:00
public async Task RenderHtmlAsync(string htmlBody)
2025-09-29 11:16:14 +02:00
{
await EnsureEditorReadyAsync();
2025-09-29 11:16:14 +02:00
if (_chromium == null)
{
return;
}
await _chromium.ExecuteScriptFunctionAsync("RenderHTML", JsonSerializer.Serialize(string.IsNullOrEmpty(htmlBody) ? " " : htmlBody, BasicTypesJsonContext.Default.String));
await RefreshStateAsync();
2025-09-29 11:16:14 +02:00
}
public async Task FocusEditorAsync(bool focusControlAsWell)
2025-09-29 11:16:14 +02:00
{
await EnsureEditorReadyAsync();
if (_chromium == null)
{
return;
}
2025-09-29 11:16:14 +02:00
await _chromium.ExecuteScriptSafeAsync("focusEditor();");
2025-09-29 11:16:14 +02:00
if (focusControlAsWell)
{
_chromium.Focus(FocusState.Keyboard);
_chromium.Focus(FocusState.Programmatic);
}
2025-09-29 11:16:14 +02:00
}
public async Task UpdateEditorThemeAsync(bool force = false)
2025-09-29 11:16:14 +02:00
{
if (_chromium?.CoreWebView2 == null)
{
return;
}
if (!_editorReadyTask.Task.IsCompleted)
{
return;
}
var isDark = IsEditorDarkMode;
if (!force && _lastAppliedDarkTheme == isDark)
{
return;
}
_lastAppliedDarkTheme = isDark;
_chromium.CoreWebView2.Profile.PreferredColorScheme = isDark
? CoreWebView2PreferredColorScheme.Dark
: CoreWebView2PreferredColorScheme.Light;
await _chromium.ExecuteScriptFunctionSafeAsync(isDark ? "SetDarkEditor" : "SetLightEditor");
UpdateState(CurrentState with { IsDarkMode = isDark });
}
private async Task InitializeWebViewAsync()
{
if (_chromium == null)
{
return;
}
2026-02-08 10:35:24 +01:00
WebViewExtensions.EnsureWebView2Environment();
2026-02-08 10:35:24 +01:00
_chromium.CoreWebView2Initialized -= ChromiumInitialized;
2025-09-29 11:16:14 +02:00
_chromium.CoreWebView2Initialized += ChromiumInitialized;
await _chromium.EnsureCoreWebView2Async();
}
private Task EnsureEditorReadyAsync()
2025-09-29 11:16:14 +02:00
{
if (_chromium == null || _disposedValue)
{
return Task.CompletedTask;
}
return EnsureEditorReadyCoreAsync();
2025-09-29 11:16:14 +02:00
}
private async Task EnsureEditorReadyCoreAsync()
2025-09-29 11:16:14 +02:00
{
await EnsureEditorInitializedAsync();
await _editorReadyTask.Task;
2025-09-29 11:16:14 +02:00
}
private Task EnsureEditorInitializedAsync()
2025-09-29 11:16:14 +02:00
{
lock (_editorInitializationLock)
{
_editorInitializationTask ??= EnsureEditorInitializedCoreAsync();
return _editorInitializationTask;
}
2025-09-29 11:16:14 +02:00
}
private async Task EnsureEditorInitializedCoreAsync()
2025-09-29 11:16:14 +02:00
{
if (_chromium == null || _disposedValue)
{
_editorReadyTask.TrySetResult(true);
return;
}
2025-09-29 11:16:14 +02:00
await _domLoadedTask.Task;
2025-09-29 11:16:14 +02:00
if (_chromium == null || _disposedValue)
{
_editorReadyTask.TrySetResult(true);
return;
}
var fonts = _fontService.GetFonts();
await _chromium.ExecuteScriptFunctionAsync(
"initializeJodit",
JsonSerializer.Serialize(fonts, BasicTypesJsonContext.Default.ListString),
JsonSerializer.Serialize(_preferencesService.ComposerFont, BasicTypesJsonContext.Default.String),
JsonSerializer.Serialize(_preferencesService.ComposerFontSize, BasicTypesJsonContext.Default.Int32),
JsonSerializer.Serialize(_preferencesService.ReaderFont, BasicTypesJsonContext.Default.String),
2026-03-07 23:33:25 +01:00
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);
await UpdateEditorThemeAsync(force: true);
await ApplyBuiltInToolbarVisibilityAsync(force: true);
await RefreshStateAsync();
2025-09-29 11:16:14 +02:00
}
private async Task ExecuteEditorCommandAsync(string command)
2025-09-29 11:16:14 +02:00
{
if (_chromium != null)
{
await _chromium.ExecuteScriptFunctionSafeAsync("executeEditorCommand", JsonSerializer.Serialize(command, BasicTypesJsonContext.Default.String));
}
2025-09-29 11:16:14 +02:00
}
private async Task ApplyBuiltInToolbarVisibilityAsync(bool force = false)
2025-09-29 11:16:14 +02:00
{
if (_chromium == null)
{
return;
}
2025-09-29 11:16:14 +02:00
if (!_editorReadyTask.Task.IsCompleted)
{
return;
}
2025-09-29 11:16:14 +02:00
await _chromium.ExecuteScriptFunctionSafeAsync("toggleToolbar", JsonSerializer.Serialize(IsEditorWebViewEditor, BasicTypesJsonContext.Default.Boolean));
UpdateState(CurrentState with { IsBuiltInToolbarVisible = IsEditorWebViewEditor });
}
2025-09-29 11:16:14 +02:00
private async Task RefreshStateAsync()
2025-09-29 11:16:14 +02:00
{
if (_chromium == null || !_editorReadyTask.Task.IsCompleted)
{
return;
}
2025-09-29 11:16:14 +02:00
var stateResult = await _chromium.ExecuteScriptFunctionSafeAsync("getEditorState");
if (string.IsNullOrWhiteSpace(stateResult))
{
return;
}
2025-09-29 11:16:14 +02:00
var snapshot = DeserializeStateSnapshot(stateResult);
if (snapshot != null)
{
UpdateState(MapState(snapshot));
}
2025-09-29 11:16:14 +02:00
}
private void ChromiumInitialized(WebView2 sender, CoreWebView2InitializedEventArgs args)
2025-09-29 11:16:14 +02:00
{
if (args.Exception != null || _chromium?.CoreWebView2 == null)
{
return;
}
_ = ConfigureChromiumAsync();
2025-09-29 11:16:14 +02:00
}
private async Task ConfigureChromiumAsync()
2025-09-29 11:16:14 +02:00
{
if (_chromium?.CoreWebView2 == null)
{
return;
}
2025-09-29 11:16:14 +02:00
var editorBundlePath = (await _nativeAppService.GetEditorBundlePathAsync()).Replace("editor.html", string.Empty, StringComparison.OrdinalIgnoreCase);
2025-09-29 11:16:14 +02:00
_chromium.CoreWebView2.SetVirtualHostNameToFolderMapping("app.editor", editorBundlePath, CoreWebView2HostResourceAccessKind.Allow);
2026-02-08 10:35:24 +01:00
_chromium.CoreWebView2.DOMContentLoaded -= DomLoaded;
2025-09-29 11:16:14 +02:00
_chromium.CoreWebView2.DOMContentLoaded += DomLoaded;
2026-02-08 10:35:24 +01:00
_chromium.CoreWebView2.WebMessageReceived -= ScriptMessageReceived;
2025-09-29 11:16:14 +02:00
_chromium.CoreWebView2.WebMessageReceived += ScriptMessageReceived;
_chromium.Source = new Uri("https://app.editor/editor.html");
2025-09-29 11:16:14 +02:00
}
private void DomLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
2025-09-29 11:16:14 +02:00
{
_domLoadedTask.TrySetResult(true);
_ = EnsureEditorInitializedAsync();
}
2026-02-08 10:35:24 +01:00
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{
using var document = JsonDocument.Parse(args.WebMessageAsJson);
if (!document.RootElement.TryGetProperty("type", out var typeElement) ||
!string.Equals(typeElement.GetString(), "state", StringComparison.OrdinalIgnoreCase))
2025-09-29 11:16:14 +02:00
{
return;
2025-09-29 11:16:14 +02:00
}
if (document.RootElement.TryGetProperty("state", out var stateElement))
2025-09-29 11:16:14 +02:00
{
var snapshot = DeserializeStateSnapshot(stateElement);
if (snapshot != null)
{
UpdateState(MapState(snapshot));
}
2025-09-29 11:16:14 +02:00
}
}
private EditorState MapState(EditorStateSnapshot snapshot)
2025-09-29 11:16:14 +02:00
{
return new EditorState
{
IsBold = snapshot.IsBold,
IsItalic = snapshot.IsItalic,
IsUnderline = snapshot.IsUnderline,
IsStrikethrough = snapshot.IsStrikethrough,
IsOrderedList = snapshot.IsOrderedList,
IsUnorderedList = snapshot.IsUnorderedList,
CanIndent = snapshot.CanIndent,
CanOutdent = snapshot.CanOutdent,
HasSelection = snapshot.HasSelection,
IsDarkMode = IsEditorDarkMode,
IsBuiltInToolbarVisible = IsEditorWebViewEditor,
IsSpellCheckEnabled = snapshot.IsSpellCheckEnabled,
Alignment = ParseAlignment(snapshot.Alignment),
FontFamily = string.IsNullOrWhiteSpace(snapshot.FontFamily) ? _preferencesService.ComposerFont : snapshot.FontFamily,
FontSize = snapshot.FontSize ?? _preferencesService.ComposerFontSize,
ParagraphStyle = string.IsNullOrWhiteSpace(snapshot.ParagraphStyle) ? "div" : snapshot.ParagraphStyle,
TextColor = snapshot.TextColor ?? string.Empty,
HighlightColor = snapshot.HighlightColor ?? string.Empty,
LineHeight = string.IsNullOrWhiteSpace(snapshot.LineHeight) ? "normal" : snapshot.LineHeight,
LinkUrl = snapshot.LinkUrl,
SelectedText = snapshot.SelectedText
};
}
2025-09-29 11:16:14 +02:00
private static EditorTextAlignment ParseAlignment(string? alignment)
{
return alignment?.ToLowerInvariant() switch
2025-09-29 11:16:14 +02:00
{
"center" => EditorTextAlignment.Center,
"right" => EditorTextAlignment.Right,
"justify" => EditorTextAlignment.Justify,
_ => EditorTextAlignment.Left
};
2025-09-29 11:16:14 +02:00
}
private static string SerializeLinkArgs(EditorLinkCommandArgs args)
{
var url = JsonSerializer.Serialize(args.Url, BasicTypesJsonContext.Default.String);
var text = JsonSerializer.Serialize(args.Text, BasicTypesJsonContext.Default.String);
var openInNewWindow = JsonSerializer.Serialize(args.OpenInNewWindow, BasicTypesJsonContext.Default.Boolean);
return $"{{\"url\":{url},\"text\":{text},\"openInNewWindow\":{openInNewWindow}}}";
}
private static string SerializeTableArgs(EditorTableCommandArgs args)
2025-09-29 11:16:14 +02:00
{
var rows = JsonSerializer.Serialize(args.Rows, BasicTypesJsonContext.Default.Int32);
var columns = JsonSerializer.Serialize(args.Columns, BasicTypesJsonContext.Default.Int32);
return $"{{\"rows\":{rows},\"columns\":{columns}}}";
}
2025-09-29 11:16:14 +02:00
private static EditorStateSnapshot? DeserializeStateSnapshot(string json)
{
using var document = JsonDocument.Parse(json);
return DeserializeStateSnapshot(document.RootElement);
}
2025-11-14 18:51:48 +01:00
private static EditorStateSnapshot? DeserializeStateSnapshot(JsonElement element)
{
if (element.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined)
2025-09-29 11:16:14 +02:00
{
return null;
2025-09-29 11:16:14 +02:00
}
return new EditorStateSnapshot
2025-09-29 11:16:14 +02:00
{
IsBold = GetBoolean(element, "bold"),
IsItalic = GetBoolean(element, "italic"),
IsUnderline = GetBoolean(element, "underline"),
IsStrikethrough = GetBoolean(element, "strikethrough"),
IsOrderedList = GetBoolean(element, "orderedList"),
IsUnorderedList = GetBoolean(element, "unorderedList"),
CanIndent = GetBoolean(element, "canIndent", true),
CanOutdent = GetBoolean(element, "canOutdent"),
HasSelection = GetBoolean(element, "hasSelection"),
IsSpellCheckEnabled = GetBoolean(element, "isSpellCheckEnabled", true),
Alignment = GetString(element, "alignment"),
FontFamily = GetString(element, "fontFamily"),
FontSize = GetNullableInt32(element, "fontSize"),
ParagraphStyle = GetString(element, "paragraphStyle"),
TextColor = GetString(element, "textColor"),
HighlightColor = GetString(element, "highlightColor"),
LineHeight = GetString(element, "lineHeight"),
LinkUrl = GetString(element, "linkUrl"),
SelectedText = GetString(element, "selectedText")
};
}
private static bool GetBoolean(JsonElement element, string propertyName, bool defaultValue = false)
{
if (element.TryGetProperty(propertyName, out var valueElement) && valueElement.ValueKind is JsonValueKind.True or JsonValueKind.False)
2025-09-29 11:16:14 +02:00
{
return valueElement.GetBoolean();
2025-09-29 11:16:14 +02:00
}
return defaultValue;
}
private static int? GetNullableInt32(JsonElement element, string propertyName)
{
if (element.TryGetProperty(propertyName, out var valueElement) && valueElement.ValueKind == JsonValueKind.Number && valueElement.TryGetInt32(out var value))
2025-09-29 11:16:14 +02:00
{
return value;
2025-09-29 11:16:14 +02:00
}
return null;
}
private static string? GetString(JsonElement element, string propertyName)
{
if (element.TryGetProperty(propertyName, out var valueElement) && valueElement.ValueKind == JsonValueKind.String)
2025-09-29 11:16:14 +02:00
{
return valueElement.GetString();
2025-09-29 11:16:14 +02:00
}
return null;
}
private static EditorCapabilities BuildCapabilities(IReadOnlyList<string> fonts)
{
return new EditorCapabilities
{
Fonts = fonts,
FontSizes = DefaultFontSizes,
TextColors = DefaultTextColors,
HighlightColors = DefaultHighlightColors,
ParagraphStyles = DefaultParagraphStyles,
LineHeights = DefaultLineHeights,
Alignments = DefaultAlignments
};
}
private EditorState GetCurrentStateOrDefault()
{
return CurrentState ?? CreateDefaultState();
}
private EditorState CreateDefaultState()
{
return new EditorState
{
IsDarkMode = IsEditorDarkMode,
IsBuiltInToolbarVisible = IsEditorWebViewEditor,
IsSpellCheckEnabled = true,
FontFamily = _preferencesService.ComposerFont,
FontSize = _preferencesService.ComposerFontSize,
ParagraphStyle = "div",
TextColor = string.Empty,
HighlightColor = string.Empty,
LineHeight = "normal"
};
}
private void UpdateState(EditorState newState)
{
if (newState == CurrentState)
2025-09-29 11:16:14 +02:00
{
return;
2025-09-29 11:16:14 +02:00
}
CurrentState = newState;
StateChanged?.Invoke(this, CurrentState);
}
private void UpdateCapabilities(EditorCapabilities newCapabilities)
{
if (newCapabilities == Capabilities)
2025-09-29 11:16:14 +02:00
{
return;
2025-09-29 11:16:14 +02:00
}
Capabilities = newCapabilities;
CapabilitiesChanged?.Invoke(this, Capabilities);
}
private void DetachChromium(WebView2 chromium, bool closeWebView)
{
chromium.CoreWebView2Initialized -= ChromiumInitialized;
if (chromium.CoreWebView2 != null)
2025-09-29 11:16:14 +02:00
{
chromium.CoreWebView2.DOMContentLoaded -= DomLoaded;
chromium.CoreWebView2.WebMessageReceived -= ScriptMessageReceived;
2025-09-29 11:16:14 +02:00
}
if (closeWebView)
2025-09-29 11:16:14 +02:00
{
chromium.Close();
2025-09-29 11:16:14 +02:00
}
}
private void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing && _chromium != null)
{
DetachChromium(_chromium, closeWebView: true);
_chromium = null;
2025-09-29 11:16:14 +02:00
}
2025-09-29 11:16:14 +02:00
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private sealed class EditorWebMessage
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("state")]
public EditorStateSnapshot? State { get; set; }
}
private sealed class EditorStateSnapshot
{
[JsonPropertyName("bold")]
public bool IsBold { get; set; }
[JsonPropertyName("italic")]
public bool IsItalic { get; set; }
[JsonPropertyName("underline")]
public bool IsUnderline { get; set; }
[JsonPropertyName("strikethrough")]
public bool IsStrikethrough { get; set; }
[JsonPropertyName("orderedList")]
public bool IsOrderedList { get; set; }
[JsonPropertyName("unorderedList")]
public bool IsUnorderedList { get; set; }
[JsonPropertyName("canIndent")]
public bool CanIndent { get; set; }
[JsonPropertyName("canOutdent")]
public bool CanOutdent { get; set; }
[JsonPropertyName("hasSelection")]
public bool HasSelection { get; set; }
[JsonPropertyName("isSpellCheckEnabled")]
public bool IsSpellCheckEnabled { get; set; } = true;
[JsonPropertyName("alignment")]
public string? Alignment { get; set; }
[JsonPropertyName("fontFamily")]
public string? FontFamily { get; set; }
[JsonPropertyName("fontSize")]
public int? FontSize { get; set; }
[JsonPropertyName("paragraphStyle")]
public string? ParagraphStyle { get; set; }
[JsonPropertyName("textColor")]
public string? TextColor { get; set; }
[JsonPropertyName("highlightColor")]
public string? HighlightColor { get; set; }
[JsonPropertyName("lineHeight")]
public string? LineHeight { get; set; }
[JsonPropertyName("linkUrl")]
public string? LinkUrl { get; set; }
[JsonPropertyName("selectedText")]
public string? SelectedText { get; set; }
}
2025-09-29 11:16:14 +02:00
}