Embedded images replaced with cid linked resources. (#313)

* Added logic to replace embedded images with linked resources

* Added alt text for images and replaced NewtonSoft with Text.Json

* Fix draft mime preparation

* Fix crashes for signatures without images.

---------

Co-authored-by: Burak Kaan Köse <bkaankose@outlook.com>
This commit is contained in:
Tiktack
2024-08-11 23:58:54 +02:00
committed by GitHub
parent 983bc21448
commit 5912adff93
23 changed files with 148 additions and 253 deletions

View File

@@ -1,22 +1,22 @@
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.AutoDiscovery namespace Wino.Core.Domain.Models.AutoDiscovery
{ {
public class AutoDiscoveryProviderSetting public class AutoDiscoveryProviderSetting
{ {
[JsonProperty("protocol")] [JsonPropertyName("protocol")]
public string Protocol { get; set; } public string Protocol { get; set; }
[JsonProperty("address")] [JsonPropertyName("address")]
public string Address { get; set; } public string Address { get; set; }
[JsonProperty("port")] [JsonPropertyName("port")]
public int Port { get; set; } public int Port { get; set; }
[JsonProperty("secure")] [JsonPropertyName("secure")]
public string Secure { get; set; } public string Secure { get; set; }
[JsonProperty("username")] [JsonPropertyName("username")]
public string Username { get; set; } public string Username { get; set; }
} }
} }

View File

@@ -1,19 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using System.Text.Json.Serialization;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
namespace Wino.Core.Domain.Models.AutoDiscovery namespace Wino.Core.Domain.Models.AutoDiscovery
{ {
public class AutoDiscoverySettings public class AutoDiscoverySettings
{ {
[JsonProperty("domain")] [JsonPropertyName("domain")]
public string Domain { get; set; } public string Domain { get; set; }
[JsonProperty("password")] [JsonPropertyName("password")]
public string Password { get; set; } public string Password { get; set; }
[JsonProperty("settings")] [JsonPropertyName("settings")]
public List<AutoDiscoveryProviderSetting> Settings { get; set; } public List<AutoDiscoveryProviderSetting> Settings { get; set; }
/// <summary> /// <summary>

View File

@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Reader;
public class ImageInfo
{
[JsonPropertyName("data")]
public string Data { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
}

View File

@@ -1,4 +1,4 @@
using Newtonsoft.Json; using System.Text.Json.Serialization;
namespace Wino.Core.Domain.Models.Reader namespace Wino.Core.Domain.Models.Reader
{ {
@@ -7,10 +7,10 @@ namespace Wino.Core.Domain.Models.Reader
/// </summary> /// </summary>
public class WebViewMessage public class WebViewMessage
{ {
[JsonProperty("type")] [JsonPropertyName("type")]
public string Type { get; set; } public string Type { get; set; }
[JsonProperty("value")] [JsonPropertyName("value")]
public string Value { get; set; } public string Value { get; set; }
} }
} }

View File

@@ -62,7 +62,6 @@
</PackageReference> </PackageReference>
<PackageReference Include="MimeKit" Version="4.7.1" /> <PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="MailKit" Version="4.7.1.1" /> <PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" /> <PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="System.Text.Json" Version="8.0.4" /> <PackageReference Include="System.Text.Json" Version="8.0.4" />
</ItemGroup> </ItemGroup>

View File

@@ -4,10 +4,10 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime; using System.Runtime.InteropServices.WindowsRuntime;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Uwp.Helpers; using Microsoft.Toolkit.Uwp.Helpers;
using Newtonsoft.Json;
using Windows.Storage; using Windows.Storage;
using Windows.UI; using Windows.UI;
using Windows.UI.ViewManagement; using Windows.UI.ViewManagement;
@@ -406,7 +406,7 @@ namespace Wino.Services
// Save metadata. // Save metadata.
var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting); var metadataFile = await themeFolder.CreateFileAsync($"{newTheme.Id}.json", CreationCollisionOption.ReplaceExisting);
var serialized = JsonConvert.SerializeObject(newTheme); var serialized = JsonSerializer.Serialize(newTheme);
await FileIO.WriteTextAsync(metadataFile, serialized); await FileIO.WriteTextAsync(metadataFile, serialized);
return newTheme; return newTheme;
@@ -438,7 +438,7 @@ namespace Wino.Services
{ {
var fileContent = await FileIO.ReadTextAsync(file); var fileContent = await FileIO.ReadTextAsync(file);
return JsonConvert.DeserializeObject<CustomThemeMetadata>(fileContent); return JsonSerializer.Deserialize<CustomThemeMetadata>(fileContent);
} }
public string GetSystemAccentColorHex() public string GetSystemAccentColorHex()

View File

@@ -1,8 +1,8 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Text.Json.Nodes;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -58,14 +58,14 @@ namespace Wino.Core.Authenticators
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed); throw new GoogleAuthenticationException(Translator.Exception_GoogleAuthorizationCodeExchangeFailed);
var parsed = JObject.Parse(responseString); var parsed = JsonNode.Parse(responseString).AsObject();
if (parsed.ContainsKey("error")) if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error"]["message"].Value<string>()); throw new GoogleAuthenticationException(parsed["error"]["message"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>(); var accessToken = parsed["access_token"].GetValue<string>();
var refreshToken = parsed["refresh_token"].Value<string>(); var refreshToken = parsed["refresh_token"].GetValue<string>();
var expiresIn = parsed["expires_in"].Value<long>(); var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn); var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
@@ -76,12 +76,12 @@ namespace Wino.Core.Authenticators
var userinfoResponse = await client.GetAsync(UserInfoEndpoint); var userinfoResponse = await client.GetAsync(UserInfoEndpoint);
string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync(); string userinfoResponseContent = await userinfoResponse.Content.ReadAsStringAsync();
var parsedUserInfo = JObject.Parse(userinfoResponseContent); var parsedUserInfo = JsonNode.Parse(userinfoResponseContent).AsObject();
if (parsedUserInfo.ContainsKey("error")) if (parsedUserInfo.ContainsKey("error"))
throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].Value<string>()); throw new GoogleAuthenticationException(parsedUserInfo["error"]["message"].GetValue<string>());
var username = parsedUserInfo["emailAddress"].Value<string>(); var username = parsedUserInfo["emailAddress"].GetValue<string>();
return new TokenInformation() return new TokenInformation()
{ {
@@ -166,13 +166,13 @@ namespace Wino.Core.Authenticators
string responseString = await response.Content.ReadAsStringAsync(); string responseString = await response.Content.ReadAsStringAsync();
var parsed = JObject.Parse(responseString); var parsed = JsonNode.Parse(responseString).AsObject();
// TODO: Error parsing is incorrect. // TODO: Error parsing is incorrect.
if (parsed.ContainsKey("error")) if (parsed.ContainsKey("error"))
throw new GoogleAuthenticationException(parsed["error_description"].Value<string>()); throw new GoogleAuthenticationException(parsed["error_description"].GetValue<string>());
var accessToken = parsed["access_token"].Value<string>(); var accessToken = parsed["access_token"].GetValue<string>();
string activeRefreshToken = refresh_token; string activeRefreshToken = refresh_token;
@@ -182,10 +182,10 @@ namespace Wino.Core.Authenticators
if (parsed.ContainsKey("refresh_token")) if (parsed.ContainsKey("refresh_token"))
{ {
activeRefreshToken = parsed["refresh_token"].Value<string>(); activeRefreshToken = parsed["refresh_token"].GetValue<string>();
} }
var expiresIn = parsed["expires_in"].Value<long>(); var expiresIn = parsed["expires_in"].GetValue<long>();
var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn); var expirationDate = DateTime.UtcNow.AddSeconds(expiresIn);
return new TokenInformationBase() return new TokenInformationBase()

View File

@@ -1,9 +1,12 @@
using System.IO; using System;
using System.IO;
using System.Text; using System.Text;
using Google.Apis.Gmail.v1.Data; using Google.Apis.Gmail.v1.Data;
using HtmlAgilityPack;
using MimeKit; using MimeKit;
using MimeKit.IO; using MimeKit.IO;
using MimeKit.IO.Filters; using MimeKit.IO.Filters;
using MimeKit.Utils;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
@@ -48,5 +51,71 @@ namespace Wino.Core.Extensions
return new AddressInformation() { Name = address.Name, Address = address.Address }; return new AddressInformation() { Name = address.Name, Address = address.Address };
} }
/// <summary>
/// Sets html body replacing base64 images with cid linked resources.
/// Updates text body based on html.
/// </summary>
/// <param name="bodyBuilder">Body builder.</param>
/// <param name="htmlContent">Html content that can have embedded images.</param>
/// <returns>Body builder with set HtmlBody.</returns>
public static BodyBuilder SetHtmlBody(this BodyBuilder bodyBuilder, string htmlContent)
{
if (string.IsNullOrEmpty(htmlContent)) return bodyBuilder;
var doc = new HtmlDocument();
doc.LoadHtml(htmlContent);
var imgNodes = doc.DocumentNode.SelectNodes("//img");
if (imgNodes != null)
{
foreach (var node in imgNodes)
{
var src = node.GetAttributeValue("src", string.Empty);
if (string.IsNullOrEmpty(src)) continue;
if (!src.StartsWith("data:image"))
{
continue;
}
var parts = src.Substring(11).Split([";base64,"], StringSplitOptions.None);
string mimeType = parts[0];
string base64Content = parts[1];
var alt = node.GetAttributeValue("alt", $"Embedded_Image.{mimeType}");
// Convert the base64 content to binary data
byte[] imageData = Convert.FromBase64String(base64Content);
// Create a new linked resource as MimePart
var image = new MimePart("image", mimeType)
{
ContentId = MimeUtils.GenerateMessageId(),
Content = new MimeContent(new MemoryStream(imageData)),
ContentDisposition = new ContentDisposition(ContentDisposition.Inline),
ContentDescription = alt.Replace(" ", "_"),
FileName = alt,
ContentTransferEncoding = ContentEncoding.Base64
};
bodyBuilder.LinkedResources.Add(image);
node.SetAttributeValue("src", $"cid:{image.ContentId}");
}
}
bodyBuilder.HtmlBody = doc.DocumentNode.InnerHtml;
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
{
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
}
return bodyBuilder;
}
} }
} }

View File

@@ -1,36 +0,0 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
namespace Wino.Core.Http
{
/// <summary>
/// We need to generate HttpRequestMessage for batch requests, and sometimes we need to
/// serialize content as json. However, some of the fields like 'ODataType' must be ignored
/// in order PATCH requests to succeed. Therefore Microsoft account synchronizer uses
/// special JsonSerializerSettings for ignoring some of the properties.
/// </summary>
public class MicrosoftJsonContractResolver : DefaultContractResolver
{
private readonly HashSet<string> ignoreProps = new HashSet<string>()
{
"ODataType"
};
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
JsonProperty property = base.CreateProperty(member, memberSerialization);
if (ignoreProps.Contains(property.PropertyName))
{
property.ShouldSerialize = _ => false;
}
return property;
}
}
}

View File

@@ -1,15 +1,13 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests namespace Wino.Core.Domain.Models.Requests
{ {
/// <summary> /// <summary>
/// Bundle that encapsulates batch request and native request without a response. /// Bundle that encapsulates batch request and native request without a response.
/// </summary> /// </summary>
@@ -43,7 +41,7 @@ namespace Wino.Core.Domain.Models.Requests
{ {
var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); var content = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
return JsonConvert.DeserializeObject<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization"); return JsonSerializer.Deserialize<TResponse>(content) ?? throw new InvalidOperationException("Invalid Http Response Deserialization");
} }
public override string ToString() public override string ToString()

View File

@@ -1,7 +1,7 @@
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
using Serilog; using Serilog;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.AutoDiscovery; using Wino.Core.Domain.Models.AutoDiscovery;
@@ -43,7 +43,7 @@ namespace Wino.Core.Services
{ {
var content = await response.Content.ReadAsStringAsync(); var content = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<AutoDiscoverySettings>(content); return JsonSerializer.Deserialize<AutoDiscoverySettings>(content);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -636,10 +636,7 @@ namespace Wino.Core.Services
_ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature), _ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature),
}; };
if (!string.IsNullOrEmpty(builder.HtmlBody)) builder.SetHtmlBody(builder.HtmlBody);
{
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
}
message.Body = builder.ToMessageBody(); message.Body = builder.ToMessageBody();

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Newtonsoft.Json;
using Serilog; using Serilog;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -44,7 +44,7 @@ namespace Wino.Core.Services
var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false); var stremValue = await new StreamReader(resourceStream).ReadToEndAsync().ConfigureAwait(false);
var translationLookups = JsonConvert.DeserializeObject<Dictionary<string, string>>(stremValue); var translationLookups = JsonSerializer.Deserialize<Dictionary<string, string>>(stremValue);
// Insert new translation key-value pairs. // Insert new translation key-value pairs.
// Overwrite existing values for the same keys. // Overwrite existing values for the same keys.

View File

@@ -31,7 +31,6 @@
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" /> <PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
<PackageReference Include="MimeKit" Version="4.7.1" /> <PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="morelinq" Version="4.1.0" /> <PackageReference Include="morelinq" Version="4.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" /> <PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Exceptions" Version="8.4.0" /> <PackageReference Include="Serilog.Exceptions" Version="8.4.0" />

View File

@@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using MimeKit; using MimeKit;
using MimeKit.Utils;
using Wino.Core.Domain; using Wino.Core.Domain;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
@@ -237,12 +238,9 @@ namespace Wino.Mail.ViewModels
{ {
if (GetHTMLBodyFunction != null) if (GetHTMLBodyFunction != null)
{ {
bodyBuilder.HtmlBody = await GetHTMLBodyFunction(); bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
} }
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
bodyBuilder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(bodyBuilder.HtmlBody);
if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null) if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null)
CurrentMimeMessage.Body = bodyBuilder.ToMessageBody(); CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
} }
@@ -401,7 +399,6 @@ namespace Wino.Mail.ViewModels
{ {
DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning); DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning);
} }
catch (ComposerMimeNotFoundException) catch (ComposerMimeNotFoundException)
{ {
DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error); DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);

View File

@@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Helpers;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
using Windows.Storage; using Windows.Storage;
using Windows.UI.Xaml; using Windows.UI.Xaml;
@@ -9,7 +10,6 @@ using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Animation; using Windows.UI.Xaml.Media.Animation;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Services; using Wino.Core.Services;
using Wino.Helpers;
using Wino.Views; using Wino.Views;
namespace Wino.Activation namespace Wino.Activation

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;
using Windows.UI.ViewManagement.Core; using Windows.UI.ViewManagement.Core;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
@@ -78,7 +78,7 @@ namespace Wino.Dialogs
{ {
var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();"); var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();");
return JsonConvert.DeserializeObject<string>(editorContent); return JsonSerializer.Deserialize<string>(editorContent);
}); });
var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>(); var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>();
@@ -193,7 +193,7 @@ namespace Wino.Dialogs
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";
@@ -327,7 +327,7 @@ namespace Wino.Dialogs
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{ {
var change = JsonConvert.DeserializeObject<WebViewMessage>(args.WebMessageAsJson); var change = JsonSerializer.Deserialize<WebViewMessage>(args.WebMessageAsJson);
if (change.Type == "bold") if (change.Type == "bold")
{ {

View File

@@ -1,25 +0,0 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Wino.Helpers
{
public static class JsonHelpers
{
public static async Task<T> ToObjectAsync<T>(string value)
{
return await Task.Run<T>(() =>
{
return JsonConvert.DeserializeObject<T>(value);
});
}
public static async Task<string> StringifyAsync(object value)
{
return await Task.Run<string>(() =>
{
return JsonConvert.SerializeObject(value);
});
}
}
}

View File

@@ -1,119 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
namespace Wino.Helpers
{
// Use these extension methods to store and retrieve local and roaming app data
// More details regarding storing and retrieving app data at https://docs.microsoft.com/windows/uwp/app-settings/store-and-retrieve-app-data
public static class SettingsStorageExtensions
{
private const string FileExtension = ".json";
public static bool IsRoamingStorageAvailable(this ApplicationData appData)
{
return appData.RoamingStorageQuota == 0;
}
public static async Task SaveAsync<T>(this StorageFolder folder, string name, T content)
{
var file = await folder.CreateFileAsync(GetFileName(name), CreationCollisionOption.ReplaceExisting);
var fileContent = await JsonHelpers.StringifyAsync(content);
await FileIO.WriteTextAsync(file, fileContent);
}
public static async Task<T> ReadAsync<T>(this StorageFolder folder, string name)
{
if (!File.Exists(Path.Combine(folder.Path, GetFileName(name))))
{
return default;
}
var file = await folder.GetFileAsync($"{name}.json");
var fileContent = await FileIO.ReadTextAsync(file);
return await JsonHelpers.ToObjectAsync<T>(fileContent);
}
public static async Task SaveAsync<T>(this ApplicationDataContainer settings, string key, T value)
{
settings.SaveString(key, await JsonHelpers.StringifyAsync(value));
}
public static void SaveString(this ApplicationDataContainer settings, string key, string value)
{
settings.Values[key] = value;
}
public static async Task<T> ReadAsync<T>(this ApplicationDataContainer settings, string key)
{
object obj = null;
if (settings.Values.TryGetValue(key, out obj))
{
return await JsonHelpers.ToObjectAsync<T>((string)obj);
}
return default;
}
public static async Task<StorageFile> SaveFileAsync(this StorageFolder folder, byte[] content, string fileName, CreationCollisionOption options = CreationCollisionOption.ReplaceExisting)
{
if (content == null)
{
throw new ArgumentNullException(nameof(content));
}
if (string.IsNullOrEmpty(fileName))
{
throw new ArgumentException("File name is null or empty. Specify a valid file name", nameof(fileName));
}
var storageFile = await folder.CreateFileAsync(fileName, options);
await FileIO.WriteBytesAsync(storageFile, content);
return storageFile;
}
public static async Task<byte[]> ReadFileAsync(this StorageFolder folder, string fileName)
{
var item = await folder.TryGetItemAsync(fileName).AsTask().ConfigureAwait(false);
if ((item != null) && item.IsOfType(StorageItemTypes.File))
{
var storageFile = await folder.GetFileAsync(fileName);
byte[] content = await storageFile.ReadBytesAsync();
return content;
}
return null;
}
public static async Task<byte[]> ReadBytesAsync(this StorageFile file)
{
if (file != null)
{
using (IRandomAccessStream stream = await file.OpenReadAsync())
{
using (var reader = new DataReader(stream.GetInputStreamAt(0)))
{
await reader.LoadAsync((uint)stream.Size);
var bytes = new byte[stream.Size];
reader.ReadBytes(bytes);
return bytes;
}
}
}
return null;
}
private static string GetFileName(string name)
{
return string.Concat(name, FileExtension);
}
}
}

View File

@@ -48,7 +48,7 @@ function initializeJodit(fonts, defaultComposerFont, defaultComposerFontSize, de
const reader = new FileReader(); const reader = new FileReader();
reader.onload = function (event) { reader.onload = function (event) {
const base64Image = event.target.result; const base64Image = event.target.result;
insertImages([base64Image]); insertImages([{ data: base64Image, name: file.name }]);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
@@ -121,8 +121,8 @@ function toggleToolbar(enable) {
} }
} }
function insertImages(images) { function insertImages(imagesInfo) {
images.forEach(image => { imagesInfo.forEach(imageInfo => {
editor.selection.insertHTML(`<img src="${image}" alt="Embedded Image">`); editor.selection.insertHTML(`<img src="${imageInfo.data}" alt="${imageInfo.name}">`);
}); });
}; };

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
@@ -13,7 +14,6 @@ using Microsoft.Toolkit.Uwp.Helpers;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using MimeKit; using MimeKit;
using Newtonsoft.Json;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.Foundation; using Windows.Foundation;
using Windows.Storage; using Windows.Storage;
@@ -167,7 +167,7 @@ namespace Wino.Views
foreach (var file in files) foreach (var file in files)
{ {
if (ValidateImageFile(file)) if (IsValidImageFile(file))
{ {
isValid = true; isValid = true;
} }
@@ -200,15 +200,21 @@ namespace Wino.Views
var storageItems = await e.DataView.GetStorageItemsAsync(); var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>(); var files = storageItems.OfType<StorageFile>();
var imageDataURLs = new List<string>(); var imagesInformation = new List<ImageInfo>();
foreach (var file in files) foreach (var file in files)
{ {
if (ValidateImageFile(file)) if (IsValidImageFile(file))
imageDataURLs.Add(await GetDataURL(file)); {
imagesInformation.Add(new ImageInfo
{
Data = await GetDataURL(file),
Name = file.Name
});
}
} }
await InvokeScriptSafeAsync($"insertImages({JsonConvert.SerializeObject(imageDataURLs)});"); await InvokeScriptSafeAsync($"insertImages({JsonSerializer.Serialize(imagesInformation)});");
} }
} }
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state. // State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
@@ -240,7 +246,7 @@ namespace Wino.Views
} }
} }
private bool ValidateImageFile(StorageFile file) private bool IsValidImageFile(StorageFile file)
{ {
string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" }; string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" };
var fileType = file.FileType.ToLower(); var fileType = file.FileType.ToLower();
@@ -321,7 +327,7 @@ namespace Wino.Views
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";
@@ -463,7 +469,7 @@ namespace Wino.Views
{ {
var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();"); var editorContent = await InvokeScriptSafeAsync("GetHTMLContent();");
return JsonConvert.DeserializeObject<string>(editorContent); return JsonSerializer.Deserialize<string>(editorContent);
}); });
var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>(); var underlyingThemeService = App.Current.Services.GetService<IUnderlyingThemeService>();
@@ -487,7 +493,7 @@ namespace Wino.Views
private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) private void ScriptMessageReceived(CoreWebView2 sender, CoreWebView2WebMessageReceivedEventArgs args)
{ {
var change = JsonConvert.DeserializeObject<WebViewMessage>(args.WebMessageAsJson); var change = JsonSerializer.Deserialize<WebViewMessage>(args.WebMessageAsJson);
if (change.Type == "bold") if (change.Type == "bold")
{ {

View File

@@ -1,12 +1,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.AppCenter.Crashes; using Microsoft.AppCenter.Crashes;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Newtonsoft.Json;
using Windows.System; using Windows.System;
using Windows.UI.Xaml; using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Controls;
@@ -70,7 +70,7 @@ namespace Wino.Views
string script = functionName + "("; string script = functionName + "(";
for (int i = 0; i < parameters.Length; i++) for (int i = 0; i < parameters.Length; i++)
{ {
script += JsonConvert.SerializeObject(parameters[i]); script += JsonSerializer.Serialize(parameters[i]);
if (i < parameters.Length - 1) if (i < parameters.Length - 1)
{ {
script += ", "; script += ", ";

View File

@@ -298,14 +298,12 @@
<Compile Include="Dialogs\AccountCreationDialog.xaml.cs"> <Compile Include="Dialogs\AccountCreationDialog.xaml.cs">
<DependentUpon>AccountCreationDialog.xaml</DependentUpon> <DependentUpon>AccountCreationDialog.xaml</DependentUpon>
</Compile> </Compile>
<Compile Include="Helpers\JsonHelpers.cs" />
<Compile Include="Extensions\AnimationExtensions.cs" /> <Compile Include="Extensions\AnimationExtensions.cs" />
<Compile Include="Extensions\CompositionExtensions.Implicit.cs" /> <Compile Include="Extensions\CompositionExtensions.Implicit.cs" />
<Compile Include="Extensions\CompositionExtensions.Size.cs" /> <Compile Include="Extensions\CompositionExtensions.Size.cs" />
<Compile Include="Extensions\CompositionEnums.cs" /> <Compile Include="Extensions\CompositionEnums.cs" />
<Compile Include="Extensions\EnumerableExtensions.cs" /> <Compile Include="Extensions\EnumerableExtensions.cs" />
<Compile Include="Extensions\UtilExtensions.cs" /> <Compile Include="Extensions\UtilExtensions.cs" />
<Compile Include="Helpers\SettingsStorageExtensions.cs" />
<Compile Include="MenuFlyouts\FilterMenuFlyout.cs" /> <Compile Include="MenuFlyouts\FilterMenuFlyout.cs" />
<Compile Include="Controls\ImagePreviewControl.cs" /> <Compile Include="Controls\ImagePreviewControl.cs" />
<Compile Include="Controls\MailItemDisplayInformationControl.xaml.cs"> <Compile Include="Controls\MailItemDisplayInformationControl.xaml.cs">