Added drag and drop for images

Fixed dropzones visual states
Corrected border radius
Fixed null reference exception when event dispatched when chromium still not initialized
This commit is contained in:
Aleh Khantsevich
2024-07-07 01:42:12 +02:00
parent 8cde976358
commit 5923d72803
7 changed files with 167 additions and 576 deletions

View File

@@ -52,6 +52,7 @@
"ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.", "ClipboardTextCopyFailed_Message": "Failed to copy {0} to clipboard.",
"ComposerToPlaceholder": "click enter to input addresses", "ComposerToPlaceholder": "click enter to input addresses",
"ComposerAttachmentsDropZone_Message": "Drop your files here", "ComposerAttachmentsDropZone_Message": "Drop your files here",
"ComposerImagesDropZone_Message": "Drop your images here",
"ComposerAttachmentsDragDropAttach_Message": "Attach", "ComposerAttachmentsDragDropAttach_Message": "Attach",
"CustomThemeBuilder_AccentColorDescription": "Set custom accent color if you wish. Not selecting a color will use your Windows accent color.", "CustomThemeBuilder_AccentColorDescription": "Set custom accent color if you wish. Not selecting a color will use your Windows accent color.",
"CustomThemeBuilder_AccentColorTitle": "Accent color", "CustomThemeBuilder_AccentColorTitle": "Accent color",

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,10 @@ namespace Wino.Mail.ViewModels
private bool isDraggingOverComposerGrid; private bool isDraggingOverComposerGrid;
[ObservableProperty] [ObservableProperty]
private bool isDraggingOverDropZone; private bool isDraggingOverFilesDropZone;
[ObservableProperty]
private bool isDraggingOverImagesDropZone;
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>(); public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>();

View File

@@ -24,7 +24,7 @@ imageInput.addEventListener('change', () => {
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;
editor.selection.insertHTML(`<img src="${base64Image}" alt="Embedded Image">`); insertImages([base64Image]);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
@@ -96,3 +96,9 @@ function toggleToolbar(enable) {
toolbar.style.display = 'none'; toolbar.style.display = 'none';
} }
} }
function insertImages(images) {
images.forEach(image => {
editor.selection.insertHTML(`<img src="${image}" alt="Embedded Image">`);
});
};

View File

@@ -595,7 +595,7 @@
Visibility="{x:Bind ViewModel.IsDraggingOverComposerGrid, Mode=OneWay}"> Visibility="{x:Bind ViewModel.IsDraggingOverComposerGrid, Mode=OneWay}">
<Grid Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" CornerRadius="9"> <Grid Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" CornerRadius="9">
<Rectangle <Rectangle
x:Name="DropZoneBorder" x:Name="FilesDropZoneBorder"
Fill="Transparent" Fill="Transparent"
Opacity="0.5" Opacity="0.5"
RadiusX="9" RadiusX="9"
@@ -604,7 +604,7 @@
StrokeDashArray="3,4" StrokeDashArray="3,4"
StrokeThickness="2" /> StrokeThickness="2" />
<TextBlock <TextBlock
x:Name="DropZoneText" x:Name="FilesDropZoneText"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="20" FontSize="20"
@@ -613,21 +613,6 @@
Text="{x:Bind domain:Translator.ComposerAttachmentsDropZone_Message}" /> Text="{x:Bind domain:Translator.ComposerAttachmentsDropZone_Message}" />
</Grid> </Grid>
</Grid> </Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="DropZoneState">
<VisualState x:Name="Hovered">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.IsDraggingOverDropZone, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="DropZoneText.Opacity" Value="1" />
<Setter Target="DropZoneBorder.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="NotHovered" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid> </Grid>
</Border> </Border>
<Border <Border
@@ -635,13 +620,67 @@
Background="{ThemeResource WinoContentZoneBackgroud}" Background="{ThemeResource WinoContentZoneBackgroud}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}" BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1" BorderThickness="1"
CornerRadius="10"> CornerRadius="7">
<Grid> <Grid Margin="1" CornerRadius="7">
<Grid Background="White" Visibility="{x:Bind IsComposerDarkMode, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" /> <Grid Background="White" Visibility="{x:Bind IsComposerDarkMode, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
<muxc:WebView2 x:Name="Chromium" /> <muxc:WebView2 x:Name="Chromium" />
<!-- Dropzone for images -->
<Grid
AllowDrop="True"
DragEnter="OnImageDropGridDragEnter"
DragLeave="OnImageDropGridDragLeave"
Drop="OnImageDropGridImageDropped"
Visibility="{x:Bind ViewModel.IsDraggingOverComposerGrid, Mode=OneWay}">
<Grid Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}" CornerRadius="9">
<Rectangle
x:Name="ImagesDropZoneBorder"
Fill="Transparent"
Opacity="0.5"
RadiusX="9"
RadiusY="9"
Stroke="{ThemeResource TextFillColorPrimaryBrush}"
StrokeDashArray="3,4"
StrokeThickness="2" />
<TextBlock
x:Name="ImagesDropZoneText"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="20"
FontWeight="SemiBold"
Opacity="0.5"
Text="{x:Bind domain:Translator.ComposerImagesDropZone_Message}" />
</Grid>
</Grid>
</Grid> </Grid>
</Border> </Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FilesDropZoneState">
<VisualState x:Name="FilesDropZoneHovered">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.IsDraggingOverFilesDropZone, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="FilesDropZoneText.Opacity" Value="1" />
<Setter Target="FilesDropZoneBorder.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="FilesDropZoneNotHovered" />
</VisualStateGroup>
<VisualStateGroup x:Name="ImagesDropZoneState">
<VisualState x:Name="ImagesDropZoneHovered">
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind ViewModel.IsDraggingOverImagesDropZone, Mode=OneWay}" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ImagesDropZoneText.Opacity" Value="1" />
<Setter Target="ImagesDropZoneBorder.Opacity" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="ImagesDropZoneNotHovered" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid> </Grid>

View File

