From 96c98a69878978ed99f449e9851710917320ba92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Wed, 27 Nov 2024 19:49:10 +0100 Subject: [PATCH] New attachment templates that support saving and opening attachment when composing message. --- .../Interfaces/IDialogServiceBase.cs | 3 + Wino.Core.Domain/Models/Common/SharedFile.cs | 14 ++++ .../Translations/en_US/resources.json | 2 + .../Extensions/StorageFileExtensions.cs | 16 +++++ Wino.Core.UWP/Services/DialogServiceBase.cs | 64 ++++++++++++++++++- Wino.Core.UWP/Wino.Core.UWP.csproj | 1 + Wino.Mail.ViewModels/ComposePageViewModel.cs | 49 ++++++++++++++ .../Data/MailAttachmentViewModel.cs | 11 ++-- Wino.Mail/Extensions/MimeKitExtensions.cs | 20 ------ Wino.Mail/Views/ComposePage.xaml | 44 ++++++++++--- Wino.Mail/Views/ComposePage.xaml.cs | 29 +-------- Wino.Mail/Wino.Mail.csproj | 5 +- 12 files changed, 196 insertions(+), 62 deletions(-) create mode 100644 Wino.Core.Domain/Models/Common/SharedFile.cs create mode 100644 Wino.Core.UWP/Extensions/StorageFileExtensions.cs delete mode 100644 Wino.Mail/Extensions/MimeKitExtensions.cs diff --git a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs index 075616dd..901582b9 100644 --- a/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs +++ b/Wino.Core.Domain/Interfaces/IDialogServiceBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Common; namespace Wino.Core.Domain.Interfaces { @@ -25,5 +26,7 @@ namespace Wino.Core.Domain.Interfaces Task ShowCustomThemeBuilderDialogAsync(); Task ShowAccountProviderSelectionDialogAsync(List availableProviders); IAccountCreationDialog GetAccountCreationDialog(MailProviderType type); + Task> PickFilesAsync(params object[] typeFilters); + Task PickFilePathAsync(string saveFileName); } } diff --git a/Wino.Core.Domain/Models/Common/SharedFile.cs b/Wino.Core.Domain/Models/Common/SharedFile.cs new file mode 100644 index 00000000..bace85b3 --- /dev/null +++ b/Wino.Core.Domain/Models/Common/SharedFile.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace Wino.Core.Domain.Models.Common +{ + /// + /// Abstraction for StorageFile + /// + /// Full path of the file. + /// Content + public record SharedFile(string FullFilePath, byte[] Data) + { + public string FileName => Path.GetFileName(FullFilePath); + } +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 01c4dadb..db5fe7ce 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -261,6 +261,8 @@ "Info_BackgroundExecutionDeniedTitle": "Denied Background Execution", "Info_BackgroundExecutionUnknownErrorMessage": "Unknown exception occurred when registering background synchronizer.", "Info_BackgroundExecutionUnknownErrorTitle": "Background Execution Failure", + "Info_FailedToOpenFileTitle": "Failed to launch file.", + "Info_FailedToOpenFileMessage": "File might be removed from the disk.", "Info_ComposerMissingMIMEMessage": "Couldn't locate the MIME file. Synchronizing may help.", "Info_ComposerMissingMIMETitle": "Failed", "Info_ContactExistsMessage": "This contact is already in the recipient list.", diff --git a/Wino.Core.UWP/Extensions/StorageFileExtensions.cs b/Wino.Core.UWP/Extensions/StorageFileExtensions.cs new file mode 100644 index 00000000..d61317e1 --- /dev/null +++ b/Wino.Core.UWP/Extensions/StorageFileExtensions.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp.Helpers; +using Wino.Core.Domain.Models.Common; + +namespace Wino.Core.UWP.Extensions +{ + public static class StorageFileExtensions + { + public static async Task ToSharedFileAsync(this Windows.Storage.StorageFile storageFile) + { + var content = await storageFile.ReadBytesAsync(); + + return new SharedFile(storageFile.Path, content); + } + } +} diff --git a/Wino.Core.UWP/Services/DialogServiceBase.cs b/Wino.Core.UWP/Services/DialogServiceBase.cs index 3ad55d5e..f81fae1d 100644 --- a/Wino.Core.UWP/Services/DialogServiceBase.cs +++ b/Wino.Core.UWP/Services/DialogServiceBase.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging; using Microsoft.Toolkit.Uwp.Helpers; using Serilog; using Windows.Storage; +using Windows.Storage.AccessCache; using Windows.Storage.Pickers; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -13,6 +14,7 @@ using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Accounts; +using Wino.Core.Domain.Models.Common; using Wino.Core.UWP.Dialogs; using Wino.Core.UWP.Extensions; using Wino.Dialogs; @@ -36,6 +38,66 @@ namespace Wino.Core.UWP.Services ApplicationResourceManager = applicationResourceManager; } + public async Task PickFilePathAsync(string saveFileName) + { + var picker = new FolderPicker() + { + SuggestedStartLocation = PickerLocationId.Desktop + }; + + picker.FileTypeFilter.Add("*"); + + var folder = await picker.PickSingleFolderAsync(); + if (folder == null) return string.Empty; + + StorageApplicationPermissions.FutureAccessList.Add(folder); + + return folder.Path; + + //var picker = new FileSavePicker + //{ + // SuggestedStartLocation = PickerLocationId.Desktop, + // SuggestedFileName = saveFileName + //}; + + //picker.FileTypeChoices.Add(Translator.FilteringOption_All, [".*"]); + + //var file = await picker.PickSaveFileAsync(); + //if (file == null) return string.Empty; + + //StorageApplicationPermissions.FutureAccessList.Add(file); + + //return file.Path; + } + + public async Task> PickFilesAsync(params object[] typeFilters) + { + var returnList = new List(); + var picker = new FileOpenPicker + { + ViewMode = PickerViewMode.Thumbnail, + SuggestedStartLocation = PickerLocationId.Desktop + }; + + foreach (var filter in typeFilters) + { + picker.FileTypeFilter.Add(filter.ToString()); + } + + var files = await picker.PickMultipleFilesAsync(); + if (files == null) return returnList; + + foreach (var file in files) + { + StorageApplicationPermissions.FutureAccessList.Add(file); + + var sharedFile = await file.ToSharedFileAsync(); + returnList.Add(sharedFile); + } + + return returnList; + } + private async Task PickFileAsync(params object[] typeFilters) { var picker = new FileOpenPicker @@ -52,7 +114,7 @@ namespace Wino.Core.UWP.Services if (file == null) return null; - Windows.Storage.AccessCache.StorageApplicationPermissions.FutureAccessList.AddOrReplace("FilePickerPath", file); + StorageApplicationPermissions.FutureAccessList.Add(file); return file; } diff --git a/Wino.Core.UWP/Wino.Core.UWP.csproj b/Wino.Core.UWP/Wino.Core.UWP.csproj index 131918e2..f08a977a 100644 --- a/Wino.Core.UWP/Wino.Core.UWP.csproj +++ b/Wino.Core.UWP/Wino.Core.UWP.csproj @@ -100,6 +100,7 @@ NewAccountDialog.xaml + diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index 9e350bcc..c88ca96a 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -96,6 +96,7 @@ namespace Wino.Mail.ViewModels private readonly IMailDialogService _dialogService; private readonly IMailService _mailService; private readonly IMimeFileService _mimeFileService; + private readonly IFileService _fileService; private readonly IFolderService _folderService; private readonly IAccountService _accountService; private readonly IWinoRequestDelegator _worker; @@ -107,6 +108,7 @@ namespace Wino.Mail.ViewModels public ComposePageViewModel(IMailDialogService dialogService, IMailService mailService, IMimeFileService mimeFileService, + IFileService fileService, INativeAppService nativeAppService, IFolderService folderService, IAccountService accountService, @@ -125,11 +127,58 @@ namespace Wino.Mail.ViewModels _dialogService = dialogService; _mailService = mailService; _mimeFileService = mimeFileService; + _fileService = fileService; _accountService = accountService; _worker = worker; _winoServerConnectionManager = winoServerConnectionManager; } + [RelayCommand] + private async Task OpenAttachmentAsync(MailAttachmentViewModel attachmentViewModel) + { + if (string.IsNullOrEmpty(attachmentViewModel.FilePath)) return; + + try + { + await NativeAppService.LaunchFileAsync(attachmentViewModel.FilePath); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage(Translator.Info_FailedToOpenFileTitle, Translator.Info_FailedToOpenFileMessage, InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task SaveAttachmentAsync(MailAttachmentViewModel attachmentViewModel) + { + if (attachmentViewModel.Content == null) return; + var pickedFilePath = await _dialogService.PickFilePathAsync(attachmentViewModel.FileName); + if (string.IsNullOrWhiteSpace(pickedFilePath)) return; + + try + { + await _fileService.CopyFileAsync(attachmentViewModel.FilePath, pickedFilePath); + } + catch (Exception ex) + { + _dialogService.InfoBarMessage(Translator.Info_FailedToOpenFileTitle, Translator.Info_FailedToOpenFileMessage, InfoBarMessageType.Error); + } + } + + [RelayCommand] + private async Task AttachFilesAsync() + { + var pickedFiles = await _dialogService.PickFilesAsync("*"); + + if (pickedFiles?.Count == 0) return; + + foreach (var file in pickedFiles) + { + var attachmentViewModel = new MailAttachmentViewModel(file); + IncludedAttachments.Add(attachmentViewModel); + } + } + [RelayCommand] private void RemoveAttachment(MailAttachmentViewModel attachmentViewModel) => IncludedAttachments.Remove(attachmentViewModel); diff --git a/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs b/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs index 469b55ba..033a30b6 100644 --- a/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs +++ b/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs @@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using MimeKit; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Common; using Wino.Core.Extensions; namespace Wino.Mail.ViewModels.Data @@ -41,14 +42,14 @@ namespace Wino.Mail.ViewModels.Data AttachmentType = GetAttachmentType(extension); } - public MailAttachmentViewModel(string fullFilePath, byte[] content) + public MailAttachmentViewModel(SharedFile sharedFile) { - Content = content; + Content = sharedFile.Data; - FileName = Path.GetFileName(fullFilePath); - FilePath = fullFilePath; + FileName = sharedFile.FileName; + FilePath = sharedFile.FullFilePath; - ReadableSize = ((long)content.Length).GetBytesReadable(); + ReadableSize = ((long)sharedFile.Data.Length).GetBytesReadable(); var extension = Path.GetExtension(FileName); AttachmentType = GetAttachmentType(extension); diff --git a/Wino.Mail/Extensions/MimeKitExtensions.cs b/Wino.Mail/Extensions/MimeKitExtensions.cs deleted file mode 100644 index 1e400e16..00000000 --- a/Wino.Mail/Extensions/MimeKitExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Helpers; -using Windows.Storage; -using Wino.Mail.ViewModels.Data; - -namespace Wino.Extensions -{ - public static class MimeKitExtensions - { - public static async Task ToAttachmentViewModelAsync(this StorageFile storageFile) - { - if (storageFile == null) return null; - - var bytes = await storageFile.ReadBytesAsync(); - - return new MailAttachmentViewModel(storageFile.Name, bytes); - } - } -} diff --git a/Wino.Mail/Views/ComposePage.xaml b/Wino.Mail/Views/ComposePage.xaml index 341b833c..084f54ab 100644 --- a/Wino.Mail/Views/ComposePage.xaml +++ b/Wino.Mail/Views/ComposePage.xaml @@ -5,6 +5,7 @@ xmlns:abstract="using:Wino.Views.Abstract" xmlns:controls="using:Wino.Controls" xmlns:coreControls="using:Wino.Core.UWP.Controls" + xmlns:customcontrols="using:Wino.Core.UWP.Controls.CustomControls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:data="using:Wino.Mail.ViewModels.Data" xmlns:domain="using:Wino.Core.Domain" @@ -15,6 +16,7 @@ xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:reader="using:Wino.Core.Domain.Models.Reader" xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" + xmlns:ui="using:CommunityToolkit.WinUI" x:Name="root" d:Background="White" Loaded="ComposerLoaded" @@ -74,8 +76,24 @@ + + + + + + + + + @@ -113,11 +131,21 @@ Text="{x:Bind ReadableSize}" /> - + Margin="0,4,-8,4" + VerticalAlignment="Stretch" + Background="Transparent" + BorderThickness="0" + Command="{Binding ElementName=root, Path=ViewModel.RemoveAttachmentCommand}" + CommandParameter="{Binding}"> + + + @@ -333,7 +361,7 @@ @@ -402,7 +430,7 @@ @@ -550,15 +578,15 @@ x:Name="AttachmentsListView" Grid.Row="5" Grid.ColumnSpan="2" + ui:ListViewExtensions.Command="{x:Bind ViewModel.OpenAttachmentCommand}" x:Load="{x:Bind helpers:XamlHelpers.CountToBooleanConverter(ViewModel.IncludedAttachments.Count), Mode=OneWay}" IsItemClickEnabled="True" - ItemClick="AttachmentClicked" ItemTemplate="{StaticResource ComposerFileAttachmentTemplate}" ItemsSource="{x:Bind ViewModel.IncludedAttachments, Mode=OneWay}" SelectionMode="None"> - + diff --git a/Wino.Mail/Views/ComposePage.xaml.cs b/Wino.Mail/Views/ComposePage.xaml.cs index 6da00cae..19fb6106 100644 --- a/Wino.Mail/Views/ComposePage.xaml.cs +++ b/Wino.Mail/Views/ComposePage.xaml.cs @@ -17,7 +17,6 @@ using MimeKit; using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; using Windows.Storage; -using Windows.Storage.Pickers; using Windows.UI.ViewManagement.Core; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -28,7 +27,7 @@ using Wino.Core.Domain; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Reader; -using Wino.Extensions; +using Wino.Core.UWP.Extensions; using Wino.Mail.ViewModels.Data; using Wino.Messaging.Client.Mails; using Wino.Messaging.Client.Shell; @@ -109,20 +108,6 @@ namespace Wino.Views }); } - private async void AddFilesClicked(object sender, RoutedEventArgs e) - { - // TODO: Pick files - var picker = new FileOpenPicker() - { - SuggestedStartLocation = PickerLocationId.Desktop - }; - - picker.FileTypeFilter.Add("*"); - var files = await picker.PickMultipleFilesAsync(); - - await AttachFiles(files); - } - private void OnComposeGridDragOver(object sender, DragEventArgs e) { ViewModel.IsDraggingOverComposerGrid = true; @@ -250,9 +235,9 @@ namespace Wino.Views // Convert files to MailAttachmentViewModel. foreach (var file in files) { - var attachmentViewModel = await file.ToAttachmentViewModelAsync(); + var sharedFile = await file.ToSharedFileAsync(); - ViewModel.IncludedAttachments.Add(attachmentViewModel); + ViewModel.IncludedAttachments.Add(new MailAttachmentViewModel(sharedFile)); } } @@ -628,14 +613,6 @@ namespace Wino.Views } } - private void AttachmentClicked(object sender, ItemClickEventArgs e) - { - if (e.ClickedItem is MailAttachmentViewModel attachmentViewModel) - { - ViewModel.RemoveAttachmentCommand.Execute(attachmentViewModel); - } - } - private async void AddressBoxLostFocus(object sender, RoutedEventArgs e) { // Automatically add current text as item if it is valid mail address. diff --git a/Wino.Mail/Wino.Mail.csproj b/Wino.Mail/Wino.Mail.csproj index 1ba4ef5e..176689b4 100644 --- a/Wino.Mail/Wino.Mail.csproj +++ b/Wino.Mail/Wino.Mail.csproj @@ -238,7 +238,6 @@ SystemFolderConfigurationDialog.xaml - @@ -623,7 +622,9 @@ Windows Desktop Extensions for the UWP - + + + 14.0