From e4a224bd68f6127bdb64ec29bfb1643d0f1a8503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sun, 8 Mar 2026 15:48:11 +0100 Subject: [PATCH] =?UTF-8?q?Ema=C4=B1l=20templates.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Entities/Mail/EmailTemplate.cs | 16 +++ Wino.Core.Domain/Enums/WinoPage.cs | 2 + .../Interfaces/IEmailTemplateService.cs | 15 +++ .../Translations/en_US/resources.json | 16 +++ .../SettingOptionsPageViewModel.cs | 1 + Wino.Mail.ViewModels/ComposePageViewModel.cs | 21 ++++ .../CreateEmailTemplatePageViewModel.cs | 112 ++++++++++++++++++ .../EmailTemplatesPageViewModel.cs | 55 +++++++++ Wino.Mail.WinUI/App.xaml.cs | 2 + Wino.Mail.WinUI/Services/NavigationService.cs | 2 + .../CreateEmailTemplatePageAbstract.cs | 7 ++ .../Abstract/EmailTemplatesPageAbstract.cs | 7 ++ Wino.Mail.WinUI/Views/Mail/ComposePage.xaml | 19 ++- .../Views/Mail/ComposePage.xaml.cs | 10 ++ Wino.Mail.WinUI/Views/SettingOptionsPage.xaml | 32 +++-- .../Settings/CreateEmailTemplatePage.xaml | 66 +++++++++++ .../Settings/CreateEmailTemplatePage.xaml.cs | 42 +++++++ .../Views/Settings/EmailTemplatesPage.xaml | 44 +++++++ .../Views/Settings/EmailTemplatesPage.xaml.cs | 33 ++++++ Wino.Mail.WinUI/Views/SettingsPage.xaml.cs | 3 + Wino.Mail.WinUI/Wino.Mail.WinUI.csproj | 6 + Wino.Services/DatabaseService.cs | 2 + Wino.Services/EmailTemplateService.cs | 42 +++++++ Wino.Services/ServicesContainerSetup.cs | 1 + 24 files changed, 541 insertions(+), 15 deletions(-) create mode 100644 Wino.Core.Domain/Entities/Mail/EmailTemplate.cs create mode 100644 Wino.Core.Domain/Interfaces/IEmailTemplateService.cs create mode 100644 Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs create mode 100644 Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs create mode 100644 Wino.Mail.WinUI/Views/Abstract/CreateEmailTemplatePageAbstract.cs create mode 100644 Wino.Mail.WinUI/Views/Abstract/EmailTemplatesPageAbstract.cs create mode 100644 Wino.Mail.WinUI/Views/Settings/CreateEmailTemplatePage.xaml create mode 100644 Wino.Mail.WinUI/Views/Settings/CreateEmailTemplatePage.xaml.cs create mode 100644 Wino.Mail.WinUI/Views/Settings/EmailTemplatesPage.xaml create mode 100644 Wino.Mail.WinUI/Views/Settings/EmailTemplatesPage.xaml.cs create mode 100644 Wino.Services/EmailTemplateService.cs diff --git a/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs new file mode 100644 index 00000000..271e51b0 --- /dev/null +++ b/Wino.Core.Domain/Entities/Mail/EmailTemplate.cs @@ -0,0 +1,16 @@ +using System; +using SQLite; + +namespace Wino.Core.Domain.Entities.Mail; + +public class EmailTemplate +{ + [PrimaryKey] + public Guid Id { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Description { get; set; } = string.Empty; + + public string HtmlContent { get; set; } = string.Empty; +} diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index cee8e7b5..f7525f39 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -35,6 +35,8 @@ public enum WinoPage EventDetailsPage, CalendarEventComposePage, SignatureAndEncryptionPage, + EmailTemplatesPage, + CreateEmailTemplatePage, StoragePage, WelcomePageV2, WelcomeHostPage, diff --git a/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs new file mode 100644 index 00000000..52afff79 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IEmailTemplateService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities.Mail; + +namespace Wino.Core.Domain.Interfaces; + +public interface IEmailTemplateService +{ + Task> GetEmailTemplatesAsync(); + Task GetEmailTemplateAsync(Guid templateId); + Task CreateEmailTemplateAsync(EmailTemplate template); + Task UpdateEmailTemplateAsync(EmailTemplate template); + Task DeleteEmailTemplateAsync(EmailTemplate template); +} diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 51b0f6d5..2fea340e 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -254,6 +254,8 @@ "DialogMessage_DeleteAccountConfirmationTitle": "All data associated with this account will be deleted from disk permanently.", "DialogMessage_DeleteRecurringSeriesMessage": "This will delete all events in the series. Do you want to continue?", "DialogMessage_DeleteRecurringSeriesTitle": "Delete Recurring Series", + "DialogMessage_DeleteEmailTemplateConfirmationMessage": "Delete template \"{0}\"?", + "DialogMessage_DeleteEmailTemplateConfirmationTitle": "Delete Email Template", "DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?", "DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft", "DialogMessage_EmptySubjectConfirmation": "Missing Subject", @@ -849,6 +851,19 @@ "SettingsShowSenderPictures_Title": "Show Sender Avatars", "SettingsEnableGravatarAvatars_Title": "Gravatar", "SettingsEnableGravatarAvatars_Description": "Use gravatar (if available) as sender picture", + "SettingsEmailTemplates_Title": "Email Templates", + "SettingsEmailTemplates_Description": "Manage e-mail templates", + "SettingsEmailTemplates_CreatePageTitle": "New template", + "SettingsEmailTemplates_EditPageTitle": "Edit template", + "SettingsEmailTemplates_NewTemplateTitle": "New template", + "SettingsEmailTemplates_NewTemplateDescription": "Create a new e-mail template", + "SettingsEmailTemplates_NameTitle": "Name", + "SettingsEmailTemplates_NamePlaceholder": "Template name", + "SettingsEmailTemplates_DescriptionTitle": "Description", + "SettingsEmailTemplates_DescriptionPlaceholder": "Optional description", + "SettingsEmailTemplates_ContentTitle": "Template content", + "SettingsEmailTemplates_ContentDescription": "Edit the HTML content for this template.", + "SettingsEmailTemplates_NameRequired": "Template name is required.", "SettingsEnableFavicons_Title": "Domain icons (Favicons)", "SettingsEnableFavicons_Description": "Use domain favicons (if available) as sender picture", "SettingsMailList_ClearAvatarsCache_Button": "Clear cached avatars", @@ -1006,6 +1021,7 @@ "Composer_CertificateExpires": "Expires on: ", "Composer_SmimeSignature": "S/MIME Signature", "Composer_SmimeEncryption": "S/MIME Encryption", + "Composer_EmailTemplatesPlaceholder": "E-mail templates", "SettingsAppPreferences_EmailSyncInterval_Title": "Email sync interval", "SettingsAppPreferences_EmailSyncInterval_Description": "Automatic email synchronization interval (minutes). This setting will be applied only after restarting Wino Mail.", "ContactsPage_Title": "Contacts", diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index 9f55c8b2..1eff7136 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -76,6 +76,7 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, WinoPage.LanguageTimePage => Translator.SettingsLanguageTime_Title, WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, + WinoPage.EmailTemplatesPage => Translator.SettingsEmailTemplates_Title, WinoPage.CalendarSettingsPage => Translator.SettingsCalendarSettings_Title, WinoPage.SignatureAndEncryptionPage => Translator.SettingsSignatureAndEncryption_Title, WinoPage.KeyboardShortcutsPage => Translator.Settings_KeyboardShortcuts_Title, diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index be5b3e31..bf6c23b6 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -130,6 +130,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, public bool AreCertificatesAvailable => AvailableCertificates.Count > 0; + public ObservableCollection AvailableEmailTemplates { get; } = []; public ObservableCollection IncludedAttachments { get; set; } = []; public ObservableCollection Accounts { get; set; } = []; public ObservableCollection ToItems { get; set; } = []; @@ -148,6 +149,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, private readonly IFileService _fileService; private readonly IFolderService _folderService; private readonly IAccountService _accountService; + private readonly IEmailTemplateService _emailTemplateService; private readonly IWinoRequestDelegator _worker; public readonly IFontService FontService; public readonly IPreferencesService PreferencesService; @@ -161,6 +163,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, INativeAppService nativeAppService, IFolderService folderService, IAccountService accountService, + IEmailTemplateService emailTemplateService, IWinoRequestDelegator worker, IContactService contactService, IFontService fontService, @@ -178,6 +181,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, _mimeFileService = mimeFileService; _fileService = fileService; _accountService = accountService; + _emailTemplateService = emailTemplateService; _worker = worker; _smimeCertificateService = smimeCertificateService; @@ -520,6 +524,7 @@ public partial class ComposePageViewModel : MailBaseViewModel, CurrentMailDraftItem = mailItem; await UpdatePendingOperationStateAsync(); + await LoadEmailTemplatesAsync(); await TryPrepareComposeAsync(true); } } @@ -539,9 +544,25 @@ public partial class ComposePageViewModel : MailBaseViewModel, // Set the new draft item and prepare it. CurrentMailDraftItem = message.MailItemViewModel; await UpdatePendingOperationStateAsync(); + await LoadEmailTemplatesAsync(); await TryPrepareComposeAsync(true); } + private async Task LoadEmailTemplatesAsync() + { + var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false); + + await ExecuteUIThread(() => + { + AvailableEmailTemplates.Clear(); + + foreach (var template in templates) + { + AvailableEmailTemplates.Add(template); + } + }); + } + public async void Receive(SynchronizationActionsAdded message) { if (!ShouldTrackDraftSynchronizationState(message.AccountId)) diff --git a/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs b/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs new file mode 100644 index 00000000..7e678132 --- /dev/null +++ b/Wino.Mail.ViewModels/CreateEmailTemplatePageViewModel.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Mail.ViewModels; + +public partial class CreateEmailTemplatePageViewModel( + IEmailTemplateService emailTemplateService, + IMailDialogService dialogService, + INavigationService navigationService) : MailBaseViewModel +{ + private readonly IEmailTemplateService _emailTemplateService = emailTemplateService; + private readonly IMailDialogService _dialogService = dialogService; + + private EmailTemplate _editingTemplate; + + public INavigationService NavigationService { get; } = navigationService; + + [ObservableProperty] + public partial string TemplateName { get; set; } = string.Empty; + + [ObservableProperty] + public partial string TemplateDescription { get; set; } = string.Empty; + + [ObservableProperty] + public partial bool IsExistingTemplate { get; set; } + + public async Task LoadAsync(object parameter) + { + EmailTemplate template = null; + + var templateId = parameter switch + { + Guid guid when guid != Guid.Empty => guid, + string value when Guid.TryParse(value, out var parsedGuid) => parsedGuid, + EmailTemplate emailTemplate when emailTemplate.Id != Guid.Empty => emailTemplate.Id, + _ => Guid.Empty + }; + + if (templateId != Guid.Empty) + { + template = await _emailTemplateService.GetEmailTemplateAsync(templateId).ConfigureAwait(false); + } + + _editingTemplate = template; + + await ExecuteUIThread(() => + { + IsExistingTemplate = template != null; + TemplateName = template?.Name ?? string.Empty; + TemplateDescription = template?.Description ?? string.Empty; + }); + + return template?.HtmlContent ?? string.Empty; + } + + public async Task SaveAsync(string htmlContent) + { + var trimmedName = TemplateName?.Trim() ?? string.Empty; + + if (string.IsNullOrWhiteSpace(trimmedName)) + { + _dialogService.InfoBarMessage( + Translator.GeneralTitle_Error, + Translator.SettingsEmailTemplates_NameRequired, + InfoBarMessageType.Warning); + return; + } + + var template = _editingTemplate ?? new EmailTemplate + { + Id = Guid.NewGuid() + }; + + template.Name = trimmedName; + template.Description = TemplateDescription?.Trim() ?? string.Empty; + template.HtmlContent = htmlContent ?? string.Empty; + + if (_editingTemplate == null) + { + await _emailTemplateService.CreateEmailTemplateAsync(template).ConfigureAwait(false); + } + else + { + await _emailTemplateService.UpdateEmailTemplateAsync(template).ConfigureAwait(false); + } + + _editingTemplate = template; + NavigationService.GoBack(); + } + + public async Task DeleteAsync() + { + if (_editingTemplate == null) + return; + + var shouldDelete = await _dialogService.ShowConfirmationDialogAsync( + string.Format(Translator.DialogMessage_DeleteEmailTemplateConfirmationMessage, _editingTemplate.Name), + Translator.DialogMessage_DeleteEmailTemplateConfirmationTitle, + Translator.Buttons_Delete).ConfigureAwait(false); + + if (!shouldDelete) + return; + + await _emailTemplateService.DeleteEmailTemplateAsync(_editingTemplate).ConfigureAwait(false); + NavigationService.GoBack(); + } +} diff --git a/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs b/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs new file mode 100644 index 00000000..e3106e67 --- /dev/null +++ b/Wino.Mail.ViewModels/EmailTemplatesPageViewModel.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Messaging.Client.Navigation; + +namespace Wino.Mail.ViewModels; + +public partial class EmailTemplatesPageViewModel(IEmailTemplateService emailTemplateService) : MailBaseViewModel +{ + private readonly IEmailTemplateService _emailTemplateService = emailTemplateService; + + public ObservableCollection EmailTemplates { get; } = []; + + public async Task LoadAsync() + { + var templates = await _emailTemplateService.GetEmailTemplatesAsync().ConfigureAwait(false); + + await ExecuteUIThread(() => + { + EmailTemplates.Clear(); + + foreach (var template in templates) + { + EmailTemplates.Add(template); + } + }); + } + + public void CreateTemplate() + { + Messenger.Send(new BreadcrumbNavigationRequested( + Translator.SettingsEmailTemplates_CreatePageTitle, + WinoPage.CreateEmailTemplatePage)); + } + + public void OpenTemplate(EmailTemplate template) + { + if (template == null) + return; + + var title = string.IsNullOrWhiteSpace(template.Name) + ? Translator.SettingsEmailTemplates_EditPageTitle + : template.Name; + + Messenger.Send(new BreadcrumbNavigationRequested( + title, + WinoPage.CreateEmailTemplatePage, + template.Id)); + } +} diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index 650cddd0..0d5273e0 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -167,6 +167,8 @@ public partial class App : WinoApplication, services.AddTransient(typeof(AliasManagementPageViewModel)); services.AddTransient(typeof(ContactsPageViewModel)); services.AddTransient(typeof(SignatureAndEncryptionPageViewModel)); + services.AddTransient(typeof(EmailTemplatesPageViewModel)); + services.AddTransient(typeof(CreateEmailTemplatePageViewModel)); services.AddTransient(typeof(CalendarPageViewModel)); services.AddTransient(typeof(CalendarSettingsPageViewModel)); services.AddTransient(typeof(CalendarAccountSettingsPageViewModel)); diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index 060d3eed..348defaf 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -120,6 +120,8 @@ public class NavigationService : NavigationServiceBase, INavigationService WinoPage.KeyboardShortcutsPage => typeof(KeyboardShortcutsPage), WinoPage.ContactsPage => typeof(ContactsPage), WinoPage.SignatureAndEncryptionPage => typeof(SignatureAndEncryptionPage), + WinoPage.EmailTemplatesPage => typeof(EmailTemplatesPage), + WinoPage.CreateEmailTemplatePage => typeof(CreateEmailTemplatePage), WinoPage.StoragePage => typeof(StoragePage), WinoPage.WelcomeHostPage => typeof(WelcomeHostPage), WinoPage.ProviderSelectionPage => typeof(ProviderSelectionPage), diff --git a/Wino.Mail.WinUI/Views/Abstract/CreateEmailTemplatePageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/CreateEmailTemplatePageAbstract.cs new file mode 100644 index 00000000..75d454dd --- /dev/null +++ b/Wino.Mail.WinUI/Views/Abstract/CreateEmailTemplatePageAbstract.cs @@ -0,0 +1,7 @@ +using Wino.Mail.ViewModels; + +namespace Wino.Views.Abstract; + +public abstract class CreateEmailTemplatePageAbstract : SettingsPageBase +{ +} diff --git a/Wino.Mail.WinUI/Views/Abstract/EmailTemplatesPageAbstract.cs b/Wino.Mail.WinUI/Views/Abstract/EmailTemplatesPageAbstract.cs new file mode 100644 index 00000000..79809225 --- /dev/null +++ b/Wino.Mail.WinUI/Views/Abstract/EmailTemplatesPageAbstract.cs @@ -0,0 +1,7 @@ +using Wino.Mail.ViewModels; + +namespace Wino.Views.Abstract; + +public abstract class EmailTemplatesPageAbstract : SettingsPageBase +{ +} diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml index 195a86ba..e4d847de 100644 --- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml +++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml @@ -138,6 +138,21 @@ CommandAlignment="Right" IsDynamicOverflowEnabled="True" OverflowButtonAlignment="Left"> + + + + + + + + + - + diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs index 4819c7b3..f15daa46 100644 --- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs +++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs @@ -18,6 +18,7 @@ using Windows.Foundation; using Windows.Storage; using Windows.UI.Core.Preview; using Wino.Core.Domain; +using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Models.Reader; using Wino.Mail.ViewModels.Data; @@ -53,6 +54,15 @@ public sealed partial class ComposePage : ComposePageAbstract, WebViewEditor.ToggleEditorTheme(); } + private async void EmailTemplateSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is not ComboBox comboBox || comboBox.SelectedItem is not EmailTemplate template) + return; + + await WebViewEditor.RenderHtmlAsync(template.HtmlContent); + comboBox.SelectedItem = null; + } + private async void GlobalFocusManagerGotFocus(object? sender, FocusManagerGotFocusEventArgs e) { // In order to delegate cursor to the inner editor for WebView2. diff --git a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml index 880f2bc2..0c170833 100644 --- a/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml +++ b/Wino.Mail.WinUI/Views/SettingOptionsPage.xaml @@ -14,11 +14,14 @@ - + + + - + - - + + @@ -194,6 +191,17 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +