12 Commits

Author SHA1 Message Date
Burak Kaan Köse e0f517e993 Updated translations. 2026-04-20 23:20:08 +02:00
Burak Kaan Köse 66c556b587 Hide email address for calendar only accounts and fix startup crash. 2026-04-20 23:19:53 +02:00
Burak Kaan Köse 877fb0dbd4 Do not popup success sync message on executing requests. 2026-04-20 23:06:11 +02:00
Burak Kaan Köse 2ea65dc556 Remove the confirmamtion when compose page is dismissed after closing the window. 2026-04-20 22:51:20 +02:00
Burak Kaan Köse 23dce29ff8 Fix the issue with Chinese fonts on menu flyout items and appbar buttons in compose page. 2026-04-20 22:51:08 +02:00
Burak Kaan Köse 9292c963d5 Fix missing additional properties loading for mails. 2026-04-20 20:37:52 +02:00
Burak Kaan Köse 2b1676a4f7 Dispose mail webviews when closing the shell 2026-04-20 19:40:45 +02:00
Burak Kaan Köse d85812ed7b Add capability-first account and calendar setup flow 2026-04-20 19:38:37 +02:00
Burak Kaan Köse 54148716bb Fixing UI thread issues with bulk operations and request queue refactoring. 2026-04-20 02:18:23 +02:00
Burak Kaan Köse 3bd0b69429 Imap flow. 2026-04-19 20:13:09 +02:00
Burak Kaan Köse 496c7735f7 Add configurable thread item sorting 2026-04-19 16:26:30 +02:00
Burak Kaan Köse bfbc3d40b3 Make system tray icon optional 2026-04-19 10:47:42 +02:00
126 changed files with 4544 additions and 729 deletions
+1 -1
View File
@@ -45,6 +45,6 @@ public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator
return GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets()
{
ClientId = ClientId
}, AuthenticatorConfig.GmailScope, account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}, AuthenticatorConfig.GetGmailScope(account?.IsMailAccessGranted != false, account?.IsCalendarAccessGranted == true), account.Id.ToString(), CancellationToken.None, new FileDataStore(AuthenticatorConfig.GmailTokenStoreIdentifier));
}
}
+6 -3
View File
@@ -65,7 +65,10 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
_publicClientApplication = outlookAppBuilder.Build();
}
public string[] Scope => AuthenticatorConfig.OutlookScope;
private string[] GetScope(MailAccount account)
=> AuthenticatorConfig.GetOutlookScope(
account?.IsMailAccessGranted != false,
account?.IsCalendarAccessGranted == true);
private async Task EnsureTokenCacheAttachedAsync()
{
@@ -91,7 +94,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
try
{
var authResult = await _publicClientApplication.AcquireTokenSilent(Scope, storedAccount).ExecuteAsync();
var authResult = await _publicClientApplication.AcquireTokenSilent(GetScope(account), storedAccount).ExecuteAsync();
return new TokenInformationEx(authResult.AccessToken, authResult.Account.Username);
}
@@ -122,7 +125,7 @@ public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator
if (_nativeAppService.GetCoreWindowHwnd == null) throw new AuthenticationAttentionException(account);
AuthenticationResult authResult = await _publicClientApplication
.AcquireTokenInteractive(Scope)
.AcquireTokenInteractive(GetScope(account))
.ExecuteAsync();
// If the account is null, it means it's the initial creation of it.
@@ -299,6 +299,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
foreach (var account in accounts)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var accountCalendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var calendarViewModels = accountCalendars.Select(calendar => new AccountCalendarViewModel(account, calendar)).ToList();
var groupedAccountCalendarViewModel = new GroupedAccountCalendarViewModel(account, calendarViewModels);
@@ -408,6 +408,9 @@ public partial class CalendarEventComposePageViewModel : CalendarBaseViewModel
foreach (var account in accounts)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(account))
continue;
var calendars = await _calendarService.GetAccountCalendarsAsync(account.Id).ConfigureAwait(false);
var viewModels = calendars
.Select(calendar => new AccountCalendarViewModel(account, calendar))
@@ -17,6 +17,9 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
public MailAccount Account { get; }
public ObservableCollection<AccountCalendarViewModel> AccountCalendars { get; }
public static bool SupportsCalendar(MailAccount account)
=> account?.IsCalendarAccessGranted == true;
public GroupedAccountCalendarViewModel(MailAccount account, IEnumerable<AccountCalendarViewModel> calendarViewModels)
{
Account = account;
@@ -95,6 +98,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
public bool CanSynchronize => !IsSynchronizationInProgress;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public string AccountAddressDisplay => string.IsNullOrWhiteSpace(Account?.Address) ? string.Empty : $" ({Account.Address})";
public double SynchronizationProgress
{
@@ -198,5 +202,6 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
Account.MergedInboxId = updatedAccount.MergedInboxId;
AccountColorHex = updatedAccount.AccountColorHex;
OnPropertyChanged(nameof(Account));
OnPropertyChanged(nameof(AccountAddressDisplay));
}
}
@@ -78,6 +78,13 @@ public class MailAccount
/// </summary>
public SpecialImapProvider SpecialImapProvider { get; set; }
/// <summary>
/// Gets or sets whether mail access is granted for this account.
/// When false, mail folders, aliases, compose flows, and mail synchronization are unavailable.
/// Default is true for legacy accounts to preserve existing behavior.
/// </summary>
public bool IsMailAccessGranted { get; set; } = true;
/// <summary>
/// Gets or sets whether calendar access is granted for this account.
/// When false, synchronizers will not process EventMessages or calendar invitations.
@@ -0,0 +1,8 @@
namespace Wino.Core.Domain.Enums;
public enum ThreeButtonDialogResult
{
Primary,
Secondary,
Cancel
}
@@ -3,8 +3,8 @@
public interface IAuthenticatorConfig
{
string OutlookAuthenticatorClientId { get; }
string[] OutlookScope { get; }
string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
string GmailAuthenticatorClientId { get; }
string[] GmailScope { get; }
string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted);
string GmailTokenStoreIdentifier { get; }
}
@@ -19,6 +19,12 @@ public interface IMailDialogService : IDialogServiceBase
{
void ShowReadOnlyCalendarMessage();
Task<bool> ShowHardDeleteConfirmationAsync();
Task<ThreeButtonDialogResult> ShowThreeButtonDialogAsync(string title,
string description,
string primaryButtonText,
string secondaryButtonText,
string cancelButtonText,
WinoCustomMessageDialogIcon? icon = null);
Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService);
// Custom dialogs
@@ -36,6 +36,7 @@ public interface IMailService
Task ChangeReadStatusAsync(string mailCopyId, bool isRead);
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
Task ApplyMailStateUpdatesAsync(IEnumerable<MailCopyStateUpdate> updates);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
@@ -62,6 +62,11 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary>
bool IsStoreUpdateNotificationsEnabled { get; set; }
/// <summary>
/// Setting: Whether the system tray icon should be created while the shell is available.
/// </summary>
bool IsSystemTrayIconEnabled { get; set; }
/// <summary>
/// Setting: Whether the Wino account profile button in the shell title bar should be hidden.
/// </summary>
@@ -127,6 +132,11 @@ public interface IPreferencesService : INotifyPropertyChanged
/// </summary>
bool IsThreadingEnabled { get; set; }
/// <summary>
/// Setting: Whether the newest message in a conversation should appear first.
/// </summary>
bool IsNewestThreadMailFirst { get; set; }
/// <summary>
/// Setting: Show sender pictures in mail list.
/// </summary>
@@ -7,4 +7,6 @@ public record AccountCreationDialogResult(
string AccountName,
SpecialImapProviderDetails SpecialImapProviderDetails,
string AccountColorHex,
InitialSynchronizationRange InitialSynchronizationRange);
InitialSynchronizationRange InitialSynchronizationRange,
bool IsMailAccessGranted,
bool IsCalendarAccessGranted);
@@ -0,0 +1,3 @@
namespace Wino.Core.Domain.Models.MailItem;
public sealed record MailCopyStateUpdate(string MailCopyId, bool? IsRead = null, bool? IsFlagged = null);
@@ -0,0 +1,26 @@
namespace Wino.Core.Domain.Models.Navigation;
public enum ProviderSelectionHostMode
{
Wizard,
SettingsAddAccount
}
public sealed class ProviderSelectionNavigationContext
{
public ProviderSelectionHostMode HostMode { get; init; } = ProviderSelectionHostMode.Wizard;
public static ProviderSelectionNavigationContext CreateForWizard()
=> new()
{
HostMode = ProviderSelectionHostMode.Wizard
};
public static ProviderSelectionNavigationContext CreateForSettingsAddAccount()
=> new()
{
HostMode = ProviderSelectionHostMode.SettingsAddAccount
};
public bool IsWizardHost => HostMode == ProviderSelectionHostMode.Wizard;
}
@@ -44,6 +44,6 @@ public class BatchCollection<TRequestType> : List<TRequestType>, IUIChangeReques
public BatchCollection(IEnumerable<TRequestType> collection) : base(collection)
{
}
public void ApplyUIChanges() => ForEach(x => x.ApplyUIChanges());
public void RevertUIChanges() => ForEach(x => x.RevertUIChanges());
public virtual void ApplyUIChanges() => ForEach(x => x.ApplyUIChanges());
public virtual void RevertUIChanges() => ForEach(x => x.RevertUIChanges());
}
@@ -141,7 +141,9 @@ public static class SettingsNavigationInfoProvider
public static SettingsNavigationItemInfo GetInfo(WinoPage pageType, string manageAccountsDescription = "")
{
var rootPage = GetRootPage(pageType);
return GetNavigationItems(manageAccountsDescription).First(item => item.PageType == rootPage);
return GetNavigationItems(manageAccountsDescription)
.FirstOrDefault(item => item.PageType == rootPage)
?? GetNavigationItems(manageAccountsDescription).First(item => item.PageType == WinoPage.SettingOptionsPage);
}
public static string GetPageTitle(WinoPage pageType)
@@ -180,6 +182,9 @@ public static class SettingsNavigationInfoProvider
WinoPage.MailCategoryManagementPage => WinoPage.ManageAccountsPage,
WinoPage.SignatureManagementPage => WinoPage.ManageAccountsPage,
WinoPage.ImapCalDavSettingsPage => WinoPage.ManageAccountsPage,
WinoPage.ProviderSelectionPage => WinoPage.ManageAccountsPage,
WinoPage.SpecialImapCredentialsPage => WinoPage.ManageAccountsPage,
WinoPage.AccountSetupProgressPage => WinoPage.ManageAccountsPage,
WinoPage.CreateEmailTemplatePage => WinoPage.EmailTemplatesPage,
WinoPage.CalendarSettingsPage => WinoPage.CalendarPreferenceSettingsPage,
WinoPage.CalendarAccountSettingsPage => WinoPage.CalendarPreferenceSettingsPage,
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Изберете календар, за да конфигурирате неговите настройки.",
"AccountDetailsPage_InitialSynchronization_Title": "Начална синхронизация",
"AccountDetailsPage_InitialSynchronization_Description": "Wino синхронизира вашите имейли до {0} назад.",
"AccountDetailsPage_CapabilityTitle": "Свързани функции",
"AccountDetailsPage_CapabilityDescription": "Изберете дали този акаунт да се използва за поща, календар или и двете. Активирането на нова функция може да ви подтикне да се впишете отново.",
"AccountCapability_MailOnly": "Само поща",
"AccountCapability_CalendarOnly": "Само календар",
"AccountCapability_MailAndCalendar": "Поща и календар",
"AddHyperlink": "Добавяне",
"AppCloseBackgroundSynchronizationWarningTitle": "Синхронизация на заден план",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Приложението не е настроено да се стартира при стартиране на Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Прилагане на темата",
"Buttons_PopOut": "Извън прозореца",
"Buttons_Browse": "Преглед",
"Buttons_Back": "Назад",
"Buttons_Cancel": "Отказ",
"Buttons_Close": "Затваряне",
"Buttons_Copy": "Копиране",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Изтриване на повтаряща се серия",
"DialogMessage_DiscardDraftConfirmationMessage": "Тази чернова ще бъде отхвърлена. Искате ли да продължите?",
"DialogMessage_DiscardDraftConfirmationTitle": "Отхвърляне на черновата",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Черновикът все още е отворен. Запазете го преди да затворите прозореца?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Затвори прозореца",
"DialogMessage_EmptySubjectConfirmation": "Липсваща тема",
"DialogMessage_EmptySubjectConfirmationMessage": "Съобщението няма тема. Искате ли да продължите?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Можете да активирате автоматично стартиране от Настройки -> Предпочитания за приложението.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Не е избрано съобщение",
"NoMessageCrieteria": "Няма съобщения, които да отговарят на критериите ви за търсене",
"NoMessageEmptyFolder": "Тази папка е празна",
"MailEmptyState_Title": "Няма акаунти за поща",
"MailEmptyState_Message": "Имате свързани акаунти за календар, но нито един от тях не е активен за поща. Добавете акаунт за поща или обновете съществуващ акаунт, за да използва поща.",
"MailEmptyState_AddAccount": "Добави акаунт",
"MailEmptyState_ManageAccounts": "Управление на акаунти",
"Notifications_MultipleNotificationsMessage": "Имате {0} нови съобщения.",
"Notifications_MultipleNotificationsTitle": "Нов имейл",
"Notifications_WinoUpdatedMessage": "Вижте новата версия {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Значка на лентата на задачите",
"SettingsThreads_Description": "Организиране на съобщенията в разговори.",
"SettingsThreads_Title": "Групиране в разговори",
"SettingsThreads_Enabled_Description": "Групирайте свързаните съобщения в един разговор.",
"SettingsThreads_Enabled_Title": "Включване на групиране на разговорите по нишки",
"SettingsThreadOrder_Description": "Изберете как се подреждат елементите в разговорната нишка.",
"SettingsThreadOrder_Title": "Подреждане на елементи в нишката",
"SettingsThreadOrder_LastItemFirst": "Последният елемент първо",
"SettingsThreadOrder_FirstItemFirst": "Първият елемент първо",
"SettingsUnlinkAccounts_Description": "Премахване на връзката между акаунтите. Това няма да доведе до изтриване на акаунтите ви.",
"SettingsUnlinkAccounts_Title": "Премахване на връзката между акаунтите",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Добави акаунт",
"WelcomeWindow_GetStartedDescription": "Добавете своя акаунт Outlook, Gmail или IMAP, за да започнете с Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Импортирайте от вашия Wino акаунт.",
"WelcomeWindow_ImportFromJsonFile": "Импортирай от JSON файл",
"WelcomeWindow_ImportInProgress": "Импортиране на синхронизирани настройки и акаунти...",
"WelcomeWindow_ImportNoAccountsFound": "Не са намерени синхронизирани акаунти във вашия Wino акаунт. Ако имаше налични настройки, те са възстановени. Използвайте Започнете, за да добавите акаунт ръчно.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронизирани акаунти вече са налични на това устройство. Ако е необходимо, използвайте Започнете, за да добавите още акаунт ръчно.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Паролите, токените и друга чувствителна информация не се синхронират.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Импортираните акаунти на друг компютър ще трябва да влезете отново, преди да могат да се използват.",
"WinoAccount_Management_ExportDialog_InProgress": "Експортът на избраните от вас Wino данни...",
"WinoAccount_Management_LocalDataSectionTitle": "Прехвърляне с JSON файл",
"WinoAccount_Management_LocalDataSectionDescription": "Импортирайте от или експортирайте към локален JSON файл. Пароли, токени и друга чувствителна информация не са включени.",
"WinoAccount_Management_LocalDataImportAction": "Импортиране",
"WinoAccount_Management_LocalDataExportAction": "Експортиране",
"WinoAccount_Management_LocalDataSaved": "Запазихте експортираните данни на Wino в {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Избраният JSON файл не съдържа валиден Wino експорт.",
"WinoAccount_Management_LoadFailed": "Wino не успя да зареди най-новата информация за Wino акаунта.",
"WinoAccount_Management_ActionFailed": "Заявката за Wino акаунт не може да бъде завършена.",
"WinoAccount_SettingsSection_Title": "Wino акаунт",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Завършване на настройката",
"ProviderSelection_Title": "Изберете вашия имейл доставчик",
"ProviderSelection_Subtitle": "Изберете доставчик по-долу, за да добавите вашия имейл акаунт към Wino Mail.",
"ProviderSelection_StepProgress": "Стъпка {0} от 3",
"ProviderSelection_IdentityTitle": "Идентичност на акаунта",
"ProviderSelection_IdentityDescription": "Изберете как тази акаунт се появява във Wino.",
"ProviderSelection_ProviderSectionTitle": "Провайдер",
"ProviderSelection_ProviderSectionDescription": "Изберете услугата, която искате да свържете.",
"ProviderSelection_CapabilitySectionTitle": "Използвайте този акаунт за",
"ProviderSelection_CapabilitySectionDescription": "Изберете дали искате поща, календар или и двете.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "В следващата стъпка сигурното вписване ще свърже акаунта ви. Ако активирате календар, Wino ще свърже автоматично Outlook Calendar или Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "В следващата стъпка ще въведете идентификационните данни за доставчика. Пощата използва IMAP/SMTP, а календарът може да използва CalDAV или да остане локално на това устройство.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "В следващата стъпка ще въведете данните за сървъра си. Пощата използва IMAP/SMTP, а календарът може да използва CalDAV или да остане локално на това устройство.",
"ProviderSelection_AccountNameHeader": "Име на акаунт",
"ProviderSelection_AccountNamePlaceholder": "напр. Personal, Work",
"ProviderSelection_UseForMail": "Поща",
"ProviderSelection_UseForCalendar": "Календар",
"ProviderSelection_CapabilityValidationMessage": "Изберете поне една възможност преди да продължите.",
"ProviderSelection_CalendarOnlyServerHint": "Ако продължите само с календар, следващата страница няма да изисква имейл адрес.",
"ProviderSelection_DisplayNameHeader": "Име за показване",
"ProviderSelection_DisplayNamePlaceholder": "напр. John Doe",
"ProviderSelection_EmailHeader": "Имейл адрес",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selecciona un calendari per configurar la seva configuració.",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronització inicial",
"AccountDetailsPage_InitialSynchronization_Description": "Wino ha sincronitzat els teus correus fins a {0} enrere.",
"AccountDetailsPage_CapabilityTitle": "Característiques connectades",
"AccountDetailsPage_CapabilityDescription": "Trieu si aquest compte s'utilitza per a correu, calendari o tots dos. Habilitar una nova funció pot exigir que tornis a iniciar sessió.",
"AccountCapability_MailOnly": "Només correu",
"AccountCapability_CalendarOnly": "Només calendari",
"AccountCapability_MailAndCalendar": "Correu i Calendari",
"AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Obrir en una finestra separada",
"Buttons_Browse": "Browse",
"Buttons_Back": "Enrere",
"Buttons_Cancel": "Cancel",
"Buttons_Close": "Close",
"Buttons_Copy": "Copy",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Eliminar sèrie recurrent",
"DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?",
"DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Encara hi ha un esborrany obert. Desa'l abans de tancar la finestra?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Tanca la finestra",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "No hi ha cap compte habilitat per al correu",
"MailEmptyState_Message": "Tens comptes connectats per al calendari, però cap d'ells està habilitat per al correu. Afegeix un compte de correu o actualitza un compte existent per utilitzar el correu.",
"MailEmptyState_AddAccount": "Afegeix un compte",
"MailEmptyState_ManageAccounts": "Gestiona els comptes",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Agrupa els missatges relacionats en una única conversa.",
"SettingsThreads_Enabled_Title": "Activa el fil de converses",
"SettingsThreadOrder_Description": "Trieu com s'ordenen els elements dins d'un fil de conversa.",
"SettingsThreadOrder_Title": "Ordenació d'elements del fil de conversa",
"SettingsThreadOrder_LastItemFirst": "Últim element primer",
"SettingsThreadOrder_FirstItemFirst": "Primer element primer",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Comença afegint un compte",
"WelcomeWindow_GetStartedDescription": "Afegeix el teu compte de Outlook, Gmail o IMAP per començar a utilitzar Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importa des del teu compte Wino.",
"WelcomeWindow_ImportFromJsonFile": "Importa des d'un fitxer JSON",
"WelcomeWindow_ImportInProgress": "S'importen les teves preferències i comptes sincronitzats.",
"WelcomeWindow_ImportNoAccountsFound": "No s'han trobat comptes sincronitzats al teu compte Wino. Si hi havia preferències disponibles, s'han restaurat. Utilitza Comença per afegir un compte manualment.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} comptes sincronitzats ja estan disponibles en aquest dispositiu. Utilitza Comença per afegir un altre compte manualment si és necessari.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Contrasenyes, tokens i altra informació sensible no es sincronitzen.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Els comptes importats en un altre PC encara necessitaran que inicis sessió de nou abans de poder utilitzar-los.",
"WinoAccount_Management_ExportDialog_InProgress": "S'està exportant les teves dades de Wino seleccionades.",
"WinoAccount_Management_LocalDataSectionTitle": "Transferir amb un fitxer JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importa des d'un fitxer JSON local o exporta a un fitxer JSON local. Contrasenyes, tokens i altra informació sensible no s'inclou.",
"WinoAccount_Management_LocalDataImportAction": "Importa",
"WinoAccount_Management_LocalDataExportAction": "Exporta",
"WinoAccount_Management_LocalDataSaved": "Les teves dades exportades de Wino s'han desat a {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "El fitxer JSON seleccionat no conté una exportació vàlida de Wino.",
"WinoAccount_Management_LoadFailed": "No s'ha pogut carregar la darrera informació del compte Wino.",
"WinoAccount_Management_ActionFailed": "La sol·licitud del compte Wino no s'ha pogut completar.",
"WinoAccount_SettingsSection_Title": "Compte Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Finalitza la configuració",
"ProviderSelection_Title": "Trieu el vostre proveïdor de correu electrònic",
"ProviderSelection_Subtitle": "Seleccioneu un proveïdor a continuació per afegir el vostre compte de correu a Wino Mail.",
"ProviderSelection_StepProgress": "Pas {0} de 3",
"ProviderSelection_IdentityTitle": "Identitat del compte",
"ProviderSelection_IdentityDescription": "Trieu com apareix aquest compte dins de Wino.",
"ProviderSelection_ProviderSectionTitle": "Proveïdor",
"ProviderSelection_ProviderSectionDescription": "Trieu el servei que voleu connectar.",
"ProviderSelection_CapabilitySectionTitle": "Utilitza aquest compte per a",
"ProviderSelection_CapabilitySectionDescription": "Trieu si vols correu, calendar, o tots dos.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "En el pròxim pas, l'inici de sessió segur connectarà el vostre compte. Si activeu el calendari, Wino també connectarà automàticament Outlook Calendar o Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "En el proper pas, introduïu les vostres credencials del proveïdor. El correu fa servir IMAP/SMTP, i el calendari pot utilitzar CalDAV o quedar-se local en aquest dispositiu.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "En el proper pas, introduïu les dades del vostre servidor. El correu fa servir IMAP/SMTP, i el calendari pot utilitzar CalDAV o quedar-se local en aquest dispositiu.",
"ProviderSelection_AccountNameHeader": "Nom del compte",
"ProviderSelection_AccountNamePlaceholder": "p. ex. Personal, Treball",
"ProviderSelection_UseForMail": "Correu",
"ProviderSelection_UseForCalendar": "Calendari",
"ProviderSelection_CapabilityValidationMessage": "Trieu almenys una capacitat abans de continuar.",
"ProviderSelection_CalendarOnlyServerHint": "Si continues amb només el calendari, la següent pàgina no requerirà una adreça de correu.",
"ProviderSelection_DisplayNameHeader": "Nom a mostrar",
"ProviderSelection_DisplayNamePlaceholder": "p. ex. John Doe",
"ProviderSelection_EmailHeader": "Adreça de correu electrònic",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Vyberte kalendář pro konfiguraci jeho nastavení.",
"AccountDetailsPage_InitialSynchronization_Title": "První synchronizace",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronizoval vaše e-maily až do {0}.",
"AccountDetailsPage_CapabilityTitle": "Připojené funkce",
"AccountDetailsPage_CapabilityDescription": "Vyberte, zda se tento účet použije pro poštu, kalendář nebo obojí. Aktivace nové funkce může vyžadovat opětovné přihlášení.",
"AccountCapability_MailOnly": "Pouze pošta",
"AccountCapability_CalendarOnly": "Pouze kalendář",
"AccountCapability_MailAndCalendar": "Pošta a Kalendář",
"AddHyperlink": "Přidat",
"AppCloseBackgroundSynchronizationWarningTitle": "Synchronizace na pozadí",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Použít motiv",
"Buttons_PopOut": "Otevřít v novém okně",
"Buttons_Browse": "Procházet",
"Buttons_Back": "Zpět",
"Buttons_Cancel": "Zrušit",
"Buttons_Close": "Zavřít",
"Buttons_Copy": "Kopírovat",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Smazat opakující se sérii",
"DialogMessage_DiscardDraftConfirmationMessage": "Tento koncept bude zahozen. Chcete pokračovat?",
"DialogMessage_DiscardDraftConfirmationTitle": "Zahodit koncept",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Rozpracovaný návrh je stále otevřený. Chcete jej před zavřením okna uložit?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Zavřít okno",
"DialogMessage_EmptySubjectConfirmation": "Chybějící Předmět",
"DialogMessage_EmptySubjectConfirmationMessage": "Zpráva nemá Předmět. Chcete pokračovat?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Nebyly vybrány žádné zprávy",
"NoMessageCrieteria": "Žádná zpráva neodpovídá kritériím vyhledávání",
"NoMessageEmptyFolder": "Tato složka je prázdná",
"MailEmptyState_Title": "Žádné účty povolené pro poštu",
"MailEmptyState_Message": "Máte účty pro kalendář, ale žádný z nich není povolen pro poštu. Přidejte poštovní účet nebo upravte existující účet, aby používal poštu.",
"MailEmptyState_AddAccount": "Přidat účet",
"MailEmptyState_ManageAccounts": "Spravovat účty",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Vyzkoušejte novou verzi {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Uspořádat zprávy do konverzačních vláken.",
"SettingsThreads_Title": "Vlákna konverzací",
"SettingsThreads_Enabled_Description": "Seskupovat související zprávy do jedné konverzace.",
"SettingsThreads_Enabled_Title": "Povolit vlákna konverací",
"SettingsThreadOrder_Description": "Zvolte, jak budou položky seřazeny v konverzačním vláknu.",
"SettingsThreadOrder_Title": "Řazení položek konverzačního vlákna",
"SettingsThreadOrder_LastItemFirst": "Poslední položka jako první",
"SettingsThreadOrder_FirstItemFirst": "První položka jako první",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Rozpojit účty",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Začněte přidáním účtu",
"WelcomeWindow_GetStartedDescription": "Přidejte svůj účet Outlook, Gmail nebo IMAP a začněte s Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importovat z vašeho Wino účtu",
"WelcomeWindow_ImportFromJsonFile": "Importovat z JSON souboru",
"WelcomeWindow_ImportInProgress": "Importuji synchronizované preference a účty...",
"WelcomeWindow_ImportNoAccountsFound": "Ve vašem Wino účtu nebyly nalezeny žádné synchronizované účty. Pokud byly k dispozici preference, byly obnoveny. Pro ruční přidání účtu použijte možnost Začít.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronizovaných účtů je již k dispozici na tomto zařízení. Pokud je potřeba, použijte možnost Začít pro ruční přidání dalšího účtu.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Hesla, tokeny a další citlivé údaje se nesynchronizují.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Importované účty na jiném počítači budou i nadále vyžadovat opětovné přihlášení před jejich použitím.",
"WinoAccount_Management_ExportDialog_InProgress": "Probíhá export vybraných dat Wino.",
"WinoAccount_Management_LocalDataSectionTitle": "Přenos pomocí JSON souboru",
"WinoAccount_Management_LocalDataSectionDescription": "Importovat z nebo exportovat do místního JSON souboru. Hesla, tokeny a další citlivé údaje nejsou zahrnuty.",
"WinoAccount_Management_LocalDataImportAction": "Importovat",
"WinoAccount_Management_LocalDataExportAction": "Exportovat",
"WinoAccount_Management_LocalDataSaved": "Exportovaná data Wino byla uložena do {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Vybraný JSON soubor neobsahuje platný export Wino.",
"WinoAccount_Management_LoadFailed": "Nepodařilo se načíst nejnovější informace o účtu Wino.",
"WinoAccount_Management_ActionFailed": "Požadavek na účet Wino nebyl dokončen.",
"WinoAccount_SettingsSection_Title": "Wino účet",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Dokončit nastavení",
"ProviderSelection_Title": "Zvolte poskytovatele e-mailu",
"ProviderSelection_Subtitle": "Vyberte níže uvedeného poskytovatele a přidejte si svůj e-mailový účet do Wino Mail.",
"ProviderSelection_StepProgress": "Krok {0} z 3",
"ProviderSelection_IdentityTitle": "Identita účtu",
"ProviderSelection_IdentityDescription": "Zvolte, jak se tento účet bude zobrazovat ve Wino.",
"ProviderSelection_ProviderSectionTitle": "Poskytovatel",
"ProviderSelection_ProviderSectionDescription": "Vyberte službu, ke které se chcete připojit.",
"ProviderSelection_CapabilitySectionTitle": "Použít tento účet pro",
"ProviderSelection_CapabilitySectionDescription": "Vyberte, zda chcete používat poštu, kalendář, nebo obojí.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "V dalším kroku zabezpečené přihlášení propojí váš účet. Pokud povolíte kalendář, Wino také automaticky propojí Kalendář Outlooku nebo Kalendář Google.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "V dalším kroku zadáte údaje poskytovatele. Pošta používá IMAP/SMTP a kalendář může používat CalDAV nebo zůstat místní na tomto zařízení.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "V dalším kroku zadáte údaje o serveru. Pošta používá IMAP/SMTP a kalendář může používat CalDAV nebo zůstat místní na tomto zařízení.",
"ProviderSelection_AccountNameHeader": "Název účtu",
"ProviderSelection_AccountNamePlaceholder": "např. Osobní, Pracovní",
"ProviderSelection_UseForMail": "Pošta",
"ProviderSelection_UseForCalendar": "Kalendář",
"ProviderSelection_CapabilityValidationMessage": "Zvolte alespoň jednu možnost před pokračováním.",
"ProviderSelection_CalendarOnlyServerHint": "Pokud budete pokračovat pouze s kalendářem, další stránka nebude vyžadovat e-mailovou adresu.",
"ProviderSelection_DisplayNameHeader": "Zobrazené jméno",
"ProviderSelection_DisplayNamePlaceholder": "např. Jan Novák",
"ProviderSelection_EmailHeader": "E-mailová adresa",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Vælg en kalender for at konfigurere dens indstillinger.",
"AccountDetailsPage_InitialSynchronization_Title": "Første synkronisering",
"AccountDetailsPage_InitialSynchronization_Description": "Wino har synkroniseret dine mails helt tilbage til {0}.",
"AccountDetailsPage_CapabilityTitle": "Forbundne funktioner",
"AccountDetailsPage_CapabilityDescription": "Vælg om denne konto skal bruges til mail, kalender eller begge dele. Hvis du aktiverer en ny funktion, kan det bede dig logge ind igen.",
"AccountCapability_MailOnly": "Kun mail",
"AccountCapability_CalendarOnly": "Kun kalender",
"AccountCapability_MailAndCalendar": "Mail og Kalender",
"AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Pop ud",
"Buttons_Browse": "Browse",
"Buttons_Back": "Tilbage",
"Buttons_Cancel": "Cancel",
"Buttons_Close": "Close",
"Buttons_Copy": "Copy",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Slet tilbagevendende serie",
"DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?",
"DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Et udkast er stadig åbent. Gem det, før du lukker vinduet?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Luk vinduet",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "Ingen konti med mail aktiveret",
"MailEmptyState_Message": "Du har konti tilsluttet til kalender, men ingen af dem er aktiveret til mail. Tilføj en mailkonto eller opdater en eksisterende konto for at bruge mail.",
"MailEmptyState_AddAccount": "Tilføj konto",
"MailEmptyState_ManageAccounts": "Administrer konti",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Grupper relaterede meddelelser i en enkelt samtale.",
"SettingsThreads_Enabled_Title": "Aktivér samtale-trådning",
"SettingsThreadOrder_Description": "Vælg hvordan elementer ordnes i en samtale-tråd.",
"SettingsThreadOrder_Title": "Sortering af tråd-elementer",
"SettingsThreadOrder_LastItemFirst": "Sidste element først",
"SettingsThreadOrder_FirstItemFirst": "Første element først",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Kom i gang ved at tilføje en konto",
"WelcomeWindow_GetStartedDescription": "Tilføj din Outlook-, Gmail- eller IMAP-konto for at komme i gang med Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importer fra din Wino-konto",
"WelcomeWindow_ImportFromJsonFile": "Importér fra en JSON-fil",
"WelcomeWindow_ImportInProgress": "Importerere dine synkroniserede præferencer og konti...",
"WelcomeWindow_ImportNoAccountsFound": "Ingen synkroniserede konti blev fundet i din Wino-konto. Hvis præferencer var tilgængelige, blev de gendannet. Brug Kom i gang til manuelt at tilføje en konto.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synkroniserede konti er allerede tilgængelige på denne enhed. Brug Kom i gang til manuelt at tilføje en anden konto, hvis det er nødvendigt.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Adgangskoder, tokens og andre følsomme oplysninger synkroniseres ikke.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Importerede konti på en anden PC kræver stadig, at du logger ind igen, før de kan bruges.",
"WinoAccount_Management_ExportDialog_InProgress": "Eksporterer dine valgte Wino-data...",
"WinoAccount_Management_LocalDataSectionTitle": "Overfør data med en JSON-fil",
"WinoAccount_Management_LocalDataSectionDescription": "Importér fra eller eksporter til en lokal JSON-fil. Adgangskoder, tokens og anden følsom information er ikke inkluderet.",
"WinoAccount_Management_LocalDataImportAction": "Importér",
"WinoAccount_Management_LocalDataExportAction": "Eksportér",
"WinoAccount_Management_LocalDataSaved": "Dine eksporterede Wino-data er gemt til {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Den valgte JSON-fil indeholder ikke en gyldig Wino-eksport.",
"WinoAccount_Management_LoadFailed": "Wino kunne ikke indlæse de seneste oplysninger om Wino-kontoen.",
"WinoAccount_Management_ActionFailed": "Anmodningen til Wino-kontoen kunne ikke gennemføres.",
"WinoAccount_SettingsSection_Title": "Wino-konto",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Afslut opsætningen",
"ProviderSelection_Title": "Vælg din e-mail-udbyder",
"ProviderSelection_Subtitle": "Vælg en udbyder nedenfor for at tilføje din e-mail-konto til Wino Mail.",
"ProviderSelection_StepProgress": "Trin {0} af 3",
"ProviderSelection_IdentityTitle": "Kontoidentitet",
"ProviderSelection_IdentityDescription": "Vælg hvordan denne konto vises i Wino.",
"ProviderSelection_ProviderSectionTitle": "Udbyder",
"ProviderSelection_ProviderSectionDescription": "Vælg den tjeneste, du vil oprette forbindelse til.",
"ProviderSelection_CapabilitySectionTitle": "Brug denne konto til",
"ProviderSelection_CapabilitySectionDescription": "Vælg om du vil bruge mail, kalender eller begge dele.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "På næste trin vil et sikkert login forbinde din konto. Hvis du aktiverer kalender, vil Wino også automatisk oprette forbindelse til Outlook Kalender eller Google Kalender.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "På næste trin indtaster du dine udbyderoplysninger. Mail bruger IMAP/SMTP, og kalender kan bruge CalDAV eller forblive lokalt på denne enhed.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "På næste trin indtaster du dine serveroplysninger. Mail bruger IMAP/SMTP, og kalender kan bruge CalDAV eller forblive lokalt på denne enhed.",
"ProviderSelection_AccountNameHeader": "Konto navn",
"ProviderSelection_AccountNamePlaceholder": "f.eks. Personlig, Arbejde",
"ProviderSelection_UseForMail": "Mail",
"ProviderSelection_UseForCalendar": "Kalender",
"ProviderSelection_CapabilityValidationMessage": "Vælg mindst én funktion, før du fortsætter.",
"ProviderSelection_CalendarOnlyServerHint": "Hvis du fortsætter med kalender alene, kræver næste side ikke en e-mailadresse.",
"ProviderSelection_DisplayNameHeader": "Vis navn",
"ProviderSelection_DisplayNamePlaceholder": "f.eks. John Doe",
"ProviderSelection_EmailHeader": "E-mailadresse",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Wähle einen Kalender aus, um dessen Einstellungen zu konfigurieren.",
"AccountDetailsPage_InitialSynchronization_Title": "Erste Synchronisierung",
"AccountDetailsPage_InitialSynchronization_Description": "Wino hat Ihre E-Mails bis {0} synchronisiert.",
"AccountDetailsPage_CapabilityTitle": "Verbundene Funktionen",
"AccountDetailsPage_CapabilityDescription": "Wählen Sie aus, ob dieses Konto für Mail, Kalender oder beides verwendet wird. Wenn Sie eine neue Funktion aktivieren, müssen Sie sich möglicherweise erneut anmelden.",
"AccountCapability_MailOnly": "Nur Mail",
"AccountCapability_CalendarOnly": "Nur Kalender",
"AccountCapability_MailAndCalendar": "Mail + Kalender",
"AddHyperlink": "Hinzufügen",
"AppCloseBackgroundSynchronizationWarningTitle": "Hintergrundsynchronisierung",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Die Anwendung wird beim Start von Windows nicht gestartet.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Thema anwenden",
"Buttons_PopOut": "In neues Fenster öffnen",
"Buttons_Browse": "Durchsuchen",
"Buttons_Back": "Zurück",
"Buttons_Cancel": "Abbrechen",
"Buttons_Close": "Schließen",
"Buttons_Copy": "Kopieren",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Wiederkehrende Serie löschen",
"DialogMessage_DiscardDraftConfirmationMessage": "Dieser Entwurf wird verworfen. Möchten Sie fortfahren?",
"DialogMessage_DiscardDraftConfirmationTitle": "Entwurf verwerfen",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Ein Entwurf ist noch geöffnet. Speichern Sie ihn, bevor Sie das Fenster schließen?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Fenster schließen",
"DialogMessage_EmptySubjectConfirmation": "Betreff fehlt",
"DialogMessage_EmptySubjectConfirmationMessage": "Nachricht hat keinen Betreff. Möchten Sie fortfahren?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Sie können den Autostart unter Einstellungen -> App-Einstellungen aktivieren.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Keine Nachricht ausgewählt",
"NoMessageCrieteria": "Keine Nachrichten entsprechen Ihren Suchkriterien",
"NoMessageEmptyFolder": "Dieser Ordner ist leer",
"MailEmptyState_Title": "Keine Mail-fähigen Konten",
"MailEmptyState_Message": "Sie haben Konten verbunden, die für den Kalender verwendet werden, aber keines davon ist für Mail aktiviert. Fügen Sie ein Mail-Konto hinzu oder aktualisieren Sie ein bestehendes Konto, um Mail zu verwenden.",
"MailEmptyState_AddAccount": "Konto hinzufügen",
"MailEmptyState_ManageAccounts": "Konten verwalten",
"Notifications_MultipleNotificationsMessage": "Sie haben {0} neue Nachrichten.",
"Notifications_MultipleNotificationsTitle": "Neue Mail",
"Notifications_WinoUpdatedMessage": "Neue Version {0} herunterladen",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskleisten-Symbol",
"SettingsThreads_Description": "Nachrichten in Unterhaltungsthreads organisieren.",
"SettingsThreads_Title": "Unterhaltungsthreading",
"SettingsThreads_Enabled_Description": "Verwandte Nachrichten zu einer einzigen Unterhaltung bündeln.",
"SettingsThreads_Enabled_Title": "Unterhaltungen bündeln aktivieren",
"SettingsThreadOrder_Description": "Wählen Sie aus, wie Elemente in einem Thread sortiert werden.",
"SettingsThreadOrder_Title": "Thread-Elemente sortieren",
"SettingsThreadOrder_LastItemFirst": "Letztes Element zuerst",
"SettingsThreadOrder_FirstItemFirst": "Erstes Element zuerst",
"SettingsUnlinkAccounts_Description": "Entfernen Sie den Link zwischen den Konten. Dies wird Ihre Konten nicht löschen.",
"SettingsUnlinkAccounts_Title": "Konten trennen",
"SettingsMailRendering_ActionLabels_Title": "Aktions-Labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Konto hinzufügen und loslegen",
"WelcomeWindow_GetStartedDescription": "Fügen Sie Ihr Outlook-, Gmail- oder IMAP-Konto hinzu, um mit Wino Mail zu beginnen.",
"WelcomeWindow_ImportFromWinoAccount": "Aus Ihrem Wino-Konto importieren",
"WelcomeWindow_ImportFromJsonFile": "Aus einer JSON-Datei importieren",
"WelcomeWindow_ImportInProgress": "Import Ihrer synchronisierten Einstellungen und Konten läuft.",
"WelcomeWindow_ImportNoAccountsFound": "Es wurden keine synchronisierten Konten in Ihrem Wino-Konto gefunden. Falls Einstellungen verfügbar waren, wurden sie wiederhergestellt. Verwenden Sie Loslegen, um manuell ein Konto hinzuzufügen.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronisierte Konten sind auf diesem Gerät bereits verfügbar. Verwenden Sie Loslegen, um bei Bedarf manuell ein weiteres Konto hinzuzufügen.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Passwörter, Tokens und andere sensible Informationen werden nicht synchronisiert.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Auf einem anderen PC importierte Konten müssen sich weiterhin erneut anmelden, bevor sie verwendet werden können.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportieren Sie Ihre ausgewählten Wino-Daten.",
"WinoAccount_Management_LocalDataSectionTitle": "Übertragung mit einer JSON-Datei",
"WinoAccount_Management_LocalDataSectionDescription": "Importieren Sie aus oder exportieren Sie in eine lokale JSON-Datei. Passwörter, Tokens und andere sensible Informationen sind nicht enthalten.",
"WinoAccount_Management_LocalDataImportAction": "Importieren",
"WinoAccount_Management_LocalDataExportAction": "Exportieren",
"WinoAccount_Management_LocalDataSaved": "Ihre exportierten Wino-Daten wurden in {0} gespeichert.",
"WinoAccount_Management_LocalDataInvalidFile": "Die ausgewählte JSON-Datei enthält keinen gültigen Wino-Export.",
"WinoAccount_Management_LoadFailed": "Wino konnte die neuesten Informationen zum Wino-Konto nicht laden.",
"WinoAccount_Management_ActionFailed": "Die Wino-Konto-Anfrage konnte nicht abgeschlossen werden.",
"WinoAccount_SettingsSection_Title": "Wino-Konto",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Einrichtung abschließen",
"ProviderSelection_Title": "E-Mail-Anbieter auswählen",
"ProviderSelection_Subtitle": "Wählen Sie unten einen Anbieter aus, um Ihr E-Mail-Konto zu Wino Mail hinzuzufügen.",
"ProviderSelection_StepProgress": "Schritt {0} von 3",
"ProviderSelection_IdentityTitle": "Kontenidentität",
"ProviderSelection_IdentityDescription": "Wählen Sie aus, wie dieses Konto in Wino angezeigt wird.",
"ProviderSelection_ProviderSectionTitle": "Anbieter",
"ProviderSelection_ProviderSectionDescription": "Wählen Sie den Dienst aus, den Sie verbinden möchten.",
"ProviderSelection_CapabilitySectionTitle": "Verwendung dieses Kontos für",
"ProviderSelection_CapabilitySectionDescription": "Wählen Sie aus, ob Sie Mail, Kalender oder beides verwenden möchten.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Im nächsten Schritt wird sicheres Sign-in Ihr Konto verbinden. Wenn Sie Kalender aktivieren, verbindet Wino automatisch Outlook-Kalender oder Google Kalender.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Im nächsten Schritt geben Sie Ihre Anmeldeinformationen des Anbieters ein. Mail verwendet IMAP/SMTP, und Kalender kann CalDAV verwenden oder lokal auf diesem Gerät bleiben.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Im nächsten Schritt geben Sie Ihre Serverdaten ein. Mail verwendet IMAP/SMTP, und Kalender kann CalDAV verwenden oder lokal auf diesem Gerät bleiben.",
"ProviderSelection_AccountNameHeader": "Kontoname",
"ProviderSelection_AccountNamePlaceholder": "z. B. Privat, Arbeit",
"ProviderSelection_UseForMail": "Mail",
"ProviderSelection_UseForCalendar": "Kalender",
"ProviderSelection_CapabilityValidationMessage": "Wählen Sie vor dem Fortfahren mindestens eine Funktion.",
"ProviderSelection_CalendarOnlyServerHint": "Wenn Sie nur Kalender verwenden, ist auf der nächsten Seite keine E-Mail-Adresse erforderlich.",
"ProviderSelection_DisplayNameHeader": "Anzeigename",
"ProviderSelection_DisplayNamePlaceholder": "z. B. John Doe",
"ProviderSelection_EmailHeader": "E-Mail-Adresse",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Επιλέξτε ένα ημερολόγιο για να ορίσετε τις ρυθμίσεις του.",
"AccountDetailsPage_InitialSynchronization_Title": "Αρχικός συγχρονισμός",
"AccountDetailsPage_InitialSynchronization_Description": "Ο Wino συγχρόνισε τα μηνύματά σας μέχρι το {0} πίσω.",
"AccountDetailsPage_CapabilityTitle": "Συνδεδεμένες δυνατότητες",
"AccountDetailsPage_CapabilityDescription": "Επιλέξτε αν ο λογαριασμός αυτός χρησιμοποιείται για αλληλογραφία, ημερολόγιο ή και τα δύο. Η ενεργοποίηση μιας νέας δυνατότητας ενδέχεται να σας ζητήσει να συνδεθείτε ξανά.",
"AccountCapability_MailOnly": "Μόνο Αλληλογραφία",
"AccountCapability_CalendarOnly": "Μόνο Ημερολόγιο",
"AccountCapability_MailAndCalendar": "Αλληλογραφία + Ημερολόγιο",
"AddHyperlink": "Προσθήκη",
"AppCloseBackgroundSynchronizationWarningTitle": "Συγχρονισμός Παρασκηνίου",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Η εφαρμογή δεν έχει οριστεί για εκκίνηση κατά την έναρξη των Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Εφαρμογή Θέματος",
"Buttons_PopOut": "Άνοιγμα σε νέο παράθυρο",
"Buttons_Browse": "Περιήγηση",
"Buttons_Back": "Πίσω",
"Buttons_Cancel": "Ακύρωση",
"Buttons_Close": "Κλείσιμο",
"Buttons_Copy": "Αντιγραφή",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Διαγραφή Επαναλαμβανόμενης Σειράς",
"DialogMessage_DiscardDraftConfirmationMessage": "Αυτό το πρόχειρο θα απορριφθεί. Θέλετε να συνεχίσετε;",
"DialogMessage_DiscardDraftConfirmationTitle": "Απόρριψη προχείρου",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Ένα προσχέδιο παραμένει ανοιχτό. Αποθηκεύστε το προτού κλείσετε το παράθυρο;",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Κλείσιμο Παραθύρου",
"DialogMessage_EmptySubjectConfirmation": "Χωρίς Θέμα",
"DialogMessage_EmptySubjectConfirmationMessage": "Το μήνυμα δεν έχει θέμα. Θέλετε να συνεχίσετε;",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Μπορείτε να ενεργοποιήσετε την εκκίνηση κατά την έναρξη από τις Ρυθμίσεις -> Προτιμήσεις εφαρμογών.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Δεν επιλέχθηκε μήνυμα",
"NoMessageCrieteria": "Κανένα μήνυμα δεν ταιριάζει με τα κριτήρια αναζήτησής σας",
"NoMessageEmptyFolder": "Αυτός ο φάκελος είναι κενός",
"MailEmptyState_Title": "Δεν υπάρχουν λογαριασμοί με δυνατότητα αλληλογραφίας",
"MailEmptyState_Message": "Έχετε λογαριασμούς συνδεδεμένους για το ημερολόγιο, αλλά κανένας από αυτούς δεν είναι ενεργοποιημένος για αλληλογραφία. Προσθέστε έναν λογαριασμό αλληλογραφίας ή ενημερώστε έναν υπάρχοντα λογαριασμό ώστε να χρησιμοποιεί αλληλογραφία.",
"MailEmptyState_AddAccount": "Προσθήκη λογαριασμού",
"MailEmptyState_ManageAccounts": "Διαχείριση λογαριασμών",
"Notifications_MultipleNotificationsMessage": "Έχετε {0} νέα μηνύματα.",
"Notifications_MultipleNotificationsTitle": "Νέα Αλληλογραφία",
"Notifications_WinoUpdatedMessage": "Ρίξτε μια ματιά στη νέα έκδοση {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Σήμα Γραμμής Εργασιών",
"SettingsThreads_Description": "Οργάνωση μηνυμάτων σε νήματα συνομιλίας.",
"SettingsThreads_Title": "Νηματοποίηση Συζήτησης",
"SettingsThreads_Enabled_Description": "Ομαδοποιήστε σχετικά μηνύματα σε μια ενιαία συνομιλία.",
"SettingsThreads_Enabled_Title": "Ενεργοποίηση ομαδοποίησης συνομιλιών",
"SettingsThreadOrder_Description": "Επιλέξτε πώς ταξινομούνται τα στοιχεία εντός μιας συνομιλίας.",
"SettingsThreadOrder_Title": "Ταξινόμηση στοιχείων συνομιλίας",
"SettingsThreadOrder_LastItemFirst": "Τελευταίο στοιχείο πρώτο",
"SettingsThreadOrder_FirstItemFirst": "Πρώτο στοιχείο πρώτο",
"SettingsUnlinkAccounts_Description": "Αφαιρέστε τη σύνδεση μεταξύ των λογαριασμών. Δε θα διαγράψει τους λογαριασμούς σας.",
"SettingsUnlinkAccounts_Title": "Αποδέσμευση Λογαριασμών",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Ξεκινήστε προσθέτοντας έναν λογαριασμό",
"WelcomeWindow_GetStartedDescription": "Προσθέστε τον λογαριασμό σας Outlook, Gmail ή IMAP για να ξεκινήσετε με το Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Εισαγωγή από τον λογαριασμό σας Wino",
"WelcomeWindow_ImportFromJsonFile": "Εισαγωγή από αρχείο JSON",
"WelcomeWindow_ImportInProgress": "Εισαγωγή των συγχρονισμένων προτιμήσεων και λογαριασμών σας...",
"WelcomeWindow_ImportNoAccountsFound": "Δεν βρέθηκαν συγχρονισμένοι λογαριασμοί σε αυτόν τον λογαριασμό Wino. Εάν υπήρχαν προτιμήσεις, επαναφέρθηκαν. Χρησιμοποιήστε το Ξεκινήστε για να προσθέσετε έναν λογαριασμό χειροκίνητα.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} συγχρονισμένοι λογαριασμοί είναι ήδη διαθέσιμοι σε αυτήν τη συσκευή. Χρησιμοποιήστε το Ξεκινήστε για να προσθέσετε ακόμη έναν λογαριασμό χειροκίνητα εάν χρειάζεται.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Κωδικοί πρόσβασης, διακριτικά και άλλες ευαίσθητες πληροφορίες δεν συγχρονίζονται.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Οι εισαγόμενοι λογαριασμοί σε άλλον υπολογιστή θα χρειαστεί να συνδεθείτε ξανά πριν μπορέσουν να χρησιμοποιηθούν.",
"WinoAccount_Management_ExportDialog_InProgress": "Εξάγονται τα επιλεγμένα δεδομένα Wino.",
"WinoAccount_Management_LocalDataSectionTitle": "Μεταφορά με αρχείο JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Εισαγωγή από ή εξαγωγή σε ένα τοπικό αρχείο JSON. Οι κωδικοί πρόσβασης, τα διακριτικά και άλλες ευαίσθητες πληροφορίες δεν περιλαμβάνονται.",
"WinoAccount_Management_LocalDataImportAction": "Εισαγωγή",
"WinoAccount_Management_LocalDataExportAction": "Εξαγωγή",
"WinoAccount_Management_LocalDataSaved": "Αποθηκεύσατε τα εξαγόμενα δεδομένα Wino στο {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Το επιλεγμένο αρχείο JSON δεν περιέχει έγκυρη εξαγωγή του Wino.",
"WinoAccount_Management_LoadFailed": "Το Wino δεν μπόρεσε να φορτώσει τις τελευταίες πληροφορίες του Λογαριασμού Wino.",
"WinoAccount_Management_ActionFailed": "Το αίτημα Λογαριασμού Wino δεν μπόρεσε να ολοκληρωθεί.",
"WinoAccount_SettingsSection_Title": "Λογαριασμός Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Ολοκλήρωση Ρύθμισης",
"ProviderSelection_Title": "Επιλέξτε τον πάροχο email σας",
"ProviderSelection_Subtitle": "Επιλέξτε έναν πάροχο παρακάτω για να προσθέσετε τον λογαριασμό email σας στο Wino Mail.",
"ProviderSelection_StepProgress": "Βήμα {0} από 3",
"ProviderSelection_IdentityTitle": "Ταυτότητα λογαριασμού",
"ProviderSelection_IdentityDescription": "Επιλέξτε πώς θα εμφανίζεται αυτός ο λογαριασμός μέσα στο Wino.",
"ProviderSelection_ProviderSectionTitle": "Πάροχος",
"ProviderSelection_ProviderSectionDescription": "Επιλέξτε την υπηρεσία που θέλετε να συνδέσετε.",
"ProviderSelection_CapabilitySectionTitle": "Χρήση αυτού του λογαριασμού για",
"ProviderSelection_CapabilitySectionDescription": "Επιλέξτε αν θέλετε αλληλογραφία, ημερολόγιο ή και τα δύο.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Στο επόμενο βήμα, μια ασφαλής σύνδεση θα συνδέσει τον λογαριασμό σας. Εάν ενεργοποιήσετε το ημερολόγιο, το Wino θα συνδέσει αυτόματα το Outlook Calendar ή το Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Στο επόμενο βήμα θα εισάγετε τα διαπιστευτήρια παροχέα σας. Το IMAP/SMTP χρησιμοποιείται για το mail, και το ημερολόγιο μπορεί να χρησιμοποιήσει CalDAV ή να παραμείνει τοπικά σε αυτή τη συσκευή.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Στο επόμενο βήμα θα εισάγετε τις ρυθμίσεις διακομιστή σας. Το Mail χρησιμοποιεί IMAP/SMTP, και το ημερολόγιο μπορεί να χρησιμοποιήσει CalDAV ή να παραμείνει τοπικά σε αυτή τη συσκευή.",
"ProviderSelection_AccountNameHeader": "Όνομα Λογαριασμού",
"ProviderSelection_AccountNamePlaceholder": "π.χ. Προσωπικό, Εργασία",
"ProviderSelection_UseForMail": "Αλληλογραφία",
"ProviderSelection_UseForCalendar": "Ημερολόγιο",
"ProviderSelection_CapabilityValidationMessage": "Επιλέξτε τουλάχιστον μια δυνατότητα πριν συνεχίσετε.",
"ProviderSelection_CalendarOnlyServerHint": "Αν συνεχίσετε μόνο με το ημερολόγιο, η επόμενη σελίδα δεν θα απαιτεί διεύθυνση email.",
"ProviderSelection_DisplayNameHeader": "Όνομα εμφάνισης",
"ProviderSelection_DisplayNamePlaceholder": "π.χ. John Doe",
"ProviderSelection_EmailHeader": "Διεύθυνση Ηλεκτρονικού Ταχυδρομείου",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
"AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.",
"AccountDetailsPage_CapabilityTitle": "Connected features",
"AccountDetailsPage_CapabilityDescription": "Choose whether this account is used for mail, calendar, or both. Enabling a new feature may ask you to sign in again.",
"AccountCapability_MailOnly": "Mail only",
"AccountCapability_CalendarOnly": "Calendar only",
"AccountCapability_MailAndCalendar": "Mail + Calendar",
"AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Pop out",
"Buttons_Browse": "Browse",
"Buttons_Back": "Back",
"Buttons_Cancel": "Cancel",
"Buttons_Close": "Close",
"Buttons_Copy": "Copy",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Delete Recurring Series",
"DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?",
"DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft",
"DialogMessage_CloseDraftWindowConfirmationMessage": "A draft is still open. Save it before closing the window?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Close Window",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -438,7 +446,7 @@
"IMAPAdvancedSetupDialog_ValidationAuthMethodRequired": "Authentication method is required",
"IMAPAdvancedSetupDialog_ValidationConnectionSecurityRequired": "Connection security type is required",
"IMAPAdvancedSetupDialog_ValidationDisplayNameRequired": "Display name is required",
"IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid email address",
"IMAPAdvancedSetupDialog_ValidationEmailInvalid": "Please enter a valid mailbox address, such as user@example.com or user@localhost",
"IMAPAdvancedSetupDialog_ValidationEmailRequired": "Email address is required",
"IMAPAdvancedSetupDialog_ValidationErrorTitle": "Please check the following:",
"IMAPAdvancedSetupDialog_ValidationIncomingPortInvalid": "Incoming port must be between 1-65535",
@@ -485,7 +493,7 @@
"IMAPSetupDialog_IMAPSettings": "IMAP Server Settings",
"IMAPSetupDialog_SMTPSettings": "SMTP Server Settings",
"IMAPSetupDialog_MailAddress": "Email address",
"IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com",
"IMAPSetupDialog_MailAddressPlaceholder": "someone@example.com or user@localhost",
"IMAPSetupDialog_OutgoingMailServer": "Outgoing (SMTP) mail server",
"IMAPSetupDialog_OutgoingMailServerPassword": "Outgoing server password",
"IMAPSetupDialog_OutgoingMailServerPort": "Port",
@@ -502,7 +510,7 @@
"ImapCalDavSettingsPage_TitleEdit": "Edit IMAP and Calendar Settings",
"ImapCalDavSettingsPage_Subtitle": "Configure IMAP/SMTP and optional calendar synchronization for this account.",
"ImapCalDavSettingsPage_BasicSectionTitle": "Basic setup",
"ImapCalDavSettingsPage_BasicSectionDescription": "Enter your identity and credentials. Wino can try to detect server settings automatically.",
"ImapCalDavSettingsPage_BasicSectionDescription": "Enter your identity and credentials. Wino supports manual addresses such as user@localhost and can try to detect server settings automatically.",
"ImapCalDavSettingsPage_BasicTab": "Basic",
"ImapCalDavSettingsPage_EnableCalendarSupport": "Enable calendar support",
"ImapCalDavSettingsPage_AutoDiscoverButton": "Autodiscover mail settings",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "No mail-enabled accounts",
"MailEmptyState_Message": "You have accounts connected for calendar, but none of them are enabled for mail. Add a mail account or update an existing account to use mail.",
"MailEmptyState_AddAccount": "Add account",
"MailEmptyState_ManageAccounts": "Manage accounts",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -702,8 +714,8 @@
"ProviderDetail_Gmail_Description": "Google Account",
"ProviderDetail_iCloud_Description": "Apple iCloud Account",
"ProviderDetail_iCloud_Title": "iCloud",
"ProviderDetail_IMAP_Description": "Custom IMAP/SMTP server",
"ProviderDetail_IMAP_Title": "IMAP Server",
"ProviderDetail_IMAP_Description": "IMAP/SMTP mail with CalDAV or local calendar",
"ProviderDetail_IMAP_Title": "Custom server",
"ProviderDetail_Yahoo_Description": "Yahoo Account",
"ProviderDetail_Yahoo_Title": "Yahoo Mail",
"QuickEventDialog_EventName": "Event name",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Group related messages into a single conversation.",
"SettingsThreads_Enabled_Title": "Enable conversation threading",
"SettingsThreadOrder_Description": "Choose how items are ordered inside a conversation thread.",
"SettingsThreadOrder_Title": "Thread item sorting",
"SettingsThreadOrder_LastItemFirst": "Last item first",
"SettingsThreadOrder_FirstItemFirst": "First item first",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1421,8 +1439,8 @@
"WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...",
"WinoAccount_Management_LocalDataSectionTitle": "Transfer with a JSON file",
"WinoAccount_Management_LocalDataSectionDescription": "Import from or export to a local JSON file. Passwords, tokens, and other sensitive information are not included.",
"WinoAccount_Management_LocalDataImportAction": "Import JSON",
"WinoAccount_Management_LocalDataExportAction": "Export JSON",
"WinoAccount_Management_LocalDataImportAction": "Import",
"WinoAccount_Management_LocalDataExportAction": "Export",
"WinoAccount_Management_LocalDataSaved": "Saved your exported Wino data to {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "The selected JSON file doesn't contain a valid Wino export.",
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
@@ -1521,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Finish Setup",
"ProviderSelection_Title": "Choose your email provider",
"ProviderSelection_Subtitle": "Select a provider below to add your email account to Wino Mail.",
"ProviderSelection_StepProgress": "Step {0} of 3",
"ProviderSelection_IdentityTitle": "Account identity",
"ProviderSelection_IdentityDescription": "Choose how this account appears inside Wino.",
"ProviderSelection_ProviderSectionTitle": "Provider",
"ProviderSelection_ProviderSectionDescription": "Select the service you want to connect.",
"ProviderSelection_CapabilitySectionTitle": "Use this account for",
"ProviderSelection_CapabilitySectionDescription": "Choose whether you want mail, calendar, or both.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "On the next step, secure sign-in will connect your account. If you enable calendar, Wino will also connect Outlook Calendar or Google Calendar automatically.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "On the next step, you'll enter your provider credentials. Mail uses IMAP/SMTP, and calendar can use CalDAV or stay local on this device.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "On the next step, you'll enter your server details. Mail uses IMAP/SMTP, and calendar can use CalDAV or stay local on this device.",
"ProviderSelection_AccountNameHeader": "Account Name",
"ProviderSelection_AccountNamePlaceholder": "e.g. Personal, Work",
"ProviderSelection_UseForMail": "Mail",
"ProviderSelection_UseForCalendar": "Calendar",
"ProviderSelection_CapabilityValidationMessage": "Choose at least one capability before continuing.",
"ProviderSelection_CalendarOnlyServerHint": "If you continue with calendar only, the next page will not require an email address.",
"ProviderSelection_DisplayNameHeader": "Display Name",
"ProviderSelection_DisplayNamePlaceholder": "e.g. John Doe",
"ProviderSelection_EmailHeader": "E-mail Address",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selecciona un calendario para configurar sus ajustes",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronización inicial",
"AccountDetailsPage_InitialSynchronization_Description": "Wino ha sincronizado tus correos hasta {0} atrás.",
"AccountDetailsPage_CapabilityTitle": "Características conectadas",
"AccountDetailsPage_CapabilityDescription": "Elija si esta cuenta se utilizará para correo, calendario o ambos. Al habilitar una nueva función podría pedirle que vuelva a iniciar sesión.",
"AccountCapability_MailOnly": "Solo correo",
"AccountCapability_CalendarOnly": "Solo calendario",
"AccountCapability_MailAndCalendar": "Correo y Calendario",
"AddHyperlink": "Añadir",
"AppCloseBackgroundSynchronizationWarningTitle": "Sincronización en segundo plano",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "La aplicación no está configurada para iniciarse con Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Aplicar Tema",
"Buttons_PopOut": "Desanclar",
"Buttons_Browse": "Buscar",
"Buttons_Back": "Atrás",
"Buttons_Cancel": "Cancelar",
"Buttons_Close": "Cerrar",
"Buttons_Copy": "Copiar",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Eliminar serie recurrente",
"DialogMessage_DiscardDraftConfirmationMessage": "Este borrador se descartará. ¿Desea continuar?",
"DialogMessage_DiscardDraftConfirmationTitle": "Descartar borrador",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Aún hay un borrador abierto. ¿Desea guardarlo antes de cerrar la ventana?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Cerrar ventana",
"DialogMessage_EmptySubjectConfirmation": "Sin asunto",
"DialogMessage_EmptySubjectConfirmationMessage": "El mensaje no tiene asunto. ¿Desea continuar?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Puedes habilitar esta opción en cualquier momento desde Ajustes > Configuración de la aplicación.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Ningún mensaje seleccionado",
"NoMessageCrieteria": "Ningún mensaje coincide con su criterio de búsqueda",
"NoMessageEmptyFolder": "Esta carpeta está vacía",
"MailEmptyState_Title": "No hay cuentas habilitadas para correo",
"MailEmptyState_Message": "Tiene cuentas conectadas para el calendario, pero ninguna está habilitada para el correo. Añada una cuenta de correo o actualice una cuenta existente para usar el correo.",
"MailEmptyState_AddAccount": "Añadir cuenta",
"MailEmptyState_ManageAccounts": "Gestionar cuentas",
"Notifications_MultipleNotificationsMessage": "Tienes {0} mensaje(s) nuevo(s).",
"Notifications_MultipleNotificationsTitle": "Mensajes nuevos",
"Notifications_WinoUpdatedMessage": "Comprobar nueva versión {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Insignia de la barra de tareas",
"SettingsThreads_Description": "Organizar mensajes en hilos de conversación.",
"SettingsThreads_Title": "Hilos de conversación",
"SettingsThreads_Enabled_Description": "Agrupa mensajes relacionados en una sola conversación.",
"SettingsThreads_Enabled_Title": "Habilitar el hilo de conversación",
"SettingsThreadOrder_Description": "Elija cómo se ordenan los elementos dentro de un hilo de conversación.",
"SettingsThreadOrder_Title": "Orden de elementos del hilo",
"SettingsThreadOrder_LastItemFirst": "Último elemento primero",
"SettingsThreadOrder_FirstItemFirst": "Primer elemento primero",
"SettingsUnlinkAccounts_Description": "Eliminar el enlace entre cuentas. No eliminará sus cuentas.",
"SettingsUnlinkAccounts_Title": "Desvincular Cuentas",
"SettingsMailRendering_ActionLabels_Title": "Etiquetas de acción",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Comienza añadiendo una cuenta",
"WelcomeWindow_GetStartedDescription": "Agrega tu cuenta de Outlook, Gmail o IMAP para empezar a usar Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importar desde tu cuenta de Wino",
"WelcomeWindow_ImportFromJsonFile": "Importar desde un archivo JSON",
"WelcomeWindow_ImportInProgress": "Importando tus preferencias y cuentas sincronizadas...",
"WelcomeWindow_ImportNoAccountsFound": "No se encontraron cuentas sincronizadas en su Cuenta de Wino. Si las preferencias estaban disponibles, se restauraron. Usa Comenzar para añadir una cuenta manualmente.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} cuentas sincronizadas ya están disponibles en este dispositivo. Usa Comenzar para añadir otra cuenta manualmente si es necesario.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Las contraseñas, los tokens y otra información sensible no se sincronizan.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Las cuentas importadas en otro PC seguirán requiriéndote iniciar sesión de nuevo antes de poder usarlas.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportando tus datos de Wino seleccionados...",
"WinoAccount_Management_LocalDataSectionTitle": "Transferir con un archivo JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importar desde o exportar a un archivo JSON local. Contraseñas, tokens y otra información sensible no se incluyen.",
"WinoAccount_Management_LocalDataImportAction": "Importar",
"WinoAccount_Management_LocalDataExportAction": "Exportar",
"WinoAccount_Management_LocalDataSaved": "Guardó sus datos exportados de Wino en {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "El archivo JSON seleccionado no contiene una exportación válida de Wino.",
"WinoAccount_Management_LoadFailed": "No se pudo cargar la información más reciente de la cuenta de Wino.",
"WinoAccount_Management_ActionFailed": "No se pudo completar la solicitud de la cuenta de Wino.",
"WinoAccount_SettingsSection_Title": "Cuenta de Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Finalizar configuración",
"ProviderSelection_Title": "Selecciona tu proveedor de correo",
"ProviderSelection_Subtitle": "Selecciona un proveedor a continuación para añadir tu cuenta de correo a Wino Mail.",
"ProviderSelection_StepProgress": "Paso {0} de 3",
"ProviderSelection_IdentityTitle": "Identidad de la cuenta",
"ProviderSelection_IdentityDescription": "Elija cómo aparecerá esta cuenta dentro de Wino.",
"ProviderSelection_ProviderSectionTitle": "Proveedor",
"ProviderSelection_ProviderSectionDescription": "Seleccione el servicio que desea conectar.",
"ProviderSelection_CapabilitySectionTitle": "Usar esta cuenta para",
"ProviderSelection_CapabilitySectionDescription": "Elija si desea correo, calendario o ambos.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "En el siguiente paso, el inicio de sesión seguro conectará su cuenta. Si habilita el calendario, Wino también conectará automáticamente Outlook Calendar o Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "En el siguiente paso, introducirá sus credenciales del proveedor. El correo utiliza IMAP/SMTP, y el calendario puede usar CalDAV o permanecer local en este dispositivo.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "En el siguiente paso, introducirá los detalles de su servidor. El correo utiliza IMAP/SMTP, y el calendario puede usar CalDAV o permanecer local en este dispositivo.",
"ProviderSelection_AccountNameHeader": "Nombre de la cuenta",
"ProviderSelection_AccountNamePlaceholder": "p. ej. Personal, Trabajo",
"ProviderSelection_UseForMail": "Correo",
"ProviderSelection_UseForCalendar": "Calendario",
"ProviderSelection_CapabilityValidationMessage": "Elija al menos una capacidad antes de continuar.",
"ProviderSelection_CalendarOnlyServerHint": "Si continúa solo con el calendario, la página siguiente no requerirá una dirección de correo electrónico.",
"ProviderSelection_DisplayNameHeader": "Nombre para mostrar",
"ProviderSelection_DisplayNamePlaceholder": "p. ej. John Doe",
"ProviderSelection_EmailHeader": "Correo electrónico",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Valitse kalenteri määrittääksesi sen asetukset",
"AccountDetailsPage_InitialSynchronization_Title": "Ensisynkronointi",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synkronoi sähköpostisi {0} taaksepäin.",
"AccountDetailsPage_CapabilityTitle": "Yhdistetyt ominaisuudet",
"AccountDetailsPage_CapabilityDescription": "Valitse, käytetäänkö tätä tiliä sähköpostiin, kalenteriin vai molempiin. Ominaisuuden käyttöönotto saattaa pyytää sinua kirjautumaan sisään uudelleen.",
"AccountCapability_MailOnly": "Vain sähköposti",
"AccountCapability_CalendarOnly": "Vain kalenteri",
"AccountCapability_MailAndCalendar": "Sähköposti ja Kalenteri",
"AddHyperlink": "Lisää",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Vaihda teemaa",
"Buttons_PopOut": "Avaa erillisessä ikkunassa",
"Buttons_Browse": "Selaa",
"Buttons_Back": "Takaisin",
"Buttons_Cancel": "Peruuta",
"Buttons_Close": "Sulje",
"Buttons_Copy": "Kopioi",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Poista toistuva sarja",
"DialogMessage_DiscardDraftConfirmationMessage": "Luonnos hylätään. Haluatko jatkaa?",
"DialogMessage_DiscardDraftConfirmationTitle": "Hylkää luonnos",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Luonnos on vielä auki. Tallenna se ennen ikkunan sulkemista?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Sulje ikkuna",
"DialogMessage_EmptySubjectConfirmation": "Aihe puuttuu",
"DialogMessage_EmptySubjectConfirmationMessage": "Viestillä ei ole aihetta. Haluatko jatkaa?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Ei viestiä valittuna",
"NoMessageCrieteria": "Mikään viesti ei vastaa hakuasi",
"NoMessageEmptyFolder": "Kansio tyhjä",
"MailEmptyState_Title": "Ei sähköpostia tukevia tilejä",
"MailEmptyState_Message": "Sinulla on tilejä, jotka on yhdistetty kalenteriin, mutta mikään niistä ei ole käytössä sähköpostin kannalta. Lisää sähköpostitili tai päivitä jokin olemassa oleva tili käyttämään sähköpostia.",
"MailEmptyState_AddAccount": "Lisää tili",
"MailEmptyState_ManageAccounts": "Hallitse tilejä",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Tarkasta uusi versio {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Ryhmittele liittyvät viestit yhdeksi keskusteluksi.",
"SettingsThreads_Enabled_Title": "Ota keskusteluketjutus käyttöön",
"SettingsThreadOrder_Description": "Valitse, miten kohteet järjestetään keskusteluketjussa.",
"SettingsThreadOrder_Title": "Ketjun kohteiden lajittelu",
"SettingsThreadOrder_LastItemFirst": "Viimeinen kohde ensin",
"SettingsThreadOrder_FirstItemFirst": "Ensimmäinen kohde ensin",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Aloita lisäämällä tilin",
"WelcomeWindow_GetStartedDescription": "Lisää Outlook-, Gmail- tai IMAP-tili aloittaaksesi Wino Mailin käytön.",
"WelcomeWindow_ImportFromWinoAccount": "Tuo Wino-tililtäsi",
"WelcomeWindow_ImportFromJsonFile": "Tuo JSON-tiedosto",
"WelcomeWindow_ImportInProgress": "Tuodaan synkronoitujen asetusten ja tilien tiedot...",
"WelcomeWindow_ImportNoAccountsFound": "Wino-tililläsi ei löytynyt synkronoituja tilejä. Jos asetukset olivat käytettävissä, ne palautettiin. Käytä Aloita tilin lisäämisen manuaalisesti.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synkronoitua tiliä on jo käytettävissä tässä laitteessa. Käytä Aloita tilin lisääminen manuaalisesti, jos tarvitset toisen tilin.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Salasanat, tunnukset ja muut arkaluonteiset tiedot eivät synkronoidu.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Toisella tietokoneella tuodut tilit vaativat vielä uudelleen kirjautumisen ennen kuin niitä voidaan käyttää.",
"WinoAccount_Management_ExportDialog_InProgress": "Viedään valitut Wino-tiedot...",
"WinoAccount_Management_LocalDataSectionTitle": "Siirrä JSON-tiedoston avulla",
"WinoAccount_Management_LocalDataSectionDescription": "Tuo tai vie paikalliseen JSON-tiedostoon. Salasanat, tokenit ja muut arkaluontoiset tiedot eivät sisälly.",
"WinoAccount_Management_LocalDataImportAction": "Tuo",
"WinoAccount_Management_LocalDataExportAction": "Vie",
"WinoAccount_Management_LocalDataSaved": "Tallensin vietyt Wino-tiedot kohteeseen {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Valittu JSON-tiedosto ei sisällä kelvollista Wino-vientiä.",
"WinoAccount_Management_LoadFailed": "Wino ei voinut ladata uusinta Wino-tilin tietoa.",
"WinoAccount_Management_ActionFailed": "Wino-tilin pyyntöä ei voitu suorittaa.",
"WinoAccount_SettingsSection_Title": "Wino-tili",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Viimeistele asennus",
"ProviderSelection_Title": "Valitse sähköpostipalveluntarjoajasi",
"ProviderSelection_Subtitle": "Valitse alla oleva palveluntarjoaja lisätäksesi sähköpostitilisi Wino Mailiin.",
"ProviderSelection_StepProgress": "Vaihe {0} / 3",
"ProviderSelection_IdentityTitle": "Tilin identiteetti",
"ProviderSelection_IdentityDescription": "Valitse, miltä tili näyttää Wino-sovelluksessa.",
"ProviderSelection_ProviderSectionTitle": "Palveluntarjoaja",
"ProviderSelection_ProviderSectionDescription": "Valitse yhdistettävä palvelu.",
"ProviderSelection_CapabilitySectionTitle": "Käytä tätä tiliä seuraaviin",
"ProviderSelection_CapabilitySectionDescription": "Valitse, haluatko käyttää sähköpostia, kalenteria vai molempia.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Seuraavassa vaiheessa turvallinen sisäänkirjautuminen yhdistää tilisi. Jos otat kalenterin käyttöön, Wino yhdistää automaattisesti Outlook Kalenterin tai Google Kalenterin.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Seuraavassa vaiheessa syötät palveluntarjoajan tunnistetiedot. Sähköposti käyttää IMAP/SMTP, ja kalenteri voi käyttää CalDAV:ia tai pysyä paikallisena tässä laitteessa.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Seuraavassa vaiheessa syötät palvelimen tiedot. Sähköposti käyttää IMAP/SMTP, ja kalenteri voi käyttää CalDAV:ia tai pysyä paikallisena tässä laitteessa.",
"ProviderSelection_AccountNameHeader": "Tilin nimi",
"ProviderSelection_AccountNamePlaceholder": "esim. Henkilökohtainen, Työ",
"ProviderSelection_UseForMail": "Sähköposti",
"ProviderSelection_UseForCalendar": "Kalenteri",
"ProviderSelection_CapabilityValidationMessage": "Jatkaaksesi valitse vähintään yksi ominaisuus.",
"ProviderSelection_CalendarOnlyServerHint": "Jos jatkat vain kalenterin kanssa, seuraavalla sivulla sähköpostiosoitetta ei tarvita.",
"ProviderSelection_DisplayNameHeader": "Näytettävä nimi",
"ProviderSelection_DisplayNamePlaceholder": "esim. John Doe",
"ProviderSelection_EmailHeader": "Sähköpostiosoite",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Sélectionnez un calendrier pour configurer ses paramètres.",
"AccountDetailsPage_InitialSynchronization_Title": "Synchronisation initiale",
"AccountDetailsPage_InitialSynchronization_Description": "Wino a synchronisé vos messages jusqu'au {0} en remontant.",
"AccountDetailsPage_CapabilityTitle": "Fonctionnalités connectées",
"AccountDetailsPage_CapabilityDescription": "Choisissez si ce compte est utilisé pour le courrier, le calendrier, ou les deux. L'activation d'une nouvelle fonctionnalité peut vous demander de vous reconnecter.",
"AccountCapability_MailOnly": "Courrier uniquement",
"AccountCapability_CalendarOnly": "Calendrier uniquement",
"AccountCapability_MailAndCalendar": "Courrier + Calendrier",
"AddHyperlink": "Ajouter",
"AppCloseBackgroundSynchronizationWarningTitle": "Synchronisation en arrière-plan",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "L'application n'a pas été configurée pour être lancée au démarrage de Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Appliquer le thème",
"Buttons_PopOut": "Détacher",
"Buttons_Browse": "Parcourir",
"Buttons_Back": "Retour",
"Buttons_Cancel": "Annuler",
"Buttons_Close": "Fermer",
"Buttons_Copy": "Copier",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Supprimer la série récurrente",
"DialogMessage_DiscardDraftConfirmationMessage": "Ce brouillon sera supprimé. Voulez-vous continuer ?",
"DialogMessage_DiscardDraftConfirmationTitle": "Supprimer le brouillon",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Un brouillon est encore ouvert. Enregistrez-le avant de fermer la fenêtre ?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Fermer la fenêtre",
"DialogMessage_EmptySubjectConfirmation": "Objet manquant",
"DialogMessage_EmptySubjectConfirmationMessage": "Le message n'a pas d'objet. Voulez-vous continuer ?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Vous pouvez activer le lancement au démarrage depuis Paramètres -> Préférences de l'application.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Aucun message sélectionné",
"NoMessageCrieteria": "Aucun message ne correspond à vos critères de recherche",
"NoMessageEmptyFolder": "Ce dossier est vide",
"MailEmptyState_Title": "Aucun compte activé pour le courrier",
"MailEmptyState_Message": "Vous avez des comptes connectés au calendrier, mais aucun dentre eux nest activé pour le courrier. Ajoutez un compte de courrier ou mettez à jour un compte existant pour utiliser le courrier.",
"MailEmptyState_AddAccount": "Ajouter un compte",
"MailEmptyState_ManageAccounts": "Gérer les comptes",
"Notifications_MultipleNotificationsMessage": "Vous avez {0} nouveaux messages.",
"Notifications_MultipleNotificationsTitle": "Nouveau message",
"Notifications_WinoUpdatedMessage": "Vérifier la nouvelle version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Badge de la barre de tâches",
"SettingsThreads_Description": "Organiser les messages en fils de conversation.",
"SettingsThreads_Title": "Affichage en mode conversation",
"SettingsThreads_Enabled_Description": "Regrouper les messages liés en une seule conversation.",
"SettingsThreads_Enabled_Title": "Activer le fil de discussion",
"SettingsThreadOrder_Description": "Choisissez l'ordre des éléments dans un fil de discussion.",
"SettingsThreadOrder_Title": "Tri des éléments du fil de discussion",
"SettingsThreadOrder_LastItemFirst": "Dernier élément en premier",
"SettingsThreadOrder_FirstItemFirst": "Premier élément en premier",
"SettingsUnlinkAccounts_Description": "Supprimer le lien entre les comptes. Cela ne supprimera pas vos comptes.",
"SettingsUnlinkAccounts_Title": "Dissocier les comptes",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Commencer en ajoutant un compte",
"WelcomeWindow_GetStartedDescription": "Ajoutez votre compte Outlook, Gmail ou IMAP pour commencer avec Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importer depuis votre compte Wino",
"WelcomeWindow_ImportFromJsonFile": "Importer à partir d'un fichier JSON",
"WelcomeWindow_ImportInProgress": "Importation de vos préférences et comptes synchronisés...",
"WelcomeWindow_ImportNoAccountsFound": "Aucun compte synchronisé n'a été trouvé dans votre compte Wino. Si des préférences étaient disponibles, elles ont été restaurées. Utilisez Commencer pour ajouter manuellement un compte.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} comptes synchronisés sont déjà disponibles sur cet appareil. Utilisez Commencer pour en ajouter un autre manuellement si nécessaire.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Les mots de passe, jetons et autres informations sensibles ne sont pas synchronisés.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Les comptes importés sur un autre PC devront quand même être reconnectés avant de pouvoir être utilisés.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportation de vos données Wino sélectionnées...",
"WinoAccount_Management_LocalDataSectionTitle": "Transfert via un fichier JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importer ou exporter vers un fichier JSON local. Les mots de passe, jetons et autres informations sensibles ne sont pas incluses.",
"WinoAccount_Management_LocalDataImportAction": "Importer",
"WinoAccount_Management_LocalDataExportAction": "Exporter",
"WinoAccount_Management_LocalDataSaved": "Vos données Wino exportées ont été sauvegardées dans {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Le fichier JSON sélectionné ne contient pas d'export Wino valide.",
"WinoAccount_Management_LoadFailed": "Wino n'a pas pu charger les dernières informations du compte Wino.",
"WinoAccount_Management_ActionFailed": "La requête du compte Wino n'a pas pu être complétée.",
"WinoAccount_SettingsSection_Title": "Compte Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Terminer la configuration",
"ProviderSelection_Title": "Choisissez votre fournisseur de messagerie",
"ProviderSelection_Subtitle": "Sélectionnez un fournisseur ci-dessous pour ajouter votre compte de messagerie à Wino Mail.",
"ProviderSelection_StepProgress": "Étape {0} sur 3",
"ProviderSelection_IdentityTitle": "Identité du compte",
"ProviderSelection_IdentityDescription": "Choisissez comment ce compte apparaît dans Wino.",
"ProviderSelection_ProviderSectionTitle": "Fournisseur",
"ProviderSelection_ProviderSectionDescription": "Sélectionnez le service que vous souhaitez connecter.",
"ProviderSelection_CapabilitySectionTitle": "Utiliser ce compte pour",
"ProviderSelection_CapabilitySectionDescription": "Choisissez si vous souhaitez le courrier, le calendrier, ou les deux.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "À l'étape suivante, une connexion sécurisée reliera votre compte. Si vous activez le calendrier, Wino connectera automatiquement Outlook Calendar ou Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "À l'étape suivante, vous saisirez vos identifiants du fournisseur. Le courrier utilise IMAP/SMTP, et le calendrier peut utiliser CalDAV ou rester local sur cet appareil.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "À l'étape suivante, vous saisirez les détails du serveur. Le courrier utilise IMAP/SMTP, et le calendrier peut utiliser CalDAV ou rester local sur cet appareil.",
"ProviderSelection_AccountNameHeader": "Nom du compte",
"ProviderSelection_AccountNamePlaceholder": "par ex. Personnel, Professionnel",
"ProviderSelection_UseForMail": "Courrier",
"ProviderSelection_UseForCalendar": "Calendrier",
"ProviderSelection_CapabilityValidationMessage": "Choisissez au moins une capacité avant de continuer.",
"ProviderSelection_CalendarOnlyServerHint": "Si vous continuez avec le calendrier uniquement, la page suivante ne nécessitera pas d'adresse e-mail.",
"ProviderSelection_DisplayNameHeader": "Nom affiché",
"ProviderSelection_DisplayNamePlaceholder": "par ex. John Doe",
"ProviderSelection_EmailHeader": "Adresse e-mail",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selecciona un calendario para configurar as súas configuracións.",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronización inicial",
"AccountDetailsPage_InitialSynchronization_Description": "Wino sincronizou os teus correos ata {0} no pasado.",
"AccountDetailsPage_CapabilityTitle": "Características conectadas",
"AccountDetailsPage_CapabilityDescription": "Elixe se esta conta se usa para correo, calendario ou para ambos. Activar unha nova función pode pedirche que inicies sesión de novo.",
"AccountCapability_MailOnly": "Só correo",
"AccountCapability_CalendarOnly": "Só calendario",
"AccountCapability_MailAndCalendar": "Correo e Calendario",
"AddHyperlink": "Engadir",
"AppCloseBackgroundSynchronizationWarningTitle": "Sincronización en segundo plano",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Aplicar tema",
"Buttons_PopOut": "Abrir en nova xanela",
"Buttons_Browse": "Procurar",
"Buttons_Back": "Atrás",
"Buttons_Cancel": "Anular",
"Buttons_Close": "Pechar",
"Buttons_Copy": "Copiar",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Eliminar serie recurrente",
"DialogMessage_DiscardDraftConfirmationMessage": "O borrador será eliminado. Quere continuar?",
"DialogMessage_DiscardDraftConfirmationTitle": "Eliminar borrador",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Un borrador aínda está aberto. Garda-o antes de pechar a xanela?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Pechar a xanela",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "A mensaxe non ten asunto. Queres continuar?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "Non hai contas habilitadas para correo",
"MailEmptyState_Message": "Tes contas conectadas para calendario, pero ningunha delas está habilitada para correo. Engade unha conta de correo ou actualiza unha conta existente para usar correo.",
"MailEmptyState_AddAccount": "Engadir conta",
"MailEmptyState_ManageAccounts": "Xestionar contas",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Agrupa as mensaxes relacionadas nunha única conversa.",
"SettingsThreads_Enabled_Title": "Habilitar o fío da conversa",
"SettingsThreadOrder_Description": "Elixe como se ordenan os elementos dentro do fío de conversa.",
"SettingsThreadOrder_Title": "Ordenación de elementos do fío",
"SettingsThreadOrder_LastItemFirst": "O último elemento primeiro",
"SettingsThreadOrder_FirstItemFirst": "O primeiro elemento primeiro",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Comeza engadindo unha conta",
"WelcomeWindow_GetStartedDescription": "Engade a túa conta de Outlook, Gmail ou IMAP para comezar a usar Wino Mail",
"WelcomeWindow_ImportFromWinoAccount": "Importar desde a túa Conta Wino",
"WelcomeWindow_ImportFromJsonFile": "Importar desde un arquivo JSON",
"WelcomeWindow_ImportInProgress": "Importando as túas preferencias e contas sincronizadas...",
"WelcomeWindow_ImportNoAccountsFound": "Non se atoparon contas sincronizadas na túa Conta Wino. Se as preferencias estaban dispoñibles, foron restauradas. Usa Comeza para engadir unha conta manualmente",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} contas sincronizadas xa están dispoñibles neste dispositivo. Usa Comeza para engadir outra conta manualmente se é necesario",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Contrasinais, tokens e outra información sensible non se sincronizan.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "As contas importadas noutro PC seguirán a necesitar que inicies sesión de novo antes de poder usalas.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportando os datos de Wino seleccionados",
"WinoAccount_Management_LocalDataSectionTitle": "Transferir cun arquivo JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importar desde ou exportar a un arquivo JSON local. Contrasinais, tokens e outras informacións sensibles non están incluídas.",
"WinoAccount_Management_LocalDataImportAction": "Importar",
"WinoAccount_Management_LocalDataExportAction": "Exportar",
"WinoAccount_Management_LocalDataSaved": "Os teus datos exportados de Wino gardáronse en {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "O ficheiro JSON seleccionado non contén unha exportación válida de Wino.",
"WinoAccount_Management_LoadFailed": "Wino non conseguiu carregar a información máis recente da Conta Wino.",
"WinoAccount_Management_ActionFailed": "A solicitude da Conta Wino non puido completarse.",
"WinoAccount_SettingsSection_Title": "Conta Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Rematar a configuración",
"ProviderSelection_Title": "Elixe o teu provedor de correo",
"ProviderSelection_Subtitle": "Selecciona un provedor a continuación para engadir a túa conta de correo a Wino Mail.",
"ProviderSelection_StepProgress": "Fase {0} de 3",
"ProviderSelection_IdentityTitle": "Identidade da conta",
"ProviderSelection_IdentityDescription": "Elixe como aparecerá esta conta dentro de Wino.",
"ProviderSelection_ProviderSectionTitle": "Provedor",
"ProviderSelection_ProviderSectionDescription": "Selecciona o servizo que queres conectar.",
"ProviderSelection_CapabilitySectionTitle": "Usar esta conta para",
"ProviderSelection_CapabilitySectionDescription": "Elixe se queres correo, calendario ou ambos.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "No seguinte paso, o inicio de sesión seguro conectará a túa conta. Se habilitas o calendario, Wino conectará automaticamente Outlook Calendar ou Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "No seguinte paso, introducirás as credenciais do provedor. O correo usa IMAP/SMTP, e o calendario pode usar CalDAV ou permanecer local neste dispositivo.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "No seguinte paso, introducirás os datos do servidor. O correo usa IMAP/SMTP, e o calendario pode usar CalDAV ou permanecer local neste dispositivo.",
"ProviderSelection_AccountNameHeader": "Nome da conta",
"ProviderSelection_AccountNamePlaceholder": "p. ex. Persoal, Laboral",
"ProviderSelection_UseForMail": "Correo",
"ProviderSelection_UseForCalendar": "Calendario",
"ProviderSelection_CapabilityValidationMessage": "Elixe polo menos unha capacidade antes de continuar.",
"ProviderSelection_CalendarOnlyServerHint": "Se continúas só co calendario, a seguinte páxina non requirirá unha dirección de correo.",
"ProviderSelection_DisplayNameHeader": "Nome para amosar",
"ProviderSelection_DisplayNamePlaceholder": "p. ex. John Doe",
"ProviderSelection_EmailHeader": "Enderezo de correo electrónico",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Pilih kalender untuk mengonfigurasi pengaturannya",
"AccountDetailsPage_InitialSynchronization_Title": "Sinkronisasi awal",
"AccountDetailsPage_InitialSynchronization_Description": "Wino telah menyinkronkan surel Anda hingga {0} ke belakang.",
"AccountDetailsPage_CapabilityTitle": "Fitur yang Terhubung",
"AccountDetailsPage_CapabilityDescription": "Pilih apakah akun ini digunakan untuk email, kalender, atau keduanya. Mengaktifkan fitur baru mungkin meminta Anda untuk masuk lagi.",
"AccountCapability_MailOnly": "Hanya Email",
"AccountCapability_CalendarOnly": "Hanya Kalender",
"AccountCapability_MailAndCalendar": "Email dan Kalender",
"AddHyperlink": "Tambahkan",
"AppCloseBackgroundSynchronizationWarningTitle": "Penyelarasan Latar Belakang",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikasi tidak diatur untuk berjalan saat Windows dimulai.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Terapkan Tema",
"Buttons_PopOut": "Buka jendela terpisah",
"Buttons_Browse": "Telusuri",
"Buttons_Back": "Kembali",
"Buttons_Cancel": "Batal",
"Buttons_Close": "Tutup",
"Buttons_Copy": "Salin",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Hapus Seri Berulang",
"DialogMessage_DiscardDraftConfirmationMessage": "Konsep ini akan dibuang. Apakah Anda ingin melanjutkan?",
"DialogMessage_DiscardDraftConfirmationTitle": "Buang Konsep",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Sebuah draf masih terbuka. Simpan sebelum menutup jendela?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Tutup Jendela",
"DialogMessage_EmptySubjectConfirmation": "Tidak Ada Perihal",
"DialogMessage_EmptySubjectConfirmationMessage": "Pesan tidak memiliki perihal. Apakah Anda ingin melanjutkan?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Anda dapat menyalakan Luncurkan ketika Mulai di Pengaturan -> Pengaturan Aplikasi.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Tidak ada pesan yang dipilih",
"NoMessageCrieteria": "Tidak ada pesan yang sesuai kriteria",
"NoMessageEmptyFolder": "Folder ini kosong",
"MailEmptyState_Title": "Tidak ada akun yang mendukung email",
"MailEmptyState_Message": "Anda memiliki akun yang terhubung untuk kalender, tetapi tidak ada yang diaktifkan untuk email.Tambahkan akun email atau perbarui akun yang ada untuk menggunakan email.",
"MailEmptyState_AddAccount": "Tambahkan akun",
"MailEmptyState_ManageAccounts": "Kelola akun",
"Notifications_MultipleNotificationsMessage": "Anda memiliki {0} pesan baru.",
"Notifications_MultipleNotificationsTitle": "Pesan Baru",
"Notifications_WinoUpdatedMessage": "Versi baru terpasang {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Lencana Bilah Tugas",
"SettingsThreads_Description": "Kelompokkan pesan menjadi utas.",
"SettingsThreads_Title": "Utas Percakapan",
"SettingsThreads_Enabled_Description": "Gabungkan pesan terkait menjadi satu percakapan.",
"SettingsThreads_Enabled_Title": "Aktifkan pengelompokan percakapan",
"SettingsThreadOrder_Description": "Pilih bagaimana item diurutkan di dalam percakapan.",
"SettingsThreadOrder_Title": "Pengurutan item percakapan",
"SettingsThreadOrder_LastItemFirst": "Item terakhir pertama",
"SettingsThreadOrder_FirstItemFirst": "Item pertama dulu",
"SettingsUnlinkAccounts_Description": "Hapus tautan antara akun. Ini tidak akan menghapus akun Anda.",
"SettingsUnlinkAccounts_Title": "Lepaskan Tautan Akun",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Mulai dengan menambahkan akun",
"WelcomeWindow_GetStartedDescription": "Tambahkan akun Outlook, Gmail, atau IMAP Anda untuk memulai dengan Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Impor dari Akun Wino Anda",
"WelcomeWindow_ImportFromJsonFile": "Impor dari file JSON",
"WelcomeWindow_ImportInProgress": "Mengimpor preferensi dan akun yang tersinkronkan...",
"WelcomeWindow_ImportNoAccountsFound": "Tidak ada akun yang tersinkronisasi ditemukan di Akun Wino Anda. Jika preferensi tersedia, mereka telah dipulihkan. Gunakan Mulai untuk menambahkan akun secara manual.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} akun yang tersinkronisasi sudah tersedia di perangkat ini. Gunakan Mulai untuk menambahkan akun lain secara manual jika diperlukan.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Kata sandi, token, dan informasi sensitif lainnya tidak disinkronkan.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Akun yang diimpor di PC lain masih perlu Anda masuk lagi sebelum dapat digunakan.",
"WinoAccount_Management_ExportDialog_InProgress": "Sedang mengekspor data Wino yang Anda pilih...",
"WinoAccount_Management_LocalDataSectionTitle": "Transfer dengan file JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Impor dari atau ekspor ke file JSON lokal. Kata sandi, token, dan informasi sensitif lainnya tidak disertakan.",
"WinoAccount_Management_LocalDataImportAction": "Impor",
"WinoAccount_Management_LocalDataExportAction": "Ekspor",
"WinoAccount_Management_LocalDataSaved": "Data Wino yang diekspor telah disimpan ke {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "File JSON yang dipilih tidak berisi ekspor Wino yang valid.",
"WinoAccount_Management_LoadFailed": "Wino tidak dapat memuat informasi Akun Wino terbaru.",
"WinoAccount_Management_ActionFailed": "Permintaan Akun Wino tidak dapat diselesaikan.",
"WinoAccount_SettingsSection_Title": "Akun Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Selesaikan Pengaturan",
"ProviderSelection_Title": "Pilih penyedia email Anda",
"ProviderSelection_Subtitle": "Pilih penyedia di bawah ini untuk menambahkan akun email Anda ke Wino Mail.",
"ProviderSelection_StepProgress": "Langkah {0} dari 3",
"ProviderSelection_IdentityTitle": "Identitas akun",
"ProviderSelection_IdentityDescription": "Pilih bagaimana akun ini ditampilkan di dalam Wino.",
"ProviderSelection_ProviderSectionTitle": "Penyedia",
"ProviderSelection_ProviderSectionDescription": "Pilih layanan yang ingin Anda hubungkan.",
"ProviderSelection_CapabilitySectionTitle": "Gunakan akun ini untuk",
"ProviderSelection_CapabilitySectionDescription": "Pilih apakah Anda ingin email, kalender, atau keduanya.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Pada langkah berikutnya, proses masuk yang aman akan menghubungkan akun Anda. Jika Anda mengaktifkan kalender, Wino juga akan secara otomatis menghubungkan Outlook Calendar atau Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Pada langkah berikutnya, Anda akan memasukkan kredensial penyedia Anda. Email menggunakan IMAP/SMTP, dan kalender dapat menggunakan CalDAV atau tetap lokal di perangkat ini.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Pada langkah berikutnya, Anda akan memasukkan detail server Anda. Email menggunakan IMAP/SMTP, dan kalender dapat menggunakan CalDAV atau tetap lokal di perangkat ini.",
"ProviderSelection_AccountNameHeader": "Nama Akun",
"ProviderSelection_AccountNamePlaceholder": "contoh: Pribadi, Kerja",
"ProviderSelection_UseForMail": "Email",
"ProviderSelection_UseForCalendar": "Kalender",
"ProviderSelection_CapabilityValidationMessage": "Pilih setidaknya satu kemampuan sebelum melanjutkan.",
"ProviderSelection_CalendarOnlyServerHint": "Jika Anda melanjutkan dengan kalender saja, halaman berikutnya tidak akan memerlukan alamat email.",
"ProviderSelection_DisplayNameHeader": "Nama Tampilan",
"ProviderSelection_DisplayNamePlaceholder": "contoh: John Doe",
"ProviderSelection_EmailHeader": "Alamat Email",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Seleziona un calendario per configurarne le impostazioni.",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronizzazione iniziale",
"AccountDetailsPage_InitialSynchronization_Description": "Wino ha sincronizzato la tua posta fino a {0} nel passato.",
"AccountDetailsPage_CapabilityTitle": "Funzionalità connesse",
"AccountDetailsPage_CapabilityDescription": "Scegli se questo account viene utilizzato per la posta, il calendario o entrambi. Attivando una nuova funzionalità potrebbe chiederti di accedere nuovamente.",
"AccountCapability_MailOnly": "Solo posta",
"AccountCapability_CalendarOnly": "Solo calendario",
"AccountCapability_MailAndCalendar": "Posta e Calendario",
"AddHyperlink": "Aggiungi",
"AppCloseBackgroundSynchronizationWarningTitle": "Sincronizzazione dietro le quinte",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "L'applicazione non è stata impostata per avviarsi all'avvio di Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Applica tema",
"Buttons_PopOut": "Apri in una nuova finestra",
"Buttons_Browse": "Sfoglia",
"Buttons_Back": "Indietro",
"Buttons_Cancel": "Annulla",
"Buttons_Close": "Chiudi",
"Buttons_Copy": "Copia",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Elimina serie ricorrente",
"DialogMessage_DiscardDraftConfirmationMessage": "Questa bozza sarà scartata. Vuoi continuare?",
"DialogMessage_DiscardDraftConfirmationTitle": "Scarta bozza",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Una bozza è ancora aperta. Salvala prima di chiudere la finestra?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Chiudi finestra",
"DialogMessage_EmptySubjectConfirmation": "Oggetto mancante",
"DialogMessage_EmptySubjectConfirmationMessage": "Il messaggio non ha oggetto. Vuoi continuare?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "È possibile abilitare l'avvio da Impostazioni -> Preferenze app.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Nessun messaggio selezionato",
"NoMessageCrieteria": "Nessun messaggio corrisponde ai tuoi criteri di ricerca",
"NoMessageEmptyFolder": "Questa cartella è vuota",
"MailEmptyState_Title": "Nessun account abilitato per la posta",
"MailEmptyState_Message": "Hai account collegati al calendario, ma nessuno è abilitato per la posta. Aggiungi un account di posta o aggiorna un account esistente per utilizzare la posta.",
"MailEmptyState_AddAccount": "Aggiungi account",
"MailEmptyState_ManageAccounts": "Gestisci account",
"Notifications_MultipleNotificationsMessage": "Hai {0} nuovi messaggi.",
"Notifications_MultipleNotificationsTitle": "Nuova posta",
"Notifications_WinoUpdatedMessage": "Controlla la nuova versione {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "In barra attività come",
"SettingsThreads_Description": "Organizza i messaggi in gruppi di conversazione.",
"SettingsThreads_Title": "Raggruppamento conversazioni",
"SettingsThreads_Enabled_Description": "Raggruppa i messaggi correlati in una singola conversazione.",
"SettingsThreads_Enabled_Title": "Abilita la visualizzazione a thread delle conversazioni.",
"SettingsThreadOrder_Description": "Scegli come vengono ordinati gli elementi all'interno di una conversazione a thread.",
"SettingsThreadOrder_Title": "Ordinamento degli elementi della conversazione",
"SettingsThreadOrder_LastItemFirst": "Ultimo elemento per primo",
"SettingsThreadOrder_FirstItemFirst": "Primo elemento per primo",
"SettingsUnlinkAccounts_Description": "Rimuovi il collegamento tra gli account. Questo non eliminerà i tuoi account.",
"SettingsUnlinkAccounts_Title": "Scollega account",
"SettingsMailRendering_ActionLabels_Title": "Etichette azioni",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Inizia aggiungendo un account",
"WelcomeWindow_GetStartedDescription": "Aggiungi il tuo account Outlook, Gmail o IMAP per iniziare a usare Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importa dal tuo account Wino",
"WelcomeWindow_ImportFromJsonFile": "Importa da un file JSON",
"WelcomeWindow_ImportInProgress": "Importazione delle preferenze e degli account sincronizzati in corso...",
"WelcomeWindow_ImportNoAccountsFound": "Nessun account sincronizzato è stato trovato nel tuo account Wino. Se erano disponibili le preferenze, sono state ripristinate. Usa Inizia per aggiungere manualmente un account.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} account sincronizzati sono già disponibili su questo dispositivo. Usa Inizia per aggiungere manualmente un altro account se necessario.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Le password, i token e altre informazioni sensibili non sono sincronizzati.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Gli account importati su un altro PC richiederanno comunque di effettuare nuovamente l'accesso prima che possano essere utilizzati.",
"WinoAccount_Management_ExportDialog_InProgress": "Esportazione dei dati Wino selezionati in corso...",
"WinoAccount_Management_LocalDataSectionTitle": "Trasferisci con un file JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importa o esporta in un file JSON locale. password, token e altre informazioni sensibili non sono inclusi.",
"WinoAccount_Management_LocalDataImportAction": "Importa",
"WinoAccount_Management_LocalDataExportAction": "Esporta",
"WinoAccount_Management_LocalDataSaved": "Hai salvato i dati esportati di Wino in {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Il file JSON selezionato non contiene un'esportazione Wino valida.",
"WinoAccount_Management_LoadFailed": "Impossibile caricare le ultime informazioni dell'Account Wino.",
"WinoAccount_Management_ActionFailed": "La richiesta dell'Account Wino non può essere completata.",
"WinoAccount_SettingsSection_Title": "Account Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Completa la configurazione",
"ProviderSelection_Title": "Scegli il provider di posta elettronica",
"ProviderSelection_Subtitle": "Seleziona un provider qui sotto per aggiungere il tuo account di posta a Wino Mail.",
"ProviderSelection_StepProgress": "Fase {0} di 3",
"ProviderSelection_IdentityTitle": "Identità dell'account",
"ProviderSelection_IdentityDescription": "Scegli come apparirà questo account in Wino.",
"ProviderSelection_ProviderSectionTitle": "Fornitore",
"ProviderSelection_ProviderSectionDescription": "Seleziona il servizio che vuoi connettere.",
"ProviderSelection_CapabilitySectionTitle": "Usa questo account per",
"ProviderSelection_CapabilitySectionDescription": "Scegli se vuoi posta, calendario o entrambi.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Nel passaggio successivo, l'autenticazione sicura collegherà il tuo account. Se abiliti il calendario, Wino collegherà automaticamente anche Outlook Calendar o Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Nel passaggio successivo inserirai le credenziali del provider. La posta utilizza IMAP/SMTP, e il calendario può utilizzare CalDAV o rimanere locale su questo dispositivo.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Nel passaggio successivo inserirai i dettagli del server. La posta utilizza IMAP/SMTP, e il calendario può utilizzare CalDAV o rimanere locale su questo dispositivo.",
"ProviderSelection_AccountNameHeader": "Nome account",
"ProviderSelection_AccountNamePlaceholder": "ad es. Personale, Lavoro",
"ProviderSelection_UseForMail": "Posta",
"ProviderSelection_UseForCalendar": "Calendario",
"ProviderSelection_CapabilityValidationMessage": "Seleziona almeno una funzionalità prima di procedere.",
"ProviderSelection_CalendarOnlyServerHint": "Se prosegui con solo calendario, la pagina successiva non richiederà un indirizzo email.",
"ProviderSelection_DisplayNameHeader": "Nome visualizzato",
"ProviderSelection_DisplayNamePlaceholder": "es. John Doe",
"ProviderSelection_EmailHeader": "Indirizzo e-mail",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "設定を構成するカレンダーを選択してください。",
"AccountDetailsPage_InitialSynchronization_Title": "初期同期",
"AccountDetailsPage_InitialSynchronization_Description": "Wino は {0} まで遡ってメールを同期しました。",
"AccountDetailsPage_CapabilityTitle": "接続機能",
"AccountDetailsPage_CapabilityDescription": "このアカウントをメール、カレンダー、または両方に使用するかを選択します。新機能を有効にすると、再度サインインを求められることがあります。",
"AccountCapability_MailOnly": "メールのみ",
"AccountCapability_CalendarOnly": "カレンダーのみ",
"AccountCapability_MailAndCalendar": "メール+カレンダー",
"AddHyperlink": "追加",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "テーマを適用",
"Buttons_PopOut": "ポップアウト",
"Buttons_Browse": "閲覧",
"Buttons_Back": "戻る",
"Buttons_Cancel": "キャンセル",
"Buttons_Close": "閉じる",
"Buttons_Copy": "コピー",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "繰り返しシリーズを削除",
"DialogMessage_DiscardDraftConfirmationMessage": "この下書きは破棄されます。続行しますか?",
"DialogMessage_DiscardDraftConfirmationTitle": "下書きを破棄",
"DialogMessage_CloseDraftWindowConfirmationMessage": "下書きがまだ開いています。ウィンドウを閉じる前に保存しますか?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "ウィンドウを閉じる",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "メールが有効なアカウントがありません",
"MailEmptyState_Message": "カレンダー用に接続されているアカウントはありますが、メール用として有効になっているものはありません。メールアカウントを追加するか、既存のアカウントを更新してメールを使用してください。",
"MailEmptyState_AddAccount": "アカウントを追加",
"MailEmptyState_ManageAccounts": "アカウントを管理",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "関連するメッセージを1つの会話にまとめます。",
"SettingsThreads_Enabled_Title": "会話のスレッド化を有効にする",
"SettingsThreadOrder_Description": "会話スレッド内のアイテムの並べ替え方法を選択します。",
"SettingsThreadOrder_Title": "スレッド内アイテムの並べ替え",
"SettingsThreadOrder_LastItemFirst": "最後のアイテムを先頭に表示",
"SettingsThreadOrder_FirstItemFirst": "最初のアイテムを先頭に表示",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "アカウントを追加して始める",
"WelcomeWindow_GetStartedDescription": "Wino Mail を始めるには、Outlook、Gmail、または IMAP アカウントを追加してください。",
"WelcomeWindow_ImportFromWinoAccount": "Wino アカウントからインポート",
"WelcomeWindow_ImportFromJsonFile": "JSONファイルからインポート",
"WelcomeWindow_ImportInProgress": "同期された設定とアカウントをインポートしています...",
"WelcomeWindow_ImportNoAccountsFound": "Wino アカウントに同期されたアカウントは見つかりませんでした。設定が利用可能だった場合、それらは復元されました。アカウントを手動で追加するには、はじめるを使ってください。",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} 個の同期済みアカウントはすでにこのデバイスにあります。必要に応じて、別のアカウントを手動で追加するにははじめるを使用してください。",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "パスワード、トークン、その他の機密情報は同期されません。",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "他のPCでインポートされたアカウントを使用するには、再度サインインする必要があります。",
"WinoAccount_Management_ExportDialog_InProgress": "選択した Wino データをエクスポートしています...",
"WinoAccount_Management_LocalDataSectionTitle": "JSONファイルで転送",
"WinoAccount_Management_LocalDataSectionDescription": "ローカルJSONファイルからのインポートまたはエクスポートを行います。パスワード、トークン、その他の機密情報は含まれません。",
"WinoAccount_Management_LocalDataImportAction": "インポート",
"WinoAccount_Management_LocalDataExportAction": "エクスポート",
"WinoAccount_Management_LocalDataSaved": "エクスポートしたWinoデータを {0} に保存しました。",
"WinoAccount_Management_LocalDataInvalidFile": "選択したJSONファイルには有効なWinoエクスポートが含まれていません。",
"WinoAccount_Management_LoadFailed": "Wino は最新の Wino アカウント情報を読み込めませんでした。",
"WinoAccount_Management_ActionFailed": "Wino アカウントのリクエストを完了できませんでした。",
"WinoAccount_SettingsSection_Title": "Wino アカウント",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "設定を完了",
"ProviderSelection_Title": "メールプロバイダを選択",
"ProviderSelection_Subtitle": "以下のプロバイダを選択して、Wino Mail にメールアカウントを追加します。",
"ProviderSelection_StepProgress": "ステップ {0} / 3",
"ProviderSelection_IdentityTitle": "アカウントの識別情報",
"ProviderSelection_IdentityDescription": "このアカウントがWino内でどのように表示されるかを選択します。",
"ProviderSelection_ProviderSectionTitle": "プロバイダ",
"ProviderSelection_ProviderSectionDescription": "接続したいサービスを選択します。",
"ProviderSelection_CapabilitySectionTitle": "このアカウントを使用する用途",
"ProviderSelection_CapabilitySectionDescription": "メール、カレンダー、または両方を使用するかを選択します。",
"ProviderSelection_CapabilityProviderDescription_OAuth": "次のステップで安全なサインインがあなたのアカウントを接続します。カレンダーを有効にすると、Winoは自動的にOutlook CalendarまたはGoogle Calendarにも接続します。",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "次のステップで、プロバイダの認証情報を入力します。メールはIMAP/SMTPを使用します。カレンダーはCalDAVを使用するか、このデバイスでローカルに保存します。",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "次のステップでサーバー情報を入力します。メールはIMAP/SMTPを使用します。カレンダーはCalDAVを使用するか、このデバイスにローカルで保存します。",
"ProviderSelection_AccountNameHeader": "アカウント名",
"ProviderSelection_AccountNamePlaceholder": "例: Personal、Work",
"ProviderSelection_UseForMail": "メール",
"ProviderSelection_UseForCalendar": "カレンダー",
"ProviderSelection_CapabilityValidationMessage": "続行する前に、少なくとも1つの機能を選択してください。",
"ProviderSelection_CalendarOnlyServerHint": "カレンダーのみで続行する場合、次のページではメールアドレスを必要としません。",
"ProviderSelection_DisplayNameHeader": "表示名",
"ProviderSelection_DisplayNamePlaceholder": "例: John Doe",
"ProviderSelection_EmailHeader": "メールアドレス",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Pasirinkite kalendorių, kad sukonfigūruotumėte jo nustatymus.",
"AccountDetailsPage_InitialSynchronization_Title": "Pradinė sinchronizacija",
"AccountDetailsPage_InitialSynchronization_Description": "Wino sinchronizavo jūsų laiškus iki {0}.",
"AccountDetailsPage_CapabilityTitle": "Prisijungtos funkcijos",
"AccountDetailsPage_CapabilityDescription": "Pasirinkite, ar ši paskyra bus naudojama el. paštu, kalendoriui ar abiem. Įjungus naują funkciją gali tekti prisijungti dar kartą.",
"AccountCapability_MailOnly": "Tik el. paštas",
"AccountCapability_CalendarOnly": "Tik kalendorius",
"AccountCapability_MailAndCalendar": "El. paštas ir kalendorius",
"AddHyperlink": "Add",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Apply Theme",
"Buttons_PopOut": "Iššokti",
"Buttons_Browse": "Browse",
"Buttons_Back": "Atgal",
"Buttons_Cancel": "Cancel",
"Buttons_Close": "Close",
"Buttons_Copy": "Copy",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Ištrinti pakartojančią seriją",
"DialogMessage_DiscardDraftConfirmationMessage": "This draft will be discarded. Do you want to continue?",
"DialogMessage_DiscardDraftConfirmationTitle": "Discard Draft",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Juodraštis vis dar atviras. Išsaugoti prieš uždarant langą?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Uždaryti langą",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "No message selected",
"NoMessageCrieteria": "No messages match your search criteria",
"NoMessageEmptyFolder": "This folder is empty",
"MailEmptyState_Title": "Nėra el. paštą palaikančių paskyrų",
"MailEmptyState_Message": "Turite paskyras, skirtas kalendoriui, bet nė viena iš jų nepalaiko el. pašto. Pridėkite el. pašto paskyrą arba atnaujinkite esamą paskyrą, kad naudotumėte el. paštą.",
"MailEmptyState_AddAccount": "Pridėti paskyrą",
"MailEmptyState_ManageAccounts": "Valdyti paskyras",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Organize messages into conversation threads.",
"SettingsThreads_Title": "Conversation Threading",
"SettingsThreads_Enabled_Description": "Susijusius pranešimus sugrupuoti į vieną pokalbį.",
"SettingsThreads_Enabled_Title": "Įjungti pokalbių gijas",
"SettingsThreadOrder_Description": "Pasirinkite, kaip elementai rūšiuojami pokalbių gijoje.",
"SettingsThreadOrder_Title": "Pokalbių gijų rūšiavimas",
"SettingsThreadOrder_LastItemFirst": "Paskutinis elementas pirmas",
"SettingsThreadOrder_FirstItemFirst": "Pirmasis elementas pirmas",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Unlink Accounts",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Pradėti pridėdami paskyrą",
"WelcomeWindow_GetStartedDescription": "Pridėkite paskyrą Outlook, Gmail arba IMAP, kad pradėtumėte naudotis Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importuoti iš jūsų Wino paskyros",
"WelcomeWindow_ImportFromJsonFile": "Importuoti iš JSON failo",
"WelcomeWindow_ImportInProgress": "Importuojami jūsų sinchronizuoti nustatymai ir paskyros...",
"WelcomeWindow_ImportNoAccountsFound": "Nerasta sinchronizuotų paskyrų jūsų Wino paskyroje. Jei buvo nustatymų, jie buvo atstatyti. Naudokite Pradėti, kad pridėtumėte paskyrą rankiniu būdu.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} sinchronizuotų paskyrų jau yra šiame įrenginyje. Jei reikia, naudokite Pradėti, kad pridėtumėte dar vieną paskyrą rankiniu būdu.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Slaptažodžiai, tokenai ir kita jautri informacija nėra sinchronizuojama.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Importuotos paskyros kituose kompiuteriuose vis tiek reikės prisijungti iš naujo prieš jų naudojimą.",
"WinoAccount_Management_ExportDialog_InProgress": "Eksportuojami jūsų pasirinkti Wino duomenys...",
"WinoAccount_Management_LocalDataSectionTitle": "Perduoti su JSON failu",
"WinoAccount_Management_LocalDataSectionDescription": "Importuoti iš arba eksportuoti į vietinį JSON failą. Slaptažodžiai, tokenai ir kita jautri informacija nėra įtraukiama.",
"WinoAccount_Management_LocalDataImportAction": "Importuoti",
"WinoAccount_Management_LocalDataExportAction": "Eksportuoti",
"WinoAccount_Management_LocalDataSaved": "Išsaugojau jūsų eksportuotus Wino duomenis į {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Pasirinktas JSON failas neapima galiojančio Wino eksporto.",
"WinoAccount_Management_LoadFailed": "Wino negalėjo įkelti naujausios Wino paskyros informacijos.",
"WinoAccount_Management_ActionFailed": "Prašymas Wino paskyrai negalėjo būti užbaigtas.",
"WinoAccount_SettingsSection_Title": "Wino paskyra",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Nustatymo užbaigimas",
"ProviderSelection_Title": "Pasirinkite savo el. pašto teikėją",
"ProviderSelection_Subtitle": "Pasirinkite teikėją žemiau, kad pridėtumėte savo el. pašto paskyrą prie Wino Mail.",
"ProviderSelection_StepProgress": "Žingsnis {0} iš 3",
"ProviderSelection_IdentityTitle": "Paskyros tapatybė",
"ProviderSelection_IdentityDescription": "Pasirinkite, kaip ši paskyra pasirodys Wino viduje.",
"ProviderSelection_ProviderSectionTitle": "Paslaugų teikėjas",
"ProviderSelection_ProviderSectionDescription": "Pasirinkite paslaugą, kurią norite prijungti.",
"ProviderSelection_CapabilitySectionTitle": "Naudoti šią paskyrą",
"ProviderSelection_CapabilitySectionDescription": "Pasirinkite, ar norite el. paštą, kalendorių ar abiejų.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Kitoje pakopoje saugus prisijungimas sujungs jūsų paskyrą. Jei įjungs kalendorių, Wino automatiškai sujungs Outlook Calendar arba Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Kitoje pakopoje įvesite savo teikėjo prisijungimo duomenis. Paštas naudoja IMAP/SMTP, o kalendorius gali naudoti CalDAV arba likti šiame įrenginyje.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Kitoje pakopoje įvesite serverio duomenis. El. paštas naudoja IMAP/SMTP, o kalendorius gali naudoti CalDAV arba likti šiame įrenginyje.",
"ProviderSelection_AccountNameHeader": "Paskyros pavadinimas",
"ProviderSelection_AccountNamePlaceholder": "pvz. Asmeninė, Darbo",
"ProviderSelection_UseForMail": "El. paštas",
"ProviderSelection_UseForCalendar": "Kalendorius",
"ProviderSelection_CapabilityValidationMessage": "Prieš tęsiant pasirinkite bent vieną galimybę.",
"ProviderSelection_CalendarOnlyServerHint": "Jei tęsite tik kalendoriui, kitame puslapyje el. pašto adreso nereikės.",
"ProviderSelection_DisplayNameHeader": "Rodomas vardas",
"ProviderSelection_DisplayNamePlaceholder": "pvz. Jonas Doe",
"ProviderSelection_EmailHeader": "El. pašto adresas",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selecteer een kalender om de instellingen ervan te configureren.",
"AccountDetailsPage_InitialSynchronization_Title": "Initiële synchronisatie",
"AccountDetailsPage_InitialSynchronization_Description": "Wino heeft je e-mails gesynchroniseerd tot {0} terug.",
"AccountDetailsPage_CapabilityTitle": "Verbonden functies",
"AccountDetailsPage_CapabilityDescription": "Kies of dit account wordt gebruikt voor e-mail, agenda, of beide. Het inschakelen van een nieuwe functie kan vereisen dat je opnieuw inlogt.",
"AccountCapability_MailOnly": "Alleen e-mail",
"AccountCapability_CalendarOnly": "Alleen agenda",
"AccountCapability_MailAndCalendar": "Mail en Agenda",
"AddHyperlink": "Toevoegen",
"AppCloseBackgroundSynchronizationWarningTitle": "Achtergrondsynchronisatie",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Applicatie is niet ingesteld om te laden bij het opstarten van Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Thema toepassen",
"Buttons_PopOut": "Pop-out",
"Buttons_Browse": "Bladeren",
"Buttons_Back": "Terug",
"Buttons_Cancel": "Annuleren",
"Buttons_Close": "Sluiten",
"Buttons_Copy": "Kopiëren",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Doorlopende serie verwijderen",
"DialogMessage_DiscardDraftConfirmationMessage": "Dit concept zal worden verwijderd. Wilt u doorgaan?",
"DialogMessage_DiscardDraftConfirmationTitle": "Concept verwijderen",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Er is nog een concept geopend. Sla het op voordat je het venster sluit?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Venster sluiten",
"DialogMessage_EmptySubjectConfirmation": "Onderwerp ontbreekt",
"DialogMessage_EmptySubjectConfirmationMessage": "Het bericht heeft geen onderwerp. Wilt u doorgaan?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "U kunt laden bij opstarten inschakelen via Instellingen -> App voorkeuren.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Geen berichten geselecteerd",
"NoMessageCrieteria": "Er zijn geen berichten die voldoen aan u zoekcriteria",
"NoMessageEmptyFolder": "Deze map is leeg",
"MailEmptyState_Title": "Geen e-mailaccounts ingeschakeld.",
"MailEmptyState_Message": "Je hebt accounts verbonden voor agenda, maar geen van deze accounts is ingeschakeld voor e-mail. Voeg een e-mailaccount toe of werk een bestaand account bij om e-mail te gebruiken.",
"MailEmptyState_AddAccount": "Account toevoegen",
"MailEmptyState_ManageAccounts": "Accounts beheren",
"Notifications_MultipleNotificationsMessage": "U heeft {0} nieuwe berichten.",
"Notifications_MultipleNotificationsTitle": "Nieuwe berichten",
"Notifications_WinoUpdatedMessage": "Bekijk de nieuwe versie {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taakbalk badge",
"SettingsThreads_Description": "Voeg berichten samen tot gesprekken.",
"SettingsThreads_Title": "Gesprekken",
"SettingsThreads_Enabled_Description": "Groepeer gerelateerde berichten in één gesprek.",
"SettingsThreads_Enabled_Title": "Gespreksdraad inschakelen",
"SettingsThreadOrder_Description": "Kies hoe items binnen een gesprek worden geordend.",
"SettingsThreadOrder_Title": "Sortering van gesprek-items",
"SettingsThreadOrder_LastItemFirst": "Laatste item eerst",
"SettingsThreadOrder_FirstItemFirst": "Eerste item eerst",
"SettingsUnlinkAccounts_Description": "Verwijder de koppeling tussen accounts. Dit zal niet uw accounts verwijderen.",
"SettingsUnlinkAccounts_Title": "Ontkoppel accounts",
"SettingsMailRendering_ActionLabels_Title": "Actielabels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Beginnen met het toevoegen van een account.",
"WelcomeWindow_GetStartedDescription": "Voeg uw Outlook-, Gmail- of IMAP-account toe om aan de slag te gaan met Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importeren vanuit uw Wino-account",
"WelcomeWindow_ImportFromJsonFile": "Importeren vanuit een JSON-bestand",
"WelcomeWindow_ImportInProgress": "Uw gesynchroniseerde voorkeuren en accounts worden geïmporteerd...",
"WelcomeWindow_ImportNoAccountsFound": "Er zijn geen gesynchroniseerde accounts gevonden in uw Wino-account. Als er voorkeuren beschikbaar waren, zijn ze hersteld. Gebruik Aan de slag om handmatig een account toe te voegen.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} gesynchroniseerde accounts zijn al beschikbaar op dit apparaat. Gebruik Aan de slag om indien nodig handmatig nog een account toe te voegen.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Wachtwoorden, tokens en andere gevoelige informatie worden niet gesynchroniseerd.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Geïmporteerde accounts op een andere pc vereisen nog steeds dat u zich opnieuw aanmeldt voordat ze kunnen worden gebruikt.",
"WinoAccount_Management_ExportDialog_InProgress": "Uw geselecteerde Wino-gegevens worden geëxporteerd...",
"WinoAccount_Management_LocalDataSectionTitle": "Overzetten met een JSON-bestand",
"WinoAccount_Management_LocalDataSectionDescription": "Importeer vanuit of exporteer naar een lokaal JSON-bestand. Wachtwoorden, tokens en andere gevoelige informatie worden niet opgenomen.",
"WinoAccount_Management_LocalDataImportAction": "Importeren",
"WinoAccount_Management_LocalDataExportAction": "Exporteren",
"WinoAccount_Management_LocalDataSaved": "Je geëxporteerde Wino-gegevens zijn opgeslagen naar {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Het geselecteerde JSON-bestand bevat geen geldige Wino-export.",
"WinoAccount_Management_LoadFailed": "Wino kon de nieuwste Wino Account-informatie niet laden.",
"WinoAccount_Management_ActionFailed": "Het Wino Account-verzoek kon niet worden voltooid.",
"WinoAccount_SettingsSection_Title": "Wino-account",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Configuratie voltooien",
"ProviderSelection_Title": "Kies uw e-mailprovider",
"ProviderSelection_Subtitle": "Selecteer hieronder een provider om uw e-mailaccount toe te voegen aan Wino Mail.",
"ProviderSelection_StepProgress": "Stap {0} van 3",
"ProviderSelection_IdentityTitle": "Accountidentiteit",
"ProviderSelection_IdentityDescription": "Kies hoe dit account in Wino wordt weergegeven.",
"ProviderSelection_ProviderSectionTitle": "Provider",
"ProviderSelection_ProviderSectionDescription": "Selecteer de dienst die je wilt verbinden.",
"ProviderSelection_CapabilitySectionTitle": "Gebruik dit account voor",
"ProviderSelection_CapabilitySectionDescription": "Kies of je e-mail, agenda, of beide wilt gebruiken.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "In de volgende stap wordt een beveiligde aanmelding uitgevoerd die je account verbindt. Als je agenda inschakelt, verbindt Wino automatisch Outlook Agenda of Google Agenda.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "In de volgende stap voer je je provider-inloggegevens in. Mail gebruikt IMAP/SMTP, en agenda kan CalDAV gebruiken of lokaal op dit apparaat blijven.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "In de volgende stap voer je je servergegevens in. Mail gebruikt IMAP/SMTP, en agenda kan CalDAV gebruiken of lokaal op dit apparaat blijven.",
"ProviderSelection_AccountNameHeader": "Accountnaam",
"ProviderSelection_AccountNamePlaceholder": "bijv. Persoonlijk, Werk",
"ProviderSelection_UseForMail": "E-mail",
"ProviderSelection_UseForCalendar": "Agenda",
"ProviderSelection_CapabilityValidationMessage": "Kies ten minste één mogelijkheid voordat je verdergaat.",
"ProviderSelection_CalendarOnlyServerHint": "Als je doorgaat met alleen agenda, vereist de volgende pagina geen e-mailadres.",
"ProviderSelection_DisplayNameHeader": "Weergavenaam",
"ProviderSelection_DisplayNamePlaceholder": "bijv. John Doe",
"ProviderSelection_EmailHeader": "E-mailadres",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Wybierz kalendarz, aby skonfigurować jego ustawienia.",
"AccountDetailsPage_InitialSynchronization_Title": "Początkowa synchronizacja",
"AccountDetailsPage_InitialSynchronization_Description": "Wino zsynchronizował maile do {0} wstecz.",
"AccountDetailsPage_CapabilityTitle": "Funkcje połączone",
"AccountDetailsPage_CapabilityDescription": "Wybierz, czy to konto ma być używane do poczty, kalendarza, czy obu. Włączenie nowej funkcji może wymagać ponownego zalogowania.",
"AccountCapability_MailOnly": "Tylko Poczta",
"AccountCapability_CalendarOnly": "Tylko Kalendarz",
"AccountCapability_MailAndCalendar": "Poczta i Kalendarz",
"AddHyperlink": "Dodaj",
"AppCloseBackgroundSynchronizationWarningTitle": "Synchronizacja w tle",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikacja nie została ustawiona do uruchamiania przy starcie systemu Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Zastosuj motyw",
"Buttons_PopOut": "Wysuń",
"Buttons_Browse": "Przeglądaj",
"Buttons_Back": "Wstecz",
"Buttons_Cancel": "Anuluj",
"Buttons_Close": "Zamknij",
"Buttons_Copy": "Kopiuj",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Usuń serię cykliczną",
"DialogMessage_DiscardDraftConfirmationMessage": "Ta wersja robocza zostanie odrzucona. Czy chcesz kontynuować?",
"DialogMessage_DiscardDraftConfirmationTitle": "Porzuć wersje roboczą",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Szkic jest nadal otwarty. Zapisz go przed zamknięciem okna?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Zamknij okno",
"DialogMessage_EmptySubjectConfirmation": "Brak tematu",
"DialogMessage_EmptySubjectConfirmationMessage": "Wiadomość nie ma tematu. Czy chcesz kontynuować?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Możesz włączyć uruchamianie automatyczne w Ustawieniach -> Preferencje Aplikacji.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Nie wybrano wiadomości",
"NoMessageCrieteria": "Żadne wiadomości nie spełniają kryteriów wyszukiwania",
"NoMessageEmptyFolder": "Ten katalog jest pusty",
"MailEmptyState_Title": "Brak kont obsługujących pocztę",
"MailEmptyState_Message": "Masz konta podłączone do kalendarza, ale żadne z nich nie ma włączonej obsługi poczty. Dodaj konto pocztowe lub zaktualizuj istniejące konto, aby korzystać z poczty.",
"MailEmptyState_AddAccount": "Dodaj konto",
"MailEmptyState_ManageAccounts": "Zarządzaj kontami",
"Notifications_MultipleNotificationsMessage": "Masz {0} nowych wiadomości.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Sprawdź nową wersję {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Ikona paska zadań",
"SettingsThreads_Description": "Organizuj wiadomości w wątki konwersacji.",
"SettingsThreads_Title": "Wątkowanie konwersacji",
"SettingsThreads_Enabled_Description": "Łącz powiązane wiadomości w jedną rozmowę.",
"SettingsThreads_Enabled_Title": "Włącz wątki konwersacyjne",
"SettingsThreadOrder_Description": "Wybierz, jak elementy będą uporządkowane w wątku konwersacyjnym.",
"SettingsThreadOrder_Title": "Sortowanie elementów wątku",
"SettingsThreadOrder_LastItemFirst": "Ostatni element na początku",
"SettingsThreadOrder_FirstItemFirst": "Pierwszy element na początku",
"SettingsUnlinkAccounts_Description": "Usuń łącze pomiędzy kontami. To nie spowoduje usunięcie Twoich kont.",
"SettingsUnlinkAccounts_Title": "Odłącz konto",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Rozpocznij od dodania konta",
"WelcomeWindow_GetStartedDescription": "Dodaj swoje konta Outlook, Gmail lub IMAP, aby rozpocząć z Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importuj z konta Wino",
"WelcomeWindow_ImportFromJsonFile": "Importuj z pliku JSON",
"WelcomeWindow_ImportInProgress": "Importowanie zsynchronizowanych ustawień i kont...",
"WelcomeWindow_ImportNoAccountsFound": "Nie znaleziono zsynchronizowanych kont w Twoim koncie Wino. Jeśli ustawienia były dostępne, zostały przywrócone. Aby dodać konto ręcznie, użyj Rozpocznij.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} zsynchronizowanych kont są już dostępne na tym urządzeniu. Aby dodać inne konto ręcznie, jeśli to potrzebne, użyj Rozpocznij.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Hasła, tokeny i inne wrażliwe dane nie są synchronizowane.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Zaimportowane konta na innym komputerze będą nadal wymagały ponownego zalogowania przed użyciem.",
"WinoAccount_Management_ExportDialog_InProgress": "Eksportowanie wybranych danych Wino...",
"WinoAccount_Management_LocalDataSectionTitle": "Przenoszenie danych za pomocą pliku JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importuj z lokalnego pliku JSON lub eksportuj do niego. Hasła, tokeny i inne poufne informacje nie są dołączane.",
"WinoAccount_Management_LocalDataImportAction": "Importuj",
"WinoAccount_Management_LocalDataExportAction": "Eksportuj",
"WinoAccount_Management_LocalDataSaved": "Zapisano wyeksportowane dane Wino do {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Wybrany plik JSON nie zawiera prawidłowego eksportu danych Wino.",
"WinoAccount_Management_LoadFailed": "Nie udało się załadować najnowszych informacji o koncie Wino.",
"WinoAccount_Management_ActionFailed": "Żądanie konta Wino nie mogło zostać zakończone.",
"WinoAccount_SettingsSection_Title": "Konto Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Zakończ konfigurację",
"ProviderSelection_Title": "Wybierz dostawcę poczty e-mail",
"ProviderSelection_Subtitle": "Wybierz dostawcę poniżej, aby dodać swoje konto e-mail do Wino Mail.",
"ProviderSelection_StepProgress": "Krok {0} z 3",
"ProviderSelection_IdentityTitle": "Tożsamość konta",
"ProviderSelection_IdentityDescription": "Wybierz, jak to konto będzie wyświetlane w Wino.",
"ProviderSelection_ProviderSectionTitle": "Dostawca",
"ProviderSelection_ProviderSectionDescription": "Wybierz usługę, którą chcesz połączyć.",
"ProviderSelection_CapabilitySectionTitle": "Użyj tego konta do",
"ProviderSelection_CapabilitySectionDescription": "Wybierz, czy chcesz używać poczty, kalendarza, czy obu.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "W następnym kroku bezpieczne logowanie połączy twoje konto. Jeśli włączysz kalendarz, Wino automatycznie połączy Outlook Calendar lub Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "W kolejnym kroku wprowadzisz dane uwierzytelniające dostawcy. Poczta używa IMAP/SMTP, a kalendarz może korzystać z CalDAV lub pozostawać lokalny na tym urządzeniu.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "W następnym kroku wprowadzisz szczegóły serwera. Poczta używa IMAP/SMTP, a kalendarz może używać CalDAV lub zostać lokalny na tym urządzeniu.",
"ProviderSelection_AccountNameHeader": "Nazwa konta",
"ProviderSelection_AccountNamePlaceholder": "np. Osobiste, Służbowe",
"ProviderSelection_UseForMail": "Poczta",
"ProviderSelection_UseForCalendar": "Kalendarz",
"ProviderSelection_CapabilityValidationMessage": "Wybierz co najmniej jedną funkcję przed kontynuowaniem.",
"ProviderSelection_CalendarOnlyServerHint": "Jeśli kontynuujesz wyłącznie z kalendarzem, na następnym kroku nie będzie wymagany adres e-mail.",
"ProviderSelection_DisplayNameHeader": "Nazwa wyświetlana",
"ProviderSelection_DisplayNamePlaceholder": "np. Jan Kowalski",
"ProviderSelection_EmailHeader": "Adres e-mail",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selecione um calendário para configurar suas configurações.",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronização inicial",
"AccountDetailsPage_InitialSynchronization_Description": "Wino sincronizou seus e-mails até {0} no passado.",
"AccountDetailsPage_CapabilityTitle": "Recursos conectados",
"AccountDetailsPage_CapabilityDescription": "Escolha se esta conta será usada para E-mail, Calendário ou ambos. Ativar um novo recurso pode exigir que você faça login novamente.",
"AccountCapability_MailOnly": "Somente E-mail",
"AccountCapability_CalendarOnly": "Somente Calendário",
"AccountCapability_MailAndCalendar": "E-mail + Calendário",
"AddHyperlink": "Adicionar",
"AppCloseBackgroundSynchronizationWarningTitle": "Sincronização em segundo plano",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "O aplicativo não foi configurado para iniciar com o Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Aplicar Tema",
"Buttons_PopOut": "Abrir em nova janela",
"Buttons_Browse": "Navegar",
"Buttons_Back": "Voltar",
"Buttons_Cancel": "Cancelar",
"Buttons_Close": "Fechar",
"Buttons_Copy": "Copiar",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Excluir Série Recorrente",
"DialogMessage_DiscardDraftConfirmationMessage": "Este rascunho será descartado. Você quer continuar?",
"DialogMessage_DiscardDraftConfirmationTitle": "Descartar Rascunho",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Há um rascunho aberto. Salve-o antes de fechar a janela?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Fechar Janela",
"DialogMessage_EmptySubjectConfirmation": "Faltando Assunto",
"DialogMessage_EmptySubjectConfirmationMessage": "Mensagem não possui assunto. Você deseja continuar?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Você pode habilitar a inicialização em Configurações -> Preferências do Aplicativo.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Nenhuma mensagem selecionada",
"NoMessageCrieteria": "Nenhuma mensagem corresponde aos critérios da sua pesquisa",
"NoMessageEmptyFolder": "Esta pasta está vazia",
"MailEmptyState_Title": "Nenhuma conta habilitada para E-mail",
"MailEmptyState_Message": "Você tem contas conectadas para calendário, mas nenhuma delas está habilitada para e-mail. Adicione uma conta de e-mail ou atualize uma conta existente para usar e-mail.",
"MailEmptyState_AddAccount": "Adicionar conta",
"MailEmptyState_ManageAccounts": "Gerenciar contas",
"Notifications_MultipleNotificationsMessage": "Você tem {0} novos emails.",
"Notifications_MultipleNotificationsTitle": "Novo E-mail",
"Notifications_WinoUpdatedMessage": "Veja a nova versão {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Ícone da Barra de Tarefas",
"SettingsThreads_Description": "Organizar mensagens em tópicos de conversa.",
"SettingsThreads_Title": "* Encadeamento de conversas",
"SettingsThreads_Enabled_Description": "Agrupar mensagens relacionadas em uma única conversa.",
"SettingsThreads_Enabled_Title": "Ativar o encadeamento de conversas",
"SettingsThreadOrder_Description": "Escolha como os itens são ordenados dentro de uma conversa.",
"SettingsThreadOrder_Title": "Ordenação de itens da conversa",
"SettingsThreadOrder_LastItemFirst": "Último item primeiro",
"SettingsThreadOrder_FirstItemFirst": "Primeiro item primeiro",
"SettingsUnlinkAccounts_Description": "Remova o vínculo entre as contas. Isso não excluirá suas contas.",
"SettingsUnlinkAccounts_Title": "Desvincular Contas",
"SettingsMailRendering_ActionLabels_Title": "Rótulos de ação",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Comece adicionando uma conta",
"WelcomeWindow_GetStartedDescription": "Adicione sua conta do Outlook, Gmail ou IMAP para começar com o Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importar da sua Conta Wino",
"WelcomeWindow_ImportFromJsonFile": "Importar de um arquivo JSON",
"WelcomeWindow_ImportInProgress": "Importando suas preferências e contas sincronizadas...",
"WelcomeWindow_ImportNoAccountsFound": "Nenhuma conta sincronizada foi encontrada na sua Conta Wino. Se havia preferências disponíveis, elas foram restauradas. Use Começar para adicionar uma conta manualmente.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} contas sincronizadas já estão disponíveis neste dispositivo. Use Começar para adicionar outra conta manualmente, se necessário.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Senhas, tokens e outras informações sensíveis não são sincronizados.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Contas importadas em outro PC ainda precisarão que você faça login novamente antes que possam ser usadas.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportando seus dados Wino selecionados...",
"WinoAccount_Management_LocalDataSectionTitle": "Transferir com um arquivo JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importar de ou exportar para um arquivo JSON local. Senhas, tokens e outras informações sensíveis não são incluídas.",
"WinoAccount_Management_LocalDataImportAction": "Importar",
"WinoAccount_Management_LocalDataExportAction": "Exportar",
"WinoAccount_Management_LocalDataSaved": "Seus dados do Wino exportados foram salvos em {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "O arquivo JSON selecionado não contém uma exportação válida do Wino.",
"WinoAccount_Management_LoadFailed": "Não foi possível carregar as informações mais recentes da Conta Wino.",
"WinoAccount_Management_ActionFailed": "A solicitação da Conta Wino não pôde ser concluída.",
"WinoAccount_SettingsSection_Title": "Conta Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Concluir Configuração",
"ProviderSelection_Title": "Escolha seu provedor de e-mail",
"ProviderSelection_Subtitle": "Selecione um provedor abaixo para adicionar sua conta de e-mail ao Wino Mail.",
"ProviderSelection_StepProgress": "Etapa {0} de 3",
"ProviderSelection_IdentityTitle": "Identidade da conta",
"ProviderSelection_IdentityDescription": "Escolha como esta conta aparece dentro do Wino.",
"ProviderSelection_ProviderSectionTitle": "Provedor",
"ProviderSelection_ProviderSectionDescription": "Selecione o serviço que deseja conectar.",
"ProviderSelection_CapabilitySectionTitle": "Usar esta conta para",
"ProviderSelection_CapabilitySectionDescription": "Escolha se deseja E-mail, Calendário ou ambos.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Na próxima etapa, o login seguro conectará sua conta. Se você ativar o calendário, o Wino também conectará automaticamente o Outlook Calendar ou Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Na próxima etapa, você inserirá as credenciais do provedor. O e-mail usa IMAP/SMTP, e o calendário pode usar CalDAV ou permanecer local neste dispositivo.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Na próxima etapa, você informará os detalhes do servidor. O e-mail usa IMAP/SMTP, e o calendário pode usar CalDAV ou permanecer local neste dispositivo.",
"ProviderSelection_AccountNameHeader": "Nome da Conta",
"ProviderSelection_AccountNamePlaceholder": "ex.: Pessoal, Trabalho",
"ProviderSelection_UseForMail": "E-mail",
"ProviderSelection_UseForCalendar": "Calendário",
"ProviderSelection_CapabilityValidationMessage": "Escolha pelo menos uma capacidade antes de continuar.",
"ProviderSelection_CalendarOnlyServerHint": "Se você continuar apenas com o calendário, a próxima página não exigirá um endereço de e-mail.",
"ProviderSelection_DisplayNameHeader": "Nome para exibição",
"ProviderSelection_DisplayNamePlaceholder": "ex.: João da Silva",
"ProviderSelection_EmailHeader": "Endereço de E-mail",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Selectați un calendar pentru a-i configura setările.",
"AccountDetailsPage_InitialSynchronization_Title": "Sincronizare inițială",
"AccountDetailsPage_InitialSynchronization_Description": "Wino și-a sincronizat e-mailurile până la {0} înapoi.",
"AccountDetailsPage_CapabilityTitle": "Funcționalități conectate",
"AccountDetailsPage_CapabilityDescription": "Alege dacă acest cont este utilizat pentru mail, calendar sau ambele. Activarea unei noi funcționalități poate solicita autentificarea din nou.",
"AccountCapability_MailOnly": "Doar Mail",
"AccountCapability_CalendarOnly": "Doar Calendar",
"AccountCapability_MailAndCalendar": "Mail și Calendar",
"AddHyperlink": "Adăugare",
"AppCloseBackgroundSynchronizationWarningTitle": "Sincronizare în Fundal",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplicația nu a fost setată să pornească la pornirea Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Aplicați Tema",
"Buttons_PopOut": "Deschide într-o fereastră separată",
"Buttons_Browse": "Răsfoire",
"Buttons_Back": "Înapoi",
"Buttons_Cancel": "Anulare",
"Buttons_Close": "Închidere",
"Buttons_Copy": "Copiere",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Șterge seria repetitivă",
"DialogMessage_DiscardDraftConfirmationMessage": "Această schiță va fi eliminat. Doriți să continuați?",
"DialogMessage_DiscardDraftConfirmationTitle": "Eliminare Schiță",
"DialogMessage_CloseDraftWindowConfirmationMessage": "O schiță este încă deschisă. Salvați-o înainte să închideți fereastra?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Închide fereastra",
"DialogMessage_EmptySubjectConfirmation": "Subiect Lipsă",
"DialogMessage_EmptySubjectConfirmationMessage": "Mesajul nu are subiect. Doriți să continuați?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Puteți activa lansarea la pornire din Setări -> Preferințe Aplicație.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Niciun mesaj selectat",
"NoMessageCrieteria": "Niciun mesaj nu corespunde criteriilor dvs. de căutare",
"NoMessageEmptyFolder": "Acest folder este gol",
"MailEmptyState_Title": "Nu există conturi cu funcționalitate de mail activată.",
"MailEmptyState_Message": "Aveți conturi conectate pentru calendar, dar niciunul dintre ele nu are funcționalitatea de mail activată. Adăugați un cont de mail sau actualizați un cont existent pentru a utiliza mail.",
"MailEmptyState_AddAccount": "Adaugă cont",
"MailEmptyState_ManageAccounts": "Gestionați conturi",
"Notifications_MultipleNotificationsMessage": "Aveți {0} mesaje noi.",
"Notifications_MultipleNotificationsTitle": "Mesaj E-Mail Nou",
"Notifications_WinoUpdatedMessage": "Verificați versiunea nouă {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Insignă Bară de activități",
"SettingsThreads_Description": "Organizați mesajele în fire de conversație.",
"SettingsThreads_Title": "Aranjare mesaje în conversație",
"SettingsThreads_Enabled_Description": "Grupați mesajele legate într-o singură conversație.",
"SettingsThreads_Enabled_Title": "Activează firul de conversații",
"SettingsThreadOrder_Description": "Alegeți cum sunt ordonate elementele într-un fir de conversație.",
"SettingsThreadOrder_Title": "Sortarea elementelor firului de conversație",
"SettingsThreadOrder_LastItemFirst": "Ultimul element, primul.",
"SettingsThreadOrder_FirstItemFirst": "Primul element, primul.",
"SettingsUnlinkAccounts_Description": "Eliminați legătura dintre conturi. Acest lucru nu va șterge conturile dvs.",
"SettingsUnlinkAccounts_Title": "Deasociere Conturi",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Începe",
"WelcomeWindow_GetStartedDescription": "Adăugați contul dvs. Outlook, Gmail sau IMAP pentru a începe cu Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importă din contul tău Wino",
"WelcomeWindow_ImportFromJsonFile": "Importă dintr-un fișier JSON",
"WelcomeWindow_ImportInProgress": "Se importă preferințele și conturile sincronizate...",
"WelcomeWindow_ImportNoAccountsFound": "Nu au fost găsite conturi sincronizate în contul tău Wino. Dacă preferințele erau disponibile, acestea au fost restaurate. Folosește Începe pentru a adăuga un cont manual.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} conturi sincronizate sunt deja disponibile pe acest dispozitiv. Folosește Începe pentru a adăuga manual un alt cont, dacă este necesar.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "/** placeholder **/",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "/** placeholder **/",
"WinoAccount_Management_ExportDialog_InProgress": "/** placeholder **/",
"WinoAccount_Management_LocalDataSectionTitle": "Transfer cu un fișier JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importați din sau exportați într-un fișier JSON local. Parolele, token-urile și alte informații sensibile nu sunt incluse.",
"WinoAccount_Management_LocalDataImportAction": "Importă",
"WinoAccount_Management_LocalDataExportAction": "Exportă",
"WinoAccount_Management_LocalDataSaved": "Datele exportate ale Wino au fost salvate în {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Fișierul JSON selectat nu conține un export Wino valid.",
"WinoAccount_Management_LoadFailed": "/** placeholder **/",
"WinoAccount_Management_ActionFailed": "/** placeholder **/",
"WinoAccount_SettingsSection_Title": "/** placeholder **/",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Finalizați configurarea",
"ProviderSelection_Title": "Alegeți furnizorul de e-mail",
"ProviderSelection_Subtitle": "Selectați un furnizor mai jos pentru a adăuga contul dvs. de e-mail în Wino Mail.",
"ProviderSelection_StepProgress": "Pasul {0} din 3",
"ProviderSelection_IdentityTitle": "Identitatea contului",
"ProviderSelection_IdentityDescription": "Alegeți cum va apărea acest cont în Wino.",
"ProviderSelection_ProviderSectionTitle": "Furnizor",
"ProviderSelection_ProviderSectionDescription": "Selectați serviciul pe care doriți să îl conectați.",
"ProviderSelection_CapabilitySectionTitle": "Folosirea acestui cont pentru",
"ProviderSelection_CapabilitySectionDescription": "Alegeți dacă doriți mail, calendar sau ambele.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "În pasul următor, autentificarea securizată vă va conecta contul. Dacă activați calendarul, Wino se va conecta automat și la Outlook Calendar sau Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "În pasul următor veți introduce acreditările furnizorului. Mail utilizează IMAP/SMTP, iar calendarul poate folosi CalDAV sau poate rămâne local pe acest dispozitiv.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "În pasul următor veți introduce detaliile serverului. Mail utilizează IMAP/SMTP, iar calendarul poate folosi CalDAV sau poate rămâne local pe acest dispozitiv.",
"ProviderSelection_AccountNameHeader": "Nume cont",
"ProviderSelection_AccountNamePlaceholder": "ex. Personal, Birou",
"ProviderSelection_UseForMail": "Poștă",
"ProviderSelection_UseForCalendar": "Calendar",
"ProviderSelection_CapabilityValidationMessage": "Alegeți cel puțin o capabilitate înainte de a continua.",
"ProviderSelection_CalendarOnlyServerHint": "Dacă continuați doar cu calendarul, pagina următoare nu va solicita o adresă de e-mail.",
"ProviderSelection_DisplayNameHeader": "Nume afișat",
"ProviderSelection_DisplayNamePlaceholder": "ex. John Doe",
"ProviderSelection_EmailHeader": "Adresă de e-mail",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Выберите календарь, чтобы настроить его параметры.",
"AccountDetailsPage_InitialSynchronization_Title": "Начальная синхронизация",
"AccountDetailsPage_InitialSynchronization_Description": "Wino синхронизировал вашу почту до {0} назад.",
"AccountDetailsPage_CapabilityTitle": "Подключенные функции",
"AccountDetailsPage_CapabilityDescription": "Выберите, будет ли эта учетная запись использоваться для почты, календаря или обоих. Включение новой функции может потребовать повторного входа в систему.",
"AccountCapability_MailOnly": "Только почта",
"AccountCapability_CalendarOnly": "Только календарь",
"AccountCapability_MailAndCalendar": "Почта и календарь",
"AddHyperlink": "Добавить",
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Применить тему",
"Buttons_PopOut": "Открыть в отдельном окне",
"Buttons_Browse": "Обзор",
"Buttons_Back": "Назад",
"Buttons_Cancel": "Отмена",
"Buttons_Close": "Закрыть",
"Buttons_Copy": "Копировать",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Удалить повторяющуюся серию",
"DialogMessage_DiscardDraftConfirmationMessage": "Этот черновик будет удален. Вы хотите продолжить?",
"DialogMessage_DiscardDraftConfirmationTitle": "Удалить черновик",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Черновик всё ещё открыт. Сохранить его перед закрытием окна?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Закрыть окно",
"DialogMessage_EmptySubjectConfirmation": "Missing Subject",
"DialogMessage_EmptySubjectConfirmationMessage": "Сообщение не имеет темы. Вы хотите продолжить?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "You can enable startup launch from Settings -> App Preferences.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Не выбрано сообщение",
"NoMessageCrieteria": "Нет сообщений, соответствующих критериям поиска",
"NoMessageEmptyFolder": "Эта папка пуста",
"MailEmptyState_Title": "Нет учетных записей, поддерживающих почту",
"MailEmptyState_Message": "У вас подключены учетные записи для календаря, но ни одна из них не поддерживает почту. Добавьте учетную запись почты или обновите существующую учетную запись, чтобы использовать почту.",
"MailEmptyState_AddAccount": "Добавить учетную запись",
"MailEmptyState_ManageAccounts": "Управление учетными записями",
"Notifications_MultipleNotificationsMessage": "You have {0} new messages.",
"Notifications_MultipleNotificationsTitle": "New Mail",
"Notifications_WinoUpdatedMessage": "Ознакомьтесь с новой версией {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Taskbar Badge",
"SettingsThreads_Description": "Организуйте сообщения в беседы.",
"SettingsThreads_Title": "Беседы",
"SettingsThreads_Enabled_Description": "Сгруппировать связанные сообщения в одну переписку.",
"SettingsThreads_Enabled_Title": "Включить группировку переписки",
"SettingsThreadOrder_Description": "Выберите порядок сортировки элементов внутри переписки.",
"SettingsThreadOrder_Title": "Сортировка элементов переписки",
"SettingsThreadOrder_LastItemFirst": "Последний элемент первым",
"SettingsThreadOrder_FirstItemFirst": "Первый элемент первым",
"SettingsUnlinkAccounts_Description": "Remove the link between accounts. his will not delete your accounts.",
"SettingsUnlinkAccounts_Title": "Отвязать учетные записи",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Начать, добавив учетную запись",
"WelcomeWindow_GetStartedDescription": "Добавьте учетную запись Outlook, Gmail или IMAP, чтобы начать работу с Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Импорт из вашей учетной записи Wino",
"WelcomeWindow_ImportFromJsonFile": "Импорт из файла JSON",
"WelcomeWindow_ImportInProgress": "Импорт синхронизированных настроек и учетных записей...",
"WelcomeWindow_ImportNoAccountsFound": "В вашей учетной записи Wino не найдено синхронизированных учетных записей. Если настройки были доступны, они были восстановлены. Используйте Начать, чтобы добавить учетную запись вручную.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронизированных учетных записей уже доступны на этом устройстве. При необходимости используйте Начать, чтобы вручную добавить еще одну учетную запись.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Пароли, токены и другие конфиденциальные данные не синхронизируются.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Импортированные на другом ПК учетные записи по-прежнему потребуют повторного входа в систему перед использованием.",
"WinoAccount_Management_ExportDialog_InProgress": "Выполняется экспорт выбранных данных Wino...",
"WinoAccount_Management_LocalDataSectionTitle": "Перенос с помощью файла JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Импортируйте или экспортируйте локальный файл JSON. Пароли, токены и другая конфиденциальная информация не включаются.",
"WinoAccount_Management_LocalDataImportAction": "Импорт",
"WinoAccount_Management_LocalDataExportAction": "Экспорт",
"WinoAccount_Management_LocalDataSaved": "Сохранены экспортированные данные Wino в {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Выбранный файл JSON не содержит допустимого экспорта Wino.",
"WinoAccount_Management_LoadFailed": "Не удалось загрузить последнюю информацию об учетной записи Wino.",
"WinoAccount_Management_ActionFailed": "Запрос к учетной записи Wino не удалось выполнить.",
"WinoAccount_SettingsSection_Title": "Учетная запись Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Завершение настройки",
"ProviderSelection_Title": "Выберите поставщика электронной почты",
"ProviderSelection_Subtitle": "Выберите ниже поставщика, чтобы добавить вашу учетную запись электронной почты в Wino Mail.",
"ProviderSelection_StepProgress": "Шаг {0} из 3",
"ProviderSelection_IdentityTitle": "Идентификатор учетной записи",
"ProviderSelection_IdentityDescription": "Выберите, как эта учетная запись будет отображаться в Wino.",
"ProviderSelection_ProviderSectionTitle": "Провайдер",
"ProviderSelection_ProviderSectionDescription": "Выберите сервис, который вы хотите подключить.",
"ProviderSelection_CapabilitySectionTitle": "Использовать эту учетную запись для",
"ProviderSelection_CapabilitySectionDescription": "Выберите, хотите ли вы использовать почту, календарь или оба.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "На следующем шаге безопасный вход подключит вашу учетную запись. Если вы включите календарь, Wino автоматически подключит календарь Outlook или Google Календарь.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "На следующем шаге вы введёте учётные данные вашего провайдера. Почта использует IMAP/SMTP, а календарь может использовать CalDAV или оставаться локальным на этом устройстве.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "На следующем шаге вы введёте данные сервера. Почта использует IMAP/SMTP, а календарь может использовать CalDAV или оставаться локальным на этом устройстве.",
"ProviderSelection_AccountNameHeader": "Имя учетной записи",
"ProviderSelection_AccountNamePlaceholder": "например, Личное, Рабочее",
"ProviderSelection_UseForMail": "Почта",
"ProviderSelection_UseForCalendar": "Календарь",
"ProviderSelection_CapabilityValidationMessage": "Выберите как минимум одну возможность перед продолжением.",
"ProviderSelection_CalendarOnlyServerHint": "Если вы продолжите только с календарём, на следующей странице адрес электронной почты не потребуется.",
"ProviderSelection_DisplayNameHeader": "Отображаемое имя",
"ProviderSelection_DisplayNamePlaceholder": "например, Иван Иванов",
"ProviderSelection_EmailHeader": "Адрес электронной почты",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Vyberte kalendár na konfiguráciu jeho nastavení.",
"AccountDetailsPage_InitialSynchronization_Title": "Počiatočná synchronizácia",
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronizoval vašu poštu až do {0} späť.",
"AccountDetailsPage_CapabilityTitle": "Pripojené funkcie",
"AccountDetailsPage_CapabilityDescription": "Vyberte, či sa tento účet používa na poštu, kalendár alebo oboje. Aktivácia novej funkcie môže vyžadovať opätovné prihlásenie.",
"AccountCapability_MailOnly": "Iba pošta",
"AccountCapability_CalendarOnly": "Iba kalendár",
"AccountCapability_MailAndCalendar": "Pošta a kalendár",
"AddHyperlink": "Pridať",
"AppCloseBackgroundSynchronizationWarningTitle": "Synchronizácia na pozadí",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Aplikácia sa nebude spúšťať pri spustení systému Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Použiť motív",
"Buttons_PopOut": "Otvoriť v novom okne",
"Buttons_Browse": "Prehľadávať",
"Buttons_Back": "Späť",
"Buttons_Cancel": "Zrušiť",
"Buttons_Close": "Zavrieť",
"Buttons_Copy": "Kopírovať",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Odstrániť opakovanú sériu",
"DialogMessage_DiscardDraftConfirmationMessage": "Tento koncept sa zahodí. Chcete pokračovať?",
"DialogMessage_DiscardDraftConfirmationTitle": "Zrušiť koncept",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Návrh je stále otvorený. Uložiť ho pred zatvorením okna?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Zatvoriť okno",
"DialogMessage_EmptySubjectConfirmation": "Chýbajúci predmet",
"DialogMessage_EmptySubjectConfirmationMessage": "Správa nemá vyplnený predmet. Chcete pokračovať?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Spúšťanie pri štarte môžete zapnúť v Nastavenia -> Nastavenia aplikácie.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Nie je vybraná žiadna správa",
"NoMessageCrieteria": "Žiadne správy nezodpovedajú vašim kritériám vyhľadávania",
"NoMessageEmptyFolder": "Tento priečinok je prázdny",
"MailEmptyState_Title": "Žiadne účty s poštou nie sú zapojené",
"MailEmptyState_Message": "Máte účty pripojené k kalendáru, ale žiadny z nich nemá povolenú poštu. Pridajte poštový účet alebo aktualizujte existujúci účet, aby používal poštu.",
"MailEmptyState_AddAccount": "Pridať účet",
"MailEmptyState_ManageAccounts": "Spravovať účty",
"Notifications_MultipleNotificationsMessage": "Máte nové správy: {0}.",
"Notifications_MultipleNotificationsTitle": "Nová pošta",
"Notifications_WinoUpdatedMessage": "Pozrite si novú verziu {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Odznak na paneli úloh",
"SettingsThreads_Description": "Usporiadanie správ do konverzácií.",
"SettingsThreads_Title": "Zobrazenie v režime konverzácie",
"SettingsThreads_Enabled_Description": "Zoskupiť súvisiace správy do jednej konverzácie.",
"SettingsThreads_Enabled_Title": "Povoliť vlákna konverzácie",
"SettingsThreadOrder_Description": "Vyberte spôsob zoradenia položiek vo vlákne konverzácie.",
"SettingsThreadOrder_Title": "Zoradenie položiek vlákna",
"SettingsThreadOrder_LastItemFirst": "Posledná položka ako prvá",
"SettingsThreadOrder_FirstItemFirst": "Prvá položka ako prvá",
"SettingsUnlinkAccounts_Description": "Odstránenie prepojenia medzi účtami. Tým sa vaše účty neodstránia.",
"SettingsUnlinkAccounts_Title": "Odpojenie účtov",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Začať pridaním účtu",
"WelcomeWindow_GetStartedDescription": "Pridajte svoj účet Outlook, Gmail alebo IMAP a začnite používať Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Importovať zo svojho Wino účtu",
"WelcomeWindow_ImportFromJsonFile": "Importovať zo súboru JSON",
"WelcomeWindow_ImportInProgress": "Importujem synchronizované preferencie a účty...",
"WelcomeWindow_ImportNoAccountsFound": "Nenašli sa žiadne synchronizované účty vo vašom Wino účte. Ak boli k dispozícii preferencie, boli obnovené. Použite možnosť Začať na manuálne pridanie účtu.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synchronizovaných účtov je už k dispozícii na tomto zariadení. Ak potrebujete, použite možnosť Začať na ručné pridanie ďalšieho účtu.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Heslá, tokeny a ďalšie citlivé údaje nie sú synchronizované.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Importované účty na inom PC budú vyžadovať opätovné prihlásenie pred ich použitím.",
"WinoAccount_Management_ExportDialog_InProgress": "Exportujem vybrané údaje Wino.",
"WinoAccount_Management_LocalDataSectionTitle": "Prenos pomocou súboru JSON",
"WinoAccount_Management_LocalDataSectionDescription": "Importovať alebo exportovať do lokálneho súboru JSON. Heslá, tokeny a ďalšie citlivé informácie nie sú zahrnuté.",
"WinoAccount_Management_LocalDataImportAction": "Importovať",
"WinoAccount_Management_LocalDataExportAction": "Exportovať",
"WinoAccount_Management_LocalDataSaved": "Uložili ste exportované dáta Wino do {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Vybraný JSON súbor neobsahuje platný export Wino.",
"WinoAccount_Management_LoadFailed": "Wino nemohol načítať najnovšie informácie o Wino účte.",
"WinoAccount_Management_ActionFailed": "Žiadosť o Wino účet nemohla byť dokončená.",
"WinoAccount_SettingsSection_Title": "Wino účet",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Dokončiť nastavenie",
"ProviderSelection_Title": "Vyberte si svojho poskytovateľa e-mailu",
"ProviderSelection_Subtitle": "Vyberte nižšie uvedeného poskytovateľa a pridajte svoj e-mailový účet do Wino Mail.",
"ProviderSelection_StepProgress": "Krok {0} z 3",
"ProviderSelection_IdentityTitle": "Identita účtu",
"ProviderSelection_IdentityDescription": "Vyberte, ako sa tento účet zobrazí vo Wino.",
"ProviderSelection_ProviderSectionTitle": "Poskytovateľ",
"ProviderSelection_ProviderSectionDescription": "Vyberte službu, ktorú chcete pripojiť.",
"ProviderSelection_CapabilitySectionTitle": "Použiť tento účet pre",
"ProviderSelection_CapabilitySectionDescription": "Vyberte, či chcete poštu, kalendár alebo oboje.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Na ďalšom kroku zabezpečené prihlásenie pripojí váš účet. Ak povolíte kalendár, Wino tiež automaticky pripojí Kalendár Outlook alebo Kalendár Google.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Na ďalšom kroku zadáte prihlasovacie údaje poskytovateľa. Pošta používa IMAP/SMTP a kalendár môže používať CalDav alebo zostať lokálny na tomto zariadení.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Na ďalšom kroku zadáte údaje o serveri. Pošta používa IMAP/SMTP a kalendár môže používať CalDav alebo zostať lokálny na tomto zariadení.",
"ProviderSelection_AccountNameHeader": "Názov účtu",
"ProviderSelection_AccountNamePlaceholder": "napr. Osobný, Práca",
"ProviderSelection_UseForMail": "Pošta",
"ProviderSelection_UseForCalendar": "Kalendár",
"ProviderSelection_CapabilityValidationMessage": "Pred pokračovaním vyberte aspoň jednu možnosť.",
"ProviderSelection_CalendarOnlyServerHint": "Ak budete pokračovať iba s kalendárom, na ďalšej stránke nebude vyžadovaná e-mailová adresa.",
"ProviderSelection_DisplayNameHeader": "Zobrazené meno",
"ProviderSelection_DisplayNamePlaceholder": "napr. Ján Novak",
"ProviderSelection_EmailHeader": "E-mailová adresa",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Ayarlarını yapılandırmak için bir takvim seçin.",
"AccountDetailsPage_InitialSynchronization_Title": "İlk senkronizasyon",
"AccountDetailsPage_InitialSynchronization_Description": "Wino, {0} tarihine kadar geriye giderek maillerinizi senkronize etti.",
"AccountDetailsPage_CapabilityTitle": "Bağlı Özellikler",
"AccountDetailsPage_CapabilityDescription": "Bu hesabın posta, takvim için mi yoksa her ikisi için mi kullanılacağını seçin. Yeni bir özellik etkinleştirildiğinde yeniden oturum açmanız istenebilir.",
"AccountCapability_MailOnly": "Sadece Posta",
"AccountCapability_CalendarOnly": "Sadece Takvim",
"AccountCapability_MailAndCalendar": "Posta + Takvim",
"AddHyperlink": "Ekle",
"AppCloseBackgroundSynchronizationWarningTitle": "Arkaplan Senkronizasyonu",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Uygulama Windows açılırken çalıştırılacak şekilde ayarlanmadı.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Temayı Uygula",
"Buttons_PopOut": "Yeni pencerede aç",
"Buttons_Browse": "Gözat",
"Buttons_Back": "Geri",
"Buttons_Cancel": "İptal Et",
"Buttons_Close": "Kapat",
"Buttons_Copy": "Kopyala",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Tekrarlayan Seriyi Sil",
"DialogMessage_DiscardDraftConfirmationMessage": "Taslak yoksayılacaktır. Devam etmek istiyor musunuz?",
"DialogMessage_DiscardDraftConfirmationTitle": "Taslağı Sil",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Bir taslak hâlâ açık. Pencereyi kapatmadan önce kaydetmek ister misiniz?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Pencereyi Kapat",
"DialogMessage_EmptySubjectConfirmation": "Konu Eksik",
"DialogMessage_EmptySubjectConfirmationMessage": "Mesajın bir konusu yok. Devam etmek istiyor musunuz?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Oturum açma seçeneklerini Ayarlar -> Uygulama Tercihleri kısmından değiştirebilirsiniz.",
@@ -681,6 +689,10 @@
"NoMailSelected": "İleti seçilmedi",
"NoMessageCrieteria": "Kriterlere uygun mesaj bulunamadı",
"NoMessageEmptyFolder": "Bu klasör boş",
"MailEmptyState_Title": "Posta için etkinleştirilmiş hesap yok",
"MailEmptyState_Message": "Takvim için bağlı hesaplarınız var, ancak bunlardan hiçbiri posta için etkinleştirilmedi. Bir posta hesabı ekleyin veya mevcut hesabı posta kullanacak şekilde güncelleyin.",
"MailEmptyState_AddAccount": "Hesap Ekle",
"MailEmptyState_ManageAccounts": "Hesapları Yönet",
"Notifications_MultipleNotificationsMessage": "{0} yeni mesajınız var.",
"Notifications_MultipleNotificationsTitle": "Yeni E-Posta",
"Notifications_WinoUpdatedMessage": "{0} sürümünü deneyin",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Sistem Çubuğu Balonu",
"SettingsThreads_Description": "Bağlı mesajları tek bir liste halinde kategorize eder.",
"SettingsThreads_Title": "Mesaj Gruplama",
"SettingsThreads_Enabled_Description": "İlişkili mesajları tek bir konuşmada grupla.",
"SettingsThreads_Enabled_Title": "İletilerin konuşma zincirini etkinleştir.",
"SettingsThreadOrder_Description": "Bir konuşma içindeki öğelerin nasıl sıralanacağını seçin.",
"SettingsThreadOrder_Title": "İleti Sıralaması",
"SettingsThreadOrder_LastItemFirst": "Son öğe önce",
"SettingsThreadOrder_FirstItemFirst": "İlk öğe önce",
"SettingsUnlinkAccounts_Description": "Hesap bağlantısını koparın. Bu işlem hesaplarınızı silmez.",
"SettingsUnlinkAccounts_Title": "Hesap Bağlantısını Kopar",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Bir hesap ekleyerek başlayın",
"WelcomeWindow_GetStartedDescription": "Wino Mail'e başlamak için Outlook, Gmail veya IMAP hesabınızı ekleyin.",
"WelcomeWindow_ImportFromWinoAccount": "Wino Hesabınızdan İçe Aktar",
"WelcomeWindow_ImportFromJsonFile": "JSON dosyasından içe aktar",
"WelcomeWindow_ImportInProgress": "Senkronize edilmiş tercihleriniz ve hesaplarınız içe aktarılıyor...",
"WelcomeWindow_ImportNoAccountsFound": "Wino Hesabınızda senkronize edilen hesap bulunamadı. Tercihler mevcutsa geri yüklendi. Bir hesabı manuel olarak eklemek için Başla'ya tıklayın.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} senkronize edilmiş hesap bu cihazda zaten mevcut. Gerekirse başka bir hesap eklemek için Başla'ya tıklayın.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Şifreler, erişim belirteçleri ve diğer hassas bilgiler senkronize edilmez.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Başka bir PC'de içe aktarılan hesaplar, kullanılabilir hale gelmeden önce yine oturum açmanızı gerektirecek.",
"WinoAccount_Management_ExportDialog_InProgress": "Seçili Wino verileri dışa aktarılıyor...",
"WinoAccount_Management_LocalDataSectionTitle": "JSON Dosyası ile Aktarım",
"WinoAccount_Management_LocalDataSectionDescription": "Yerel bir JSON dosyasından içe aktarın veya dışa aktarın. Parolalar, erişim belirteçleri ve diğer hassas bilgiler dahil değildir.",
"WinoAccount_Management_LocalDataImportAction": "İçe aktar",
"WinoAccount_Management_LocalDataExportAction": "Dışa aktar",
"WinoAccount_Management_LocalDataSaved": "Dışa aktarılan Wino verinizi {0} konumuna kaydettiniz.",
"WinoAccount_Management_LocalDataInvalidFile": "Seçilen JSON dosyasında geçerli bir Wino dışa aktarımı bulunmuyor.",
"WinoAccount_Management_LoadFailed": "Wino en son Wino Hesap bilgilerini yükleyemedi.",
"WinoAccount_Management_ActionFailed": "Wino Hesap isteği tamamlanamadı.",
"WinoAccount_SettingsSection_Title": "Wino Hesabı",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Kurulumu Tamamla",
"ProviderSelection_Title": "E-posta sağlayıcınızı seçin",
"ProviderSelection_Subtitle": "Aşağıdaki sağlayıcıyı seçerek e-posta hesabınızı Wino Mail'e ekleyin.",
"ProviderSelection_StepProgress": "3 adımın {0}. Adımı",
"ProviderSelection_IdentityTitle": "Hesap Kimliği",
"ProviderSelection_IdentityDescription": "Bu hesabın Wino'da nasıl görüneceğini seçin.",
"ProviderSelection_ProviderSectionTitle": "Sağlayıcı",
"ProviderSelection_ProviderSectionDescription": "Bağlamak istediğiniz hizmeti seçin.",
"ProviderSelection_CapabilitySectionTitle": "Bu hesap için kullanın",
"ProviderSelection_CapabilitySectionDescription": "Posta, takvim veya her ikisini birden kullanmak istiyorsanız seçin.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "Bir sonraki adımda güvenli oturum açma hesabınızı bağlayacaktır. Takvim etkinleştirildiğinde, Wino Outlook Takvimi veya Google Takvimi'ni otomatik olarak bağlayacaktır.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "Bir sonraki adımda sağlayıcı kimlik bilgilerinizi gireceksiniz. Posta IMAP/SMTP kullanır; takvim CalDAV kullanabilir veya bu cihazda yerel olarak kalabilir.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "Bir sonraki adımda sunucu bilgilerinizi gireceksiniz. Posta IMAP/SMTP kullanır; takvim CalDAV kullanabilir veya bu cihazda yerel olarak kalabilir.",
"ProviderSelection_AccountNameHeader": "Hesap Adı",
"ProviderSelection_AccountNamePlaceholder": "örn. Kişisel, İş",
"ProviderSelection_UseForMail": "Posta",
"ProviderSelection_UseForCalendar": "Takvim",
"ProviderSelection_CapabilityValidationMessage": "Devam etmeden önce en az bir özellik seçin.",
"ProviderSelection_CalendarOnlyServerHint": "Yalnızca takvim ile devam ederseniz, sonraki sayfada bir e-posta adresi gerekmeyecektir.",
"ProviderSelection_DisplayNameHeader": "Görünen Ad",
"ProviderSelection_DisplayNamePlaceholder": "örn. John Doe",
"ProviderSelection_EmailHeader": "E-posta Adresi",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "Виберіть календар, щоб налаштувати його параметри.",
"AccountDetailsPage_InitialSynchronization_Title": "Початкова синхронізація",
"AccountDetailsPage_InitialSynchronization_Description": "Wino синхронізував ваші листи до {0} назад.",
"AccountDetailsPage_CapabilityTitle": "Підключені функції",
"AccountDetailsPage_CapabilityDescription": "Виберіть, чи використовується цей обліковий запис для пошти, календаря або обох. Увімкнення нової функції може вимагати повторного входу.",
"AccountCapability_MailOnly": "Лише пошта",
"AccountCapability_CalendarOnly": "Лише календар",
"AccountCapability_MailAndCalendar": "Пошта та календар",
"AddHyperlink": "Додати",
"AppCloseBackgroundSynchronizationWarningTitle": "Фонова синхронізація",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Програму не налаштовано на автозапуск під час запуску Windows.",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "Застосувати тему",
"Buttons_PopOut": "Відкрити у окремому вікні",
"Buttons_Browse": "Огляд",
"Buttons_Back": "Назад",
"Buttons_Cancel": "Скасувати",
"Buttons_Close": "Закрити",
"Buttons_Copy": "Копіювати",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "Видалити повторювану серію",
"DialogMessage_DiscardDraftConfirmationMessage": "Цю чернетку буде відкинуто. Продовжити?",
"DialogMessage_DiscardDraftConfirmationTitle": "Відкинути чернетку",
"DialogMessage_CloseDraftWindowConfirmationMessage": "Чернетку ще відкрито. Зберегти її перед закриттям вікна?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "Закрити вікно",
"DialogMessage_EmptySubjectConfirmation": "Теми не вказано",
"DialogMessage_EmptySubjectConfirmationMessage": "Повідомлення не має теми. Продовжити?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "Увімкнути автозапуск можна в Налаштуваннях -> Параметри програми.",
@@ -681,6 +689,10 @@
"NoMailSelected": "Повідомлення не вибрано",
"NoMessageCrieteria": "Немає повідомлень, що відповідають критеріям пошуку",
"NoMessageEmptyFolder": "Ця тека порожня",
"MailEmptyState_Title": "Немає облікових записів, увімкнених для пошти",
"MailEmptyState_Message": "У вас підключені облікові записи для календаря, але жоден з них не увімкнено для пошти. Додайте обліковий запис пошти або оновіть існуючий обліковий запис, щоб використовувати пошту.",
"MailEmptyState_AddAccount": "Додати обліковий запис",
"MailEmptyState_ManageAccounts": "Керувати обліковими записами",
"Notifications_MultipleNotificationsMessage": "У вас {0} нових повідомлень.",
"Notifications_MultipleNotificationsTitle": "Свіжа пошта",
"Notifications_WinoUpdatedMessage": "Погляньте на нову версію {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "Значок на панелі завдань",
"SettingsThreads_Description": "Організуйте повідомлення в ланцюжки розмов.",
"SettingsThreads_Title": "Ланцюжки розмов",
"SettingsThreads_Enabled_Description": "Групуйте пов'язані повідомлення в одну розмову.",
"SettingsThreads_Enabled_Title": "Увімкнути групування розмов.",
"SettingsThreadOrder_Description": "Виберіть порядок сортування елементів у розмові.",
"SettingsThreadOrder_Title": "Сортування елементів розмови",
"SettingsThreadOrder_LastItemFirst": "Останній елемент першим",
"SettingsThreadOrder_FirstItemFirst": "Перший елемент першим",
"SettingsUnlinkAccounts_Description": "Видалити зв'язку між обліковими записами. Це не видалить самі облікові записи.",
"SettingsUnlinkAccounts_Title": "Відв'язати облікові записи",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "Почати, додавши обліковий запис",
"WelcomeWindow_GetStartedDescription": "Додайте ваш Outlook, Gmail або IMAP обліковий запис, щоб почати з Wino Mail.",
"WelcomeWindow_ImportFromWinoAccount": "Імпортувати з вашого облікового запису Wino",
"WelcomeWindow_ImportFromJsonFile": "Імпортувати з файлу JSON",
"WelcomeWindow_ImportInProgress": "Імпорт ваших синхронізованих налаштувань та облікових записів...",
"WelcomeWindow_ImportNoAccountsFound": "У вашому обліковому записі Wino не знайдено синхронізованих облікових записів. Якщо налаштування були доступні, вони відновлені. Використайте 'Почати', щоб додати обліковий запис вручну.",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} синхронізованих облікових записів вже доступні на цьому пристрої. Використайте 'Почати', щоб додати ще один обліковий запис вручну, якщо потрібно.",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Паролі, токени та інша чутлива інформація не синхронізуються.",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Імпортовані облікові записи на іншому ПК все ще потребуватимуть повторного входу перед їх використанням.",
"WinoAccount_Management_ExportDialog_InProgress": "Експорт ваших вибраних даних Wino...",
"WinoAccount_Management_LocalDataSectionTitle": "Перенесення за допомогою JSON-файлу",
"WinoAccount_Management_LocalDataSectionDescription": "Імпорт або експорт до локального файлу JSON. Паролі, токени та інша чутлива інформація не включаються.",
"WinoAccount_Management_LocalDataImportAction": "Імпортувати",
"WinoAccount_Management_LocalDataExportAction": "Експортувати",
"WinoAccount_Management_LocalDataSaved": "Збережено ваші дані Wino, експортовані до {0}.",
"WinoAccount_Management_LocalDataInvalidFile": "Вибраний файл JSON не містить дійсного експорту Wino.",
"WinoAccount_Management_LoadFailed": "Не вдалося завантажити останню інформацію про обліковий запис Wino.",
"WinoAccount_Management_ActionFailed": "Запит до облікового запису Wino не вдалося виконати.",
"WinoAccount_SettingsSection_Title": "Обліковий запис Wino",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "Завершити налаштування",
"ProviderSelection_Title": "Виберіть постачальника електронної пошти",
"ProviderSelection_Subtitle": "Виберіть постачальника нижче, щоб додати свій обліковий ящик електронної пошти до Wino Mail.",
"ProviderSelection_StepProgress": "Крок {0} з 3",
"ProviderSelection_IdentityTitle": "Ідентифікація облікового запису",
"ProviderSelection_IdentityDescription": "Виберіть, як цей обліковий запис відображатиметься в Wino.",
"ProviderSelection_ProviderSectionTitle": "Провайдер",
"ProviderSelection_ProviderSectionDescription": "Виберіть службу, яку хочете підключити.",
"ProviderSelection_CapabilitySectionTitle": "Використовувати цей обліковий запис для",
"ProviderSelection_CapabilitySectionDescription": "Виберіть, чи хочете використовувати пошту, календар або обидва.",
"ProviderSelection_CapabilityProviderDescription_OAuth": "На наступному кроці безпечний вхід з'єднає ваш обліковий запис. Якщо увімкнено календар, Wino також автоматично підключить календар Outlook або Google Calendar.",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "На наступному кроці ви введете облікові дані провайдера. Пошта використовує IMAP/SMTP, а календар може використовувати CalDAV або залишитися локальним на цьому пристрої.",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "На наступному кроці ви введете дані сервера. Пошта використовує IMAP/SMTP, а календар може використовувати CalDAV або залишитися локальним на цьому пристрої.",
"ProviderSelection_AccountNameHeader": "Назва облікового запису",
"ProviderSelection_AccountNamePlaceholder": "наприклад, Особистий, Робочий",
"ProviderSelection_UseForMail": "Пошта",
"ProviderSelection_UseForCalendar": "Календар",
"ProviderSelection_CapabilityValidationMessage": "Виберіть хоча б одну можливість перед продовженням.",
"ProviderSelection_CalendarOnlyServerHint": "Якщо продовжити лише зcalendar, на наступній сторінці адреса електронної пошти не буде потрібна.",
"ProviderSelection_DisplayNameHeader": "Відображуване ім'я",
"ProviderSelection_DisplayNamePlaceholder": "наприклад, Іван Іванов",
"ProviderSelection_EmailHeader": "Електронна адреса",
@@ -47,6 +47,11 @@
"AccountDetailsPage_CalendarListDescription": "选择日历以配置其设置",
"AccountDetailsPage_InitialSynchronization_Title": "初次同步",
"AccountDetailsPage_InitialSynchronization_Description": "Wino 已将邮件同步到回溯到 {0} 的时间点。",
"AccountDetailsPage_CapabilityTitle": "已连接的功能",
"AccountDetailsPage_CapabilityDescription": "选择此账户用于邮件、日历,或两者皆用。启用新功能可能会要求您重新登录。",
"AccountCapability_MailOnly": "仅邮件",
"AccountCapability_CalendarOnly": "仅日历",
"AccountCapability_MailAndCalendar": "邮件与日历",
"AddHyperlink": "添加",
"AppCloseBackgroundSynchronizationWarningTitle": "后台同步",
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "应用尚未设置为随 Windows 自启。",
@@ -76,6 +81,7 @@
"Buttons_ApplyTheme": "应用主题",
"Buttons_PopOut": "弹出",
"Buttons_Browse": "浏览",
"Buttons_Back": "返回",
"Buttons_Cancel": "取消",
"Buttons_Close": "关闭",
"Buttons_Copy": "复制",
@@ -299,6 +305,8 @@
"DialogMessage_DeleteRecurringSeriesTitle": "删除重复系列",
"DialogMessage_DiscardDraftConfirmationMessage": "草稿将被删除。你想要继续吗?",
"DialogMessage_DiscardDraftConfirmationTitle": "舍弃草稿",
"DialogMessage_CloseDraftWindowConfirmationMessage": "草稿仍未保存。在关闭窗口前保存吗?",
"DialogMessage_CloseDraftWindowConfirmationTitle": "关闭窗口",
"DialogMessage_EmptySubjectConfirmation": "缺少标题",
"DialogMessage_EmptySubjectConfirmationMessage": "邮件没有主题。您想要继续吗?",
"DialogMessage_EnableStartupLaunchDeniedMessage": "可以在「设置」-「应用设置」中启用自启动。",
@@ -681,6 +689,10 @@
"NoMailSelected": "未选择任何邮件",
"NoMessageCrieteria": "没有符合搜索条件的邮件",
"NoMessageEmptyFolder": "此文件夹为空",
"MailEmptyState_Title": "没有启用邮件的账户",
"MailEmptyState_Message": "您已连接日历账户,但没有任何账户启用邮件。请添加邮件账户,或更新现有账户以使用邮件。",
"MailEmptyState_AddAccount": "添加账户",
"MailEmptyState_ManageAccounts": "管理账户",
"Notifications_MultipleNotificationsMessage": "您有 {0} 条新消息。",
"Notifications_MultipleNotificationsTitle": "新邮件",
"Notifications_WinoUpdatedMessage": "检查新版本 {0}",
@@ -1074,6 +1086,12 @@
"SettingsTaskbarBadge_Title": "任务栏徽标",
"SettingsThreads_Description": "将邮件组织成对话主题。",
"SettingsThreads_Title": "邮件会话",
"SettingsThreads_Enabled_Description": "将相关邮件聚合为一个对话。",
"SettingsThreads_Enabled_Title": "启用对话串联",
"SettingsThreadOrder_Description": "选择对话中条目的排序方式。",
"SettingsThreadOrder_Title": "对话项排序",
"SettingsThreadOrder_LastItemFirst": "最后一项在前",
"SettingsThreadOrder_FirstItemFirst": "第一项在前",
"SettingsUnlinkAccounts_Description": "删除账户之间的链接。这不会删除您的账户。",
"SettingsUnlinkAccounts_Title": "取消链接账户",
"SettingsMailRendering_ActionLabels_Title": "Action labels",
@@ -1339,6 +1357,7 @@
"WelcomeWindow_GetStartedButton": "开始添加账户",
"WelcomeWindow_GetStartedDescription": "添加您的 Outlook、Gmail 或 IMAP 账户来开始使用 Wino Mail。",
"WelcomeWindow_ImportFromWinoAccount": "从您的 Wino 账户导入",
"WelcomeWindow_ImportFromJsonFile": "从 JSON 文件导入",
"WelcomeWindow_ImportInProgress": "正在导入同步的偏好设置和账户...",
"WelcomeWindow_ImportNoAccountsFound": "在 Wino 账户中未找到同步的账户。如果存在偏好设置,它们已被还原。使用“开始使用”手动添加账户。",
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} 同步账户已在此设备上可用。如有需要,请使用“开始使用”手动再添加一个账户。",
@@ -1418,6 +1437,12 @@
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "密码、令牌和其他敏感信息不会同步。",
"WinoAccount_Management_ExportDialog_AccountsRelogin": "在另一台电脑上导入的账户仍需要您再次登录后才能使用。",
"WinoAccount_Management_ExportDialog_InProgress": "正在导出您选择的 Wino 数据...",
"WinoAccount_Management_LocalDataSectionTitle": "通过 JSON 文件传输",
"WinoAccount_Management_LocalDataSectionDescription": "从本地 JSON 文件导入或导出。密码、令牌和其他敏感信息不包含在内。",
"WinoAccount_Management_LocalDataImportAction": "导入",
"WinoAccount_Management_LocalDataExportAction": "导出",
"WinoAccount_Management_LocalDataSaved": "已将导出的 Wino 数据保存到 {0}。",
"WinoAccount_Management_LocalDataInvalidFile": "所选 JSON 文件不包含有效的 Wino 导出。",
"WinoAccount_Management_LoadFailed": "无法加载最新的 Wino 账户信息。",
"WinoAccount_Management_ActionFailed": "Wino 账户请求无法完成。",
"WinoAccount_SettingsSection_Title": "Wino 账户",
@@ -1514,8 +1539,22 @@
"WelcomeWizard_Step3Title": "完成设置",
"ProviderSelection_Title": "选择您的电子邮件提供商",
"ProviderSelection_Subtitle": "在下方选择一个提供商,以将您的电子邮件账户添加到 Wino Mail。",
"ProviderSelection_StepProgress": "步骤 {0} / 3",
"ProviderSelection_IdentityTitle": "账户身份",
"ProviderSelection_IdentityDescription": "选择此账户在 Wino 中的显示方式。",
"ProviderSelection_ProviderSectionTitle": "提供商",
"ProviderSelection_ProviderSectionDescription": "选择要连接的服务。",
"ProviderSelection_CapabilitySectionTitle": "将此账户用于",
"ProviderSelection_CapabilitySectionDescription": "选择要使用邮件、日历,还是两者都使用。",
"ProviderSelection_CapabilityProviderDescription_OAuth": "在下一步中,安全登录将连接您的账户。如果启用日历,Wino 也会自动连接 Outlook 日历或 Google 日历。",
"ProviderSelection_CapabilityProviderDescription_SpecialImap": "在下一步中,您将输入提供商凭据。邮件使用 IMAP/SMTP,日历可以使用 CalDAV,或在此设备上本地存储。",
"ProviderSelection_CapabilityProviderDescription_CustomServer": "在下一步中,您将输入服务器详细信息。邮件使用 IMAP/SMTP,日历可以使用 CalDAV,或在此设备上本地存储。",
"ProviderSelection_AccountNameHeader": "账户名",
"ProviderSelection_AccountNamePlaceholder": "例如 个人、工作",
"ProviderSelection_UseForMail": "邮件",
"ProviderSelection_UseForCalendar": "日历",
"ProviderSelection_CapabilityValidationMessage": "继续前请至少选择一个功能。",
"ProviderSelection_CalendarOnlyServerHint": "如果仅使用日历继续,下一页将不需要电子邮件地址。",
"ProviderSelection_DisplayNameHeader": "显示名称",
"ProviderSelection_DisplayNamePlaceholder": "例如 约翰·多伊",
"ProviderSelection_EmailHeader": "电子邮件地址",
@@ -0,0 +1,64 @@
using System;
using System.Net.Mail;
namespace Wino.Core.Domain.Validation;
public static class MailAccountAddressValidator
{
public static bool IsValid(string address)
{
if (string.IsNullOrWhiteSpace(address))
return false;
var trimmedAddress = address.Trim();
if (trimmedAddress.Contains('\r') || trimmedAddress.Contains('\n'))
return false;
try
{
var parsedAddress = new MailAddress(trimmedAddress);
return parsedAddress.Address.Equals(trimmedAddress, StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
public static bool TryGetDomain(string address, out string domain)
{
domain = string.Empty;
if (!IsValid(address))
return false;
var trimmedAddress = address.Trim();
var separatorIndex = trimmedAddress.LastIndexOf('@');
if (separatorIndex <= 0 || separatorIndex >= trimmedAddress.Length - 1)
return false;
domain = trimmedAddress[(separatorIndex + 1)..];
return !string.IsNullOrWhiteSpace(domain);
}
public static bool IsImplicitlyResolvableHost(string host)
{
if (string.IsNullOrWhiteSpace(host))
return false;
var normalizedHost = host.Trim().TrimEnd('.');
if (string.IsNullOrWhiteSpace(normalizedHost))
return false;
if (normalizedHost.Equals("localhost", StringComparison.OrdinalIgnoreCase))
return true;
var hostType = Uri.CheckHostName(normalizedHost);
if (hostType is UriHostNameType.IPv4 or UriHostNameType.IPv6)
return true;
return normalizedHost.IndexOf('.') < 0;
}
}
+3
View File
@@ -0,0 +1,3 @@
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]
@@ -83,7 +83,7 @@ public class AccountServiceTests : IAsyncLifetime
var secondAccountId = Guid.NewGuid();
await _accountService.CreateAccountAsync(
CreateImapAccount(firstAccountId),
CreateImapAccount(firstAccountId, "IMAP Test Account 1", "imap1@test.local"),
new CustomServerInformation
{
Id = Guid.NewGuid(),
@@ -92,7 +92,7 @@ public class AccountServiceTests : IAsyncLifetime
});
await _accountService.CreateAccountAsync(
CreateImapAccount(secondAccountId),
CreateImapAccount(secondAccountId, "IMAP Test Account 2", "imap2@test.local"),
new CustomServerInformation
{
Id = Guid.NewGuid(),
@@ -119,13 +119,13 @@ public class AccountServiceTests : IAsyncLifetime
.BeGreaterThanOrEqualTo(50);
}
private static MailAccount CreateImapAccount(Guid accountId)
private static MailAccount CreateImapAccount(Guid accountId, string name = "IMAP Test Account", string address = "imap@test.local")
{
return new MailAccount
{
Id = accountId,
Name = "IMAP Test Account",
Address = "imap@test.local",
Name = name,
Address = address,
SenderName = "IMAP Test",
ProviderType = MailProviderType.IMAP4
};
@@ -150,6 +150,41 @@ public class AutoDiscoveryServiceTests
uri.Should().Be(new Uri("https://dav.example.net/caldav/"));
}
[Fact]
public async Task GetAutoDiscoverySettings_ReturnsGuessedLocalhostSettings_ForManualLocalAccounts()
{
var handler = new StubHttpMessageHandler(request =>
{
var uri = request.RequestUri!.ToString();
if (uri.StartsWith("https://dns.google/resolve", StringComparison.OrdinalIgnoreCase))
{
return CreateJsonResponse("{\"Status\":0}", request);
}
return CreateStatusResponse(HttpStatusCode.NotFound, request);
});
using var client = new HttpClient(handler);
var sut = new AutoDiscoveryService(client);
var settings = await sut.GetAutoDiscoverySettings(new AutoDiscoveryMinimalSettings
{
Email = "user@localhost",
DisplayName = "User",
Password = "secret"
});
settings.Should().NotBeNull();
settings!.Domain.Should().Be("localhost");
settings.GetImapSettings()!.Address.Should().Be("localhost");
settings.GetImapSettings()!.Port.Should().Be(993);
settings.GetImapSettings()!.Username.Should().Be("user@localhost");
settings.GetSmptpSettings()!.Address.Should().Be("localhost");
settings.GetSmptpSettings()!.Port.Should().Be(587);
settings.GetSmptpSettings()!.Username.Should().Be("user@localhost");
}
private static HttpResponseMessage CreateXmlResponse(string xml, HttpRequestMessage request)
=> new(HttpStatusCode.OK)
{
@@ -13,6 +13,8 @@ public sealed class MailRequestStateTests
[Fact]
public void MarkReadRequest_RevertUiChanges_RestoresOriginalReadState()
{
WeakReferenceMessenger.Default.Reset();
var mailCopy = CreateMailCopy(isRead: false, isFlagged: false);
var request = new MarkReadRequest(mailCopy, IsRead: true);
var recipient = new MailRequestRecipient();
@@ -27,20 +29,23 @@ public sealed class MailRequestStateTests
request.RevertUIChanges();
mailCopy.IsRead.Should().BeFalse();
recipient.Updated.Should().HaveCount(2);
recipient.Updated[0].Source.Should().Be(EntityUpdateSource.ClientUpdated);
recipient.Updated[1].Source.Should().Be(EntityUpdateSource.ClientReverted);
recipient.Updated[1].UpdatedMail.IsRead.Should().BeFalse();
recipient.StateUpdates.Should().HaveCount(2);
recipient.StateUpdates[0].Source.Should().Be(EntityUpdateSource.ClientUpdated);
recipient.StateUpdates[1].Source.Should().Be(EntityUpdateSource.ClientReverted);
recipient.StateUpdates[1].UpdatedState.IsRead.Should().BeFalse();
}
finally
{
WeakReferenceMessenger.Default.UnregisterAll(recipient);
WeakReferenceMessenger.Default.Reset();
}
}
[Fact]
public void ChangeFlagRequest_RevertUiChanges_RestoresOriginalFlagState()
{
WeakReferenceMessenger.Default.Reset();
var mailCopy = CreateMailCopy(isRead: true, isFlagged: false);
var request = new ChangeFlagRequest(mailCopy, IsFlagged: true);
var recipient = new MailRequestRecipient();
@@ -55,14 +60,15 @@ public sealed class MailRequestStateTests
request.RevertUIChanges();
mailCopy.IsFlagged.Should().BeFalse();
recipient.Updated.Should().HaveCount(2);
recipient.Updated[0].Source.Should().Be(EntityUpdateSource.ClientUpdated);
recipient.Updated[1].Source.Should().Be(EntityUpdateSource.ClientReverted);
recipient.Updated[1].UpdatedMail.IsFlagged.Should().BeFalse();
recipient.StateUpdates.Should().HaveCount(2);
recipient.StateUpdates[0].Source.Should().Be(EntityUpdateSource.ClientUpdated);
recipient.StateUpdates[1].Source.Should().Be(EntityUpdateSource.ClientReverted);
recipient.StateUpdates[1].UpdatedState.IsFlagged.Should().BeFalse();
}
finally
{
WeakReferenceMessenger.Default.UnregisterAll(recipient);
WeakReferenceMessenger.Default.Reset();
}
}
@@ -76,10 +82,10 @@ public sealed class MailRequestStateTests
IsFlagged = isFlagged
};
internal sealed class MailRequestRecipient : IRecipient<MailUpdatedMessage>
internal sealed class MailRequestRecipient : IRecipient<MailStateUpdatedMessage>
{
public List<MailUpdatedMessage> Updated { get; } = [];
public List<MailStateUpdatedMessage> StateUpdates { get; } = [];
public void Receive(MailUpdatedMessage message) => Updated.Add(message);
public void Receive(MailStateUpdatedMessage message) => StateUpdates.Add(message);
}
}
@@ -1,3 +1,4 @@
using CommunityToolkit.Mvvm.Messaging;
using FluentAssertions;
using MimeKit;
using Moq;
@@ -8,6 +9,7 @@ using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Tests.Helpers;
using Wino.Messaging.UI;
using Wino.Services;
using Xunit;
@@ -229,6 +231,102 @@ public class MailThreadingTests : IAsyncLifetime
mimeMessage.ReplyTo.Mailboxes.Should().ContainSingle(m => m.Address == "support@test.local");
}
[Fact]
public async Task ApplyMailStateUpdatesAsync_ForBatchReadStateChange_SendsBulkMailUpdatedMessage()
{
var mail1 = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = _draftFolder.Id,
IsRead = true,
Subject = "First"
};
var mail2 = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = _draftFolder.Id,
IsRead = true,
Subject = "Second"
};
await _databaseService.Connection.InsertAllAsync(new[] { mail1, mail2 }, typeof(MailCopy));
var recipient = new MailUpdateRecipient();
WeakReferenceMessenger.Default.Register<MailUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Register<BulkMailUpdatedMessage>(recipient);
try
{
await _mailService.ApplyMailStateUpdatesAsync(
[
new MailCopyStateUpdate(mail1.Id, IsRead: false),
new MailCopyStateUpdate(mail2.Id, IsRead: false)
]);
recipient.SingleUpdates.Should().BeEmpty();
recipient.BulkUpdates.Should().ContainSingle();
recipient.BulkUpdates[0].Source.Should().Be(EntityUpdateSource.Server);
recipient.BulkUpdates[0].ChangedProperties.Should().Be(MailCopyChangeFlags.IsRead);
recipient.BulkUpdates[0].UpdatedMails.Should().HaveCount(2);
recipient.BulkUpdates[0].UpdatedMails.Should().OnlyContain(x => !x.IsRead);
(await _databaseService.Connection.FindAsync<MailCopy>(mail1.UniqueId))!.IsRead.Should().BeFalse();
(await _databaseService.Connection.FindAsync<MailCopy>(mail2.UniqueId))!.IsRead.Should().BeFalse();
}
finally
{
WeakReferenceMessenger.Default.Unregister<MailUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Unregister<BulkMailUpdatedMessage>(recipient);
}
}
[Fact]
public async Task ApplyMailStateUpdatesAsync_ForBatchMarkRead_SendsBulkMailReadStatusChanged()
{
var mail1 = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = _draftFolder.Id,
IsRead = false,
Subject = "First unread"
};
var mail2 = new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = _draftFolder.Id,
IsRead = false,
Subject = "Second unread"
};
await _databaseService.Connection.InsertAllAsync(new[] { mail1, mail2 }, typeof(MailCopy));
var recipient = new MailReadStatusRecipient();
WeakReferenceMessenger.Default.Register<MailReadStatusChanged>(recipient);
WeakReferenceMessenger.Default.Register<BulkMailReadStatusChanged>(recipient);
try
{
await _mailService.ApplyMailStateUpdatesAsync(
[
new MailCopyStateUpdate(mail1.Id, IsRead: true),
new MailCopyStateUpdate(mail2.Id, IsRead: true)
]);
recipient.SingleUpdates.Should().BeEmpty();
recipient.BulkUpdates.Should().ContainSingle();
recipient.BulkUpdates[0].UniqueIds.Should().BeEquivalentTo([mail1.UniqueId, mail2.UniqueId]);
}
finally
{
WeakReferenceMessenger.Default.Unregister<MailReadStatusChanged>(recipient);
WeakReferenceMessenger.Default.Unregister<BulkMailReadStatusChanged>(recipient);
}
}
private static MimeMessage CreateReferencedMimeMessage(string subject, string? messageId = null)
{
var message = new MimeMessage();
@@ -243,6 +341,24 @@ public class MailThreadingTests : IAsyncLifetime
return message;
}
internal sealed class MailUpdateRecipient : IRecipient<MailUpdatedMessage>, IRecipient<BulkMailUpdatedMessage>
{
public List<MailUpdatedMessage> SingleUpdates { get; } = [];
public List<BulkMailUpdatedMessage> BulkUpdates { get; } = [];
public void Receive(MailUpdatedMessage message) => SingleUpdates.Add(message);
public void Receive(BulkMailUpdatedMessage message) => BulkUpdates.Add(message);
}
internal sealed class MailReadStatusRecipient : IRecipient<MailReadStatusChanged>, IRecipient<BulkMailReadStatusChanged>
{
public List<MailReadStatusChanged> SingleUpdates { get; } = [];
public List<BulkMailReadStatusChanged> BulkUpdates { get; } = [];
public void Receive(MailReadStatusChanged message) => SingleUpdates.Add(message);
public void Receive(BulkMailReadStatusChanged message) => BulkUpdates.Add(message);
}
private static MailService BuildMailService(InMemoryDatabaseService db)
{
var signatureService = new Mock<ISignatureService>();
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using FluentAssertions;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Requests.Bundles;
using Wino.Core.Requests.Mail;
using Wino.Core.Synchronizers;
using Wino.Messaging.UI;
using Xunit;
namespace Wino.Core.Tests.Synchronizers;
public sealed class BaseSynchronizerUiChangeTests
{
[Fact]
public void ApplyOptimisticUiChanges_UsesBundleUiChangeRequest_ForBatchBundle()
{
WeakReferenceMessenger.Default.Reset();
var folderId = Guid.NewGuid();
var account = new MailAccount { Id = Guid.NewGuid(), Name = "Test account" };
var synchronizer = new TestSynchronizer(account);
var recipient = new UiChangeRecipient();
var request1 = new MarkReadRequest(CreateMailCopy(folderId, isRead: false), IsRead: true);
var request2 = new MarkReadRequest(CreateMailCopy(folderId, isRead: false), IsRead: true);
var batchRequest = new BatchMarkReadRequest([request1, request2]);
var bundle = new HttpRequestBundle<object>(new object(), batchRequest, request1);
WeakReferenceMessenger.Default.Register<MailStateUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Register<BulkMailStateUpdatedMessage>(recipient);
try
{
synchronizer.ApplyUiChanges([bundle]);
recipient.SingleUpdates.Should().BeEmpty();
recipient.BulkUpdates.Should().ContainSingle();
recipient.BulkUpdates[0].UpdatedStates.Should().HaveCount(2);
request1.Item.IsRead.Should().BeFalse();
request2.Item.IsRead.Should().BeFalse();
}
finally
{
WeakReferenceMessenger.Default.Unregister<MailStateUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Unregister<BulkMailStateUpdatedMessage>(recipient);
WeakReferenceMessenger.Default.Reset();
}
}
private static MailCopy CreateMailCopy(Guid folderId, bool isRead) =>
new()
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = folderId,
IsRead = isRead
};
private sealed class TestSynchronizer : BaseSynchronizer<object>
{
public TestSynchronizer(MailAccount account)
: base(account, WeakReferenceMessenger.Default)
{
}
public void ApplyUiChanges(List<IRequestBundle<object>> bundles) => ApplyOptimisticUiChanges(bundles);
public override Task ExecuteNativeRequestsAsync(List<IRequestBundle<object>> batchedRequests, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
}
internal sealed class UiChangeRecipient : IRecipient<MailStateUpdatedMessage>, IRecipient<BulkMailStateUpdatedMessage>
{
public List<MailStateUpdatedMessage> SingleUpdates { get; } = [];
public List<BulkMailStateUpdatedMessage> BulkUpdates { get; } = [];
public void Receive(MailStateUpdatedMessage message) => SingleUpdates.Add(message);
public void Receive(BulkMailStateUpdatedMessage message) => BulkUpdates.Add(message);
}
}
@@ -2,12 +2,14 @@ using FluentAssertions;
using MailKit;
using MailKit.Net.Imap;
using Moq;
using System.Linq;
using System.Reflection;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Synchronizers.ImapSync;
using Wino.Services.Extensions;
using Xunit;
using IMailService = Wino.Core.Domain.Interfaces.IMailService;
@@ -223,4 +225,61 @@ public class UnifiedImapSynchronizerTests
capturedPackage.Should().NotBeNull();
capturedPackage!.MimeMessage.Should().BeNull();
}
[Fact]
public async Task ProcessSummariesAsync_ShouldBatchStateUpdates_ForExistingMailCopies()
{
var localFolder = new MailItemFolder
{
Id = Guid.NewGuid(),
MailAccountId = Guid.NewGuid(),
FolderName = "Inbox",
RemoteFolderId = "INBOX"
};
var summaryMock = new Mock<IMessageSummary>();
summaryMock.SetupGet(x => x.UniqueId).Returns(new UniqueId(42));
summaryMock.SetupGet(x => x.Flags).Returns(MessageFlags.Seen | MessageFlags.Flagged);
var existingMailCopy = new MailCopy
{
Id = MailkitClientExtensions.CreateUid(localFolder.Id, 42),
IsRead = false,
IsFlagged = false
};
var mailServiceMock = new Mock<IMailService>();
mailServiceMock
.Setup(x => x.GetExistingMailsAsync(localFolder.Id, It.IsAny<IEnumerable<UniqueId>>()))
.ReturnsAsync([existingMailCopy]);
mailServiceMock
.Setup(x => x.ApplyMailStateUpdatesAsync(It.IsAny<IEnumerable<MailCopyStateUpdate>>()))
.Returns(Task.CompletedTask);
var sut = new UnifiedImapSynchronizer(
Mock.Of<IFolderService>(),
mailServiceMock.Object,
Mock.Of<IImapSynchronizerErrorHandlerFactory>());
var imapSynchronizerMock = new Mock<IImapSynchronizer>();
var processMethod = typeof(UnifiedImapSynchronizer).GetMethod("ProcessSummariesAsync", BindingFlags.Instance | BindingFlags.NonPublic);
processMethod.Should().NotBeNull();
var task = (Task<List<string>>)processMethod!.Invoke(
sut,
[imapSynchronizerMock.Object, localFolder, new List<IMessageSummary> { summaryMock.Object }, CancellationToken.None])!;
var result = await task;
result.Should().BeEmpty();
mailServiceMock.Verify(
x => x.ApplyMailStateUpdatesAsync(It.Is<IEnumerable<MailCopyStateUpdate>>(updates =>
updates.Count() == 1
&& updates.First().MailCopyId == existingMailCopy.Id
&& updates.First().IsRead == true
&& updates.First().IsFlagged == true)),
Times.Once);
mailServiceMock.Verify(x => x.CreateMailAsync(It.IsAny<Guid>(), It.IsAny<NewMailItemPackage>()), Times.Never);
}
}
@@ -10,6 +10,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Mail;
using Wino.Core.Requests.Folder;
using Wino.Core.Synchronizers;
using Xunit;
@@ -49,6 +50,38 @@ public sealed class WinoSynchronizerMailRequestTests
synchronizer.ExecuteNativeRequestsInvocationCount.Should().Be(1);
}
[Fact]
public async Task ExecuteRequests_should_dispatch_grouped_mark_read_requests_with_composite_grouping_key()
{
var synchronizer = new TestMailSynchronizer();
var folderId = Guid.NewGuid();
synchronizer.QueueRequest(new MarkReadRequest(new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = folderId
}, IsRead: true));
synchronizer.QueueRequest(new MarkReadRequest(new MailCopy
{
UniqueId = Guid.NewGuid(),
Id = Guid.NewGuid().ToString(),
FolderId = folderId
}, IsRead: true));
var result = await synchronizer.SynchronizeMailsAsync(new MailSynchronizationOptions
{
AccountId = synchronizer.Account.Id,
Type = MailSynchronizationType.ExecuteRequests
});
result.CompletedState.Should().Be(SynchronizationCompletedState.Success);
synchronizer.MarkReadInvocationCount.Should().Be(1);
synchronizer.LastMarkReadBatchCount.Should().Be(2);
synchronizer.ExecuteNativeRequestsInvocationCount.Should().Be(1);
}
private sealed class TestMailSynchronizer
: WinoSynchronizer<object, object, object>
{
@@ -60,6 +93,8 @@ public sealed class WinoSynchronizerMailRequestTests
public override uint BatchModificationSize => 1;
public override uint InitialMessageDownloadCountPerFolder => 0;
public int CreateRootFolderInvocationCount { get; private set; }
public int MarkReadInvocationCount { get; private set; }
public int LastMarkReadBatchCount { get; private set; }
public int ExecuteNativeRequestsInvocationCount { get; private set; }
public override List<IRequestBundle<object>> CreateRootFolder(CreateRootFolderRequest request)
@@ -68,6 +103,13 @@ public sealed class WinoSynchronizerMailRequestTests
return [new TestRequestBundle(new object(), request)];
}
public override List<IRequestBundle<object>> MarkRead(BatchMarkReadRequest request)
{
MarkReadInvocationCount++;
LastMarkReadBatchCount = request.Count;
return [new TestRequestBundle(new object(), request[0])];
}
public override Task ExecuteNativeRequestsAsync(List<IRequestBundle<object>> batchedRequests, CancellationToken cancellationToken = default)
{
ExecuteNativeRequestsInvocationCount++;
@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
@@ -9,6 +10,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels.Data;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
@@ -17,6 +19,7 @@ namespace Wino.Core.ViewModels;
public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewModel
{
public ObservableCollection<IAccountProviderDetailViewModel> Accounts { get; set; } = [];
public IEnumerable<IAccountProviderDetailViewModel> StartupAccounts => Accounts.Where(IsStartupEligible);
public bool IsPurchasePanelVisible => !HasUnlimitedAccountProduct;
public bool IsAccountCreationAlmostOnLimit => Accounts != null && Accounts.Count == FREE_ACCOUNT_COUNT - 1;
@@ -130,10 +133,21 @@ public abstract partial class AccountManagementPageViewModelBase : CoreBaseViewM
private void AccountsChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged(nameof(HasAccountsDefined));
OnPropertyChanged(nameof(StartupAccounts));
}
private static string GetAccountDetailsTitle(MailAccount account)
=> !string.IsNullOrWhiteSpace(account?.Address)
? string.Format(Translator.SettingsAccountDetails_NavigationTitle, account.Address)
: account?.Name ?? Translator.AccountDetailsPage_Title;
private static bool IsStartupEligible(IAccountProviderDetailViewModel account)
{
return account switch
{
AccountProviderDetailViewModel accountViewModel => accountViewModel.Account.IsMailAccessGranted,
MergedAccountProviderDetailViewModel mergedAccountViewModel => mergedAccountViewModel.HoldingAccounts.Any(a => a.Account.IsMailAccessGranted),
_ => true
};
}
}
@@ -1,5 +1,6 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
@@ -9,6 +10,8 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CapabilitySummary))]
[NotifyPropertyChangedFor(nameof(DescriptionText))]
private MailAccount account;
public IProviderDetail ProviderDetail { get; set; }
@@ -20,6 +23,10 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
public int Order => Account.Order;
public string StartupEntityAddresses => Account.Address;
public string CapabilitySummary => BuildCapabilitySummary(Account);
public string DescriptionText => string.IsNullOrWhiteSpace(Account.Address)
? CapabilitySummary
: $"{CapabilitySummary} | {Account.Address}";
public int HoldingAccountCount => 1;
@@ -30,4 +37,15 @@ public partial class AccountProviderDetailViewModel : ObservableObject, IAccount
ProviderDetail = providerDetail;
Account = account;
}
private static string BuildCapabilitySummary(MailAccount account)
{
if (account?.IsMailAccessGranted == true && account.IsCalendarAccessGranted)
return Translator.AccountCapability_MailAndCalendar;
if (account?.IsMailAccessGranted == true)
return Translator.AccountCapability_MailOnly;
return Translator.AccountCapability_CalendarOnly;
}
}
@@ -160,7 +160,10 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
public void NavigateToAddAccount()
{
Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage));
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step2Title,
WinoPage.ProviderSelectionPage,
ProviderSelectionNavigationContext.CreateForSettingsAddAccount()));
}
public void NavigateToManageAccounts()
+8 -3
View File
@@ -58,6 +58,7 @@ public class ImapClientPool : IDisposable
private readonly int _targetMinimumConnections;
private DateTime _lastKeepAliveSentUtc = DateTime.MinValue;
private Exception _lastConnectionException;
private WinoImapClient _dedicatedIdleClient;
private bool _disposedValue;
private bool _initialized;
@@ -114,7 +115,7 @@ public class ImapClientPool : IDisposable
var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
if (initialClient == null)
{
throw CreatePoolException("Failed to create initial IMAP connection for the pool.");
throw CreatePoolException("Failed to create initial IMAP connection for the pool.", _lastConnectionException);
}
_clientStates[initialClient] = ImapClientState.Available;
@@ -192,6 +193,7 @@ public class ImapClientPool : IDisposable
}
catch (Exception ex)
{
_lastConnectionException = ex;
_logger.Warning(ex, "Pooled IMAP client was not ready. Marking as failed.");
MarkClientAsFailed(pooledClient);
}
@@ -215,12 +217,12 @@ public class ImapClientPool : IDisposable
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.");
throw CreatePoolException($"Timed out while acquiring an IMAP client after {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.", _lastConnectionException);
}
throw cancellationToken.IsCancellationRequested
? new OperationCanceledException(cancellationToken)
: CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.");
: CreatePoolException($"Failed to acquire IMAP client within {timeout.TotalSeconds:F1} seconds. Failures: {createFailures}.", _lastConnectionException);
}
/// <summary>
@@ -516,14 +518,17 @@ public class ImapClientPool : IDisposable
private async Task<WinoImapClient> CreateAndConnectClientAsync(CancellationToken cancellationToken)
{
var client = CreateNewClient();
_lastConnectionException = null;
try
{
await EnsureClientReadyAsync(client, cancellationToken).ConfigureAwait(false);
_lastConnectionException = null;
return client;
}
catch (Exception ex)
{
_lastConnectionException = ex;
_logger.Warning(ex, "Failed to create and connect IMAP client.");
DisposeClient(client);
return null;
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -14,18 +15,26 @@ public record EmptyFolderRequest(MailItemFolder Folder, List<MailCopy> MailsToDe
public bool ExcludeMustHaveFolders => false;
public override void ApplyUIChanges()
{
foreach (var item in MailsToDelete)
{
WeakReferenceMessenger.Default.Send(new MailRemovedMessage(item, EntityUpdateSource.ClientUpdated));
}
var removedMails = MailsToDelete?
.Where(item => item != null)
.ToList();
if (removedMails == null || removedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
foreach (var item in MailsToDelete)
{
WeakReferenceMessenger.Default.Send(new MailAddedMessage(item, EntityUpdateSource.ClientReverted));
}
var addedMails = MailsToDelete?
.Where(item => item != null)
.ToList();
if (addedMails == null || addedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
}
public List<Guid> SynchronizationFolderIds => [Folder.Id];
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -17,31 +18,31 @@ public record MarkFolderAsReadRequest(MailItemFolder Folder, List<MailCopy> Mail
public override void ApplyUIChanges()
{
if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return;
var updatedMails = MailsToMarkRead?
.Where(item => item != null && !item.IsRead)
.Select(item => new MailStateChange(item.UniqueId, IsRead: true))
.ToList();
foreach (var item in MailsToMarkRead)
{
// Skip if already read
if (item.IsRead) continue;
if (updatedMails == null || updatedMails.Count == 0)
return;
item.IsRead = true;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
}
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
if (MailsToMarkRead == null || MailsToMarkRead.Count == 0) return;
var updatedMails = MailsToMarkRead?
.Where(item => item != null && !item.IsRead)
.Select(item => new MailStateChange(item.UniqueId, IsRead: false))
.ToList();
foreach (var item in MailsToMarkRead)
{
// Skip if already unread (wasn't changed by ApplyUIChanges)
if (!item.IsRead) continue;
if (updatedMails == null || updatedMails.Count == 0)
return;
item.IsRead = false;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
}
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientReverted));
}
}
+20
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -38,6 +39,7 @@ public record ArchiveRequest(bool IsArchiving, MailCopy Item, MailItemFolder Fro
}
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Archive;
public override object GroupingKey() => (Operation, IsArchiving, FromFolder.Id, ToFolder?.Id ?? Guid.Empty);
public override void ApplyUIChanges()
{
@@ -55,4 +57,22 @@ public class BatchArchiveRequest : BatchCollection<ArchiveRequest>
public BatchArchiveRequest(IEnumerable<ArchiveRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (removedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (addedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
}
}
+40 -8
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -25,25 +26,26 @@ public record ChangeFlagRequest(MailCopy Item, bool IsFlagged) : MailRequestBase
/// If the mail is already in the desired flagged state, no change is needed.
/// </summary>
public bool IsNoOp { get; } = Item.IsFlagged == IsFlagged;
public bool OriginalIsFlagged => _originalIsFlagged;
public override object GroupingKey() => (Operation, Item.FolderId, IsFlagged);
public override void ApplyUIChanges()
{
// Skip UI update if the mail is already in the desired state
if (IsNoOp) return;
Item.IsFlagged = IsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsFlagged));
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
new MailStateChange(Item.UniqueId, IsFlagged: IsFlagged),
EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
// Skip UI revert if this was a no-op request
if (IsNoOp) return;
Item.IsFlagged = _originalIsFlagged;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsFlagged));
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
new MailStateChange(Item.UniqueId, IsFlagged: _originalIsFlagged),
EntityUpdateSource.ClientReverted));
}
}
@@ -52,4 +54,34 @@ public class BatchChangeFlagRequest : BatchCollection<ChangeFlagRequest>
public BatchChangeFlagRequest(IEnumerable<ChangeFlagRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var updatedMails = this
.Where(x => !x.IsNoOp)
.Select(x => new MailStateChange(x.Item.UniqueId, IsFlagged: x.IsFlagged))
.ToList();
if (updatedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var updatedMails = this
.Where(x => !x.IsNoOp)
.Select(x => new MailStateChange(x.Item.UniqueId, IsFlagged: x.OriginalIsFlagged))
.ToList();
if (updatedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientReverted));
}
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -30,6 +31,7 @@ public record ChangeJunkStateRequest(bool IsJunk, MailCopy Item, MailItemFolder
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.ChangeJunkState;
public override object GroupingKey() => (Operation, IsJunk, FromFolder.Id, TargetFolder?.Id ?? Guid.Empty);
public override void ApplyUIChanges()
{
@@ -47,4 +49,22 @@ public class BatchChangeJunkStateRequest : BatchCollection<ChangeJunkStateReques
public BatchChangeJunkStateRequest(IEnumerable<ChangeJunkStateRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (removedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (addedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
}
}
+20
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -19,6 +20,7 @@ public record DeleteRequest(MailCopy MailItem) : MailRequestBase(MailItem),
public List<Guid> SynchronizationFolderIds => [Item.FolderId];
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Delete;
public override object GroupingKey() => (Operation, Item.FolderId);
public override void ApplyUIChanges()
{
@@ -36,4 +38,22 @@ public class BatchDeleteRequest : BatchCollection<DeleteRequest>
public BatchDeleteRequest(IEnumerable<DeleteRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (removedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (addedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
}
}
+40 -8
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -24,25 +25,26 @@ public record MarkReadRequest(MailCopy Item, bool IsRead) : MailRequestBase(Item
/// If the mail is already in the desired read state, no change is needed.
/// </summary>
public bool IsNoOp { get; } = Item.IsRead == IsRead;
public bool OriginalIsRead => _originalIsRead;
public override object GroupingKey() => (Operation, Item.FolderId, IsRead);
public override void ApplyUIChanges()
{
// Skip UI update if the mail is already in the desired state
if (IsNoOp) return;
Item.IsRead = IsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientUpdated, MailCopyChangeFlags.IsRead));
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
new MailStateChange(Item.UniqueId, IsRead: IsRead),
EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
// Skip UI revert if this was a no-op request
if (IsNoOp) return;
Item.IsRead = _originalIsRead;
WeakReferenceMessenger.Default.Send(new MailUpdatedMessage(Item, EntityUpdateSource.ClientReverted, MailCopyChangeFlags.IsRead));
WeakReferenceMessenger.Default.Send(new MailStateUpdatedMessage(
new MailStateChange(Item.UniqueId, IsRead: _originalIsRead),
EntityUpdateSource.ClientReverted));
}
}
@@ -51,4 +53,34 @@ public class BatchMarkReadRequest : BatchCollection<MarkReadRequest>
public BatchMarkReadRequest(IEnumerable<MarkReadRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var updatedMails = this
.Where(x => !x.IsNoOp)
.Select(x => new MailStateChange(x.Item.UniqueId, IsRead: x.IsRead))
.ToList();
if (updatedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var updatedMails = this
.Where(x => !x.IsNoOp)
.Select(x => new MailStateChange(x.Item.UniqueId, IsRead: x.OriginalIsRead))
.ToList();
if (updatedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailStateUpdatedMessage(
updatedMails,
EntityUpdateSource.ClientReverted));
}
}
+20
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
@@ -15,6 +16,7 @@ public record MoveRequest(MailCopy Item, MailItemFolder FromFolder, MailItemFold
public List<Guid> SynchronizationFolderIds => new() { FromFolder.Id, ToFolder.Id };
public bool ExcludeMustHaveFolders => false;
public override MailSynchronizerOperation Operation => MailSynchronizerOperation.Move;
public override object GroupingKey() => (Operation, FromFolder.Id, ToFolder?.Id ?? Guid.Empty);
public override void ApplyUIChanges()
{
@@ -32,4 +34,22 @@ public class BatchMoveRequest : BatchCollection<MoveRequest>, IUIChangeRequest
public BatchMoveRequest(IEnumerable<MoveRequest> collection) : base(collection)
{
}
public override void ApplyUIChanges()
{
var removedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (removedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.ClientUpdated));
}
public override void RevertUIChanges()
{
var addedMails = this.Select(x => x.Item).Where(x => x != null).ToList();
if (addedMails.Count == 0)
return;
WeakReferenceMessenger.Default.Send(new BulkMailAddedMessage(addedMails, EntityUpdateSource.ClientReverted));
}
}
@@ -11,6 +11,7 @@ using Serilog;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models;
using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Domain.Validation;
namespace Wino.Core.Services;
@@ -450,6 +451,9 @@ public class AutoDiscoveryService : IAutoDiscoveryService
private async Task<bool> HasAnyDnsAddressRecordAsync(string host, CancellationToken cancellationToken)
{
if (MailAccountAddressValidator.IsImplicitlyResolvableHost(host))
return true;
var aRecords = await QueryDnsAsync(host, "A", cancellationToken).ConfigureAwait(false);
if (aRecords.Count > 0)
return true;
+29 -6
View File
@@ -123,7 +123,7 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient<Accoun
catch (ImapClientPoolException clientPoolException)
{
_logger.Error(clientPoolException, "IMAP connectivity test failed");
return ImapConnectivityTestResults.Failure(clientPoolException);
return ImapConnectivityTestResults.Failure(clientPoolException.InnerException ?? clientPoolException);
}
catch (Exception exception)
{
@@ -259,25 +259,48 @@ public class SynchronizationManager : ISynchronizationManager, IRecipient<Accoun
/// <param name="accountId">Account ID to queue the request for</param>
/// <param name="triggerSynchronization">Whether to automatically trigger synchronization after queuing the request</param>
public async Task QueueRequestAsync(IRequestBase request, Guid accountId, bool triggerSynchronization)
=> await QueueRequestsAsync([request], accountId, triggerSynchronization).ConfigureAwait(false);
public async Task QueueRequestsAsync(IEnumerable<IRequestBase> requests, Guid accountId, bool triggerSynchronization)
{
EnsureInitialized();
var requestList = requests?.Where(request => request != null).ToList() ?? [];
if (requestList.Count == 0)
return;
var synchronizer = await GetOrCreateSynchronizerAsync(accountId);
if (synchronizer == null)
{
_logger.Error("Could not find or create synchronizer for account {AccountId} to queue request", accountId);
_logger.Error("Could not find or create synchronizer for account {AccountId} to queue {RequestCount} request(s)", accountId, requestList.Count);
return;
}
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
request.GetType().Name, accountId);
if (requestList.Count == 1)
{
_logger.Debug("Queuing request {RequestType} for account {AccountId}",
requestList[0].GetType().Name, accountId);
}
else
{
var requestSummary = string.Join(", ", requestList
.GroupBy(request => request.GetType().Name)
.OrderBy(group => group.Key)
.Select(group => $"{group.Key} x{group.Count()}"));
synchronizer.QueueRequest(request);
_logger.Debug("Queuing {RequestCount} requests for account {AccountId}: {RequestSummary}",
requestList.Count, accountId, requestSummary);
}
foreach (var request in requestList)
{
synchronizer.QueueRequest(request);
}
if (triggerSynchronization)
{
// Determine if this is a calendar or mail operation
bool isCalendarOperation = request is ICalendarActionRequest;
bool isCalendarOperation = requestList.All(request => request is ICalendarActionRequest);
if (isCalendarOperation)
{
+10 -10
View File
@@ -93,13 +93,11 @@ public class WinoRequestDelegator : IWinoRequestDelegator
// Queue requests for each account and start synchronization.
foreach (var accountGroup in accountIds)
{
foreach (var accountRequest in accountGroup)
{
await QueueRequestAsync(accountRequest, accountGroup.Key);
}
var groupedRequests = accountGroup.Cast<IRequestBase>().ToList();
await QueueRequestsAsync(groupedRequests, accountGroup.Key).ConfigureAwait(false);
var account = accountGroup.First().Item.AssignedAccount;
var actionItems = SynchronizationActionHelper.CreateActionItems(accountGroup, accountGroup.Key, account.Name);
var actionItems = SynchronizationActionHelper.CreateActionItems(groupedRequests, accountGroup.Key, account.Name);
if (actionItems.Count > 0)
WeakReferenceMessenger.Default.Send(new SynchronizationActionsAdded(accountGroup.Key, account.Name, actionItems));
@@ -214,10 +212,7 @@ public class WinoRequestDelegator : IWinoRequestDelegator
if (requestList.Count == 0)
return;
foreach (var request in requestList)
{
await QueueRequestAsync(request, accountId).ConfigureAwait(false);
}
await QueueRequestsAsync(requestList, accountId).ConfigureAwait(false);
await SendSyncActionsAddedAsync(requestList, accountId).ConfigureAwait(false);
await QueueSynchronizationAsync(accountId).ConfigureAwait(false);
@@ -274,7 +269,12 @@ public class WinoRequestDelegator : IWinoRequestDelegator
private async Task QueueRequestAsync(IRequestBase request, Guid accountId)
{
// Don't trigger synchronization for individual requests - we'll trigger it once for all requests
await SynchronizationManager.Instance.QueueRequestAsync(request, accountId, triggerSynchronization: false);
await SynchronizationManager.Instance.QueueRequestAsync(request, accountId, triggerSynchronization: false).ConfigureAwait(false);
}
private async Task QueueRequestsAsync(IEnumerable<IRequestBase> requests, Guid accountId)
{
await SynchronizationManager.Instance.QueueRequestsAsync(requests, accountId, triggerSynchronization: false).ConfigureAwait(false);
}
private Task QueueSynchronizationAsync(Guid accountId)
+7 -3
View File
@@ -54,6 +54,10 @@ public class WinoRequestProcessor : IWinoRequestProcessor
{
var action = preperationRequest.Action;
var moveTargetStructure = preperationRequest.MoveTargetFolder;
var mailItems = preperationRequest.MailItems?.Where(item => item != null).ToList() ?? [];
if (mailItems.Count == 0)
return [];
// Ask confirmation for permanent delete operation.
// Drafts are always hard deleted without any protection.
@@ -78,12 +82,12 @@ public class WinoRequestProcessor : IWinoRequestProcessor
// Handle the case when user is trying to move multiple mails that belong to different accounts.
// We can't handle this with only 1 picker dialog.
bool isInvalidMoveTarget = preperationRequest.MailItems.Select(a => a.AssignedAccount.Id).Distinct().Count() > 1;
bool isInvalidMoveTarget = mailItems.Select(a => a.AssignedAccount.Id).Distinct().Count() > 1;
if (isInvalidMoveTarget)
throw new InvalidMoveTargetException(InvalidMoveTargetReason.MultipleAccounts);
var accountId = preperationRequest.MailItems.FirstOrDefault().AssignedAccount.Id;
var accountId = mailItems[0].AssignedAccount.Id;
moveTargetStructure = await _dialogService.PickFolderAsync(accountId, PickFolderReason.Move, _folderService);
@@ -94,7 +98,7 @@ public class WinoRequestProcessor : IWinoRequestProcessor
var requests = new List<IMailActionRequest>();
// TODO: Fix: Collection was modified; enumeration operation may not execute
foreach (var item in preperationRequest.MailItems.ToList())
foreach (var item in mailItems)
{
var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution);
@@ -12,6 +12,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Mail;
using Wino.Core.Requests.Bundles;
using Wino.Messaging.UI;
@@ -262,4 +263,75 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
return ret;
}
protected void ApplyOptimisticUiChanges(IEnumerable<IRequestBundle<TBaseRequest>> bundles, Func<IRequestBase, bool> shouldApply = null)
{
var bundleList = bundles?
.Where(b => b?.Request != null && (shouldApply?.Invoke(b.Request) ?? true))
.ToList() ?? [];
if (bundleList.Count == 0)
return;
var requestList = new List<IRequestBase>(bundleList.Count);
foreach (var bundle in bundleList)
{
if (bundle.UIChangeRequest != null && !ReferenceEquals(bundle.UIChangeRequest, bundle.Request))
{
bundle.UIChangeRequest.ApplyUIChanges();
continue;
}
requestList.Add(bundle.Request);
}
if (requestList.Count == 0)
return;
var appliedBatchRequestKeys = new HashSet<object>();
foreach (var group in requestList.GroupBy(r => r.GroupingKey()))
{
var groupRequests = group.ToList();
if (groupRequests.Count <= 1)
continue;
if (!TryApplyBatchUiChanges(groupRequests))
continue;
appliedBatchRequestKeys.Add(group.Key);
}
foreach (var request in requestList)
{
if (!appliedBatchRequestKeys.Contains(request.GroupingKey()))
{
request.ApplyUIChanges();
}
}
}
private static bool TryApplyBatchUiChanges(IReadOnlyList<IRequestBase> requests)
{
if (requests == null || requests.Count <= 1)
return false;
return requests[0] switch
{
MarkReadRequest => ApplyBatch(new BatchMarkReadRequest(requests.Cast<MarkReadRequest>())),
ChangeFlagRequest => ApplyBatch(new BatchChangeFlagRequest(requests.Cast<ChangeFlagRequest>())),
DeleteRequest => ApplyBatch(new BatchDeleteRequest(requests.Cast<DeleteRequest>())),
MoveRequest => ApplyBatch(new BatchMoveRequest(requests.Cast<MoveRequest>())),
ArchiveRequest => ApplyBatch(new BatchArchiveRequest(requests.Cast<ArchiveRequest>())),
ChangeJunkStateRequest => ApplyBatch(new BatchChangeJunkStateRequest(requests.Cast<ChangeJunkStateRequest>())),
_ => false
};
static bool ApplyBatch(IUIChangeRequest request)
{
request.ApplyUIChanges();
return true;
}
}
}
@@ -29,6 +29,8 @@ namespace Wino.Core.Synchronizers.ImapSync;
public class UnifiedImapSynchronizer
{
private static readonly TimeSpan UidReconcileInterval = TimeSpan.FromHours(12);
private const int NewMessageFetchBatchSize = 50;
private const int ExistingMessageFlagFetchBatchSize = 250;
private readonly ILogger _logger = Log.ForContext<UnifiedImapSynchronizer>();
private readonly IFolderService _folderService;
@@ -47,6 +49,9 @@ public class UnifiedImapSynchronizer
MessageSummaryItems.References |
MessageSummaryItems.ModSeq |
MessageSummaryItems.BodyStructure;
private readonly MessageSummaryItems _existingMailSynchronizationFlags =
MessageSummaryItems.Flags |
MessageSummaryItems.UniqueId;
public UnifiedImapSynchronizer(
IFolderService folderService,
@@ -182,15 +187,35 @@ public class UnifiedImapSynchronizer
var downloadedMessageIds = new List<string>();
foreach (var batch in uids.Distinct().OrderBy(a => a.Id).Batch(50))
foreach (var batch in uids.Distinct().OrderBy(a => a.Id).Batch(ExistingMessageFlagFetchBatchSize))
{
cancellationToken.ThrowIfCancellationRequested();
var summaryBatch = await remoteFolder
.FetchAsync(new UniqueIdSet(batch.ToList(), SortOrder.Ascending), _mailSynchronizationFlags, cancellationToken)
.ConfigureAwait(false);
var batchUids = batch.ToList();
var existingMails = await _mailService.GetExistingMailsAsync(localFolder.Id, batchUids).ConfigureAwait(false);
var existingByUid = CreateExistingMailLookup(existingMails);
var existingUids = batchUids.Where(uid => existingByUid.ContainsKey(uid.Id)).ToList();
var newUids = batchUids.Where(uid => !existingByUid.ContainsKey(uid.Id)).ToList();
downloadedMessageIds.AddRange(await ProcessSummariesAsync(synchronizer, localFolder, summaryBatch, cancellationToken).ConfigureAwait(false));
if (existingUids.Count > 0)
{
var existingSummaryBatch = await remoteFolder
.FetchAsync(new UniqueIdSet(existingUids, SortOrder.Ascending), _existingMailSynchronizationFlags, cancellationToken)
.ConfigureAwait(false);
await ApplySummaryFlagUpdatesAsync(existingByUid, existingSummaryBatch).ConfigureAwait(false);
}
foreach (var newBatch in newUids.Batch(NewMessageFetchBatchSize))
{
cancellationToken.ThrowIfCancellationRequested();
var newSummaryBatch = await remoteFolder
.FetchAsync(new UniqueIdSet(newBatch.ToList(), SortOrder.Ascending), _mailSynchronizationFlags, cancellationToken)
.ConfigureAwait(false);
downloadedMessageIds.AddRange(await ProcessSummariesCoreAsync(synchronizer, localFolder, newSummaryBatch, existingByUid, cancellationToken).ConfigureAwait(false));
}
}
UpdateHighestKnownUid(localFolder, remoteFolder, uids.Select(a => a.Id));
@@ -268,7 +293,29 @@ public class UnifiedImapSynchronizer
.ConfigureAwait(false);
}
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
var existingMails = await _mailService.GetExistingMailsAsync(folder.Id, changedUids).ConfigureAwait(false);
var existingByUid = CreateExistingMailLookup(existingMails);
var newOrUnknownUids = changedUids.Where(uid => !existingByUid.ContainsKey(uid.Id)).ToList();
var existingUidsWithoutFlagEvents = changedUids
.Where(uid => existingByUid.ContainsKey(uid.Id) && !changedFlags.ContainsKey(uid.Id))
.ToList();
if (existingUidsWithoutFlagEvents.Count > 0)
{
var missingEventSummaries = await remoteFolder
.FetchAsync(new UniqueIdSet(existingUidsWithoutFlagEvents, SortOrder.Ascending), _existingMailSynchronizationFlags, cancellationToken)
.ConfigureAwait(false);
foreach (var summary in missingEventSummaries)
{
if (summary.UniqueId != UniqueId.Invalid && summary.Flags != null)
{
changedFlags[summary.UniqueId.Id] = summary.Flags.Value;
}
}
}
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, newOrUnknownUids, synchronizer, cancellationToken).ConfigureAwait(false);
folder.HighestModeSeq = unchecked((long)remoteFolder.HighestModSeq);
@@ -456,11 +503,19 @@ public class UnifiedImapSynchronizer
folder.UidValidity = remoteFolder.UidValidity;
}
private async Task<List<string>> ProcessSummariesAsync(
private Task<List<string>> ProcessSummariesAsync(
IImapSynchronizer synchronizer,
MailItemFolder localFolder,
IList<IMessageSummary> summaries,
CancellationToken cancellationToken)
=> ProcessSummariesCoreAsync(synchronizer, localFolder, summaries, existingByUid: null, cancellationToken);
private async Task<List<string>> ProcessSummariesCoreAsync(
IImapSynchronizer synchronizer,
MailItemFolder localFolder,
IList<IMessageSummary> summaries,
IReadOnlyDictionary<uint, MailCopy> existingByUid,
CancellationToken cancellationToken)
{
var downloadedMessageIds = new List<string>();
@@ -475,10 +530,8 @@ public class UnifiedImapSynchronizer
if (uniqueIds.Count == 0)
return downloadedMessageIds;
var existingMails = await _mailService.GetExistingMailsAsync(localFolder.Id, uniqueIds).ConfigureAwait(false);
var existingByUid = existingMails
.Select(m => (Uid: MailkitClientExtensions.ResolveUidStruct(m.Id), Mail: m))
.ToDictionary(a => a.Uid.Id, a => a.Mail);
existingByUid ??= CreateExistingMailLookup(await _mailService.GetExistingMailsAsync(localFolder.Id, uniqueIds).ConfigureAwait(false));
var pendingStateUpdates = new List<MailCopyStateUpdate>();
foreach (var summary in summaries)
{
@@ -491,7 +544,11 @@ public class UnifiedImapSynchronizer
{
if (summary.Flags != null)
{
await UpdateMailFlagsAsync(existingMail, summary.Flags.Value).ConfigureAwait(false);
var pendingStateUpdate = CreateMailStateUpdate(existingMail, summary.Flags.Value);
if (pendingStateUpdate != null)
{
pendingStateUpdates.Add(pendingStateUpdate);
}
}
continue;
@@ -516,23 +573,79 @@ public class UnifiedImapSynchronizer
}
}
if (pendingStateUpdates.Count > 0)
{
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
}
return downloadedMessageIds;
}
private async Task UpdateMailFlagsAsync(MailCopy mailCopy, MessageFlags flags)
private async Task ApplySummaryFlagUpdatesAsync(
IReadOnlyDictionary<uint, MailCopy> existingByUid,
IList<IMessageSummary> summaries)
{
if (existingByUid == null || existingByUid.Count == 0 || summaries == null || summaries.Count == 0)
return;
var pendingStateUpdates = new List<MailCopyStateUpdate>();
foreach (var summary in summaries)
{
if (summary.UniqueId == UniqueId.Invalid || summary.Flags == null)
continue;
if (!existingByUid.TryGetValue(summary.UniqueId.Id, out var existingMail))
continue;
var pendingStateUpdate = CreateMailStateUpdate(existingMail, summary.Flags.Value);
if (pendingStateUpdate != null)
{
pendingStateUpdates.Add(pendingStateUpdate);
}
}
if (pendingStateUpdates.Count > 0)
{
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
}
}
private static IReadOnlyDictionary<uint, MailCopy> CreateExistingMailLookup(IEnumerable<MailCopy> existingMails)
{
var lookup = new Dictionary<uint, MailCopy>();
foreach (var mail in existingMails ?? [])
{
if (mail == null || string.IsNullOrEmpty(mail.Id))
continue;
try
{
lookup[MailkitClientExtensions.ResolveUidStruct(mail.Id).Id] = mail;
}
catch (ArgumentOutOfRangeException)
{
}
}
return lookup;
}
private static MailCopyStateUpdate CreateMailStateUpdate(MailCopy mailCopy, MessageFlags flags)
{
var isFlagged = MailkitClientExtensions.GetIsFlagged(flags);
var isRead = MailkitClientExtensions.GetIsRead(flags);
if (isFlagged != mailCopy.IsFlagged)
{
await _mailService.ChangeFlagStatusAsync(mailCopy.Id, isFlagged).ConfigureAwait(false);
}
bool shouldUpdateFlagged = isFlagged != mailCopy.IsFlagged;
bool shouldUpdateRead = isRead != mailCopy.IsRead;
if (isRead != mailCopy.IsRead)
{
await _mailService.ChangeReadStatusAsync(mailCopy.Id, isRead).ConfigureAwait(false);
}
return !shouldUpdateFlagged && !shouldUpdateRead
? null
: new MailCopyStateUpdate(
mailCopy.Id,
shouldUpdateRead ? isRead : null,
shouldUpdateFlagged ? isFlagged : null);
}
private async Task ApplyDeletedUidsAsync(MailItemFolder folder, IList<UniqueId> uniqueIds)
@@ -552,15 +665,14 @@ public class UnifiedImapSynchronizer
if (changedFlags == null || changedFlags.Count == 0)
return;
foreach (var changed in changedFlags)
{
var localMailCopyId = MailkitClientExtensions.CreateUid(folder.Id, changed.Key);
var isFlagged = MailkitClientExtensions.GetIsFlagged(changed.Value);
var isRead = MailkitClientExtensions.GetIsRead(changed.Value);
var stateUpdates = changedFlags
.Select(changed => new MailCopyStateUpdate(
MailkitClientExtensions.CreateUid(folder.Id, changed.Key),
MailkitClientExtensions.GetIsRead(changed.Value),
MailkitClientExtensions.GetIsFlagged(changed.Value)))
.ToList();
await _mailService.ChangeReadStatusAsync(localMailCopyId, isRead).ConfigureAwait(false);
await _mailService.ChangeFlagStatusAsync(localMailCopyId, isFlagged).ConfigureAwait(false);
}
await _mailService.ApplyMailStateUpdatesAsync(stateUpdates).ConfigureAwait(false);
}
private async Task ReconcileUidBasedFlagChangesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken)
@@ -613,13 +725,14 @@ public class UnifiedImapSynchronizer
var existingMarkReadCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, markReadCandidates, cancellationToken).ConfigureAwait(false);
var existingUnflagCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, unflagCandidates, cancellationToken).ConfigureAwait(false);
var pendingStateUpdates = new List<MailCopyStateUpdate>();
foreach (var uid in existingMarkReadCandidates)
{
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsRead)
continue;
await _mailService.ChangeReadStatusAsync(localMail.Id, true).ConfigureAwait(false);
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsRead: true));
}
foreach (var uid in remoteUnreadUids)
@@ -627,7 +740,7 @@ public class UnifiedImapSynchronizer
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsRead)
continue;
await _mailService.ChangeReadStatusAsync(localMail.Id, false).ConfigureAwait(false);
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsRead: false));
}
foreach (var uid in existingUnflagCandidates)
@@ -635,7 +748,7 @@ public class UnifiedImapSynchronizer
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsFlagged)
continue;
await _mailService.ChangeFlagStatusAsync(localMail.Id, false).ConfigureAwait(false);
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsFlagged: false));
}
foreach (var uid in remoteFlaggedUids)
@@ -643,7 +756,12 @@ public class UnifiedImapSynchronizer
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsFlagged)
continue;
await _mailService.ChangeFlagStatusAsync(localMail.Id, true).ConfigureAwait(false);
pendingStateUpdates.Add(new MailCopyStateUpdate(localMail.Id, IsFlagged: true));
}
if (pendingStateUpdates.Count > 0)
{
await _mailService.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
}
}
+77 -35
View File
@@ -112,56 +112,104 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
public override List<IRequestBundle<ImapRequest>> Move(BatchMoveRequest requests)
{
return CreateTaskBundle(async (client, item) =>
{
var sourceFolder = await client.GetFolderAsync(item.FromFolder.RemoteFolderId);
var destinationFolder = await client.GetFolderAsync(item.ToFolder.RemoteFolderId);
if (requests == null || requests.Count == 0)
return [];
return CreateSingleTaskBundle(async (client, _) =>
{
var sourceFolder = await client.GetFolderAsync(requests[0].FromFolder.RemoteFolderId).ConfigureAwait(false);
var destinationFolder = await client.GetFolderAsync(requests[0].ToFolder.RemoteFolderId).ConfigureAwait(false);
var uniqueIds = requests.Select(item => GetUniqueId(item.Item.Id)).ToList();
// Only opening source folder is enough.
await sourceFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
await sourceFolder.MoveToAsync(GetUniqueId(item.Item.Id), destinationFolder).ConfigureAwait(false);
await sourceFolder.CloseAsync().ConfigureAwait(false);
}, requests);
try
{
await sourceFolder.MoveToAsync(uniqueIds, destinationFolder).ConfigureAwait(false);
}
finally
{
await sourceFolder.CloseAsync().ConfigureAwait(false);
}
}, requests[0], requests);
}
public override List<IRequestBundle<ImapRequest>> ChangeFlag(BatchChangeFlagRequest requests)
{
return CreateTaskBundle(async (client, item) =>
if (requests == null || requests.Count == 0)
return [];
return CreateSingleTaskBundle(async (client, _) =>
{
var folder = item.Item.AssignedFolder;
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
var folder = requests[0].Item.AssignedFolder;
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
var uniqueIds = requests.Select(item => GetUniqueId(item.Item.Id)).ToList();
var request = new StoreFlagsRequest(requests[0].IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged)
{
Silent = true
};
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
await remoteFolder.StoreAsync(GetUniqueId(item.Item.Id), new StoreFlagsRequest(item.IsFlagged ? StoreAction.Add : StoreAction.Remove, MessageFlags.Flagged) { Silent = true }).ConfigureAwait(false);
await remoteFolder.CloseAsync().ConfigureAwait(false);
}, requests);
try
{
await remoteFolder.StoreAsync(uniqueIds, request).ConfigureAwait(false);
}
finally
{
await remoteFolder.CloseAsync().ConfigureAwait(false);
}
}, requests[0], requests);
}
public override List<IRequestBundle<ImapRequest>> Delete(BatchDeleteRequest requests)
{
return CreateTaskBundle(async (client, request) =>
if (requests == null || requests.Count == 0)
return [];
return CreateSingleTaskBundle(async (client, _) =>
{
var folder = request.Item.AssignedFolder;
var folder = requests[0].Item.AssignedFolder;
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
var uniqueIds = requests.Select(request => GetUniqueId(request.Item.Id)).ToList();
var storeRequest = new StoreFlagsRequest(StoreAction.Add, MessageFlags.Deleted) { Silent = true };
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
await remoteFolder.AddFlagsAsync(GetUniqueId(request.Item.Id), MessageFlags.Deleted, true);
await remoteFolder.ExpungeAsync().ConfigureAwait(false);
await remoteFolder.CloseAsync().ConfigureAwait(false);
}, requests);
try
{
await remoteFolder.StoreAsync(uniqueIds, storeRequest).ConfigureAwait(false);
await remoteFolder.ExpungeAsync(uniqueIds).ConfigureAwait(false);
}
finally
{
await remoteFolder.CloseAsync().ConfigureAwait(false);
}
}, requests[0], requests);
}
public override List<IRequestBundle<ImapRequest>> MarkRead(BatchMarkReadRequest requests)
{
return CreateTaskBundle(async (client, request) =>
if (requests == null || requests.Count == 0)
return [];
return CreateSingleTaskBundle(async (client, _) =>
{
var folder = request.Item.AssignedFolder;
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId);
var folder = requests[0].Item.AssignedFolder;
var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
var uniqueIds = requests.Select(request => GetUniqueId(request.Item.Id)).ToList();
var storeRequest = new StoreFlagsRequest(requests[0].IsRead ? StoreAction.Add : StoreAction.Remove, MessageFlags.Seen)
{
Silent = true
};
await remoteFolder.OpenAsync(FolderAccess.ReadWrite).ConfigureAwait(false);
await remoteFolder.StoreAsync(GetUniqueId(request.Item.Id), new StoreFlagsRequest(request.IsRead ? StoreAction.Add : StoreAction.Remove, MessageFlags.Seen) { Silent = true }).ConfigureAwait(false);
await remoteFolder.CloseAsync().ConfigureAwait(false);
}, requests);
try
{
await remoteFolder.StoreAsync(uniqueIds, storeRequest).ConfigureAwait(false);
}
finally
{
await remoteFolder.CloseAsync().ConfigureAwait(false);
}
}, requests[0], requests);
}
public override List<IRequestBundle<ImapRequest>> CreateDraft(CreateDraftRequest request)
@@ -718,13 +766,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
// First apply the UI changes for each bundle.
// This is important to reflect changes to the UI before the network call is done.
foreach (var item in batchedRequests)
{
if (ShouldApplyOptimisticUIChanges(item.Request))
{
item.Request.ApplyUIChanges();
}
}
ApplyOptimisticUiChanges(batchedRequests, ShouldApplyOptimisticUIChanges);
// All task bundles will execute on the same client.
// Tasks themselves don't pull the client from the pool
@@ -754,7 +796,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
if (ShouldApplyOptimisticUIChanges(item.Request))
{
item.Request.RevertUIChanges();
item.UIChangeRequest?.RevertUIChanges();
}
isCrashed = true;
@@ -795,7 +837,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
if (ShouldApplyOptimisticUIChanges(item.Request))
{
item.Request.RevertUIChanges();
item.UIChangeRequest?.RevertUIChanges();
}
throw;
}
@@ -2020,10 +2020,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
{
// First apply all UI changes immediately before any batching.
// This ensures UI reflects changes right away, regardless of batch processing.
foreach (var bundle in batchedRequests)
{
bundle.UIChangeRequest?.ApplyUIChanges();
}
ApplyOptimisticUiChanges(batchedRequests);
// SendDraft requests may include large attachments, which require upload sessions.
// Upload these attachments before the batched patch/send sequence.
+7 -7
View File
@@ -161,11 +161,11 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
foreach (var group in keys)
{
var key = group.Key;
var firstRequest = group.FirstOrDefault();
if (key is MailSynchronizerOperation mailSynchronizerOperation)
if (firstRequest is IMailActionRequest mailActionRequest)
{
switch (mailSynchronizerOperation)
switch (mailActionRequest.Operation)
{
case MailSynchronizerOperation.MarkRead:
nativeRequests.AddRange(MarkRead(new BatchMarkReadRequest(group.Cast<MarkReadRequest>())));
@@ -204,9 +204,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
break;
}
}
else if (key is FolderSynchronizerOperation folderSynchronizerOperation)
else if (firstRequest is IFolderActionRequest folderActionRequest)
{
switch (folderSynchronizerOperation)
switch (folderActionRequest.Operation)
{
case FolderSynchronizerOperation.RenameFolder:
nativeRequests.AddRange(RenameFolder(group.ElementAt(0) as RenameFolderRequest));
@@ -230,9 +230,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
break;
}
}
else if (key is CategorySynchronizerOperation categorySynchronizerOperation)
else if (firstRequest is ICategoryActionRequest categoryActionRequest)
{
switch (categorySynchronizerOperation)
switch (categoryActionRequest.Operation)
{
case CategorySynchronizerOperation.CreateCategory:
nativeRequests.AddRange(CreateCategory(group.ElementAt(0) as MailCategoryCreateRequest));
@@ -17,6 +17,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Misc;
using Wino.Core.Services;
using Wino.Core.ViewModels.Data;
@@ -96,8 +97,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
[ObservableProperty]
private bool isTaskbarBadgeEnabled;
[ObservableProperty]
public partial AccountCapabilityOption SelectedCapabilityOption { get; set; }
public bool IsFocusedInboxSupportedForAccount => Account != null && Account.Preferences.IsFocusedInboxEnabled != null;
public bool IsImapServer => ServerInformation != null;
public bool HasMailAccess => Account?.IsMailAccessGranted == true;
public bool HasCalendarAccess => Account?.IsCalendarAccessGranted == true;
public bool IsOAuthCapabilityEditable => Account?.ProviderType is MailProviderType.Outlook or MailProviderType.Gmail;
public string ProviderIconPath => Account?.SpecialImapProvider != SpecialImapProvider.None
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
@@ -130,6 +137,13 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
new ImapConnectionSecurityModel(Core.Domain.Enums.ImapConnectionSecurity.None, Translator.ImapConnectionSecurity_None)
];
public List<AccountCapabilityOption> CapabilityOptions { get; } =
[
new(true, false, Translator.AccountCapability_MailOnly),
new(false, true, Translator.AccountCapability_CalendarOnly),
new(true, true, Translator.AccountCapability_MailAndCalendar)
];
public AccountDetailsPageViewModel(IMailDialogService dialogService,
IAccountService accountService,
@@ -262,6 +276,7 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
AccountName = Account.Name;
SenderName = Account.SenderName;
ServerInformation = Account.ServerInformation;
SelectedCapabilityOption = ResolveCapabilityOption(Account.IsMailAccessGranted, Account.IsCalendarAccessGranted);
IsFocusedInboxEnabled = Account.Preferences.IsFocusedInboxEnabled.GetValueOrDefault();
AreNotificationsEnabled = Account.Preferences.IsNotificationsEnabled;
@@ -288,7 +303,11 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
SelectedOutgoingServerConnectionSecurityIndex = AvailableConnectionSecurities.FindIndex(a => a.ImapConnectionSecurity == ServerInformation.OutgoingServerSocketOption);
}
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar ? 2 : 1;
SelectedTabIndex = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar && HasCalendarAccess
? 2
: HasMailAccess
? 1
: 0;
var folderStructures = (await _folderService.GetFolderStructureForAccountAsync(Account.Id, true)).Folders;
@@ -382,11 +401,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
partial void OnAccountChanged(MailAccount value)
{
SelectedCapabilityOption = ResolveCapabilityOption(value?.IsMailAccessGranted == true, value?.IsCalendarAccessGranted == true);
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
OnPropertyChanged(nameof(ProviderIconPath));
OnPropertyChanged(nameof(Address));
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
OnPropertyChanged(nameof(InitialSynchronizationSummary));
OnPropertyChanged(nameof(HasMailAccess));
OnPropertyChanged(nameof(HasCalendarAccess));
OnPropertyChanged(nameof(IsOAuthCapabilityEditable));
}
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
@@ -417,6 +440,21 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
Account.Preferences.IsTaskbarBadgeEnabled = IsTaskbarBadgeEnabled;
await _accountService.UpdateAccountAsync(Account);
break;
case nameof(SelectedCapabilityOption) when IsOAuthCapabilityEditable && SelectedCapabilityOption != null:
if (Account.IsMailAccessGranted == SelectedCapabilityOption.IsMailAccessGranted &&
Account.IsCalendarAccessGranted == SelectedCapabilityOption.IsCalendarAccessGranted)
break;
try
{
await UpdateOAuthCapabilityAsync(SelectedCapabilityOption);
}
catch (Exception ex)
{
await ExecuteUIThread(() => SelectedCapabilityOption = ResolveCapabilityOption(Account.IsMailAccessGranted, Account.IsCalendarAccessGranted));
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, ex.Message, InfoBarMessageType.Error);
}
break;
case nameof(SelectedPrimaryCalendar) when SelectedPrimaryCalendar != null:
foreach (var calendar in AccountCalendars)
{
@@ -427,6 +465,111 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
break;
}
}
private AccountCapabilityOption ResolveCapabilityOption(bool isMailAccessGranted, bool isCalendarAccessGranted)
=> CapabilityOptions.First(option =>
option.IsMailAccessGranted == isMailAccessGranted &&
option.IsCalendarAccessGranted == isCalendarAccessGranted);
private async Task UpdateOAuthCapabilityAsync(AccountCapabilityOption selectedOption)
{
var previousMailAccess = Account.IsMailAccessGranted;
var previousCalendarAccess = Account.IsCalendarAccessGranted;
var requiresReauthorization = (selectedOption.IsMailAccessGranted && !previousMailAccess) ||
(selectedOption.IsCalendarAccessGranted && !previousCalendarAccess);
try
{
if (requiresReauthorization)
{
Account.IsMailAccessGranted = selectedOption.IsMailAccessGranted;
Account.IsCalendarAccessGranted = selectedOption.IsCalendarAccessGranted;
await SynchronizationManager.Instance.HandleAuthorizationAsync(
Account.ProviderType,
Account,
Account.ProviderType == MailProviderType.Gmail);
}
}
catch
{
Account.IsMailAccessGranted = previousMailAccess;
Account.IsCalendarAccessGranted = previousCalendarAccess;
throw;
}
Account.IsMailAccessGranted = selectedOption.IsMailAccessGranted;
Account.IsCalendarAccessGranted = selectedOption.IsCalendarAccessGranted;
await _accountService.UpdateAccountAsync(Account);
if (selectedOption.IsMailAccessGranted && !previousMailAccess)
{
await SynchronizationManager.Instance.SynchronizeProfileAsync(Account.Id);
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.FullFolders
});
if (Account.ProviderType == MailProviderType.Outlook)
{
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.Categories
});
}
if (!string.IsNullOrWhiteSpace(Account.Address))
{
var aliases = await _accountService.GetAccountAliasesAsync(Account.Id);
var hasRootAlias = aliases.Any(alias => alias.IsRootAlias);
if (!hasRootAlias)
{
await _accountService.CreateRootAliasAsync(Account.Id, Account.Address);
}
}
await SynchronizationManager.Instance.SynchronizeMailAsync(new MailSynchronizationOptions
{
AccountId = Account.Id,
Type = MailSynchronizationType.Alias
});
}
if (selectedOption.IsCalendarAccessGranted && !previousCalendarAccess)
{
await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
{
AccountId = Account.Id,
Type = CalendarSynchronizationType.CalendarMetadata
});
}
var refreshedAccount = await _accountService.GetAccountAsync(Account.Id);
await ExecuteUIThread(() =>
{
Account = refreshedAccount;
AccountName = refreshedAccount.Name;
SenderName = refreshedAccount.SenderName;
EnsureSelectedTabForCapabilities();
});
}
private void EnsureSelectedTabForCapabilities()
{
if (SelectedTabIndex == 1 && !HasMailAccess)
{
SelectedTabIndex = HasCalendarAccess ? 2 : 0;
}
else if (SelectedTabIndex == 2 && !HasCalendarAccess)
{
SelectedTabIndex = HasMailAccess ? 1 : 0;
}
}
}
public sealed class AccountCalendarShowAsOption
@@ -441,6 +584,20 @@ public sealed class AccountCalendarShowAsOption
}
}
public sealed class AccountCapabilityOption
{
public bool IsMailAccessGranted { get; }
public bool IsCalendarAccessGranted { get; }
public string DisplayText { get; }
public AccountCapabilityOption(bool isMailAccessGranted, bool isCalendarAccessGranted, string displayText)
{
IsMailAccessGranted = isMailAccessGranted;
IsCalendarAccessGranted = isCalendarAccessGranted;
DisplayText = displayText;
}
}
public partial class AccountCalendarSettingsItemViewModel : ObservableObject
{
public AccountCalendar Calendar { get; }
@@ -103,7 +103,10 @@ public partial class AccountManagementViewModel : AccountManagementPageViewModel
return;
}
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step2Title,
WinoPage.ProviderSelectionPage,
ProviderSelectionNavigationContext.CreateForSettingsAddAccount()));
}
public Task StartAddNewAccountAsync() => AddNewAccountAsync();
@@ -71,6 +71,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
private void BuildSteps()
{
Steps.Clear();
var shouldSetupMail = WizardContext.IsMailAccessEnabled;
var shouldSetupCalendar = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.IsOAuthProvider)
{
@@ -78,31 +80,47 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
{
Title = string.Format(Translator.AccountSetup_Step_Authenticating, WizardContext.SelectedProvider.Name)
});
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingProfile });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (WizardContext.SelectedProvider.Type == MailProviderType.Outlook)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingCategories });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingAliases });
if (shouldSetupCalendar)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_Finalizing });
}
else if (WizardContext.IsSpecialImapProvider)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingMailAuth });
}
if (WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
if (shouldSetupCalendar && WizardContext.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_DiscoveringCalDav });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_TestingCalendarAuth });
}
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
}
if (WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
if (shouldSetupCalendar && WizardContext.CalendarSupportMode != ImapCalendarSupportMode.Disabled)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_FetchingCalendarMetadata });
}
@@ -112,7 +130,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
else // Generic IMAP
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SavingAccount });
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
if (shouldSetupMail)
{
Steps.Add(new AccountSetupStepModel { Title = Translator.AccountSetup_Step_SyncingFolders });
}
var setupResult = WizardContext.ImapCalDavSetupResult;
if (setupResult?.IsCalendarAccessGranted == true &&
@@ -186,7 +207,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
AccountColorHex = WizardContext.AccountColorHex,
CreatedAt = accountCreatedAt,
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
IsCalendarAccessGranted = true
IsMailAccessGranted = WizardContext.IsMailAccessEnabled,
IsCalendarAccessGranted = WizardContext.IsCalendarAccessEnabled
};
if (WizardContext.IsOAuthProvider)
@@ -208,50 +230,53 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Profile
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
if (profileResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
if (profileResult.ProfileInformation != null)
if (_createdAccount.IsMailAccessGranted)
{
_createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
_createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
// Step: Profile
SetStepInProgress(Translator.AccountSetup_Step_FetchingProfile);
var profileResult = await SynchronizationManager.Instance.SynchronizeProfileAsync(_createdAccount.Id);
if (profileResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
if (profileResult.ProfileInformation != null)
{
if (await _accountService.AccountAddressExistsAsync(profileResult.ProfileInformation.AccountAddress, _createdAccount.Id))
throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage);
_createdAccount.SenderName = profileResult.ProfileInformation.SenderName;
_createdAccount.Base64ProfilePictureData = profileResult.ProfileInformation.Base64ProfilePictureData;
_createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
if (!string.IsNullOrEmpty(profileResult.ProfileInformation.AccountAddress))
{
if (await _accountService.AccountAddressExistsAsync(profileResult.ProfileInformation.AccountAddress, _createdAccount.Id))
throw new InvalidOperationException(Translator.DialogMessage_AccountAddressExistsMessage);
_createdAccount.Address = profileResult.ProfileInformation.AccountAddress;
}
await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
}
await _accountService.UpdateProfileInformationAsync(_createdAccount.Id, profileResult.ProfileInformation);
}
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
// Step: Categories
if (_createdAccount.IsCategorySyncSupported)
{
SetStepInProgress(Translator.AccountSetup_Step_SyncingCategories);
var categoryResult = await SynchronizationManager.Instance.SynchronizeCategoriesAsync(_createdAccount.Id);
if (categoryResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
// Step: Categories
if (_createdAccount.IsCategorySyncSupported)
{
SetStepInProgress(Translator.AccountSetup_Step_SyncingCategories);
var categoryResult = await SynchronizationManager.Instance.SynchronizeCategoriesAsync(_createdAccount.Id);
if (categoryResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCategories);
SetCurrentStepSucceeded();
}
}
// Step: Calendar metadata
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
if (_createdAccount.IsCalendarAccessGranted)
{
SetStepInProgress(Translator.AccountSetup_Step_FetchingCalendarMetadata);
var calResult = await SynchronizationManager.Instance.SynchronizeCalendarAsync(new CalendarSynchronizationOptions
{
AccountId = _createdAccount.Id,
@@ -259,22 +284,25 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
});
if (calResult == null || calResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeCalendarMetadata);
SetCurrentStepSucceeded();
}
SetCurrentStepSucceeded();
// Step: Aliases
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
if (_createdAccount.IsAliasSyncSupported)
if (_createdAccount.IsMailAccessGranted)
{
var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
SetStepInProgress(Translator.AccountSetup_Step_SyncingAliases);
if (_createdAccount.IsAliasSyncSupported)
{
var aliasResult = await SynchronizationManager.Instance.SynchronizeAliasesAsync(_createdAccount.Id);
if (aliasResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
}
else
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
SetCurrentStepSucceeded();
}
else
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
SetCurrentStepSucceeded();
}
else if (WizardContext.IsSpecialImapProvider)
{
@@ -288,13 +316,17 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = WizardContext.EmailAddress;
_createdAccount.SenderName = WizardContext.DisplayName;
_createdAccount.IsMailAccessGranted = dialogResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = customServerInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
_createdAccount.ServerInformation = customServerInformation;
// Step: Test IMAP
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
await ValidateImapConnectivityAsync(customServerInformation);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Test IMAP
SetStepInProgress(Translator.AccountSetup_Step_TestingMailAuth);
await ValidateImapConnectivityAsync(customServerInformation);
SetCurrentStepSucceeded();
}
// Step: CalDAV discovery and testing (if applicable)
if (customServerInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
@@ -313,12 +345,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
}
// Step: Calendar metadata (if not disabled)
if (_createdAccount.IsCalendarAccessGranted)
@@ -334,8 +369,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded();
}
// Aliases for IMAP
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
}
else // Generic IMAP
{
@@ -350,6 +387,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_createdAccount.Address = setupResult.EmailAddress;
_createdAccount.SenderName = setupResult.DisplayName;
_createdAccount.IsMailAccessGranted = setupResult.IsMailAccessGranted;
_createdAccount.IsCalendarAccessGranted = setupResult.IsCalendarAccessGranted;
_createdAccount.ServerInformation = customServerInformation;
@@ -359,12 +397,15 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
_dbWritten = true;
SetCurrentStepSucceeded();
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
if (_createdAccount.IsMailAccessGranted)
{
// Step: Folders
SetStepInProgress(Translator.AccountSetup_Step_SyncingFolders);
var folderResult = await SynchronizationManager.Instance.SynchronizeFoldersAsync(_createdAccount.Id);
if (folderResult == null || folderResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
SetCurrentStepSucceeded();
}
// Step: CalDAV (if applicable)
if (setupResult.IsCalendarAccessGranted &&
@@ -392,8 +433,10 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
SetCurrentStepSucceeded();
}
// Aliases for IMAP
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
if (_createdAccount.IsMailAccessGranted)
{
await _accountService.CreateRootAliasAsync(_createdAccount.Id, _createdAccount.Address);
}
}
// Step: Finalizing
@@ -14,6 +14,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.UI;
namespace Wino.Mail.ViewModels.Collections;
@@ -39,6 +40,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
public event EventHandler<MailItemViewModel> MailItemRemoved;
public event EventHandler ItemSelectionChanged;
public Func<string, ThreadMailItemViewModel> ThreadItemFactory { get; set; } = static threadId => new ThreadMailItemViewModel(threadId, true);
private ListItemComparer listComparer = new();
@@ -457,7 +459,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
private async Task CreateNewThreadAsync(ObservableGroup<object, IMailListItem> group, MailItemViewModel item, MailCopy addedItem)
{
var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId);
var threadViewModel = ThreadItemFactory(item.MailCopy.ThreadId);
await ExecuteUIThread(() =>
{
@@ -710,7 +712,7 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
if (batchThreadLookup.TryGetValue(item.MailCopy.ThreadId, out var threadableItems) && threadableItems.Count > 1)
{
// Create a new thread with all matching items - defer UI operations
var threadViewModel = new ThreadMailItemViewModel(item.MailCopy.ThreadId);
var threadViewModel = ThreadItemFactory(item.MailCopy.ThreadId);
// Add emails without UI thread for now
foreach (var threadItem in threadableItems)
@@ -894,6 +896,121 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
return UpdateExistingItemAsync(itemContainer, updatedMailCopy, mailUpdateSource, changedProperties);
});
public Task UpdateMailStateAsync(MailStateChange updatedState, EntityUpdateSource mailUpdateSource)
=> RunSerializedAsync(() =>
{
if (updatedState == null)
return Task.CompletedTask;
var itemContainer = GetMailItemContainer(updatedState.UniqueId);
if (itemContainer?.ItemViewModel == null)
{
return Task.CompletedTask;
}
return UpdateExistingMailStateAsync(itemContainer, updatedState, mailUpdateSource);
});
public Task UpdateMailStatesAsync(IEnumerable<MailStateChange> updatedStates, EntityUpdateSource mailUpdateSource)
=> RunSerializedAsync(() => UpdateMailStatesInternalAsync(updatedStates, mailUpdateSource));
public Task UpdateMailCopiesAsync(IEnumerable<MailCopy> updatedMailCopies, EntityUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties = MailCopyChangeFlags.None)
=> RunSerializedAsync(() => UpdateMailCopiesInternalAsync(updatedMailCopies, mailUpdateSource, changedProperties));
private async Task UpdateExistingMailStateAsync(MailItemContainer itemContainer, MailStateChange updatedState, EntityUpdateSource mailUpdateSource)
{
if (itemContainer?.ItemViewModel == null || updatedState == null)
return;
var existingItem = itemContainer.ItemViewModel;
await ExecuteUIThread(() =>
{
var appliedChanges = existingItem.ApplyStateChanges(updatedState.IsRead, updatedState.IsFlagged);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{
itemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges);
}
});
}
private async Task UpdateMailStatesInternalAsync(IEnumerable<MailStateChange> updatedStates, EntityUpdateSource mailUpdateSource)
{
var updates = updatedStates?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group =>
{
var updatedState = group.Last();
return new
{
UpdatedState = updatedState,
ItemContainer = GetMailItemContainer(updatedState.UniqueId)
};
})
.Where(x => x.ItemContainer?.ItemViewModel != null)
.ToList() ?? [];
if (updates.Count == 0)
return;
await ExecuteUIThread(() =>
{
foreach (var update in updates)
{
var existingItem = update.ItemContainer.ItemViewModel;
var appliedChanges = existingItem.ApplyStateChanges(update.UpdatedState.IsRead, update.UpdatedState.IsFlagged);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (update.ItemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{
update.ItemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges);
}
}
});
}
private async Task UpdateMailCopiesInternalAsync(IEnumerable<MailCopy> updatedMailCopies, EntityUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties)
{
var updates = updatedMailCopies?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group =>
{
var updatedMail = group.First();
return new
{
UpdatedMail = updatedMail,
ItemContainer = GetMailItemContainer(updatedMail.UniqueId)
};
})
.Where(x => x.ItemContainer?.ItemViewModel != null)
.ToList() ?? [];
if (updates.Count == 0)
return;
await ExecuteUIThread(() =>
{
foreach (var update in updates)
{
var updatedMail = update.UpdatedMail;
var itemContainer = update.ItemContainer;
var existingItem = itemContainer.ItemViewModel;
var appliedChanges = existingItem.UpdateFrom(updatedMail, changedProperties);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{
itemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges);
}
}
});
}
public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0);
public MailItemViewModel GetNextItem(MailCopy mailCopy)
@@ -962,7 +1079,32 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
public Task RemoveAsync(MailCopy removeItem)
=> RunSerializedAsync(() => RemoveInternalAsync(removeItem));
public Task RemoveRangeAsync(IEnumerable<MailCopy> removeItems)
=> RunSerializedAsync(() => RemoveRangeInternalAsync(removeItems));
private async Task RemoveInternalAsync(MailCopy removeItem)
=> await RemoveInternalAsync(removeItem, notifySelectionChanges: true);
private async Task RemoveRangeInternalAsync(IEnumerable<MailCopy> removeItems)
{
var distinctItems = removeItems?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group => group.First())
.ToList() ?? [];
if (distinctItems.Count == 0)
return;
foreach (var removeItem in distinctItems)
{
await RemoveInternalAsync(removeItem, notifySelectionChanges: false);
}
await NotifySelectionChangesAsync();
}
private async Task RemoveInternalAsync(MailCopy removeItem, bool notifySelectionChanges)
{
var itemContainer = GetMailItemContainer(removeItem.UniqueId);
@@ -1029,7 +1171,10 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
}
}
await NotifySelectionChangesAsync();
if (notifySelectionChanges)
{
await NotifySelectionChangesAsync();
}
}
private IEnumerable<IMailListItem> AllItemsIncludingThreads
@@ -1206,4 +1351,5 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
ItemSelectionChanged?.Invoke(this, null);
});
}
}
+21 -5
View File
@@ -488,6 +488,12 @@ public partial class ComposePageViewModel : MailBaseViewModel,
[RelayCommand(CanExecute = nameof(canSendMail))]
private async Task DiscardAsync()
=> await DiscardDraftAsync();
public Task SaveDraftAsync()
=> UpdateMimeChangesAsync();
public async Task DiscardDraftAsync(bool requireConfirmation = true)
{
if (ComposingAccount == null)
{
@@ -495,14 +501,19 @@ public partial class ComposePageViewModel : MailBaseViewModel,
return;
}
var confirmation = await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DiscardDraftConfirmationMessage,
Translator.DialogMessage_DiscardDraftConfirmationTitle,
Translator.Buttons_Yes);
var confirmation = !requireConfirmation || await _dialogService.ShowConfirmationDialogAsync(Translator.DialogMessage_DiscardDraftConfirmationMessage,
Translator.DialogMessage_DiscardDraftConfirmationTitle,
Translator.Buttons_Yes);
if (confirmation)
if (!confirmation)
{
isUpdatingMimeBlocked = true;
return;
}
isUpdatingMimeBlocked = true;
try
{
// Don't send delete request for local drafts. Just delete the record and mime locally.
if (CurrentMailDraftItem.MailCopy.IsLocalDraft)
{
@@ -514,6 +525,11 @@ public partial class ComposePageViewModel : MailBaseViewModel,
await _worker.ExecuteAsync(deletePackage).ConfigureAwait(false);
}
}
catch
{
isUpdatingMimeBlocked = false;
throw;
}
}
//public override void OnNavigatedFrom(NavigationMode mode, object parameters)
@@ -10,7 +10,8 @@ public enum ImapCalDavSettingsPageMode
{
Create,
Edit,
Wizard
Wizard,
AddAccount
}
public sealed class ImapCalDavSettingsNavigationContext
@@ -45,6 +46,14 @@ public sealed class ImapCalDavSettingsNavigationContext
AccountCreationDialogResult = accountCreationDialogResult
};
public static ImapCalDavSettingsNavigationContext CreateForAddAccountMode(
AccountCreationDialogResult accountCreationDialogResult)
=> new()
{
Mode = ImapCalDavSettingsPageMode.AddAccount,
AccountCreationDialogResult = accountCreationDialogResult
};
public bool IsWizardMode => Mode == ImapCalDavSettingsPageMode.Wizard;
}
@@ -52,6 +61,7 @@ public sealed class ImapCalDavSetupResult
{
public string DisplayName { get; init; }
public string EmailAddress { get; init; }
public bool IsMailAccessGranted { get; init; }
public bool IsCalendarAccessGranted { get; init; }
public CustomServerInformation ServerInformation { get; init; }
}
@@ -320,6 +320,30 @@ public partial class MailItemViewModel(MailCopy mailCopy) : ObservableRecipient,
return changedFlags;
}
public MailCopyChangeFlags ApplyStateChanges(bool? isRead = null, bool? isFlagged = null)
{
var changedFlags = MailCopyChangeFlags.None;
if (isRead.HasValue && MailCopy.IsRead != isRead.Value)
{
MailCopy.IsRead = isRead.Value;
changedFlags |= MailCopyChangeFlags.IsRead;
}
if (isFlagged.HasValue && MailCopy.IsFlagged != isFlagged.Value)
{
MailCopy.IsFlagged = isFlagged.Value;
changedFlags |= MailCopyChangeFlags.IsFlagged;
}
if (changedFlags != MailCopyChangeFlags.None)
{
RaisePropertyChanges(changedFlags);
}
return changedFlags;
}
private static MailCopyChangeFlags SetIfChanged<T>(T currentValue, T newValue, Action<T> setter, MailCopyChangeFlags flag)
{
if (EqualityComparer<T>.Default.Equals(currentValue, newValue))
@@ -17,8 +17,9 @@ namespace Wino.Mail.ViewModels.Data;
public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListItem, IMailItemDisplayInformation
{
private readonly string _threadId;
private readonly bool _isNewestEmailFirst;
private readonly HashSet<Guid> _uniqueIdSet = [];
private MailItemViewModel _cachedLatestMailViewModel;
private MailItemViewModel _cachedNewestMailViewModel;
private int _suspendChildPropertyNotificationsCount;
[ObservableProperty]
@@ -53,27 +54,27 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
/// <summary>
/// Gets the latest email's subject for display
/// </summary>
public string Subject => latestMailViewModel?.MailCopy?.Subject;
public string Subject => newestMailViewModel?.MailCopy?.Subject;
/// <summary>
/// Gets the latest email's sender name for display
/// </summary>
public string FromName => latestMailViewModel?.MailCopy?.FromName ?? Translator.UnknownSender;
public string FromName => newestMailViewModel?.MailCopy?.FromName ?? Translator.UnknownSender;
/// <summary>
/// Gets the latest email's creation date for sorting
/// </summary>
public DateTime CreationDate => latestMailViewModel?.MailCopy?.CreationDate ?? DateTime.MinValue;
public DateTime CreationDate => newestMailViewModel?.MailCopy?.CreationDate ?? DateTime.MinValue;
/// <summary>
/// Gets the latest email's sender address for display
/// </summary>
public string FromAddress => latestMailViewModel?.FromAddress ?? string.Empty;
public string FromAddress => newestMailViewModel?.FromAddress ?? string.Empty;
/// <summary>
/// Gets the preview text from the latest email
/// </summary>
public string PreviewText => latestMailViewModel?.PreviewText ?? string.Empty;
public string PreviewText => newestMailViewModel?.PreviewText ?? string.Empty;
/// <summary>
/// Gets whether any email in this thread has attachments
@@ -93,18 +94,18 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
/// <summary>
/// Gets whether the latest email is focused
/// </summary>
public bool IsFocused => latestMailViewModel?.IsFocused ?? false;
public bool IsFocused => newestMailViewModel?.IsFocused ?? false;
/// <summary>
/// Gets whether all emails in this thread are read
/// </summary>
public bool IsRead => ThreadEmails.All(e => e.IsRead);
public bool HasReadReceiptTracking => latestMailViewModel?.HasReadReceiptTracking ?? false;
public bool HasReadReceiptTracking => newestMailViewModel?.HasReadReceiptTracking ?? false;
public bool IsReadReceiptAcknowledged => latestMailViewModel?.IsReadReceiptAcknowledged ?? false;
public bool IsReadReceiptAcknowledged => newestMailViewModel?.IsReadReceiptAcknowledged ?? false;
public string ReadReceiptDisplayText => latestMailViewModel?.ReadReceiptDisplayText ?? string.Empty;
public string ReadReceiptDisplayText => newestMailViewModel?.ReadReceiptDisplayText ?? string.Empty;
/// <summary>
/// Gets whether any email in this thread is a draft
@@ -114,58 +115,58 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
/// <summary>
/// Gets the draft ID from the latest email if it's a draft
/// </summary>
public string DraftId => latestMailViewModel?.DraftId ?? string.Empty;
public string DraftId => newestMailViewModel?.DraftId ?? string.Empty;
/// <summary>
/// Gets the ID from the latest email
/// </summary>
public string Id => latestMailViewModel?.Id ?? string.Empty;
public string Id => newestMailViewModel?.Id ?? string.Empty;
/// <summary>
/// Gets the importance of the latest email
/// </summary>
public MailImportance Importance => latestMailViewModel?.Importance ?? MailImportance.Normal;
public MailImportance Importance => newestMailViewModel?.Importance ?? MailImportance.Normal;
/// <summary>
/// Gets the thread ID from the latest email
/// </summary>
public string ThreadId => latestMailViewModel?.ThreadId ?? _threadId;
public string ThreadId => newestMailViewModel?.ThreadId ?? _threadId;
/// <summary>
/// Gets the message ID from the latest email
/// </summary>
public string MessageId => latestMailViewModel?.MessageId ?? string.Empty;
public string MessageId => newestMailViewModel?.MessageId ?? string.Empty;
/// <summary>
/// Gets the references from the latest email
/// </summary>
public string References => latestMailViewModel?.References ?? string.Empty;
public string References => newestMailViewModel?.References ?? string.Empty;
/// <summary>
/// Gets the in-reply-to from the latest email
/// </summary>
public string InReplyTo => latestMailViewModel?.InReplyTo ?? string.Empty;
public string InReplyTo => newestMailViewModel?.InReplyTo ?? string.Empty;
/// <summary>
/// Gets the file ID from the latest email
/// </summary>
public Guid FileId => latestMailViewModel?.FileId ?? Guid.Empty;
public Guid FileId => newestMailViewModel?.FileId ?? Guid.Empty;
/// <summary>
/// Gets the folder ID from the latest email
/// </summary>
public Guid FolderId => latestMailViewModel?.FolderId ?? Guid.Empty;
public Guid FolderId => newestMailViewModel?.FolderId ?? Guid.Empty;
/// <summary>
/// Gets the unique ID from the latest email
/// </summary>
public Guid UniqueId => latestMailViewModel?.UniqueId ?? Guid.Empty;
public Guid UniqueId => newestMailViewModel?.UniqueId ?? Guid.Empty;
public Guid? ContactPictureFileId => latestMailViewModel?.MailCopy?.SenderContact?.ContactPictureFileId;
public Guid? ContactPictureFileId => newestMailViewModel?.MailCopy?.SenderContact?.ContactPictureFileId;
public bool ThumbnailUpdatedEvent => latestMailViewModel?.ThumbnailUpdatedEvent ?? false;
public bool ThumbnailUpdatedEvent => newestMailViewModel?.ThumbnailUpdatedEvent ?? false;
public AccountContact SenderContact => latestMailViewModel?.MailCopy?.SenderContact;
public AccountContact SenderContact => newestMailViewModel?.MailCopy?.SenderContact;
/// <summary>
/// Gets all emails in this thread (observable)
@@ -201,15 +202,16 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
[NotifyPropertyChangedFor(nameof(SenderContact))]
public partial ObservableCollection<MailItemViewModel> ThreadEmails { get; set; } = [];
private MailItemViewModel latestMailViewModel => _cachedLatestMailViewModel;
private MailItemViewModel newestMailViewModel => _cachedNewestMailViewModel;
public DateTime SortingDate => CreationDate;
public string SortingName => FromName;
public ThreadMailItemViewModel(string threadId)
public ThreadMailItemViewModel(string threadId, bool isNewestEmailFirst)
{
_threadId = threadId;
_isNewestEmailFirst = isNewestEmailFirst;
}
internal void SuspendChildPropertyNotifications() => _suspendChildPropertyNotificationsCount++;
@@ -224,11 +226,21 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
private void RefreshLatestMailCache()
{
_cachedLatestMailViewModel = ThreadEmails
_cachedNewestMailViewModel = ThreadEmails
.OrderByDescending(static item => item.MailCopy.CreationDate)
.FirstOrDefault();
}
public MailItemViewModel GetDefaultSelectedThreadEmail()
{
if (ThreadEmails.Count == 0)
{
return null;
}
return _isNewestEmailFirst ? ThreadEmails.FirstOrDefault() : ThreadEmails.LastOrDefault();
}
/// <summary>
/// Adds an email to this thread
/// </summary>
@@ -237,11 +249,15 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
if (email.MailCopy.ThreadId != _threadId)
throw new ArgumentException($"Email ThreadId '{email.MailCopy.ThreadId}' does not match expander ThreadId '{_threadId}'");
// Insert email in sorted order by CreationDate (newest first, oldest last)
// Insert email in sorted order by CreationDate based on the configured thread direction.
var insertIndex = 0;
for (int i = 0; i < ThreadEmails.Count; i++)
{
if (ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate)
bool shouldInsertBefore = _isNewestEmailFirst
? ThreadEmails[i].MailCopy.CreationDate < email.MailCopy.CreationDate
: ThreadEmails[i].MailCopy.CreationDate > email.MailCopy.CreationDate;
if (shouldInsertBefore)
{
insertIndex = i;
break;
@@ -298,7 +314,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
if (e.PropertyName == nameof(MailItemViewModel.ThumbnailUpdatedEvent))
{
if (ReferenceEquals(updatedMailItem, latestMailViewModel))
if (ReferenceEquals(updatedMailItem, newestMailViewModel))
{
OnPropertyChanged(nameof(ThumbnailUpdatedEvent));
}
@@ -329,7 +345,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
if (changedFlags == MailCopyChangeFlags.None)
return;
var previousLatest = latestMailViewModel;
var previousLatest = newestMailViewModel;
if (changedFlags == MailCopyChangeFlags.All ||
(changedFlags & MailCopyChangeFlags.CreationDate) != 0 ||
@@ -339,7 +355,7 @@ public partial class ThreadMailItemViewModel : ObservableRecipient, IMailListIte
RefreshLatestMailCache();
}
var currentLatest = latestMailViewModel;
var currentLatest = newestMailViewModel;
var latestChanged = !ReferenceEquals(previousLatest, currentLatest);
var updatesDisplayedLatest = changedFlags == MailCopyChangeFlags.All ||
@@ -21,6 +21,12 @@ public partial class WelcomeWizardContext : ObservableObject
[ObservableProperty]
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
[ObservableProperty]
public partial bool IsMailAccessEnabled { get; set; } = true;
[ObservableProperty]
public partial bool IsCalendarAccessEnabled { get; set; } = true;
// Special IMAP fields (iCloud/Yahoo)
[ObservableProperty]
public partial string DisplayName { get; set; }
@@ -66,7 +72,9 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName,
BuildSpecialImapProviderDetails(),
AccountColorHex,
SelectedInitialSynchronizationRange);
SelectedInitialSynchronizationRange,
IsMailAccessEnabled,
IsCalendarAccessEnabled);
}
public void Reset()
@@ -75,6 +83,8 @@ public partial class WelcomeWizardContext : ObservableObject
AccountName = null;
AccountColorHex = null;
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
IsMailAccessEnabled = true;
IsCalendarAccessEnabled = true;
DisplayName = null;
EmailAddress = null;
AppSpecificPassword = null;
+43 -2
View File
@@ -1,9 +1,50 @@
using Wino.Core.Domain.Interfaces;
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.ViewModels;
namespace Wino.Mail.ViewModels;
public partial class IdlePageViewModel : CoreBaseViewModel
{
public IdlePageViewModel(IMailDialogService dialogService) { }
public const string MailEmptyStateParameter = "mail-empty-state";
private readonly INavigationService _navigationService;
[ObservableProperty]
public partial bool IsMailEmptyStateVisible { get; set; }
public string MailEmptyStateTitle => Translator.MailEmptyState_Title;
public string MailEmptyStateMessage => Translator.MailEmptyState_Message;
public string AddAccountText => Translator.MailEmptyState_AddAccount;
public string ManageAccountsText => Translator.MailEmptyState_ManageAccounts;
public IdlePageViewModel(IMailDialogService dialogService, INavigationService navigationService)
{
_navigationService = navigationService;
}
public override void OnNavigatedTo(NavigationMode mode, object parameters)
{
base.OnNavigatedTo(mode, parameters);
IsMailEmptyStateVisible = string.Equals(parameters as string, MailEmptyStateParameter, StringComparison.Ordinal);
}
[RelayCommand]
private void AddAccount()
=> _navigationService.Navigate(
WinoPage.SettingsPage,
ProviderSelectionNavigationContext.CreateForSettingsAddAccount(),
NavigationReferenceFrame.ShellFrame);
[RelayCommand]
private void ManageAccounts()
=> _navigationService.Navigate(
WinoPage.SettingsPage,
WinoPage.ManageAccountsPage,
NavigationReferenceFrame.ShellFrame);
}
@@ -14,6 +14,7 @@ using Wino.Core.Domain.Models.AutoDiscovery;
using Wino.Core.Domain.Models.Calendar;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Domain.Validation;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
@@ -56,6 +57,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
[ObservableProperty]
private string password = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsMailSettingsVisible))]
[NotifyPropertyChangedFor(nameof(IsMailPasswordInputVisible))]
[NotifyPropertyChangedFor(nameof(IsMailActionsVisible))]
private bool isMailSupportEnabled = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCalendarModeSelectionVisible))]
[NotifyPropertyChangedFor(nameof(IsCalDavSettingsVisible))]
@@ -135,11 +142,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
[NotifyPropertyChangedFor(nameof(IsAdvancedSetupSelected))]
private int selectedSetupTabIndex;
public bool IsCreateMode => _pageMode == ImapCalDavSettingsPageMode.Create;
public bool IsCreateMode => _pageMode is ImapCalDavSettingsPageMode.Create or ImapCalDavSettingsPageMode.AddAccount;
public bool IsEditMode => !IsCreateMode;
public bool HasProviderHint => !string.IsNullOrWhiteSpace(ProviderHint);
public bool IsBasicSetupSelected => SelectedSetupTabIndex == 0;
public bool IsAdvancedSetupSelected => SelectedSetupTabIndex == 1;
public bool IsMailSettingsVisible => IsMailSupportEnabled;
public bool IsMailPasswordInputVisible => IsMailSupportEnabled;
public bool IsMailActionsVisible => IsMailSupportEnabled;
public bool IsCalendarModeSelectionVisible => IsCalendarSupportEnabled;
public bool IsCalDavSettingsVisible => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav;
public bool IsLocalCalendarModeSelected => IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.LocalOnly;
@@ -151,6 +161,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
public string EmailAddressHeaderText => Translator.IMAPSetupDialog_MailAddress;
public string EmailAddressPlaceholderText => Translator.IMAPSetupDialog_MailAddressPlaceholder;
public string PasswordHeaderText => Translator.IMAPSetupDialog_Password;
public string EnableMailSupportText => Translator.ProviderSelection_UseForMail;
public string EnableCalendarSupportText => Translator.ImapCalDavSettingsPage_EnableCalendarSupport;
public string AutoDiscoverButtonText => Translator.ImapCalDavSettingsPage_AutoDiscoverButton;
public string BasicTabText => Translator.ImapCalDavSettingsPage_BasicTab;
@@ -289,7 +300,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
_localOnlyInfoShown = false;
SelectedSetupTabIndex = 0;
if (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard)
if (_pageMode is ImapCalDavSettingsPageMode.Create or ImapCalDavSettingsPageMode.Wizard or ImapCalDavSettingsPageMode.AddAccount)
{
PageTitle = Translator.ImapCalDavSettingsPage_TitleCreate;
ApplyCreateContextDefaults(context.AccountCreationDialogResult);
@@ -341,6 +352,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
try
{
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync().ConfigureAwait(false);
var serverInformation = BuildServerInformation();
@@ -400,6 +412,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
try
{
ValidateCapabilitySelection();
await EnsureImapSettingsPreparedAsync();
var serverInformation = BuildServerInformation();
@@ -412,11 +425,18 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
? _editingAccountId
: (Guid?)null;
if (!await ValidateAccountUniquenessAsync(excludedAccountId).ConfigureAwait(false))
if (!await ValidateAccountUniquenessAsync(excludedAccountId))
return;
await ValidateImapConnectivityAsync(serverInformation);
IsImapValidationSucceeded = true;
if (IsMailSupportEnabled)
{
await ValidateImapConnectivityAsync(serverInformation);
IsImapValidationSucceeded = true;
}
else
{
IsImapValidationSucceeded = false;
}
if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
{
@@ -434,6 +454,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
return;
}
if (_pageMode == ImapCalDavSettingsPageMode.AddAccount)
{
CompleteAddAccountFlow(serverInformation);
return;
}
if (_pageMode == ImapCalDavSettingsPageMode.Create)
{
CompleteCreateFlow(serverInformation);
@@ -464,6 +490,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
}
private void CompleteWizardFlow(CustomServerInformation serverInformation)
=> ContinueAccountCreationFlow(serverInformation);
private void CompleteAddAccountFlow(CustomServerInformation serverInformation)
=> ContinueAccountCreationFlow(serverInformation);
private void ContinueAccountCreationFlow(CustomServerInformation serverInformation)
{
serverInformation.Id = Guid.NewGuid();
serverInformation.AccountId = Guid.Empty;
@@ -472,6 +504,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation
};
@@ -498,6 +531,14 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
}
}
partial void OnIsMailSupportEnabledChanged(bool value)
{
if (!value)
{
IsImapValidationSucceeded = false;
}
}
partial void OnSelectedCalendarSupportModeChanged(ImapCalendarSupportMode value)
{
if (value == ImapCalendarSupportMode.LocalOnly && !_localOnlyInfoShown)
@@ -512,6 +553,31 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
}
}
partial void OnEmailAddressChanged(string oldValue, string newValue)
{
var previousAddress = oldValue?.Trim() ?? string.Empty;
var currentAddress = newValue?.Trim() ?? string.Empty;
ApplyCredentialDefaultsForAddress(previousAddress, currentAddress);
ApplyManualServerDefaultsForAddress(previousAddress, currentAddress);
IsImapValidationSucceeded = false;
IsCalDavValidationSucceeded = false;
}
partial void OnPasswordChanged(string oldValue, string newValue)
{
var previousPassword = oldValue ?? string.Empty;
var currentPassword = newValue ?? string.Empty;
IncomingServerPassword = ReplaceIfEmptyOrMatchingPrevious(IncomingServerPassword, previousPassword, currentPassword);
OutgoingServerPassword = ReplaceIfEmptyOrMatchingPrevious(OutgoingServerPassword, previousPassword, currentPassword);
CalDavPassword = ReplaceIfEmptyOrMatchingPrevious(CalDavPassword, previousPassword, currentPassword);
IsImapValidationSucceeded = false;
IsCalDavValidationSucceeded = false;
}
private async Task InitializeEditModeAsync(Guid accountId)
{
var account = await _accountService.GetAccountAsync(accountId);
@@ -524,6 +590,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
ApplyProviderHint(_editingSpecialImapProvider);
ApplyServerInformation(account.ServerInformation);
IsMailSupportEnabled = account.IsMailAccessGranted;
if (account.ServerInformation != null)
{
@@ -550,8 +617,10 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (!string.IsNullOrWhiteSpace(accountCreationDialogResult?.SpecialImapProviderDetails?.SenderName))
DisplayName = accountCreationDialogResult.SpecialImapProviderDetails.SenderName;
IsCalendarSupportEnabled = true;
SelectedCalendarSupportMode = ImapCalendarSupportMode.CalDav;
IsMailSupportEnabled = accountCreationDialogResult?.IsMailAccessGranted != false;
IsCalendarSupportEnabled = accountCreationDialogResult?.IsCalendarAccessGranted == true;
SelectedCalendarSupportMode = accountCreationDialogResult?.SpecialImapProviderDetails?.CalendarSupportMode
?? (IsCalendarSupportEnabled ? ImapCalendarSupportMode.CalDav : ImapCalendarSupportMode.Disabled);
var specialProvider = accountCreationDialogResult?.SpecialImapProviderDetails?.SpecialImapProvider ?? SpecialImapProvider.None;
_editingSpecialImapProvider = specialProvider;
@@ -618,6 +687,50 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SelectedOutgoingServerAuthenticationMethodIndex = 0;
}
private void ApplyCredentialDefaultsForAddress(string previousAddress, string currentAddress)
{
IncomingServerUsername = ReplaceIfEmptyOrMatchingPrevious(IncomingServerUsername, previousAddress, currentAddress);
OutgoingServerUsername = ReplaceIfEmptyOrMatchingPrevious(OutgoingServerUsername, previousAddress, currentAddress);
CalDavUsername = ReplaceIfEmptyOrMatchingPrevious(CalDavUsername, previousAddress, currentAddress);
}
private void ApplyManualServerDefaultsForAddress(string previousAddress, string currentAddress)
{
if (!MailAccountAddressValidator.TryGetDomain(currentAddress, out var currentDomain) ||
!MailAccountAddressValidator.IsImplicitlyResolvableHost(currentDomain))
{
return;
}
MailAccountAddressValidator.TryGetDomain(previousAddress, out var previousDomain);
IncomingServer = ReplaceIfEmptyOrMatchingPrevious(IncomingServer, previousDomain, currentDomain);
OutgoingServer = ReplaceIfEmptyOrMatchingPrevious(OutgoingServer, previousDomain, currentDomain);
if (string.IsNullOrWhiteSpace(IncomingServerPort))
IncomingServerPort = "993";
if (string.IsNullOrWhiteSpace(OutgoingServerPort))
OutgoingServerPort = "587";
}
private static string ReplaceIfEmptyOrMatchingPrevious(string currentValue, string previousValue, string replacementValue)
{
var normalizedCurrentValue = currentValue?.Trim() ?? string.Empty;
var normalizedPreviousValue = previousValue?.Trim() ?? string.Empty;
var normalizedReplacementValue = replacementValue?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(normalizedReplacementValue))
return currentValue ?? string.Empty;
if (string.IsNullOrWhiteSpace(normalizedCurrentValue))
return normalizedReplacementValue;
return string.Equals(normalizedCurrentValue, normalizedPreviousValue, StringComparison.OrdinalIgnoreCase)
? normalizedReplacementValue
: currentValue ?? string.Empty;
}
private void ApplyServerInformation(CustomServerInformation serverInformation)
{
if (serverInformation == null)
@@ -655,6 +768,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task EnsureImapSettingsPreparedAsync()
{
if (!IsMailSupportEnabled)
return;
if (HasCompleteImapSettings())
return;
@@ -765,6 +881,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
{
DisplayName = DisplayName.Trim(),
EmailAddress = EmailAddress.Trim(),
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled,
ServerInformation = serverInformation
});
@@ -781,7 +898,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private async Task<bool> ValidateAccountUniquenessAsync(Guid? excludedAccountId)
{
var accountName = (_pageMode == ImapCalDavSettingsPageMode.Create || _pageMode == ImapCalDavSettingsPageMode.Wizard)
var accountName = (_pageMode == ImapCalDavSettingsPageMode.Create
|| _pageMode == ImapCalDavSettingsPageMode.Wizard
|| _pageMode == ImapCalDavSettingsPageMode.AddAccount)
? _accountCreationContext?.AccountName
: null;
@@ -807,14 +926,24 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
return true;
}
private static void ValidateCapabilitySelection(bool isMailEnabled, bool isCalendarEnabled)
{
if (!isMailEnabled && !isCalendarEnabled)
throw new InvalidOperationException(Translator.ProviderSelection_CapabilityValidationMessage);
}
private void ValidateCapabilitySelection()
=> ValidateCapabilitySelection(IsMailSupportEnabled, IsCalendarSupportEnabled);
private async Task SaveEditFlowAsync(CustomServerInformation serverInformation)
{
var account = await _accountService.GetAccountAsync(_editingAccountId).ConfigureAwait(false);
var account = await _accountService.GetAccountAsync(_editingAccountId);
if (account == null)
throw new InvalidOperationException(Translator.Exception_NullAssignedAccount);
account.SenderName = DisplayName.Trim();
account.Address = EmailAddress.Trim();
account.IsMailAccessGranted = IsMailSupportEnabled;
account.IsCalendarAccessGranted = serverInformation.CalendarSupportMode != ImapCalendarSupportMode.Disabled;
serverInformation.Id = account.ServerInformation?.Id ?? Guid.NewGuid();
@@ -823,14 +952,17 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
account.ServerInformation = serverInformation;
account.AttentionReason = AccountAttentionReason.None;
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false);
await _accountService.UpdateAccountAsync(account).ConfigureAwait(false);
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation);
await _accountService.UpdateAccountAsync(account);
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
if (account.IsMailAccessGranted)
{
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
}));
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
{
AccountId = account.Id,
Type = MailSynchronizationType.FullFolders
}));
}
if (account.IsCalendarAccessGranted)
{
@@ -916,7 +1048,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
if (string.IsNullOrWhiteSpace(EmailAddress))
throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailRequired);
if (!EmailValidation.EmailValidator.Validate(EmailAddress.Trim()))
if (!MailAccountAddressValidator.IsValid(EmailAddress))
throw new InvalidOperationException(Translator.IMAPAdvancedSetupDialog_ValidationEmailInvalid);
}
@@ -925,6 +1057,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private void ValidateImapSettings(CustomServerInformation serverInformation)
{
if (!IsMailSupportEnabled)
return;
ValidateIdentitySettings();
if (string.IsNullOrWhiteSpace(serverInformation.IncomingServer))
@@ -993,7 +1128,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
private bool TryApplyKnownProviderSettingsIfNeeded(bool requireCompleteImapSettings, bool requireCompleteCalDavSettings)
{
var needsImapSettings = requireCompleteImapSettings && !HasCompleteImapSettings();
var needsImapSettings = IsMailSupportEnabled && requireCompleteImapSettings && !HasCompleteImapSettings();
var needsCalDavSettings = requireCompleteCalDavSettings
&& IsCalendarSupportEnabled
&& SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav
@@ -1032,6 +1167,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
SenderName = DisplayName.Trim(),
ProviderType = MailProviderType.IMAP4,
SpecialImapProvider = _editingSpecialImapProvider,
IsMailAccessGranted = IsMailSupportEnabled,
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
},
new AccountCreationDialogResult(
@@ -1039,7 +1175,9 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
DisplayName.Trim(),
providerDetails,
string.Empty,
_wizardContext.SelectedInitialSynchronizationRange));
_wizardContext.SelectedInitialSynchronizationRange,
IsMailSupportEnabled,
mode != ImapCalendarSupportMode.Disabled));
if (serverInformation == null)
return false;
@@ -1076,7 +1214,8 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(CalDavPassword));
private bool HasCompleteImapSettings()
=> !string.IsNullOrWhiteSpace(IncomingServer)
=> !IsMailSupportEnabled
|| (!string.IsNullOrWhiteSpace(IncomingServer)
&& !string.IsNullOrWhiteSpace(IncomingServerPort)
&& !string.IsNullOrWhiteSpace(IncomingServerUsername)
&& !string.IsNullOrWhiteSpace(IncomingServerPassword)
@@ -1085,7 +1224,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
&& !string.IsNullOrWhiteSpace(OutgoingServerUsername)
&& !string.IsNullOrWhiteSpace(OutgoingServerPassword)
&& IsValidPort(IncomingServerPort)
&& IsValidPort(OutgoingServerPort);
&& IsValidPort(OutgoingServerPort));
private int FindAuthenticationMethodIndex(ImapAuthenticationMethod method)
{
+96 -17
View File
@@ -46,6 +46,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<AccountRemovedMessage>,
IRecipient<AccountUpdatedMessage>
{
private const string MailEmptyStateNavigationParameter = IdlePageViewModel.MailEmptyStateParameter;
#region Menu Items
[ObservableProperty]
@@ -188,7 +190,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// First clear all account menu items.
MenuItems.RemoveRange(MenuItems.Where(a => a is IAccountMenuItem));
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
var accounts = await GetMailEnabledAccountsAsync();
List<Guid> initializedAccountIds = new();
@@ -373,7 +375,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ForceAllAccountSynchronizationsAsync()
{
// Run Inbox synchronization for all accounts on startup.
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync();
foreach (var account in accounts)
{
@@ -401,7 +403,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
if (_launchProtocolService.LaunchParameter is AccountMenuItemExtended accountExtendedMessage)
{
// Find the account that this folder and mail belongs to.
var account = await _mailService.GetMailAccountByUniqueIdAsync(accountExtendedMessage.NavigateMailItem.UniqueId).ConfigureAwait(false);
var account = await _mailService.GetMailAccountByUniqueIdAsync(accountExtendedMessage.NavigateMailItem.UniqueId);
if (account != null && MenuItems.TryGetAccountMenuItem(account.Id, out IAccountMenuItem accountMenuItem))
{
@@ -442,6 +444,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task ProcessLaunchDefaultAsync()
{
if (await NavigateToMailEmptyStateIfNeededAsync())
return;
if (PreferencesService.StartupEntityId == null)
{
NavigateToWelcomeWizard();
@@ -468,8 +473,16 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
else
{
// Fallback to the welcome wizard if startup entity is not found.
NavigateToWelcomeWizard();
var firstMailAccountMenuItem = MenuItems.FirstOrDefault(a => a is IAccountMenuItem) as IAccountMenuItem;
if (firstMailAccountMenuItem != null)
{
firstMailAccountMenuItem.Expand();
await ChangeLoadedAccountAsync(firstMailAccountMenuItem);
}
else
{
NavigateToWelcomeWizard();
}
}
}
}
@@ -947,6 +960,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
operationAccount = selectedFolderMenuItem.ParentAccount;
}
if (operationAccount?.IsMailAccessGranted == false)
{
operationAccount = null;
}
// We couldn't find any account so far.
// If there is only 1 account to use, use it. If not,
// send a message for flyout so user can pick from it.
@@ -956,7 +974,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
// No selected account.
// List all accounts and let user pick one.
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync();
if (!accounts.Any())
{
@@ -1081,7 +1099,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(MailtoProtocolMessageRequested message)
{
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync();
MailAccount targetAccount = null;
@@ -1114,7 +1132,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
return;
var accounts = await _accountService.GetAccountsAsync();
var accounts = await GetMailEnabledAccountsAsync();
if (!accounts.Any())
return;
@@ -1138,7 +1156,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
private async Task RecreateMenuItemsAsync()
{
await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false);
await _menuRefreshSemaphore.WaitAsync();
try
{
await ExecuteUIThread(() =>
@@ -1188,7 +1206,10 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
else
{
await ExecuteUIThread(() => SelectedMenuItem = null);
NavigateToWelcomeWizard();
if (!await NavigateToMailEmptyStateIfNeededAsync())
{
NavigateToWelcomeWizard();
}
}
}
@@ -1199,6 +1220,37 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
NavigationReferenceFrame.ShellFrame,
NavigationTransitionType.None);
private Task<List<MailAccount>> GetMailEnabledAccountsAsync()
=> GetAccountsByCapabilityAsync(requireMail: true);
private async Task<List<MailAccount>> GetAccountsByCapabilityAsync(bool requireMail = false, bool requireCalendar = false)
{
var accounts = await _accountService.GetAccountsAsync();
return accounts
.Where(account => (!requireMail || account.IsMailAccessGranted) &&
(!requireCalendar || account.IsCalendarAccessGranted))
.ToList();
}
private async Task<bool> NavigateToMailEmptyStateIfNeededAsync()
{
var accounts = await _accountService.GetAccountsAsync();
if (!accounts.Any() || accounts.Any(account => account.IsMailAccessGranted))
return false;
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() => SelectedMenuItem = null);
NavigationService.Navigate(
WinoPage.IdlePage,
MailEmptyStateNavigationParameter,
NavigationReferenceFrame.InnerShellFrame,
NavigationTransitionType.None);
return true;
}
private bool IsAccountCurrentlyLoaded(Guid accountId)
{
return latestSelectedAccountMenuItem?.HoldingAccounts?.Any(a => a.Id == accountId) == true;
@@ -1385,7 +1437,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
public async void Receive(AccountRemovedMessage message)
{
var remainingAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
if (!remainingAccounts.Any())
var remainingMailAccounts = remainingAccounts.Where(account => account.IsMailAccessGranted).ToList();
if (!remainingMailAccounts.Any())
{
latestSelectedAccountMenuItem = null;
await ExecuteUIThread(() =>
@@ -1394,6 +1448,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
MenuItems?.Clear();
MenuItems?.Add(CreateMailMenuItem);
});
if (remainingAccounts.Any())
{
await NavigateToMailEmptyStateIfNeededAsync();
}
return;
}
@@ -1413,22 +1473,34 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
await RecreateMenuItemsAsync();
if (!createdAccount.IsMailAccessGranted)
{
await NavigateToMailEmptyStateIfNeededAsync();
}
if (!MenuItems.TryGetAccountMenuItem(createdAccount.Id, out IAccountMenuItem createdMenuItem))
{
Log.Warning("Created account {AccountId} could not be found in menu items after refresh.", createdAccount.Id);
if (!createdAccount.IsMailAccessGranted)
return;
return;
}
await ChangeLoadedAccountAsync(createdMenuItem);
// Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions()
if (createdAccount.IsMailAccessGranted)
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.FullFolders,
};
// Each created account should start a new synchronization automatically.
var options = new MailSynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = MailSynchronizationType.FullFolders,
};
Messenger.Send(new NewMailSynchronizationRequested(options));
Messenger.Send(new NewMailSynchronizationRequested(options));
}
if (createdAccount.IsCalendarAccessGranted)
{
@@ -1455,6 +1527,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
{
var updatedAccount = message.Account;
if (!updatedAccount.IsMailAccessGranted || !MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out _))
{
await RecreateMenuItemsAsync();
await RestoreSelectedAccountAfterMenuRefreshAsync(false);
return;
}
await ExecuteUIThread(() =>
{
if (MenuItems.TryGetAccountMenuItem(updatedAccount.Id, out IAccountMenuItem foundAccountMenuItem))
+54
View File
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.Messaging;
using System.Collections.Generic;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
@@ -10,8 +11,13 @@ namespace Wino.Mail.ViewModels;
public class MailBaseViewModel : CoreBaseViewModel,
IRecipient<MailAddedMessage>,
IRecipient<BulkMailAddedMessage>,
IRecipient<MailRemovedMessage>,
IRecipient<BulkMailRemovedMessage>,
IRecipient<MailStateUpdatedMessage>,
IRecipient<BulkMailStateUpdatedMessage>,
IRecipient<MailUpdatedMessage>,
IRecipient<BulkMailUpdatedMessage>,
IRecipient<MailDownloadedMessage>,
IRecipient<DraftCreated>,
IRecipient<DraftFailed>,
@@ -21,8 +27,41 @@ public class MailBaseViewModel : CoreBaseViewModel,
IRecipient<FolderSynchronizationEnabled>
{
protected virtual void OnMailAdded(MailCopy addedMail, EntityUpdateSource source) { }
protected virtual void OnBulkMailAdded(IReadOnlyList<MailCopy> addedMails, EntityUpdateSource source)
{
foreach (var addedMail in addedMails ?? [])
{
OnMailAdded(addedMail, source);
}
}
protected virtual void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source) { }
protected virtual void OnBulkMailRemoved(IReadOnlyList<MailCopy> removedMails, EntityUpdateSource source)
{
foreach (var removedMail in removedMails ?? [])
{
OnMailRemoved(removedMail, source);
}
}
protected virtual void OnMailStateUpdated(MailStateChange updatedState, EntityUpdateSource source) { }
protected virtual void OnBulkMailStateUpdated(IReadOnlyList<MailStateChange> updatedStates, EntityUpdateSource source)
{
foreach (var updatedState in updatedStates ?? [])
{
OnMailStateUpdated(updatedState, source);
}
}
protected virtual void OnMailUpdated(MailCopy updatedMail, EntityUpdateSource source, MailCopyChangeFlags changedProperties) { }
protected virtual void OnBulkMailUpdated(IReadOnlyList<MailCopy> updatedMails, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
{
foreach (var updatedMail in updatedMails ?? [])
{
OnMailUpdated(updatedMail, source, changedProperties);
}
}
protected virtual void OnMailDownloaded(MailCopy downloadedMail) { }
protected virtual void OnDraftCreated(MailCopy draftMail, MailAccount account) { }
protected virtual void OnDraftFailed(MailCopy draftMail, MailAccount account) { }
@@ -32,8 +71,13 @@ public class MailBaseViewModel : CoreBaseViewModel,
protected virtual void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { }
void IRecipient<MailAddedMessage>.Receive(MailAddedMessage message) => OnMailAdded(message.AddedMail, message.Source);
void IRecipient<BulkMailAddedMessage>.Receive(BulkMailAddedMessage message) => OnBulkMailAdded(message.AddedMails, message.Source);
void IRecipient<MailRemovedMessage>.Receive(MailRemovedMessage message) => OnMailRemoved(message.RemovedMail, message.Source);
void IRecipient<BulkMailRemovedMessage>.Receive(BulkMailRemovedMessage message) => OnBulkMailRemoved(message.RemovedMails, message.Source);
void IRecipient<MailStateUpdatedMessage>.Receive(MailStateUpdatedMessage message) => OnMailStateUpdated(message.UpdatedState, message.Source);
void IRecipient<BulkMailStateUpdatedMessage>.Receive(BulkMailStateUpdatedMessage message) => OnBulkMailStateUpdated(message.UpdatedStates, message.Source);
void IRecipient<MailUpdatedMessage>.Receive(MailUpdatedMessage message) => OnMailUpdated(message.UpdatedMail, message.Source, message.ChangedProperties);
void IRecipient<BulkMailUpdatedMessage>.Receive(BulkMailUpdatedMessage message) => OnBulkMailUpdated(message.UpdatedMails, message.Source, message.ChangedProperties);
void IRecipient<MailDownloadedMessage>.Receive(MailDownloadedMessage message) => OnMailDownloaded(message.DownloadedMail);
void IRecipient<DraftMapped>.Receive(DraftMapped message) => OnDraftMapped(message.LocalDraftCopyId, message.RemoteDraftCopyId);
@@ -51,8 +95,13 @@ public class MailBaseViewModel : CoreBaseViewModel,
UnregisterRecipients();
Messenger.Register<MailAddedMessage>(this);
Messenger.Register<BulkMailAddedMessage>(this);
Messenger.Register<MailRemovedMessage>(this);
Messenger.Register<BulkMailRemovedMessage>(this);
Messenger.Register<MailStateUpdatedMessage>(this);
Messenger.Register<BulkMailStateUpdatedMessage>(this);
Messenger.Register<MailUpdatedMessage>(this);
Messenger.Register<BulkMailUpdatedMessage>(this);
Messenger.Register<MailDownloadedMessage>(this);
Messenger.Register<DraftCreated>(this);
Messenger.Register<DraftFailed>(this);
@@ -67,8 +116,13 @@ public class MailBaseViewModel : CoreBaseViewModel,
base.UnregisterRecipients();
Messenger.Unregister<MailAddedMessage>(this);
Messenger.Unregister<BulkMailAddedMessage>(this);
Messenger.Unregister<MailRemovedMessage>(this);
Messenger.Unregister<BulkMailRemovedMessage>(this);
Messenger.Unregister<MailStateUpdatedMessage>(this);
Messenger.Unregister<BulkMailStateUpdatedMessage>(this);
Messenger.Unregister<MailUpdatedMessage>(this);
Messenger.Unregister<BulkMailUpdatedMessage>(this);
Messenger.Unregister<MailDownloadedMessage>(this);
Messenger.Unregister<DraftCreated>(this);
Messenger.Unregister<DraftFailed>(this);
@@ -10,9 +10,7 @@ using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Category;
using Wino.Core.Services;
@@ -53,11 +51,11 @@ public partial class MailCategoryManagementPageViewModel : MailBaseViewModel
if (parameters is not Guid accountId)
return;
Account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
Account = await _accountService.GetAccountAsync(accountId);
if (Account != null)
{
await LoadCategoriesAsync().ConfigureAwait(false);
await LoadCategoriesAsync();
}
}
+250 -25
View File
@@ -23,6 +23,7 @@ using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Reader;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Mail;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Collections;
@@ -77,6 +78,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private readonly INotificationBuilder _notificationBuilder;
private readonly IFolderService _folderService;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly ILogger _logger = Log.ForContext<MailListPageViewModel>();
private readonly IMailCategoryService _mailCategoryService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService;
@@ -205,6 +207,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
SelectedFilterOption = FilterOptions[0];
SelectedSortingOption = SortingOptions[0];
MailCollection.ThreadItemFactory = threadId => new ThreadMailItemViewModel(threadId, PreferencesService.IsNewestThreadMailFirst);
MailListLength = statePersistenceService.MailListPaneLength;
}
@@ -491,29 +494,29 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
if (!CanSynchronize) return;
_notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy));
return;
//_notificationBuilder.CreateNotificationsAsync(MailCollection.SelectedItems.Select(a => a.MailCopy));
//return;
// Only synchronize listed folders.
// When doing linked inbox sync, we need to save the sync id to report progress back only once.
// Otherwise, we will report progress for each folder and that's what we don't want.
//trackingSynchronizationId = Guid.NewGuid();
//completedTrackingSynchronizationCount = 0;
trackingSynchronizationId = Guid.NewGuid();
completedTrackingSynchronizationCount = 0;
//foreach (var folder in ActiveFolder.HandlingFolders)
//{
// var options = new MailSynchronizationOptions()
// {
// AccountId = folder.MailAccountId,
// Type = MailSynchronizationType.CustomFolders,
// SynchronizationFolderIds = [folder.Id],
// GroupedSynchronizationTrackingId = trackingSynchronizationId
// };
foreach (var folder in ActiveFolder.HandlingFolders)
{
var options = new MailSynchronizationOptions()
{
AccountId = folder.MailAccountId,
Type = MailSynchronizationType.CustomFolders,
SynchronizationFolderIds = [folder.Id],
GroupedSynchronizationTrackingId = trackingSynchronizationId
};
// Messenger.Send(new NewMailSynchronizationRequested(options));
//}
Messenger.Send(new NewMailSynchronizationRequested(options));
}
}
[RelayCommand]
@@ -748,7 +751,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
return;
var requests = new List<IRequestBase>();
foreach (var mailItem in targetList.Select(a => a.MailCopy).DistinctBy(a => a.UniqueId))
foreach (var mailItem in targetList.Select(a => a.MailCopy).GroupBy(a => a.UniqueId).Select(group => group.First()))
{
var categoryNames = await _mailCategoryService.GetCategoryNamesForMailAsync(mailItem.UniqueId).ConfigureAwait(false);
requests.Add(new MailCategoryAssignmentRequest(mailItem, category.Id, category.Name, categoryNames, !isAssignedToAll));
@@ -957,7 +960,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
protected override async void OnMailUpdated(MailCopy updatedMail, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
{
base.OnMailUpdated(updatedMail, source, changedProperties);
try
{
await listManipulationSemepahore.WaitAsync();
@@ -982,6 +984,115 @@ public partial class MailListPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => { SetupTopBarActions(); });
}
protected override async void OnMailStateUpdated(MailStateChange updatedState, EntityUpdateSource source)
{
base.OnMailStateUpdated(updatedState, source);
if (updatedState == null)
return;
try
{
await listManipulationSemepahore.WaitAsync();
if (!MailCollection.ContainsMailUniqueId(updatedState.UniqueId))
return;
await MailCollection.UpdateMailStateAsync(updatedState, source);
}
finally
{
listManipulationSemepahore.Release();
}
await ExecuteUIThread(() => { SetupTopBarActions(); });
}
protected override async void OnBulkMailStateUpdated(IReadOnlyList<MailStateChange> updatedStates, EntityUpdateSource source)
{
var targetStates = updatedStates?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group => group.Last())
.ToList() ?? [];
if (targetStates.Count == 0)
return;
try
{
await listManipulationSemepahore.WaitAsync();
var listedStates = targetStates
.Where(state => MailCollection.ContainsMailUniqueId(state.UniqueId))
.ToList();
if (listedStates.Count == 0)
return;
await MailCollection.UpdateMailStatesAsync(listedStates, source);
}
finally
{
listManipulationSemepahore.Release();
}
await ExecuteUIThread(() => { SetupTopBarActions(); });
}
protected override async void OnBulkMailUpdated(IReadOnlyList<MailCopy> updatedMails, EntityUpdateSource source, MailCopyChangeFlags changedProperties)
{
var targetMails = updatedMails?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group => group.First())
.ToList() ?? [];
if (targetMails.Count == 0)
return;
try
{
await listManipulationSemepahore.WaitAsync();
var listedMails = targetMails
.Where(mail => MailCollection.ContainsMailUniqueId(mail.UniqueId))
.ToList();
if (listedMails.Count == 0)
return;
var mailsToRemove = listedMails
.Where(ShouldRemoveUpdatedMailFromCurrentList)
.ToList();
var mailIdsToRemove = mailsToRemove.Select(x => x.UniqueId).ToHashSet();
var mailsToUpdate = listedMails
.Where(mail => !mailIdsToRemove.Contains(mail.UniqueId))
.ToList();
if (mailsToRemove.Count > 0)
{
await MailCollection.RemoveRangeAsync(mailsToRemove);
}
if (mailsToUpdate.Count > 0)
{
await MailCollection.UpdateMailCopiesAsync(mailsToUpdate, source, changedProperties);
}
await ExecuteUIThread(() =>
{
NotifyItemFoundState();
SetupTopBarActions();
});
}
finally
{
listManipulationSemepahore.Release();
}
}
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
{
base.OnMailRemoved(removedMail, source);
@@ -1002,18 +1113,18 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (removedItemExistsInCurrentList && !isDeletedByGmailUnreadFolderAction)
{
bool isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
// Automatically select the next item in the list if the setting is enabled.
MailItemViewModel nextItem = null;
bool isDeletedMailSelected = false;
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
await ExecuteUIThread(() =>
{
await ExecuteUIThread(() =>
isDeletedMailSelected = MailCollection.SelectedItems.Any(a => a.MailCopy.UniqueId == removedMail.UniqueId);
if (isDeletedMailSelected && PreferencesService.AutoSelectNextItem)
{
nextItem = MailCollection.GetNextItem(removedMail);
});
}
}
});
// RemoveAsync already handles UI threading internally
await MailCollection.RemoveAsync(removedMail);
@@ -1043,6 +1154,115 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
protected override async void OnBulkMailRemoved(IReadOnlyList<MailCopy> removedMails, EntityUpdateSource source)
{
var targetMails = removedMails?
.Where(x => x != null && x.AssignedAccount != null)
.GroupBy(x => x.UniqueId)
.Select(group => group.First())
.ToList() ?? [];
if (targetMails.Count == 0)
return;
try
{
await listManipulationSemepahore.WaitAsync();
var existingMails = targetMails
.Where(mail => MailCollection.ContainsMailUniqueId(mail.UniqueId))
.ToList();
if (existingMails.Count == 0)
return;
var removedMailIds = existingMails.Select(mail => mail.UniqueId).ToHashSet();
var shouldClearSelection = false;
await ExecuteUIThread(() =>
{
shouldClearSelection = MailCollection.SelectedItems.Any(item => removedMailIds.Contains(item.MailCopy.UniqueId));
});
await MailCollection.RemoveRangeAsync(existingMails);
if (shouldClearSelection)
{
await MailCollection.UnselectAllAsync();
}
await ExecuteUIThread(() =>
{
NotifyItemFoundState();
SetupTopBarActions();
});
}
finally
{
listManipulationSemepahore.Release();
}
}
protected override async void OnBulkMailAdded(IReadOnlyList<MailCopy> addedMails, EntityUpdateSource source)
{
var targetMails = addedMails?
.Where(x => x != null)
.GroupBy(x => x.UniqueId)
.Select(group => group.First())
.ToList() ?? [];
if (targetMails.Count == 0)
return;
try
{
await listManipulationSemepahore.WaitAsync();
var mailsToAdd = new List<MailCopy>();
foreach (var addedMail in targetMails)
{
if (MailCollection.ContainsMailUniqueId(addedMail.UniqueId))
continue;
if (!ShouldIncludeAddedMailInCurrentList(addedMail))
continue;
if (ShouldPreventItemAdd(addedMail))
continue;
if (SelectedFolderPivot?.IsFocused is bool isFocused && addedMail.IsFocused != isFocused)
continue;
if (IsInSearchMode)
{
if (IsOnlineSearchEnabled || AreSearchResultsOnline)
continue;
if (!IsMailMatchingLocalSearch(addedMail))
continue;
}
mailsToAdd.Add(addedMail);
}
if (mailsToAdd.Count == 0)
return;
await MailCollection.AddRangeAsync(mailsToAdd.Select(mail => new MailItemViewModel(mail)), false);
await ExecuteUIThread(() =>
{
NotifyItemFoundState();
SetupTopBarActions();
});
}
finally
{
listManipulationSemepahore.Release();
}
}
protected override async void OnFolderDeleted(MailItemFolder folder)
{
base.OnFolderDeleted(folder);
@@ -1478,7 +1698,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
switch (message.Result)
{
case SynchronizationCompletedState.Success:
UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success);
// No need to pop success message when executing requests all the time...
if (message.Type != MailSynchronizationType.ExecuteRequests)
{
UpdateBarMessage(InfoBarMessageType.Success, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Success);
}
break;
case SynchronizationCompletedState.PartiallyCompleted:
UpdateBarMessage(InfoBarMessageType.Warning, ActiveFolder.FolderName, Translator.SynchronizationFolderReport_Failed);
@@ -1647,4 +1871,5 @@ public partial class MailListPageViewModel : MailBaseViewModel,
var package = new MailOperationPreperationRequest(message.Operation, mailCopies);
await ExecuteMailOperationAsync(package);
}
}
@@ -667,6 +667,23 @@ public partial class MailRenderingPageViewModel : MailBaseViewModel,
await ExecuteUIThread(() => { InitializeCommandBarItems(); });
}
protected override async void OnMailStateUpdated(MailStateChange updatedState, EntityUpdateSource source)
{
base.OnMailStateUpdated(updatedState, source);
if (initializedMailItemViewModel == null || updatedState == null)
return;
if (initializedMailItemViewModel.MailCopy.UniqueId != updatedState.UniqueId)
return;
await ExecuteUIThread(() =>
{
initializedMailItemViewModel.ApplyStateChanges(updatedState.IsRead, updatedState.IsFlagged);
InitializeCommandBarItems();
});
}
protected override async void OnMailRemoved(MailCopy removedMail, EntityUpdateSource source)
{
base.OnMailRemoved(removedMail, source);
@@ -42,6 +42,12 @@ public partial class MessageListPageViewModel : MailBaseViewModel
Translator.HoverActionOption_MoveJunk
];
public List<string> ThreadItemSortingOptions { get; } =
[
Translator.SettingsThreadOrder_LastItemFirst,
Translator.SettingsThreadOrder_FirstItemFirst
];
public IMailItemDisplayInformation DemoPreviewMailItemInformation { get; } = new DemoMailItemDisplayInformation();
public MailListDisplayMode SelectedMailSpacingMode => availableMailSpacingOptions[selectedMailSpacingIndex];
@@ -73,6 +79,19 @@ public partial class MessageListPageViewModel : MailBaseViewModel
}
}
private int selectedThreadItemSortingIndex;
public int SelectedThreadItemSortingIndex
{
get => selectedThreadItemSortingIndex;
set
{
if (SetProperty(ref selectedThreadItemSortingIndex, value) && value >= 0)
{
PreferencesService.IsNewestThreadMailFirst = value == 0;
}
}
}
#region Properties
private int leftHoverActionIndex;
public int LeftHoverActionIndex
@@ -128,6 +147,7 @@ public partial class MessageListPageViewModel : MailBaseViewModel
rightHoverActionIndex = availableHoverActions.IndexOf(PreferencesService.RightHoverAction);
selectedMailSpacingIndex = availableMailSpacingOptions.IndexOf(PreferencesService.MailItemDisplayMode);
SelectedMarkAsOptionIndex = Array.IndexOf(Enum.GetValues<MailMarkAsOption>(), PreferencesService.MarkAsPreference);
selectedThreadItemSortingIndex = PreferencesService.IsNewestThreadMailFirst ? 0 : 1;
}
[RelayCommand]
@@ -15,12 +15,20 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels;
public enum ProviderSelectionWizardStep
{
Provider = 0,
Identity = 1,
Capabilities = 2
}
public partial class ProviderSelectionPageViewModel : MailBaseViewModel
{
private readonly IAccountService _accountService;
private readonly IDialogServiceBase _dialogService;
private readonly IProviderService _providerService;
private readonly INewThemeService _themeService;
private ProviderSelectionHostMode _hostMode = ProviderSelectionHostMode.Wizard;
public WelcomeWizardContext WizardContext { get; }
@@ -44,16 +52,50 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
[NotifyPropertyChangedFor(nameof(IsMailSynchronizationRangeVisible))]
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
[ObservableProperty]
public partial string AccountName { get; set; }
[ObservableProperty]
public partial bool CanProceed { get; set; }
[NotifyPropertyChangedFor(nameof(IsMailSynchronizationRangeVisible))]
public partial bool IsMailAccessEnabled { get; set; } = true;
[ObservableProperty]
public partial bool IsCalendarAccessEnabled { get; set; } = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CurrentStepNumber))]
[NotifyPropertyChangedFor(nameof(StepProgressValue))]
[NotifyPropertyChangedFor(nameof(StepProgressText))]
[NotifyPropertyChangedFor(nameof(IsProviderStepVisible))]
[NotifyPropertyChangedFor(nameof(IsIdentityStepVisible))]
[NotifyPropertyChangedFor(nameof(IsCapabilityStepVisible))]
[NotifyPropertyChangedFor(nameof(CanGoBack))]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
[NotifyCanExecuteChangedFor(nameof(GoBackCommand))]
public partial ProviderSelectionWizardStep CurrentStep { get; set; } = ProviderSelectionWizardStep.Provider;
public bool IsColorSelected => SelectedColor != null;
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
public bool IsInitialSynchronizationWarningVisible => IsMailSynchronizationRangeVisible && SelectedInitialSynchronizationRange?.IsEverything == true;
public bool IsMailSynchronizationRangeVisible => IsMailAccessEnabled;
public int CurrentStepNumber => (int)CurrentStep + 1;
public double StepProgressValue => CurrentStepNumber;
public string StepProgressText => string.Format(Translator.ProviderSelection_StepProgress, CurrentStepNumber);
public bool IsProviderStepVisible => CurrentStep == ProviderSelectionWizardStep.Provider;
public bool IsIdentityStepVisible => CurrentStep == ProviderSelectionWizardStep.Identity;
public bool IsCapabilityStepVisible => CurrentStep == ProviderSelectionWizardStep.Capabilities;
public bool CanGoBack => CurrentStep != ProviderSelectionWizardStep.Provider;
public string SelectedProviderName => SelectedProvider?.Name ?? string.Empty;
public string SelectedProviderDescription => SelectedProvider?.Description ?? string.Empty;
public string SelectedProviderImage => SelectedProvider?.ProviderImage ?? string.Empty;
public string SelectedProviderCapabilityDescription => GetSelectedProviderCapabilityDescription();
public bool IsCapabilitySelectionMissing => !IsMailAccessEnabled && !IsCalendarAccessEnabled;
public bool IsCalendarOnlyServerHintVisible =>
SelectedProvider?.Type == MailProviderType.IMAP4 &&
!IsMailAccessEnabled &&
IsCalendarAccessEnabled;
public ProviderSelectionPageViewModel(
IAccountService accountService,
@@ -74,6 +116,16 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
{
base.OnNavigatedTo(mode, parameters);
var navigationContext = parameters as ProviderSelectionNavigationContext
?? ProviderSelectionNavigationContext.CreateForWizard();
_hostMode = navigationContext.HostMode;
if (mode != NavigationMode.Back)
{
WizardContext.Reset();
}
Providers = _providerService.GetAvailableProviders();
AvailableColors = _themeService.GetAvailableAccountColors()
.Select(hex => new AppColorViewModel(hex))
@@ -90,54 +142,118 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
p.Type == WizardContext.SelectedProvider.Type &&
p.SpecialImapProvider == WizardContext.SelectedProvider.SpecialImapProvider);
AccountName = WizardContext.AccountName;
IsMailAccessEnabled = WizardContext.IsMailAccessEnabled;
IsCalendarAccessEnabled = WizardContext.IsCalendarAccessEnabled;
if (WizardContext.AccountColorHex != null)
SelectedColor = AvailableColors.FirstOrDefault(c => c.Hex == WizardContext.AccountColorHex);
}
else
{
IsMailAccessEnabled = true;
IsCalendarAccessEnabled = true;
}
Validate();
CurrentStep = mode == NavigationMode.Back && SelectedProvider != null
? ProviderSelectionWizardStep.Capabilities
: ProviderSelectionWizardStep.Provider;
}
partial void OnSelectedProviderChanged(IProviderDetail value)
{
Validate();
OnPropertyChanged(nameof(SelectedProviderName));
OnPropertyChanged(nameof(SelectedProviderDescription));
OnPropertyChanged(nameof(SelectedProviderImage));
OnPropertyChanged(nameof(SelectedProviderCapabilityDescription));
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
partial void OnAccountNameChanged(string value) => Validate();
partial void OnAccountNameChanged(string value) => ContinueCommand.NotifyCanExecuteChanged();
partial void OnIsMailAccessEnabledChanged(bool value)
{
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
partial void OnIsCalendarAccessEnabledChanged(bool value)
{
OnPropertyChanged(nameof(IsCapabilitySelectionMissing));
OnPropertyChanged(nameof(IsCalendarOnlyServerHintVisible));
ContinueCommand.NotifyCanExecuteChanged();
}
[RelayCommand]
private void ClearColor() => SelectedColor = null;
private void Validate()
private bool CanContinue()
{
CanProceed = SelectedProvider != null && !string.IsNullOrWhiteSpace(AccountName);
return CurrentStep switch
{
ProviderSelectionWizardStep.Provider => SelectedProvider != null,
ProviderSelectionWizardStep.Identity => !string.IsNullOrWhiteSpace(AccountName),
ProviderSelectionWizardStep.Capabilities => IsMailAccessEnabled || IsCalendarAccessEnabled,
_ => false
};
}
[RelayCommand]
private async Task ProceedAsync()
[RelayCommand(CanExecute = nameof(CanGoBack))]
private void GoBack()
{
if (!CanProceed) return;
if (await _accountService.AccountNameExistsAsync(AccountName))
{
await _dialogService.ShowMessageAsync(
Translator.DialogMessage_AccountNameExistsMessage,
Translator.DialogMessage_AccountExistsTitle,
WinoCustomMessageDialogIcon.Warning);
if (!CanGoBack)
return;
CurrentStep--;
}
[RelayCommand(CanExecute = nameof(CanContinue))]
private async Task ContinueAsync()
{
switch (CurrentStep)
{
case ProviderSelectionWizardStep.Provider:
CurrentStep = ProviderSelectionWizardStep.Identity;
return;
case ProviderSelectionWizardStep.Identity:
if (await _accountService.AccountNameExistsAsync(AccountName?.Trim()))
{
await _dialogService.ShowMessageAsync(
Translator.DialogMessage_AccountNameExistsMessage,
Translator.DialogMessage_AccountExistsTitle,
WinoCustomMessageDialogIcon.Warning);
return;
}
CurrentStep = ProviderSelectionWizardStep.Capabilities;
return;
case ProviderSelectionWizardStep.Capabilities:
await CompleteWizardAsync();
return;
}
}
private async Task CompleteWizardAsync()
{
if (!CanContinue())
return;
}
// Persist to wizard context
WizardContext.SelectedProvider = SelectedProvider;
WizardContext.AccountName = AccountName?.Trim();
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
WizardContext.IsMailAccessEnabled = IsMailAccessEnabled;
WizardContext.IsCalendarAccessEnabled = IsCalendarAccessEnabled;
if (WizardContext.IsGenericImap)
{
// Navigate to ImapCalDavSettingsPage in wizard mode
var context = ImapCalDavSettingsNavigationContext.CreateForWizardMode(
WizardContext.BuildAccountCreationDialogResult());
var context = _hostMode == ProviderSelectionHostMode.SettingsAddAccount
? ImapCalDavSettingsNavigationContext.CreateForAddAccountMode(
WizardContext.BuildAccountCreationDialogResult())
: ImapCalDavSettingsNavigationContext.CreateForWizardMode(
WizardContext.BuildAccountCreationDialogResult());
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.ImapCalDavSettingsPage_TitleCreate,
@@ -159,4 +275,23 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
WinoPage.AccountSetupProgressPage));
}
}
partial void OnSelectedProviderChanging(IProviderDetail value)
{
}
private string GetSelectedProviderCapabilityDescription()
{
if (SelectedProvider == null)
return string.Empty;
if (SelectedProvider.Type is MailProviderType.Outlook or MailProviderType.Gmail)
return Translator.ProviderSelection_CapabilityProviderDescription_OAuth;
if (SelectedProvider.SpecialImapProvider is SpecialImapProvider.iCloud or SpecialImapProvider.Yahoo)
return Translator.ProviderSelection_CapabilityProviderDescription_SpecialImap;
return Translator.ProviderSelection_CapabilityProviderDescription_CustomServer;
}
}
@@ -8,6 +8,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Validation;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
@@ -37,11 +38,15 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
public partial string AppSpecificPassword { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(RequiresAppSpecificPassword))]
public partial int SelectedCalendarModeIndex { get; set; }
[ObservableProperty]
public partial bool CanProceed { get; set; }
public bool IsCalendarModeSelectionVisible => WizardContext.IsCalendarAccessEnabled;
public bool RequiresAppSpecificPassword => WizardContext.IsMailAccessEnabled || SelectedCalendarModeIndex == 1;
public string AppPasswordHelpUrl
{
get
@@ -85,8 +90,15 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
_ => 0
};
if (!WizardContext.IsCalendarAccessEnabled)
{
SelectedCalendarModeIndex = 0;
}
OnPropertyChanged(nameof(AppPasswordHelpUrl));
OnPropertyChanged(nameof(CalendarModeCalDavDescription));
OnPropertyChanged(nameof(IsCalendarModeSelectionVisible));
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate();
}
@@ -94,13 +106,18 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
partial void OnDisplayNameChanged(string value) => Validate();
partial void OnEmailAddressChanged(string value) => Validate();
partial void OnAppSpecificPasswordChanged(string value) => Validate();
partial void OnSelectedCalendarModeIndexChanged(int value)
{
OnPropertyChanged(nameof(RequiresAppSpecificPassword));
Validate();
}
private void Validate()
{
CanProceed = !string.IsNullOrWhiteSpace(DisplayName)
&& !string.IsNullOrWhiteSpace(EmailAddress)
&& EmailValidation.EmailValidator.Validate(EmailAddress ?? string.Empty)
&& !string.IsNullOrWhiteSpace(AppSpecificPassword);
&& MailAccountAddressValidator.IsValid(EmailAddress)
&& (!RequiresAppSpecificPassword || !string.IsNullOrWhiteSpace(AppSpecificPassword));
}
[RelayCommand]
@@ -120,12 +137,14 @@ public partial class SpecialImapCredentialsPageViewModel : MailBaseViewModel
WizardContext.DisplayName = DisplayName?.Trim();
WizardContext.EmailAddress = EmailAddress?.Trim();
WizardContext.AppSpecificPassword = AppSpecificPassword?.Trim();
WizardContext.CalendarSupportMode = SelectedCalendarModeIndex switch
{
1 => ImapCalendarSupportMode.CalDav,
2 => ImapCalendarSupportMode.LocalOnly,
_ => ImapCalendarSupportMode.Disabled
};
WizardContext.CalendarSupportMode = WizardContext.IsCalendarAccessEnabled
? SelectedCalendarModeIndex switch
{
1 => ImapCalendarSupportMode.CalDav,
2 => ImapCalendarSupportMode.LocalOnly,
_ => ImapCalendarSupportMode.Disabled
}
: ImapCalendarSupportMode.Disabled;
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step3Title,
@@ -13,6 +13,7 @@ using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Domain.Models.Updates;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Navigation;
using Wino.Messaging.UI;
@@ -68,7 +69,8 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
{
Messenger.Send(new BreadcrumbNavigationRequested(
Translator.WelcomeWizard_Step2Title,
WinoPage.ProviderSelectionPage));
WinoPage.ProviderSelectionPage,
ProviderSelectionNavigationContext.CreateForWizard()));
}
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
+65 -18
View File
@@ -4,8 +4,8 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Dispatching;
@@ -16,7 +16,6 @@ using Microsoft.Windows.AppNotifications;
using MimeKit.Cryptography;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.DataTransfer;
using Windows.ApplicationModel.DataTransfer.ShareTarget;
using Windows.Storage;
using Wino.Calendar.ViewModels;
using Wino.Calendar.ViewModels.Interfaces;
@@ -137,6 +136,11 @@ public partial class App : WinoApplication,
?? windowManager.GetWindow(WinoWindowKind.Shell)
?? windowManager.GetWindow(WinoWindowKind.Welcome);
if (window is IWinoShellWindow)
{
DisposeTrayIcon();
}
InitializeNavigationDispatcher();
}
@@ -159,6 +163,37 @@ public partial class App : WinoApplication,
_trayIcon.Create();
}
private void DisposeTrayIcon()
{
_trayIcon?.Dispose();
_trayIcon = null;
}
private void EnsurePreferenceChangedSubscription()
{
if (_preferencesService == null)
return;
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
}
private bool ShouldCreateTrayIcon()
=> _hasConfiguredAccounts &&
HasShellWindow() &&
(_preferencesService?.IsSystemTrayIconEnabled ?? true);
private void UpdateTrayIconState(bool allowCreation)
{
if (!allowCreation || !ShouldCreateTrayIcon())
{
DisposeTrayIcon();
return;
}
EnsureTrayIconCreated();
}
private IReadOnlyList<NativeTrayIcon.NativeTrayMenuItem> BuildTrayMenu()
{
List<NativeTrayIcon.NativeTrayMenuItem> items =
@@ -268,6 +303,7 @@ public partial class App : WinoApplication,
if (windowManager.GetWindow(WinoWindowKind.Shell) is not ShellWindow shellWindow)
return;
DisposeTrayIcon();
windowManager.HideWindow(shellWindow);
if (ReferenceEquals(MainWindow, shellWindow))
{
@@ -289,6 +325,8 @@ public partial class App : WinoApplication,
{
await NewThemeService.ApplyThemeToActiveWindowAsync();
}
UpdateTrayIconState(window is IWinoShellWindow);
}
private Task ExitApplicationAsync()
@@ -303,8 +341,7 @@ public partial class App : WinoApplication,
return;
_isExiting = true;
_trayIcon?.Dispose();
_trayIcon = null;
DisposeTrayIcon();
Services.GetRequiredService<IWinoWindowManager>().CloseAllWindows();
Application.Current.Exit();
@@ -476,12 +513,10 @@ public partial class App : WinoApplication,
return;
EnsureWindowManagerConfigured();
EnsureTrayIconCreated();
EnsurePreferenceChangedSubscription();
if (_hasConfiguredAccounts)
{
_preferencesService!.PreferenceChanged -= PreferencesServiceChanged;
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
RestartAutoSynchronizationLoop();
}
@@ -942,11 +977,16 @@ public partial class App : WinoApplication,
if (isStartupTaskLaunch)
{
UpdateTrayIconState(allowCreation: true);
LogActivation("Launched by startup task. Window created but hidden (system tray only).");
return;
}
MainWindow?.Activate();
if (MainWindow is WindowEx window)
{
await ActivateWindowAsync(window, applyThemeToWindow: false);
}
LogActivation("Window created and activated.");
}
@@ -1186,8 +1226,10 @@ public partial class App : WinoApplication,
// Initialize theme service after window is created.
await NewThemeService.InitializeAsync();
if (MainWindow != null)
Services.GetRequiredService<IWinoWindowManager>().ActivateWindow(MainWindow);
if (MainWindow is WindowEx window)
{
await ActivateWindowAsync(window, applyThemeToWindow: false);
}
LogActivation("Window created and activated.");
}
@@ -1357,7 +1399,8 @@ public partial class App : WinoApplication,
WeakReferenceMessenger.Default.Send(new AccountSynchronizationCompleted(
message.Options.AccountId,
syncResult.CompletedState,
message.Options.GroupedSynchronizationTrackingId));
message.Options.GroupedSynchronizationTrackingId,
message.Options.Type));
if (syncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
{
@@ -1398,6 +1441,7 @@ public partial class App : WinoApplication,
public void Receive(AccountCreatedMessage message)
{
_hasConfiguredAccounts = true;
EnsurePreferenceChangedSubscription();
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
@@ -1436,11 +1480,7 @@ public partial class App : WinoApplication,
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
{
if (_preferencesService != null)
{
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
}
EnsurePreferenceChangedSubscription();
CreateWindow(
null,
@@ -1484,6 +1524,7 @@ public partial class App : WinoApplication,
// All accounts removed — go back to welcome wizard from step 1
Services.GetRequiredService<WelcomeWizardContext>().Reset();
StopAutoSynchronizationLoop();
UpdateTrayIconState(allowCreation: false);
CloseShellWindowIfPresent();
CreateWelcomeWindow();
if (MainWindow != null)
@@ -1577,10 +1618,16 @@ public partial class App : WinoApplication,
private void PreferencesServiceChanged(object? sender, string propertyName)
{
if (propertyName != nameof(IPreferencesService.EmailSyncIntervalMinutes))
if (propertyName == nameof(IPreferencesService.EmailSyncIntervalMinutes))
{
RestartAutoSynchronizationLoop();
return;
}
RestartAutoSynchronizationLoop();
if (propertyName == nameof(IPreferencesService.IsSystemTrayIconEnabled))
{
UpdateTrayIconState(allowCreation: true);
}
}
private void RestartAutoSynchronizationLoop()
@@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
@@ -9,15 +10,36 @@ internal static class MenuFlyoutLanguageHelper
{
private const string ChineseLanguageTag = "zh-CN";
public static void Apply(MenuFlyoutItemBase item)
public static void Apply(DependencyObject? element)
{
if (element == null)
{
return;
}
if (WinoApplication.Current.Services.GetRequiredService<IPreferencesService>().CurrentLanguage == AppLanguage.Chinese)
{
item.Language = ChineseLanguageTag;
switch (element)
{
case MenuFlyoutItemBase menuFlyoutItem:
menuFlyoutItem.Language = ChineseLanguageTag;
break;
case FrameworkElement frameworkElement:
frameworkElement.Language = ChineseLanguageTag;
break;
}
}
else
{
item.ClearValue(MenuFlyoutItemBase.LanguageProperty);
switch (element)
{
case MenuFlyoutItemBase menuFlyoutItem:
menuFlyoutItem.ClearValue(MenuFlyoutItemBase.LanguageProperty);
break;
case FrameworkElement frameworkElement:
frameworkElement.ClearValue(FrameworkElement.LanguageProperty);
break;
}
}
}
}
@@ -438,6 +438,11 @@ public sealed partial class OperationCommandBar : CommandBar
frameworkElement.DataContext = dataContext;
}
if (element is DependencyObject dependencyObject)
{
MenuFlyoutLanguageHelper.Apply(dependencyObject);
}
return element;
}
@@ -8,6 +8,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.Validation;
using Wino.Core.ViewModels.Data;
using Wino.Helpers;
@@ -157,7 +158,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(),
details,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
initialSynchronizationRange,
true,
calendarSupportMode != ImapCalendarSupportMode.Disabled);
Hide();
return;
@@ -184,7 +187,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(),
null,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
initialSynchronizationRange,
true,
true);
Hide();
}
}
@@ -208,7 +213,7 @@ public sealed partial class NewAccountDialog : ContentDialog
&& !string.IsNullOrEmpty(AccountNameTextbox.Text)
&& (IsSpecialImapServerPartVisible ? (!string.IsNullOrEmpty(AppSpecificPassword.Password)
&& !string.IsNullOrEmpty(DisplayNameTextBox.Text)
&& EmailValidation.EmailValidator.Validate(SpecialImapAddress.Text)) : true);
&& MailAccountAddressValidator.IsValid(SpecialImapAddress.Text)) : true);
IsPrimaryButtonEnabled = shouldEnable;
}
+20
View File
@@ -72,6 +72,26 @@ public static class XamlHelpers
return null;
}
}
public static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? StringToBitmapImage(string? imagePath)
{
if (string.IsNullOrWhiteSpace(imagePath))
return null;
try
{
var uri = imagePath.StartsWith("/")
? new Uri($"ms-appx://{imagePath}")
: new Uri(imagePath, UriKind.Absolute);
return new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri);
}
catch
{
return null;
}
}
public static InfoBarSeverity InfoBarSeverityConverter(InfoBarMessageType messageType)
{
return messageType switch
@@ -105,6 +105,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{
lock (_calendarStateLock)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(groupedAccountCalendar.Account))
return;
groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
try
@@ -180,6 +183,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{
lock (_calendarStateLock)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(accountCalendar.Account))
return;
// Find the group that this calendar belongs to.
var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
@@ -396,6 +402,16 @@ public partial class AccountCalendarStateService : ObservableRecipient,
lock (_calendarStateLock)
{
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == updatedAccount.Id);
if (!GroupedAccountCalendarViewModel.SupportsCalendar(updatedAccount))
{
if (groupedAccount != null)
{
RemoveGroupedAccountCalendar(groupedAccount);
}
return;
}
}
groupedAccount?.UpdateAccount(updatedAccount);
+35
View File
@@ -137,6 +137,41 @@ public class DialogService : DialogServiceBase, IMailDialogService
WinoCustomMessageDialogIcon.Warning,
Translator.Buttons_No);
public async Task<ThreeButtonDialogResult> ShowThreeButtonDialogAsync(string title,
string description,
string primaryButtonText,
string secondaryButtonText,
string cancelButtonText,
WinoCustomMessageDialogIcon? icon = null)
{
var informationContainer = new CustomMessageDialogInformationContainer(
title,
description,
icon ?? WinoCustomMessageDialogIcon.Information,
false);
var dialog = new ContentDialog
{
Style = ApplicationResourceManager.GetResource<Style>("WinoDialogStyle"),
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme(),
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = primaryButtonText,
SecondaryButtonText = secondaryButtonText,
CloseButtonText = cancelButtonText,
ContentTemplate = ApplicationResourceManager.GetResource<DataTemplate>("CustomWinoContentDialogContentTemplate"),
Content = informationContainer
};
var dialogResult = await HandleDialogPresentationAsync(dialog);
return dialogResult switch
{
ContentDialogResult.Primary => ThreeButtonDialogResult.Primary,
ContentDialogResult.Secondary => ThreeButtonDialogResult.Secondary,
_ => ThreeButtonDialogResult.Cancel
};
}
public async Task<MailAccount> ShowAccountPickerDialogAsync(List<MailAccount> availableAccounts)
{
var accountPicker = new AccountPickerDialog(availableAccounts)
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Wino.Core.Domain.Interfaces;
namespace Wino.Services;
@@ -6,35 +7,73 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
{
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
public string[] OutlookScope =>
[
"email",
"mail.readwrite",
"offline_access",
"mail.send",
"Mail.Send.Shared",
"Mail.ReadWrite.Shared",
"User.Read",
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"Calendars.Read",
"Calendars.Read.Shared",
];
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
public string[] GmailScope =>
[
"https://mail.google.com/",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/drive.file",
];
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
public string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"email",
"offline_access",
"User.Read"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[
"mail.readwrite",
"mail.send",
"Mail.Send.Shared",
"Mail.ReadWrite.Shared"
]);
}
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"Calendars.Read",
"Calendars.Read.Shared"
]);
}
return [.. scopes];
}
public string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.labels"
]);
}
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/drive.file"
]);
}
return [.. scopes];
}
}
+36 -10
View File
@@ -58,7 +58,12 @@ public class NotificationBuilder : INotificationBuilder
WeakReferenceMessenger.Default.Register<MailReadStatusChanged>(this, (r, msg) =>
{
RemoveNotification(msg.UniqueId);
QueueRemoveNotifications([msg.UniqueId]);
});
WeakReferenceMessenger.Default.Register<BulkMailReadStatusChanged>(this, (r, msg) =>
{
QueueRemoveNotifications(msg.UniqueIds);
});
}
@@ -156,16 +161,37 @@ public class NotificationBuilder : INotificationBuilder
public void RemoveNotification(Guid mailUniqueId)
{
try
QueueRemoveNotifications([mailUniqueId]);
}
private void QueueRemoveNotifications(IEnumerable<Guid> mailUniqueIds)
{
var uniqueIds = mailUniqueIds?
.Where(x => x != Guid.Empty)
.Distinct()
.ToList();
if (uniqueIds == null || uniqueIds.Count == 0)
return;
_ = RemoveNotificationsAsync(uniqueIds);
}
private static async Task RemoveNotificationsAsync(IReadOnlyList<Guid> mailUniqueIds)
{
foreach (var mailUniqueId in mailUniqueIds)
{
AppNotificationManager.Default.RemoveByTagAsync(mailUniqueId.ToString()).AsTask().GetAwaiter().GetResult();
}
catch (ArgumentException)
{
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to remove notification for mail {mailUniqueId}");
try
{
await AppNotificationManager.Default.RemoveByTagAsync(mailUniqueId.ToString()).AsTask().ConfigureAwait(false);
}
catch (ArgumentException)
{
}
catch (Exception ex)
{
Log.Error(ex, "Failed to remove notification for mail {MailUniqueId}", mailUniqueId);
}
}
}
@@ -117,6 +117,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(IsThreadingEnabled), value);
}
public bool IsNewestThreadMailFirst
{
get => _configurationService.Get(nameof(IsNewestThreadMailFirst), true);
set => SetPropertyAndSave(nameof(IsNewestThreadMailFirst), value);
}
public bool IsMailListActionBarEnabled
{
get => _configurationService.Get(nameof(IsMailListActionBarEnabled), false);
@@ -387,6 +393,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
set => SetPropertyAndSave(nameof(IsStoreUpdateNotificationsEnabled), value);
}
public bool IsSystemTrayIconEnabled
{
get => _configurationService.Get(nameof(IsSystemTrayIconEnabled), true);
set => SetPropertyAndSave(nameof(IsSystemTrayIconEnabled), value);
}
public bool IsWinoAccountButtonHidden
{
get => _configurationService.Get(nameof(IsWinoAccountButtonHidden), false);

Some files were not shown because too many files have changed in this diff Show More