@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
using CommunityToolkit.WinUI.Controls; using CommunityToolkit.WinUI.Controls;
using EmailValidation; using EmailValidation;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
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;
@@ -122,7 +123,7 @@ namespace Wino.Views
private void OnFileDropGridDragOver(object sender, DragEventArgs e) private void OnFileDropGridDragOver(object sender, DragEventArgs e)
{ {
ViewModel.IsDraggingOverDropZone = true; ViewModel.IsDraggingOverFilesDropZone = true;
e.AcceptedOperation = DataPackageOperation.Copy; e.AcceptedOperation = DataPackageOperation.Copy;
e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message; e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message;
@@ -133,10 +134,12 @@ namespace Wino.Views
private void OnFileDropGridDragLeave(object sender, DragEventArgs e) private void OnFileDropGridDragLeave(object sender, DragEventArgs e)
{ {
ViewModel.IsDraggingOverDropZone = false; ViewModel.IsDraggingOverFilesDropZone = false;
} }
private async void OnFileDropGridFileDropped(object sender, DragEventArgs e) private async void OnFileDropGridFileDropped(object sender, DragEventArgs e)
{
try
{ {
if (e.DataView.Contains(StandardDataFormats.StorageItems)) if (e.DataView.Contains(StandardDataFormats.StorageItems))
{ {
@@ -145,9 +148,80 @@ namespace Wino.Views
await AttachFiles(files); await AttachFiles(files);
} }
}
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
finally
{
ViewModel.IsDraggingOverComposerGrid = false; ViewModel.IsDraggingOverComposerGrid = false;
ViewModel.IsDraggingOverDropZone = false; ViewModel.IsDraggingOverFilesDropZone = false;
}
}
private void OnImageDropGridDragEnter(object sender, DragEventArgs e)
{
bool isValid = false;
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
// We can't use async/await here because DragUIOverride becomes inaccessible.
// https://github.com/microsoft/microsoft-ui-xaml/issues/9296
var files = e.DataView.GetStorageItemsAsync().GetAwaiter().GetResult().OfType<StorageFile>();
foreach (var file in files)
{
if (ValidateImageFile(file))
{
isValid = true;
}
}
}
e.AcceptedOperation = isValid ? DataPackageOperation.Copy : DataPackageOperation.None;
if (isValid)
{
ViewModel.IsDraggingOverImagesDropZone = true;
e.DragUIOverride.Caption = Translator.ComposerAttachmentsDragDropAttach_Message;
e.DragUIOverride.IsCaptionVisible = true;
e.DragUIOverride.IsGlyphVisible = true;
e.DragUIOverride.IsContentVisible = true;
}
}
private void OnImageDropGridDragLeave(object sender, DragEventArgs e)
{
ViewModel.IsDraggingOverImagesDropZone = false;
}
private async void OnImageDropGridImageDropped(object sender, DragEventArgs e)
{
try
{
if (e.DataView.Contains(StandardDataFormats.StorageItems))
{
var storageItems = await e.DataView.GetStorageItemsAsync();
var files = storageItems.OfType<StorageFile>();
var imageDataURLs = new List<string>();
foreach (var file in files)
{
if (ValidateImageFile(file))
imageDataURLs.Add(await GetDataURL(file));
}
await InvokeScriptSafeAsync($"insertImages({JsonConvert.SerializeObject(imageDataURLs)});");
}
}
// State should be reset even when an exception occurs, otherwise the UI will be stuck in a dragging state.
finally
{
ViewModel.IsDraggingOverComposerGrid = false;
ViewModel.IsDraggingOverImagesDropZone = false;
}
static async Task<string> GetDataURL(StorageFile file)
{
return $"data:image/{file.FileType.Replace(".", "")};base64,{Convert.ToBase64String(await file.ReadBytesAsync())}";
}
} }
private async Task AttachFiles(IEnumerable<StorageFile> files) private async Task AttachFiles(IEnumerable<StorageFile> files)
@@ -166,6 +240,14 @@ namespace Wino.Views
} }
} }
private bool ValidateImageFile(StorageFile file)
{
string[] allowedTypes = new string[] { ".jpg", ".jpeg", ".png" };
var fileType = file.FileType.ToLower();
return allowedTypes.Contains(fileType);
}
private async void BoldButtonClicked(object sender, RoutedEventArgs e) private async void BoldButtonClicked(object sender, RoutedEventArgs e)
{ {
await InvokeScriptSafeAsync("editor.execCommand('bold')"); await InvokeScriptSafeAsync("editor.execCommand('bold')");
@@ -254,7 +336,7 @@ namespace Wino.Views
{ {
try try
{ {
return await Chromium.ExecuteScriptAsync(function); return await Chromium?.ExecuteScriptAsync(function);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -284,17 +366,6 @@ namespace Wino.Views
await FocusEditorAsync(); await FocusEditorAsync();
} }
private async Task<string> TryGetSelectedTextAsync()
{
try
{
return await Chromium.ExecuteScriptAsync("getSelectedText();");
}
catch (Exception) { }
return string.Empty;
}
public async Task UpdateEditorThemeAsync() public async Task UpdateEditorThemeAsync()
{ {
await DOMLoadedTask.Task; await DOMLoadedTask.Task;

View File

@@ -371,7 +371,7 @@
Grid.Row="1" Grid.Row="1"
Background="{ThemeResource WinoContentZoneBackgroud}" Background="{ThemeResource WinoContentZoneBackgroud}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}" BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="0" BorderThickness="1"
CornerRadius="7"> CornerRadius="7">
<Grid Margin="1" CornerRadius="7"> <Grid Margin="1" CornerRadius="7">
<Grid Background="White" Visibility="{x:Bind IsDarkEditor, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" /> <Grid Background="White" Visibility="{x:Bind IsDarkEditor, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />