Import functionality for wino accounts, calendar sync UI, bunch of shell improvements
This commit is contained in:
@@ -4,9 +4,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
|
<PackageVersion Include="ColorHashSharp" Version="1.1.0" />
|
||||||
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.0" />
|
<PackageVersion Include="CommunityToolkit.Common" Version="8.4.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
|
<PackageVersion Include="CommunityToolkit.Diagnostics" Version="8.4.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.1-build.4" />
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Animations" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.Segmented" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251219" />
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.251219" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.250926-build.2293" />
|
||||||
|
<PackageVersion Include="CommunityToolkit.WinUI.Lottie" Version="8.2.250604" />
|
||||||
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
<PackageVersion Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.DependencyPropertyGenerator" Version="0.1.250926-build.2293" />
|
||||||
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
<PackageVersion Include="EmailValidation" Version="1.3.0" />
|
||||||
@@ -24,11 +25,11 @@
|
|||||||
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
<PackageVersion Include="IsExternalInit" Version="1.0.3" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
||||||
<PackageVersion Include="Microsoft.Graph" Version="5.103.0" />
|
<PackageVersion Include="Microsoft.Graph" Version="5.103.0" />
|
||||||
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
|
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.4.0" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client" Version="4.83.3" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client.Broker" Version="4.82.1" />
|
||||||
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
<PackageVersion Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.82.1" />
|
||||||
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
<PackageVersion Include="Microsoft.NETCore.UniversalWindowsPlatform" Version="6.2.14" />
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
<PackageVersion Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||||
<PackageVersion Include="NodaTime" Version="3.3.1" />
|
<PackageVersion Include="NodaTime" Version="3.3.1" />
|
||||||
<PackageVersion Include="Sentry.Serilog" Version="6.1.0" />
|
<PackageVersion Include="Sentry.Serilog" Version="6.3.0" />
|
||||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
<PackageVersion Include="Serilog" Version="4.3.1" />
|
||||||
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
|
<PackageVersion Include="Serilog.Exceptions" Version="8.4.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
<PackageVersion Include="Serilog.Sinks.Debug" Version="3.0.0" />
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
|
<PackageVersion Include="SkiaSharp" Version="3.119.2" />
|
||||||
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.2" />
|
<PackageVersion Include="SkiaSharp.Views.WinUI" Version="3.119.2" />
|
||||||
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
|
<PackageVersion Include="sqlite-net-pcl" Version="1.10.196-beta" />
|
||||||
<PackageVersion Include="System.Drawing.Common" Version="10.0.3" />
|
<PackageVersion Include="System.Drawing.Common" Version="10.0.5" />
|
||||||
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
|
||||||
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
|
||||||
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
|
<PackageVersion Include="System.Text.Json" Version="10.0.3" />
|
||||||
@@ -66,7 +67,7 @@
|
|||||||
<PackageVersion Include="System.Reactive" Version="6.1.0" />
|
<PackageVersion Include="System.Reactive" Version="6.1.0" />
|
||||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
|
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.3" />
|
||||||
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.3" />
|
<PackageVersion Include="System.Text.Encodings.Web" Version="10.0.3" />
|
||||||
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7705" />
|
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.1721" />
|
||||||
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
|
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
|
||||||
<PackageVersion Include="WinUIEx" Version="2.9.0" />
|
<PackageVersion Include="WinUIEx" Version="2.9.0" />
|
||||||
<!-- Testing packages -->
|
<!-- Testing packages -->
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
|
System.Windows.Input.ICommand ICalendarShellClient.DateClickedCommand => DateClickedCommand;
|
||||||
System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
|
System.Windows.Input.ICommand ICalendarShellClient.PreviousDateRangeCommand => PreviousDateRangeCommand;
|
||||||
System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
|
System.Windows.Input.ICommand ICalendarShellClient.NextDateRangeCommand => NextDateRangeCommand;
|
||||||
|
System.Windows.Input.ICommand ICalendarShellClient.SyncCommand => SyncCommand;
|
||||||
|
|
||||||
|
public bool CanSynchronizeCalendars => !AccountCalendarStateService.IsAnySynchronizationInProgress;
|
||||||
|
|
||||||
public MenuItemCollection MenuItems { get; private set; }
|
public MenuItemCollection MenuItems { get; private set; }
|
||||||
public MenuItemCollection FooterItems { get; private set; }
|
public MenuItemCollection FooterItems { get; private set; }
|
||||||
@@ -75,7 +78,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
private readonly SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1);
|
||||||
private readonly CalendarPageViewModel _calendarPageViewModel;
|
private readonly CalendarPageViewModel _calendarPageViewModel;
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IUpdateManager _updateManager;
|
|
||||||
private readonly IStoreUpdateService _storeUpdateService;
|
private readonly IStoreUpdateService _storeUpdateService;
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
private readonly ICalendarService _calendarService;
|
private readonly ICalendarService _calendarService;
|
||||||
@@ -93,7 +95,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
INavigationService navigationService,
|
INavigationService navigationService,
|
||||||
CalendarPageViewModel calendarPageViewModel,
|
CalendarPageViewModel calendarPageViewModel,
|
||||||
IMailDialogService dialogService,
|
IMailDialogService dialogService,
|
||||||
IUpdateManager updateManager,
|
|
||||||
IStoreUpdateService storeUpdateService,
|
IStoreUpdateService storeUpdateService,
|
||||||
IDateContextProvider dateContextProvider)
|
IDateContextProvider dateContextProvider)
|
||||||
{
|
{
|
||||||
@@ -105,11 +106,11 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
_calendarService = calendarService;
|
_calendarService = calendarService;
|
||||||
_calendarPageViewModel = calendarPageViewModel;
|
_calendarPageViewModel = calendarPageViewModel;
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_updateManager = updateManager;
|
|
||||||
_storeUpdateService = storeUpdateService;
|
_storeUpdateService = storeUpdateService;
|
||||||
_dateContextProvider = dateContextProvider;
|
_dateContextProvider = dateContextProvider;
|
||||||
|
|
||||||
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
|
_calendarPageViewModel.PropertyChanged += CalendarPageViewModelPropertyChanged;
|
||||||
|
AccountCalendarStateService.PropertyChanged += AccountCalendarStateServicePropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnDispatcherAssigned()
|
protected override void OnDispatcherAssigned()
|
||||||
@@ -177,11 +178,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
await InitializeAccountCalendarsAsync();
|
await InitializeAccountCalendarsAsync();
|
||||||
ValidateConfiguredNewEventCalendar();
|
ValidateConfiguredNewEventCalendar();
|
||||||
|
|
||||||
if (shouldRunStartupFlows)
|
|
||||||
{
|
|
||||||
await ShowWhatIsNewIfNeededAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
TodayClicked();
|
TodayClicked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +214,15 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
_calendarPageViewModel.CleanupForShellDeactivation();
|
_calendarPageViewModel.CleanupForShellDeactivation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AccountCalendarStateServicePropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName != nameof(IAccountCalendarStateService.IsAnySynchronizationInProgress))
|
||||||
|
return;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(CanSynchronizeCalendars));
|
||||||
|
SyncCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
private void AttachRuntimeSubscriptions()
|
private void AttachRuntimeSubscriptions()
|
||||||
{
|
{
|
||||||
if (_runtimeSubscriptionsAttached)
|
if (_runtimeSubscriptionsAttached)
|
||||||
@@ -240,18 +245,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
_runtimeSubscriptionsAttached = false;
|
_runtimeSubscriptionsAttached = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowWhatIsNewIfNeededAsync()
|
|
||||||
{
|
|
||||||
if (!_updateManager.ShouldShowUpdateNotes())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
|
||||||
if (notes.Sections.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshFooterItemsAsync(bool showNotification)
|
private async Task RefreshFooterItemsAsync(bool showNotification)
|
||||||
{
|
{
|
||||||
await ExecuteUIThread(() =>
|
await ExecuteUIThread(() =>
|
||||||
@@ -326,7 +319,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
_navigationDate = null;
|
_navigationDate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand(CanExecute = nameof(CanSynchronizeCalendars))]
|
||||||
private async Task Sync()
|
private async Task Sync()
|
||||||
{
|
{
|
||||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
@@ -335,7 +328,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
|||||||
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
Type = CalendarSynchronizationType.CalendarEvents
|
Type = CalendarSynchronizationType.Strict
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
Account = account;
|
Account = account;
|
||||||
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
|
AccountCalendars = new ObservableCollection<AccountCalendarViewModel>(calendarViewModels);
|
||||||
|
AccountColorHex = account.AccountColorHex;
|
||||||
|
|
||||||
ManageIsCheckedState();
|
ManageIsCheckedState();
|
||||||
|
|
||||||
@@ -74,6 +75,18 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial bool? IsCheckedState { get; set; } = true;
|
public partial bool? IsCheckedState { get; set; } = true;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string AccountColorHex { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsSynchronizationInProgress { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial string SynchronizationStatus { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool CanSynchronize => !IsSynchronizationInProgress;
|
||||||
|
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
|
||||||
|
|
||||||
private bool _isExternalPropChangeBlocked = false;
|
private bool _isExternalPropChangeBlocked = false;
|
||||||
|
|
||||||
private void ManageIsCheckedState()
|
private void ManageIsCheckedState()
|
||||||
@@ -142,4 +155,24 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
|||||||
|
|
||||||
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
|
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnIsSynchronizationInProgressChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CanSynchronize));
|
||||||
|
OnPropertyChanged(nameof(IsSynchronizationProgressVisible));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateAccount(MailAccount updatedAccount)
|
||||||
|
{
|
||||||
|
if (updatedAccount == null || updatedAccount.Id != Account.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Account.Name = updatedAccount.Name;
|
||||||
|
Account.Address = updatedAccount.Address;
|
||||||
|
Account.AccountColorHex = updatedAccount.AccountColorHex;
|
||||||
|
Account.AttentionReason = updatedAccount.AttentionReason;
|
||||||
|
Account.MergedInboxId = updatedAccount.MergedInboxId;
|
||||||
|
AccountColorHex = updatedAccount.AccountColorHex;
|
||||||
|
OnPropertyChanged(nameof(Account));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,5 +29,6 @@ public interface IAccountCalendarStateService : INotifyPropertyChanged
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
|
IEnumerable<AccountCalendarViewModel> ActiveCalendars { get; }
|
||||||
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
|
IEnumerable<AccountCalendarViewModel> AllCalendars { get; }
|
||||||
|
bool IsAnySynchronizationInProgress { get; }
|
||||||
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public enum CalendarSynchronizationType
|
|||||||
ExecuteRequests, // Execute all requests in the queue.
|
ExecuteRequests, // Execute all requests in the queue.
|
||||||
CalendarMetadata, // Sync calendar metadata.
|
CalendarMetadata, // Sync calendar metadata.
|
||||||
CalendarEvents, // Sync all events for all calendars.
|
CalendarEvents, // Sync all events for all calendars.
|
||||||
|
Strict, // Run metadata and event synchronization in sequence.
|
||||||
SingleCalendar, // Sync events for only specified calendars.
|
SingleCalendar, // Sync events for only specified calendars.
|
||||||
UpdateProfile // Update profile information only.
|
UpdateProfile // Update profile information only.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ using Wino.Core.Domain.Enums;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -32,10 +31,4 @@ public interface IDialogServiceBase
|
|||||||
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
Task<List<PickedFileMetadata>> PickFilesMetadataAsync(params object[] typeFilters);
|
||||||
Task<string> PickFilePathAsync(string saveFileName);
|
Task<string> PickFilePathAsync(string saveFileName);
|
||||||
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
Task<WebView2PrintSettingsModel> ShowPrintDialogAsync(WebView2PrintSettingsModel initialSettings = null);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Presents the "What's New" dialog for the current version.
|
|
||||||
/// This dialog is undismissable and runs any pending migrations when the user clicks "Get Started".
|
|
||||||
/// </summary>
|
|
||||||
Task ShowWhatIsNewDialogAsync(UpdateNotes notes);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using Wino.Core.Domain.Entities.Mail;
|
|||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Models;
|
using Wino.Core.Domain.Models;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
|
|
||||||
@@ -74,4 +75,6 @@ public interface IMailDialogService : IDialogServiceBase
|
|||||||
Task<WinoAccount?> ShowWinoAccountRegistrationDialogAsync();
|
Task<WinoAccount?> ShowWinoAccountRegistrationDialogAsync();
|
||||||
|
|
||||||
Task<WinoAccount?> ShowWinoAccountLoginDialogAsync();
|
Task<WinoAccount?> ShowWinoAccountLoginDialogAsync();
|
||||||
|
|
||||||
|
Task<WinoAccountSyncExportResult?> ShowWinoAccountExportDialogAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ public interface ICalendarShellClient : IShellClient
|
|||||||
int SelectedDateNavigationHeaderIndex { get; }
|
int SelectedDateNavigationHeaderIndex { get; }
|
||||||
VisibleDateRange? CurrentVisibleRange { get; }
|
VisibleDateRange? CurrentVisibleRange { get; }
|
||||||
string VisibleDateRangeText { get; }
|
string VisibleDateRangeText { get; }
|
||||||
|
bool CanSynchronizeCalendars { get; }
|
||||||
|
ICommand SyncCommand { get; }
|
||||||
ICommand TodayClickedCommand { get; }
|
ICommand TodayClickedCommand { get; }
|
||||||
ICommand DateClickedCommand { get; }
|
ICommand DateClickedCommand { get; }
|
||||||
ICommand PreviousDateRangeCommand { get; }
|
ICommand PreviousDateRangeCommand { get; }
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using Wino.Core.Domain.Models.Accounts;
|
|||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -26,5 +27,7 @@ public interface IWinoAccountApiClient
|
|||||||
Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<WinoStoreCollectionsIdTicketInfo>> CreatePurchaseIdTicketAsync(CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(string? storeIdKey, string? purchaseIdKey, CancellationToken cancellationToken = default);
|
||||||
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||||
Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
||||||
|
Task<UserMailboxSyncListDto> GetMailboxesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
|
public interface IWinoAccountDataSyncService
|
||||||
|
{
|
||||||
|
Task<WinoAccountSyncExportResult> ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default);
|
||||||
|
Task<WinoAccountSyncImportResult> ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ using Wino.Core.Domain.Models.Accounts;
|
|||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Interfaces;
|
namespace Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
@@ -28,6 +29,10 @@ public interface IWinoAccountProfileService
|
|||||||
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> TranslateAsync(string html, string targetLanguage, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<AiTextResultDto>> RewriteAsync(string html, string mode, CancellationToken cancellationToken = default);
|
||||||
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default);
|
Task<ApiEnvelope<JsonElement>> SyncStoreEntitlementsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default);
|
||||||
|
Task<UserMailboxSyncListDto> GetMailboxesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default);
|
||||||
Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default);
|
Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default);
|
||||||
Task SignOutAsync(CancellationToken cancellationToken = default);
|
Task SignOutAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public interface INavigationService
|
|||||||
|
|
||||||
Type GetPageType(WinoPage winoPage);
|
Type GetPageType(WinoPage winoPage);
|
||||||
bool ChangeApplicationMode(WinoApplicationMode mode);
|
bool ChangeApplicationMode(WinoApplicationMode mode);
|
||||||
|
bool ChangeApplicationMode(WinoApplicationMode mode, ShellModeActivationContext activationContext);
|
||||||
bool CanGoBack();
|
bool CanGoBack();
|
||||||
void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight);
|
void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
|
|||||||
public void UpdateAccount(MailAccount account)
|
public void UpdateAccount(MailAccount account)
|
||||||
{
|
{
|
||||||
Parameter = account;
|
Parameter = account;
|
||||||
|
AttentionReason = account.AttentionReason;
|
||||||
|
|
||||||
OnPropertyChanged(nameof(AccountName));
|
OnPropertyChanged(nameof(AccountName));
|
||||||
OnPropertyChanged(nameof(Base64ProfilePicture));
|
OnPropertyChanged(nameof(Base64ProfilePicture));
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ public static class CalendarColorPalette
|
|||||||
{
|
{
|
||||||
private static readonly string[] FlatUiColorPalette =
|
private static readonly string[] FlatUiColorPalette =
|
||||||
[
|
[
|
||||||
"#E53935", "#D81B60", "#8E24AA", "#5E35B1", "#3949AB", "#1E88E5", "#039BE5", "#00ACC1", "#00897B", "#43A047",
|
"#E53935", "#D81B60", "#C2185B", "#AD1457", "#8E24AA", "#7B1FA2", "#6A1B9A", "#5E35B1", "#512DA8", "#4527A0",
|
||||||
"#7CB342", "#C0CA33", "#FDD835", "#FFB300", "#FB8C00", "#F4511E", "#6D4C41", "#757575", "#546E7A", "#C62828",
|
"#3949AB", "#303F9F", "#283593", "#1E88E5", "#1976D2", "#1565C0", "#039BE5", "#0288D1", "#0277BD", "#00ACC1",
|
||||||
"#AD1457", "#6A1B9A", "#4527A0", "#283593", "#1565C0", "#0277BD", "#00838F", "#00695C", "#2E7D32", "#558B2F",
|
"#0097A7", "#00838F", "#00897B", "#00796B", "#00695C", "#43A047", "#388E3C", "#2E7D32", "#7CB342", "#689F38",
|
||||||
"#9E9D24", "#F9A825", "#FF8F00", "#EF6C00", "#D84315", "#4E342E", "#616161", "#455A64", "#EF5350", "#EC407A",
|
"#558B2F", "#9CCC65", "#8BC34A", "#AED581", "#C0CA33", "#AFB42B", "#9E9D24", "#D4E157", "#CDDC39", "#FDD835",
|
||||||
"#AB47BC", "#7E57C2", "#5C6BC0", "#42A5F5", "#29B6F6", "#26C6DA", "#26A69A", "#66BB6A", "#9CCC65", "#D4E157",
|
"#FBC02D", "#F9A825", "#FFB300", "#FFA000", "#FF8F00", "#FB8C00", "#F57C00", "#EF6C00", "#F4511E", "#E64A19",
|
||||||
"#FFEE58", "#FFCA28", "#FFA726", "#FF7043", "#8D6E63", "#BDBDBD", "#78909C", "#F06292", "#BA68C8", "#9575CD"
|
"#D84315", "#FF7043", "#FF8A65", "#FFAB91", "#6D4C41", "#5D4037", "#4E342E", "#8D6E63", "#795548", "#A1887F",
|
||||||
|
"#546E7A", "#455A64", "#37474F", "#607D8B", "#78909C", "#90A4AE", "#757575", "#616161", "#424242", "#9E9E9E",
|
||||||
|
"#BDBDBD", "#EC407A", "#F06292", "#F48FB1", "#BA68C8", "#CE93D8", "#9575CD", "#B39DDB", "#7986CB", "#9FA8DA",
|
||||||
|
"#64B5F6", "#90CAF9", "#4FC3F7", "#81D4FA", "#4DD0E1", "#80DEEA", "#4DB6AC", "#80CBC4", "#81C784", "#A5D6A7",
|
||||||
|
"#C5E1A5", "#E6EE9C", "#FFF176", "#FFD54F", "#FFCC80", "#FFB74D", "#FFAB40", "#FF9E80", "#BCAAA4", "#A1887F"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static IReadOnlyList<string> GetColors() => FlatUiColorPalette;
|
public static IReadOnlyList<string> GetColors() => FlatUiColorPalette;
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
public sealed class WinoAccountSyncExportResult
|
||||||
|
{
|
||||||
|
public bool IncludedPreferences { get; init; }
|
||||||
|
public bool IncludedAccounts { get; init; }
|
||||||
|
public int ExportedMailboxCount { get; init; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
public sealed class WinoAccountSyncImportResult
|
||||||
|
{
|
||||||
|
public bool IncludedPreferences { get; init; }
|
||||||
|
public bool IncludedAccounts { get; init; }
|
||||||
|
public bool HadRemotePreferences { get; init; }
|
||||||
|
public int AppliedPreferenceCount { get; init; }
|
||||||
|
public int FailedPreferenceCount { get; init; }
|
||||||
|
public int ImportedMailboxCount { get; init; }
|
||||||
|
public int SkippedDuplicateMailboxCount { get; init; }
|
||||||
|
public int RemoteMailboxCount { get; init; }
|
||||||
|
|
||||||
|
public bool HasAnyRemoteData => HadRemotePreferences || RemoteMailboxCount > 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
public sealed record WinoAccountSyncSelection(
|
||||||
|
bool IncludePreferences = true,
|
||||||
|
bool IncludeAccounts = true);
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Navigation;
|
namespace Wino.Core.Domain.Models.Navigation;
|
||||||
|
|
||||||
public sealed class ShellModeActivationContext
|
public sealed class ShellModeActivationContext
|
||||||
{
|
{
|
||||||
public bool IsInitialActivation { get; init; }
|
public bool IsInitialActivation { get; init; }
|
||||||
public object Parameter { get; init; }
|
public bool SuppressStartupFlows { get; init; }
|
||||||
|
public object? Parameter { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,6 +104,9 @@
|
|||||||
"SyncAction_SettingFlag": "Flagging {0} mail(s)",
|
"SyncAction_SettingFlag": "Flagging {0} mail(s)",
|
||||||
"SyncAction_SynchronizingAccount": "Synchronizing {0}",
|
"SyncAction_SynchronizingAccount": "Synchronizing {0}",
|
||||||
"SyncAction_SynchronizingAccounts": "Synchronizing {0} account(s)",
|
"SyncAction_SynchronizingAccounts": "Synchronizing {0} account(s)",
|
||||||
|
"SyncAction_SynchronizingCalendarData": "Synchronizing calendar data",
|
||||||
|
"SyncAction_SynchronizingCalendarEvents": "Synchronizing calendar events",
|
||||||
|
"SyncAction_SynchronizingCalendarMetadata": "Synchronizing calendar metadata",
|
||||||
"SyncAction_Unarchiving": "Unarchiving {0} mail(s)",
|
"SyncAction_Unarchiving": "Unarchiving {0} mail(s)",
|
||||||
"CalendarAllDayEventSummary": "all-day events",
|
"CalendarAllDayEventSummary": "all-day events",
|
||||||
"CalendarDisplayOptions_Color": "Color",
|
"CalendarDisplayOptions_Color": "Color",
|
||||||
@@ -313,6 +316,8 @@
|
|||||||
"Exception_CustomThemeMissingName": "You must provide a name.",
|
"Exception_CustomThemeMissingName": "You must provide a name.",
|
||||||
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
|
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
|
||||||
"Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases",
|
"Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases",
|
||||||
|
"Exception_FailedToSynchronizeCalendarData": "Failed to synchronize calendar data",
|
||||||
|
"Exception_FailedToSynchronizeCalendarEvents": "Failed to synchronize calendar events",
|
||||||
"Exception_FailedToSynchronizeCalendarMetadata": "Failed to synchronize calendar details",
|
"Exception_FailedToSynchronizeCalendarMetadata": "Failed to synchronize calendar details",
|
||||||
"Exception_FailedToSynchronizeFolders": "Failed to synchronize folders",
|
"Exception_FailedToSynchronizeFolders": "Failed to synchronize folders",
|
||||||
"Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information",
|
"Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information",
|
||||||
@@ -505,6 +510,11 @@
|
|||||||
"Info_AccountDeletedMessage": "{0} is successfuly deleted.",
|
"Info_AccountDeletedMessage": "{0} is successfuly deleted.",
|
||||||
"Info_AccountDeletedTitle": "Account Deleted",
|
"Info_AccountDeletedTitle": "Account Deleted",
|
||||||
"Info_AccountIssueFixFailedTitle": "Failed",
|
"Info_AccountIssueFixFailedTitle": "Failed",
|
||||||
|
"Info_AccountIssueFixImapMessage": "Open the IMAP and calendar settings page to enter your server credentials again.",
|
||||||
|
"Info_AccountAttentionRequiredMessage": "This account needs your attention.",
|
||||||
|
"Info_AccountAttentionRequiredClickableMessage": "Click to fix this account and resynchronize it.",
|
||||||
|
"Info_AccountAttentionRequiredAction": "Fix",
|
||||||
|
"Info_AccountAttentionRequiredActionHint": "Click Fix to resolve this account issue.",
|
||||||
"Info_AccountIssueFixSuccessMessage": "Fixed all account issues.",
|
"Info_AccountIssueFixSuccessMessage": "Fixed all account issues.",
|
||||||
"Info_AccountIssueFixSuccessTitle": "Success",
|
"Info_AccountIssueFixSuccessTitle": "Success",
|
||||||
"Info_AttachmentOpenFailedMessage": "Can't open this attachment.",
|
"Info_AttachmentOpenFailedMessage": "Can't open this attachment.",
|
||||||
@@ -678,6 +688,9 @@
|
|||||||
"SettingConfigureSpecialFolders_Button": "Configure",
|
"SettingConfigureSpecialFolders_Button": "Configure",
|
||||||
"SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration",
|
"SettingsEditAccountDetails_IMAPConfiguration_Title": "IMAP/SMTP Configuration",
|
||||||
"SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.",
|
"SettingsEditAccountDetails_IMAPConfiguration_Description": "Change your incoming/outgoing server settings.",
|
||||||
|
"SettingsEditAccountDetails_ImapCalDavSettings_Title": "IMAP and calendar settings",
|
||||||
|
"SettingsEditAccountDetails_ImapCalDavSettings_Description": "Open the dedicated IMAP, SMTP, and CalDAV settings page for this account.",
|
||||||
|
"SettingsEditAccountDetails_ImapCalDavSettings_Action": "Open settings",
|
||||||
"SettingsAbout_Description": "Learn more about Wino.",
|
"SettingsAbout_Description": "Learn more about Wino.",
|
||||||
"SettingsAbout_Title": "About",
|
"SettingsAbout_Title": "About",
|
||||||
"SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.",
|
"SettingsAboutGithub_Description": "Go to issue tracker GitHub repository.",
|
||||||
@@ -1222,6 +1235,10 @@
|
|||||||
"WelcomeWindow_FeaturesTab": "Features",
|
"WelcomeWindow_FeaturesTab": "Features",
|
||||||
"WelcomeWindow_GetStartedButton": "Get started by adding an account",
|
"WelcomeWindow_GetStartedButton": "Get started by adding an account",
|
||||||
"WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.",
|
"WelcomeWindow_GetStartedDescription": "Add your Outlook, Gmail, or IMAP account to get started with Wino Mail.",
|
||||||
|
"WelcomeWindow_ImportFromWinoAccount": "Import from your Wino Account",
|
||||||
|
"WelcomeWindow_ImportInProgress": "Importing your synchronized preferences and accounts...",
|
||||||
|
"WelcomeWindow_ImportNoAccountsFound": "No synced accounts were found in your Wino Account. If preferences were available, they were restored. Use Get started to add an account manually.",
|
||||||
|
"WelcomeWindow_ImportDuplicateAccountsSkipped": "{0} synced accounts are already available on this device. Use Get started to add another account manually if needed.",
|
||||||
"WelcomeWindow_SetupTitle": "Set up your account",
|
"WelcomeWindow_SetupTitle": "Set up your account",
|
||||||
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
"WelcomeWindow_SetupSubtitle": "Choose your email provider to get started",
|
||||||
"WelcomeWindow_AddAccountButton": "Add account",
|
"WelcomeWindow_AddAccountButton": "Add account",
|
||||||
@@ -1233,7 +1250,7 @@
|
|||||||
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
|
"WinoAccount_Titlebar_SyncBenefitDescription": "Keep your Wino preferences in sync across devices.",
|
||||||
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
|
"WinoAccount_Titlebar_AddonsBenefitTitle": "Unlock add-ons",
|
||||||
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
|
"WinoAccount_Titlebar_AddonsBenefitDescription": "Access premium features like Wino AI Pack.",
|
||||||
"WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized settings.",
|
"WinoAccount_Management_Description": "Manage your Wino Account, AI Pack access, and synchronized preferences and account details.",
|
||||||
"WinoAccount_Management_SignedOutTitle": "Sign in to Wino Mail",
|
"WinoAccount_Management_SignedOutTitle": "Sign in to Wino Mail",
|
||||||
"WinoAccount_Management_SignedOutDescription": "Sign in or create an account to sync your email, access AI features, and manage your settings across devices.",
|
"WinoAccount_Management_SignedOutDescription": "Sign in or create an account to sync your email, access AI features, and manage your settings across devices.",
|
||||||
"WinoAccount_Management_ProfileSectionHeader": "Profile",
|
"WinoAccount_Management_ProfileSectionHeader": "Profile",
|
||||||
@@ -1273,18 +1290,31 @@
|
|||||||
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
|
"WinoAccount_Management_AiPackFeatureRewrite": "Rewrite",
|
||||||
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
|
"WinoAccount_Management_AiPackFeatureSummarize": "Summarize",
|
||||||
"WinoAccount_Management_AddOnLoadFailed": "We had issues loading this add-on.",
|
"WinoAccount_Management_AddOnLoadFailed": "We had issues loading this add-on.",
|
||||||
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences",
|
"WinoAccount_Management_SyncPreferencesTitle": "Synchronize Preferences and Accounts",
|
||||||
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your preferences to cloud. Import them across devices.",
|
"WinoAccount_Management_SyncPreferencesDescription": "Import or export your Wino preferences and mailbox details across devices. Passwords, tokens, and other sensitive information are never synced.",
|
||||||
"WinoAccount_Management_SignOutTitle": "Sign out",
|
"WinoAccount_Management_SignOutTitle": "Sign out",
|
||||||
"WinoAccount_Management_SignOutDescription": "Sign out of your account on this device",
|
"WinoAccount_Management_SignOutDescription": "Sign out of your account on this device",
|
||||||
"WinoAccount_Management_StatusLabel": "Status: {0}",
|
"WinoAccount_Management_StatusLabel": "Status: {0}",
|
||||||
"WinoAccount_Management_NoRemoteSettings": "There are no synchronized settings stored for this account yet.",
|
"WinoAccount_Management_NoRemoteSettings": "There is no synchronized data stored for this account yet.",
|
||||||
"WinoAccount_Management_ExportSucceeded": "Your settings were exported to your Wino Account.",
|
"WinoAccount_Management_ExportSucceeded": "Your selected Wino data was exported successfully.",
|
||||||
"WinoAccount_Management_ImportSucceeded": "Imported {0} settings from your Wino Account.",
|
"WinoAccount_Management_ExportPreferencesSucceeded": "Your preferences were exported to your Wino Account.",
|
||||||
"WinoAccount_Management_ImportPartial": "Imported {0} settings. {1} settings could not be restored.",
|
"WinoAccount_Management_ExportAccountsSucceeded": "Exported {0} account details to your Wino Account.",
|
||||||
|
"WinoAccount_Management_ImportSucceeded": "Imported synchronized data from your Wino Account.",
|
||||||
|
"WinoAccount_Management_ImportPreferencesSucceeded": "Applied {0} synchronized preferences.",
|
||||||
|
"WinoAccount_Management_ImportAccountsSucceeded": "Imported {0} accounts.",
|
||||||
|
"WinoAccount_Management_ImportDuplicateAccountsSkipped": "Skipped {0} accounts that already exist on this device.",
|
||||||
|
"WinoAccount_Management_ImportPartial": "Applied {0} synchronized preferences. {1} preferences could not be restored.",
|
||||||
|
"WinoAccount_Management_ImportReloginReminder": "Passwords, tokens, and other sensitive information were not imported. Sign in again for each account on this device before using it.",
|
||||||
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
|
"WinoAccount_Management_SerializeFailed": "Wino could not serialize your current preferences.",
|
||||||
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
|
"WinoAccount_Management_EmptyExport": "There are no preference values to export.",
|
||||||
"WinoAccount_Management_ImportEmpty": "The synchronized settings payload does not contain any values to restore.",
|
"WinoAccount_Management_ImportEmpty": "The synchronized data payload does not contain anything new to restore.",
|
||||||
|
"WinoAccount_Management_ExportDialog_Title": "Export to your Wino Account",
|
||||||
|
"WinoAccount_Management_ExportDialog_Description": "Choose what you want to sync to your Wino Account.",
|
||||||
|
"WinoAccount_Management_ExportDialog_IncludePreferences": "Preferences",
|
||||||
|
"WinoAccount_Management_ExportDialog_IncludeAccounts": "Accounts",
|
||||||
|
"WinoAccount_Management_ExportDialog_AccountsDisclaimer": "Passwords, tokens, and other sensitive information are not synced.",
|
||||||
|
"WinoAccount_Management_ExportDialog_AccountsRelogin": "Imported accounts on another PC will still need you to sign in again before they can be used.",
|
||||||
|
"WinoAccount_Management_ExportDialog_InProgress": "Exporting your selected Wino data...",
|
||||||
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
|
"WinoAccount_Management_LoadFailed": "Wino could not load the latest Wino Account information.",
|
||||||
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
|
"WinoAccount_Management_ActionFailed": "The Wino Account request could not be completed.",
|
||||||
"WinoAccount_SettingsSection_Title": "Wino Account",
|
"WinoAccount_SettingsSection_Title": "Wino Account",
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
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.Accounts;
|
||||||
|
using Wino.Core.Tests.Helpers;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
|
using Wino.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Wino.Core.Tests.Services;
|
||||||
|
|
||||||
|
public sealed class WinoAccountDataSyncServiceTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private InMemoryDatabaseService _databaseService = null!;
|
||||||
|
private Mock<IWinoAccountProfileService> _profileService = null!;
|
||||||
|
private Mock<IPreferencesService> _preferencesService = null!;
|
||||||
|
private AccountService _accountService = null!;
|
||||||
|
private WinoAccountDataSyncService _service = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_databaseService = new InMemoryDatabaseService();
|
||||||
|
await _databaseService.InitializeAsync();
|
||||||
|
|
||||||
|
_profileService = new Mock<IWinoAccountProfileService>(MockBehavior.Strict);
|
||||||
|
_preferencesService = new Mock<IPreferencesService>();
|
||||||
|
_preferencesService.SetupProperty(a => a.StartupEntityId);
|
||||||
|
|
||||||
|
_accountService = CreateAccountService(_databaseService, _preferencesService.Object);
|
||||||
|
_service = new WinoAccountDataSyncService(_profileService.Object, _preferencesService.Object, _accountService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync()
|
||||||
|
{
|
||||||
|
await _databaseService.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExportAsync_ImapMailbox_MapsSanitizedPayload()
|
||||||
|
{
|
||||||
|
var accountId = Guid.NewGuid();
|
||||||
|
|
||||||
|
await _accountService.CreateAccountAsync(
|
||||||
|
new MailAccount
|
||||||
|
{
|
||||||
|
Id = accountId,
|
||||||
|
Name = "Custom IMAP",
|
||||||
|
SenderName = "Custom IMAP Sender",
|
||||||
|
Address = "imap@example.com",
|
||||||
|
ProviderType = MailProviderType.IMAP4,
|
||||||
|
SpecialImapProvider = SpecialImapProvider.iCloud,
|
||||||
|
AccountColorHex = "#123456",
|
||||||
|
IsCalendarAccessGranted = true,
|
||||||
|
SynchronizationDeltaIdentifier = "delta-token",
|
||||||
|
CalendarSynchronizationDeltaIdentifier = "calendar-delta",
|
||||||
|
Base64ProfilePictureData = "profile"
|
||||||
|
},
|
||||||
|
new CustomServerInformation
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccountId = accountId,
|
||||||
|
Address = "imap@example.com",
|
||||||
|
IncomingServer = "imap.example.com",
|
||||||
|
IncomingServerPort = "993",
|
||||||
|
IncomingServerUsername = "imap-user",
|
||||||
|
IncomingServerPassword = "secret-incoming",
|
||||||
|
IncomingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||||
|
IncomingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword,
|
||||||
|
OutgoingServer = "smtp.example.com",
|
||||||
|
OutgoingServerPort = "465",
|
||||||
|
OutgoingServerUsername = "smtp-user",
|
||||||
|
OutgoingServerPassword = "secret-outgoing",
|
||||||
|
OutgoingServerSocketOption = ImapConnectionSecurity.Auto,
|
||||||
|
OutgoingAuthenticationMethod = ImapAuthenticationMethod.NormalPassword,
|
||||||
|
CalendarSupportMode = ImapCalendarSupportMode.CalDav,
|
||||||
|
CalDavServiceUrl = "https://dav.example.com",
|
||||||
|
CalDavUsername = "dav-user",
|
||||||
|
CalDavPassword = "secret-caldav",
|
||||||
|
ProxyServer = "proxy.example.com",
|
||||||
|
ProxyServerPort = "8080",
|
||||||
|
MaxConcurrentClients = 7
|
||||||
|
});
|
||||||
|
|
||||||
|
ReplaceUserMailboxesRequestDto? capturedRequest = null;
|
||||||
|
_profileService
|
||||||
|
.Setup(a => a.ReplaceMailboxesAsync(It.IsAny<ReplaceUserMailboxesRequestDto>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReplaceUserMailboxesRequestDto, CancellationToken>((request, _) => capturedRequest = request)
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
var result = await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||||
|
|
||||||
|
result.ExportedMailboxCount.Should().Be(1);
|
||||||
|
capturedRequest.Should().NotBeNull();
|
||||||
|
capturedRequest!.Mailboxes.Should().ContainSingle();
|
||||||
|
|
||||||
|
var exportedMailbox = capturedRequest.Mailboxes[0];
|
||||||
|
exportedMailbox.Address.Should().Be("imap@example.com");
|
||||||
|
exportedMailbox.ProviderType.Should().Be((int)MailProviderType.IMAP4);
|
||||||
|
exportedMailbox.SpecialImapProvider.Should().Be((int)SpecialImapProvider.iCloud);
|
||||||
|
exportedMailbox.AccountName.Should().Be("Custom IMAP");
|
||||||
|
exportedMailbox.SenderName.Should().Be("Custom IMAP Sender");
|
||||||
|
exportedMailbox.AccountColorHex.Should().Be("#123456");
|
||||||
|
exportedMailbox.IsCalendarAccessGranted.Should().BeTrue();
|
||||||
|
exportedMailbox.IncomingServer.Should().Be("imap.example.com");
|
||||||
|
exportedMailbox.IncomingServerUsername.Should().Be("imap-user");
|
||||||
|
exportedMailbox.OutgoingServer.Should().Be("smtp.example.com");
|
||||||
|
exportedMailbox.OutgoingServerUsername.Should().Be("smtp-user");
|
||||||
|
exportedMailbox.CalDavServiceUrl.Should().Be("https://dav.example.com");
|
||||||
|
exportedMailbox.CalDavUsername.Should().Be("dav-user");
|
||||||
|
exportedMailbox.ProxyServer.Should().Be("proxy.example.com");
|
||||||
|
exportedMailbox.ProxyServerPort.Should().Be("8080");
|
||||||
|
exportedMailbox.MaxConcurrentClients.Should().Be(7);
|
||||||
|
|
||||||
|
_profileService.Verify(a => a.SaveSettingsAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ExportAsync_GmailMailbox_DoesNotIncludeCustomServerSettings()
|
||||||
|
{
|
||||||
|
await _accountService.CreateAccountAsync(
|
||||||
|
new MailAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Gmail",
|
||||||
|
SenderName = "Gmail Sender",
|
||||||
|
Address = "gmail@example.com",
|
||||||
|
ProviderType = MailProviderType.Gmail
|
||||||
|
},
|
||||||
|
null!);
|
||||||
|
|
||||||
|
ReplaceUserMailboxesRequestDto? capturedRequest = null;
|
||||||
|
_profileService
|
||||||
|
.Setup(a => a.ReplaceMailboxesAsync(It.IsAny<ReplaceUserMailboxesRequestDto>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Callback<ReplaceUserMailboxesRequestDto, CancellationToken>((request, _) => capturedRequest = request)
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
await _service.ExportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||||
|
|
||||||
|
var exportedMailbox = capturedRequest!.Mailboxes.Single();
|
||||||
|
exportedMailbox.IncomingServer.Should().BeNull();
|
||||||
|
exportedMailbox.OutgoingServer.Should().BeNull();
|
||||||
|
exportedMailbox.CalDavServiceUrl.Should().BeNull();
|
||||||
|
exportedMailbox.MaxConcurrentClients.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ImportAsync_SkipsDuplicateMailbox_ByAddressAndProviderCaseInsensitive()
|
||||||
|
{
|
||||||
|
await _accountService.CreateAccountAsync(
|
||||||
|
new MailAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Existing Gmail",
|
||||||
|
SenderName = "Existing Gmail",
|
||||||
|
Address = "User@Example.com",
|
||||||
|
ProviderType = MailProviderType.Gmail
|
||||||
|
},
|
||||||
|
null!);
|
||||||
|
|
||||||
|
_profileService
|
||||||
|
.Setup(a => a.GetMailboxesAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new UserMailboxSyncListDto(
|
||||||
|
[
|
||||||
|
new UserMailboxSyncItemDto
|
||||||
|
{
|
||||||
|
Address = "user@example.com",
|
||||||
|
ProviderType = (int)MailProviderType.Gmail,
|
||||||
|
AccountName = "Duplicate Gmail"
|
||||||
|
},
|
||||||
|
new UserMailboxSyncItemDto
|
||||||
|
{
|
||||||
|
Address = "second@example.com",
|
||||||
|
ProviderType = (int)MailProviderType.Outlook,
|
||||||
|
AccountName = "New Outlook"
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
|
||||||
|
var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||||
|
|
||||||
|
result.ImportedMailboxCount.Should().Be(1);
|
||||||
|
result.SkippedDuplicateMailboxCount.Should().Be(1);
|
||||||
|
|
||||||
|
var accounts = await _accountService.GetAccountsAsync();
|
||||||
|
accounts.Should().HaveCount(2);
|
||||||
|
accounts.Should().Contain(a => a.Address == "second@example.com" && a.ProviderType == MailProviderType.Outlook);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ImportAsync_ImapMailbox_CreatesRootAliasAndInvalidCredentialsAttentionWithoutPasswords()
|
||||||
|
{
|
||||||
|
_profileService
|
||||||
|
.Setup(a => a.GetMailboxesAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new UserMailboxSyncListDto(
|
||||||
|
[
|
||||||
|
new UserMailboxSyncItemDto
|
||||||
|
{
|
||||||
|
Address = "imap@example.com",
|
||||||
|
ProviderType = (int)MailProviderType.IMAP4,
|
||||||
|
SpecialImapProvider = (int)SpecialImapProvider.Yahoo,
|
||||||
|
AccountName = "Imported IMAP",
|
||||||
|
SenderName = "Imported Sender",
|
||||||
|
CalendarSupportMode = (int)ImapCalendarSupportMode.CalDav,
|
||||||
|
IncomingServer = "imap.example.com",
|
||||||
|
IncomingServerPort = "993",
|
||||||
|
IncomingServerUsername = "imap-user",
|
||||||
|
IncomingServerSocketOption = (int)ImapConnectionSecurity.Auto,
|
||||||
|
IncomingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword,
|
||||||
|
OutgoingServer = "smtp.example.com",
|
||||||
|
OutgoingServerPort = "465",
|
||||||
|
OutgoingServerUsername = "smtp-user",
|
||||||
|
OutgoingServerSocketOption = (int)ImapConnectionSecurity.Auto,
|
||||||
|
OutgoingAuthenticationMethod = (int)ImapAuthenticationMethod.NormalPassword,
|
||||||
|
CalDavServiceUrl = "https://dav.example.com",
|
||||||
|
CalDavUsername = "dav-user",
|
||||||
|
MaxConcurrentClients = 9
|
||||||
|
}
|
||||||
|
]));
|
||||||
|
|
||||||
|
var result = await _service.ImportAsync(new WinoAccountSyncSelection(IncludePreferences: false, IncludeAccounts: true));
|
||||||
|
|
||||||
|
result.ImportedMailboxCount.Should().Be(1);
|
||||||
|
|
||||||
|
var importedAccount = (await _accountService.GetAccountsAsync()).Single();
|
||||||
|
importedAccount.AttentionReason.Should().Be(AccountAttentionReason.InvalidCredentials);
|
||||||
|
importedAccount.SynchronizationDeltaIdentifier.Should().BeEmpty();
|
||||||
|
importedAccount.CalendarSynchronizationDeltaIdentifier.Should().BeEmpty();
|
||||||
|
|
||||||
|
var importedAliases = await _accountService.GetAccountAliasesAsync(importedAccount.Id);
|
||||||
|
importedAliases.Should().ContainSingle(a => a.IsRootAlias && a.IsPrimary && a.AliasAddress == "imap@example.com");
|
||||||
|
|
||||||
|
var serverInformation = await _accountService.GetAccountCustomServerInformationAsync(importedAccount.Id);
|
||||||
|
serverInformation.Should().NotBeNull();
|
||||||
|
serverInformation.IncomingServerPassword.Should().BeEmpty();
|
||||||
|
serverInformation.OutgoingServerPassword.Should().BeEmpty();
|
||||||
|
serverInformation.CalDavPassword.Should().BeEmpty();
|
||||||
|
serverInformation.MaxConcurrentClients.Should().Be(9);
|
||||||
|
serverInformation.CalDavServiceUrl.Should().Be("https://dav.example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AccountService CreateAccountService(InMemoryDatabaseService databaseService, IPreferencesService preferencesService)
|
||||||
|
{
|
||||||
|
var signatureService = new Mock<ISignatureService>();
|
||||||
|
signatureService
|
||||||
|
.Setup(a => a.CreateDefaultSignatureAsync(It.IsAny<Guid>()))
|
||||||
|
.ReturnsAsync((Guid accountId) => new AccountSignature
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
MailAccountId = accountId,
|
||||||
|
Name = "Default",
|
||||||
|
HtmlBody = string.Empty
|
||||||
|
});
|
||||||
|
|
||||||
|
return new AccountService(
|
||||||
|
databaseService,
|
||||||
|
signatureService.Object,
|
||||||
|
Mock.Of<IAuthenticationProvider>(),
|
||||||
|
Mock.Of<IMimeFileService>(),
|
||||||
|
preferencesService,
|
||||||
|
Mock.Of<IContactPictureFileService>());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -232,11 +232,10 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
.ReturnsAsync(WinoAccountApiResult<AuthResultDto>.Success(authResult));
|
.ReturnsAsync(WinoAccountApiResult<AuthResultDto>.Success(authResult));
|
||||||
|
|
||||||
_apiClient
|
_apiClient
|
||||||
.Setup(x => x.SummarizeAsync("<p>Hello</p>", default))
|
.Setup(x => x.SummarizeAsync("<p>Hello</p>", "en", default))
|
||||||
.ReturnsAsync(ApiEnvelope<AiTextResultDto>.Success(
|
.ReturnsAsync(ApiEnvelope<AiTextResultDto>.Success(
|
||||||
new AiTextResultDto("<p>Summary</p>"),
|
new AiTextResultDto("<p>Summary</p>"),
|
||||||
new QuotaInfoDto(
|
new QuotaInfoDto(
|
||||||
true,
|
|
||||||
"Active",
|
"Active",
|
||||||
DateTimeOffset.UtcNow.AddDays(-1),
|
DateTimeOffset.UtcNow.AddDays(-1),
|
||||||
DateTimeOffset.UtcNow.AddDays(29),
|
DateTimeOffset.UtcNow.AddDays(29),
|
||||||
@@ -247,7 +246,7 @@ public class WinoAccountProfileServiceTests : IAsyncLifetime
|
|||||||
|
|
||||||
await _service.LoginAsync("first@example.com", "pw");
|
await _service.LoginAsync("first@example.com", "pw");
|
||||||
|
|
||||||
var response = await _service.SummarizeAsync("<p>Hello</p>");
|
var response = await _service.SummarizeAsync("<p>Hello</p>", "en");
|
||||||
|
|
||||||
response.IsSuccess.Should().BeTrue();
|
response.IsSuccess.Should().BeTrue();
|
||||||
response.Result?.Html.Should().Be("<p>Summary</p>");
|
response.Result?.Html.Should().Be("<p>Summary</p>");
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel
|
|||||||
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
|
Messenger.Send(new BreadcrumbNavigationRequested(Translator.WelcomeWizard_Step2Title, WinoPage.ProviderSelectionPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void NavigateToManageAccounts()
|
||||||
|
{
|
||||||
|
Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAccountSettings_Title, WinoPage.ManageAccountsPage));
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadDashboardAsync()
|
private async Task LoadDashboardAsync()
|
||||||
{
|
{
|
||||||
var accounts = (await _accountService.GetAccountsAsync().ConfigureAwait(false) ?? []).ToList();
|
var accounts = (await _accountService.GetAccountsAsync().ConfigureAwait(false) ?? []).ToList();
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.ViewModels.Data;
|
using Wino.Core.ViewModels.Data;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
@@ -22,6 +23,7 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
IRecipient<WinoAccountAddOnPurchasedMessage>
|
IRecipient<WinoAccountAddOnPurchasedMessage>
|
||||||
{
|
{
|
||||||
private readonly IWinoAccountProfileService _profileService;
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
|
private readonly IWinoAccountDataSyncService _syncService;
|
||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IStoreManagementService _storeManagementService;
|
private readonly IStoreManagementService _storeManagementService;
|
||||||
private readonly WinoAddOnItemViewModel _aiPackAddOn;
|
private readonly WinoAddOnItemViewModel _aiPackAddOn;
|
||||||
@@ -49,10 +51,12 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
public bool IsSignedOut => !IsSignedIn;
|
public bool IsSignedOut => !IsSignedIn;
|
||||||
|
|
||||||
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
public WinoAccountManagementPageViewModel(IWinoAccountProfileService profileService,
|
||||||
|
IWinoAccountDataSyncService syncService,
|
||||||
IMailDialogService dialogService,
|
IMailDialogService dialogService,
|
||||||
IStoreManagementService storeManagementService)
|
IStoreManagementService storeManagementService)
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
|
_syncService = syncService;
|
||||||
_dialogService = dialogService;
|
_dialogService = dialogService;
|
||||||
_storeManagementService = storeManagementService;
|
_storeManagementService = storeManagementService;
|
||||||
|
|
||||||
@@ -220,10 +224,69 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
=> addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress;
|
=> addOn != null && !addOn.IsPurchased && !addOn.IsLoading && !IsCheckoutInProgress;
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ExportSettingsAsync() => Task.CompletedTask;
|
private async Task ExportSettingsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _dialogService.ShowWinoAccountExportDialogAsync().ConfigureAwait(false);
|
||||||
|
if (result == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.GeneralTitle_Info,
|
||||||
|
BuildExportSuccessMessage(result),
|
||||||
|
InfoBarMessageType.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.GeneralTitle_Error,
|
||||||
|
ex.Message,
|
||||||
|
InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ImportSettingsAsync() => Task.CompletedTask;
|
private async Task ImportSettingsAsync()
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() => IsBusy = true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _syncService.ImportAsync(new WinoAccountSyncSelection());
|
||||||
|
|
||||||
|
if (!result.HasAnyRemoteData)
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.GeneralTitle_Info,
|
||||||
|
Translator.WinoAccount_Management_NoRemoteSettings,
|
||||||
|
InfoBarMessageType.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var messageType = result.FailedPreferenceCount > 0
|
||||||
|
? InfoBarMessageType.Warning
|
||||||
|
: InfoBarMessageType.Success;
|
||||||
|
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
result.FailedPreferenceCount > 0 ? Translator.GeneralTitle_Warning : Translator.GeneralTitle_Info,
|
||||||
|
BuildImportMessage(result),
|
||||||
|
messageType);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.GeneralTitle_Error,
|
||||||
|
ex.Message,
|
||||||
|
InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() => IsBusy = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void RegisterRecipients()
|
protected override void RegisterRecipients()
|
||||||
{
|
{
|
||||||
@@ -392,6 +455,62 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
|||||||
_ => Translator.WinoAccount_Management_StoreSyncFailed
|
_ => Translator.WinoAccount_Management_StoreSyncFailed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static string BuildExportSuccessMessage(WinoAccountSyncExportResult result)
|
||||||
|
{
|
||||||
|
var parts = new Collection<string>();
|
||||||
|
|
||||||
|
if (result.IncludedPreferences)
|
||||||
|
{
|
||||||
|
parts.Add(Translator.WinoAccount_Management_ExportPreferencesSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.IncludedAccounts)
|
||||||
|
{
|
||||||
|
parts.Add(string.Format(Translator.WinoAccount_Management_ExportAccountsSucceeded, result.ExportedMailboxCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.Count == 0)
|
||||||
|
{
|
||||||
|
parts.Add(Translator.WinoAccount_Management_ExportSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildImportMessage(WinoAccountSyncImportResult result)
|
||||||
|
{
|
||||||
|
var parts = new Collection<string>();
|
||||||
|
|
||||||
|
if (result.HadRemotePreferences)
|
||||||
|
{
|
||||||
|
parts.Add(result.FailedPreferenceCount > 0
|
||||||
|
? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount)
|
||||||
|
: string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ImportedMailboxCount > 0)
|
||||||
|
{
|
||||||
|
parts.Add(string.Format(Translator.WinoAccount_Management_ImportAccountsSucceeded, result.ImportedMailboxCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.SkippedDuplicateMailboxCount > 0)
|
||||||
|
{
|
||||||
|
parts.Add(string.Format(Translator.WinoAccount_Management_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.Count == 0)
|
||||||
|
{
|
||||||
|
parts.Add(Translator.WinoAccount_Management_ImportEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.ImportedMailboxCount > 0)
|
||||||
|
{
|
||||||
|
parts.Add(Translator.WinoAccount_Management_ImportReloginReminder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(" ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsAccessTokenExpired(WinoAccount account)
|
private static bool IsAccessTokenExpired(WinoAccount account)
|
||||||
=> string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow;
|
=> string.IsNullOrWhiteSpace(account.AccessToken) || account.AccessTokenExpiresAtUtc <= DateTime.UtcNow;
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public static class CoreContainerSetup
|
|||||||
services.AddTransient<OutlookRateLimitHandler>();
|
services.AddTransient<OutlookRateLimitHandler>();
|
||||||
|
|
||||||
// Register Gmail error handlers
|
// Register Gmail error handlers
|
||||||
|
services.AddTransient<GmailAuthenticationFailedHandler>();
|
||||||
services.AddTransient<GmailQuotaExceededHandler>();
|
services.AddTransient<GmailQuotaExceededHandler>();
|
||||||
services.AddTransient<GmailRateLimitHandler>();
|
services.AddTransient<GmailRateLimitHandler>();
|
||||||
services.AddTransient<GmailHistoryExpiredHandler>();
|
services.AddTransient<GmailHistoryExpiredHandler>();
|
||||||
@@ -56,6 +57,9 @@ public static class CoreContainerSetup
|
|||||||
services.AddTransient<ImapFolderNotFoundHandler>();
|
services.AddTransient<ImapFolderNotFoundHandler>();
|
||||||
services.AddTransient<ImapProtocolErrorHandler>();
|
services.AddTransient<ImapProtocolErrorHandler>();
|
||||||
|
|
||||||
|
// Register Outlook auth handlers
|
||||||
|
services.AddTransient<OutlookAuthenticationFailedHandler>();
|
||||||
|
|
||||||
// Register error handler factories
|
// Register error handler factories
|
||||||
services.AddTransient<IOutlookSynchronizerErrorHandlerFactory, OutlookSynchronizerErrorHandlingFactory>();
|
services.AddTransient<IOutlookSynchronizerErrorHandlerFactory, OutlookSynchronizerErrorHandlingFactory>();
|
||||||
services.AddTransient<IGmailSynchronizerErrorHandlerFactory, GmailSynchronizerErrorHandlingFactory>();
|
services.AddTransient<IGmailSynchronizerErrorHandlerFactory, GmailSynchronizerErrorHandlingFactory>();
|
||||||
|
|||||||
@@ -147,10 +147,8 @@ public static class GoogleIntegratorExtensions
|
|||||||
// Bg color must present. Generate one if doesnt exists.
|
// Bg color must present. Generate one if doesnt exists.
|
||||||
// Text color is optional. It'll be overriden by UI for readibility.
|
// Text color is optional. It'll be overriden by UI for readibility.
|
||||||
|
|
||||||
calendar.BackgroundColorHex = string.IsNullOrEmpty(calendarListEntry.BackgroundColor)
|
calendar.BackgroundColorHex = fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex();
|
||||||
? fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex()
|
calendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(calendar.BackgroundColorHex);
|
||||||
: calendarListEntry.BackgroundColor;
|
|
||||||
calendar.TextColorHex = string.IsNullOrEmpty(calendarListEntry.ForegroundColor) ? "#000000" : calendarListEntry.ForegroundColor;
|
|
||||||
|
|
||||||
return calendar;
|
return calendar;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -191,10 +191,8 @@ public static class OutlookIntegratorExtensions
|
|||||||
// Bg must be present. Generate flat one if doesn't exists.
|
// Bg must be present. Generate flat one if doesn't exists.
|
||||||
// Text doesnt exists for Outlook.
|
// Text doesnt exists for Outlook.
|
||||||
|
|
||||||
calendar.BackgroundColorHex = string.IsNullOrEmpty(outlookCalendar.HexColor)
|
calendar.BackgroundColorHex = fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex();
|
||||||
? fallbackBackgroundColor ?? ColorHelpers.GenerateFlatColorHex()
|
calendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(calendar.BackgroundColorHex);
|
||||||
: outlookCalendar.HexColor;
|
|
||||||
calendar.TextColorHex = "#000000";
|
|
||||||
|
|
||||||
return calendar;
|
return calendar;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Wino.Core.Domain.Misc;
|
using Wino.Core.Domain.Misc;
|
||||||
|
|
||||||
@@ -12,9 +13,21 @@ public static class ColorHelpers
|
|||||||
|
|
||||||
public static string GenerateFlatColorHex() => GetDistinctFlatColorHex(Array.Empty<string>());
|
public static string GenerateFlatColorHex() => GetDistinctFlatColorHex(Array.Empty<string>());
|
||||||
|
|
||||||
public static string GetDistinctFlatColorHex(IEnumerable<string> usedColors)
|
public static string GetDistinctFlatColorHex(IEnumerable<string> usedColors, string preferredColor = null)
|
||||||
{
|
{
|
||||||
var palette = CalendarColorPalette.GetColors();
|
var palette = CalendarColorPalette.GetColors();
|
||||||
|
var normalizedUsedColors = usedColors?
|
||||||
|
.Select(NormalizeHexColor)
|
||||||
|
.Where(color => !string.IsNullOrWhiteSpace(color))
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (TryNormalizeHexColor(preferredColor, out var normalizedPreferred) &&
|
||||||
|
palette.Contains(normalizedPreferred, StringComparer.OrdinalIgnoreCase) &&
|
||||||
|
!normalizedUsedColors.Contains(normalizedPreferred))
|
||||||
|
{
|
||||||
|
return normalizedPreferred;
|
||||||
|
}
|
||||||
|
|
||||||
var distinctColor = CalendarColorPalette.GetDistinctColor(usedColors);
|
var distinctColor = CalendarColorPalette.GetDistinctColor(usedColors);
|
||||||
if (palette.Contains(distinctColor))
|
if (palette.Contains(distinctColor))
|
||||||
{
|
{
|
||||||
@@ -26,6 +39,18 @@ public static class ColorHelpers
|
|||||||
return candidate;
|
return candidate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string GetReadableTextColorHex(string backgroundColor)
|
||||||
|
{
|
||||||
|
if (!TryNormalizeHexColor(backgroundColor, out var normalizedColor))
|
||||||
|
{
|
||||||
|
return "#FFFFFF";
|
||||||
|
}
|
||||||
|
|
||||||
|
var color = ColorTranslator.FromHtml(normalizedColor);
|
||||||
|
var luminance = ((0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B)) / 255d;
|
||||||
|
return luminance > 0.6 ? "#111111" : "#FFFFFF";
|
||||||
|
}
|
||||||
|
|
||||||
public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
|
public static string ToHexString(this Color c) => $"#{c.R:X2}{c.G:X2}{c.B:X2}";
|
||||||
|
|
||||||
public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})";
|
public static string ToRgbString(this Color c) => $"RGB({c.R}, {c.G}, {c.B})";
|
||||||
@@ -41,4 +66,35 @@ public static class ColorHelpers
|
|||||||
|
|
||||||
return adjusted.ToHexString();
|
return adjusted.ToHexString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeHexColor(string value, out string normalized)
|
||||||
|
{
|
||||||
|
normalized = string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var color = value.Trim();
|
||||||
|
if (color.StartsWith('#'))
|
||||||
|
{
|
||||||
|
color = color[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (color.Length != 6)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!int.TryParse(color, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized = $"#{color.ToUpperInvariant()}";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeHexColor(string value)
|
||||||
|
=> TryNormalizeHexColor(value, out var normalized) ? normalized : string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ namespace Wino.Core.Services;
|
|||||||
public class GmailSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IGmailSynchronizerErrorHandlerFactory
|
public class GmailSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IGmailSynchronizerErrorHandlerFactory
|
||||||
{
|
{
|
||||||
public GmailSynchronizerErrorHandlingFactory(
|
public GmailSynchronizerErrorHandlingFactory(
|
||||||
|
GmailAuthenticationFailedHandler authenticationFailedHandler,
|
||||||
GmailQuotaExceededHandler quotaExceededHandler,
|
GmailQuotaExceededHandler quotaExceededHandler,
|
||||||
GmailRateLimitHandler rateLimitHandler,
|
GmailRateLimitHandler rateLimitHandler,
|
||||||
GmailHistoryExpiredHandler historyExpiredHandler,
|
GmailHistoryExpiredHandler historyExpiredHandler,
|
||||||
EntityNotFoundHandler entityNotFoundHandler)
|
EntityNotFoundHandler entityNotFoundHandler)
|
||||||
{
|
{
|
||||||
// Order matters - more specific handlers should be registered first
|
// Order matters - more specific handlers should be registered first
|
||||||
|
RegisterHandler(authenticationFailedHandler);
|
||||||
RegisterHandler(quotaExceededHandler);
|
RegisterHandler(quotaExceededHandler);
|
||||||
RegisterHandler(historyExpiredHandler);
|
RegisterHandler(historyExpiredHandler);
|
||||||
RegisterHandler(entityNotFoundHandler);
|
RegisterHandler(entityNotFoundHandler);
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ namespace Wino.Core.Services;
|
|||||||
|
|
||||||
public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory
|
public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory
|
||||||
{
|
{
|
||||||
public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted,
|
public OutlookSynchronizerErrorHandlingFactory(OutlookAuthenticationFailedHandler authenticationFailedHandler,
|
||||||
|
ObjectCannotBeDeletedHandler objectCannotBeDeleted,
|
||||||
EntityNotFoundHandler entityNotFoundHandler,
|
EntityNotFoundHandler entityNotFoundHandler,
|
||||||
DeltaTokenExpiredHandler deltaTokenExpiredHandler,
|
DeltaTokenExpiredHandler deltaTokenExpiredHandler,
|
||||||
OutlookRateLimitHandler outlookRateLimitHandler)
|
OutlookRateLimitHandler outlookRateLimitHandler)
|
||||||
{
|
{
|
||||||
|
RegisterHandler(authenticationFailedHandler);
|
||||||
RegisterHandler(outlookRateLimitHandler);
|
RegisterHandler(outlookRateLimitHandler);
|
||||||
RegisterHandler(objectCannotBeDeleted);
|
RegisterHandler(objectCannotBeDeleted);
|
||||||
RegisterHandler(entityNotFoundHandler);
|
RegisterHandler(entityNotFoundHandler);
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
@@ -13,6 +15,7 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Authentication;
|
using Wino.Core.Domain.Models.Authentication;
|
||||||
using Wino.Core.Domain.Models.Connectivity;
|
using Wino.Core.Domain.Models.Connectivity;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Core.Services;
|
namespace Wino.Core.Services;
|
||||||
|
|
||||||
@@ -27,6 +30,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
|
|
||||||
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
||||||
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> _accountSynchronizationCancellationSources = new();
|
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> _accountSynchronizationCancellationSources = new();
|
||||||
|
private readonly ConcurrentDictionary<Guid, SemaphoreSlim> _calendarSynchronizationLocks = new();
|
||||||
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||||
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
||||||
|
|
||||||
@@ -131,6 +135,12 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
|
|
||||||
|
if (await IsSynchronizationBlockedByAttentionAsync(options.AccountId).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.Information("Skipping mail synchronization for account {AccountId} because it requires credential attention.", options.AccountId);
|
||||||
|
return MailSynchronizationResult.Canceled;
|
||||||
|
}
|
||||||
|
|
||||||
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
|
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
|
||||||
if (synchronizer == null)
|
if (synchronizer == null)
|
||||||
{
|
{
|
||||||
@@ -170,6 +180,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
catch (AuthenticationAttentionException authEx)
|
catch (AuthenticationAttentionException authEx)
|
||||||
{
|
{
|
||||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
||||||
|
await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false);
|
||||||
|
|
||||||
// Create app notification for authentication attention
|
// Create app notification for authentication attention
|
||||||
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
|
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
|
||||||
@@ -348,9 +359,75 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
/// <returns>Synchronization result</returns>
|
/// <returns>Synchronization result</returns>
|
||||||
public async Task<CalendarSynchronizationResult> SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
|
public async Task<CalendarSynchronizationResult> SynchronizeCalendarAsync(CalendarSynchronizationOptions options,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
|
=> options.Type == CalendarSynchronizationType.Strict
|
||||||
|
? await SynchronizeCalendarStrictAsync(options, cancellationToken).ConfigureAwait(false)
|
||||||
|
: await RunCalendarSynchronizationWithLockAsync(
|
||||||
|
options.AccountId,
|
||||||
|
cancellationToken,
|
||||||
|
() => SynchronizeCalendarCoreAsync(options, cancellationToken, reportState: true)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
private async Task<CalendarSynchronizationResult> SynchronizeCalendarStrictAsync(
|
||||||
|
CalendarSynchronizationOptions options,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var metadataOptions = new CalendarSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = options.AccountId,
|
||||||
|
Type = CalendarSynchronizationType.CalendarMetadata,
|
||||||
|
SynchronizationCalendarIds = options.SynchronizationCalendarIds
|
||||||
|
};
|
||||||
|
|
||||||
|
var eventOptions = new CalendarSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = options.AccountId,
|
||||||
|
Type = CalendarSynchronizationType.CalendarEvents,
|
||||||
|
SynchronizationCalendarIds = options.SynchronizationCalendarIds
|
||||||
|
};
|
||||||
|
|
||||||
|
return await RunCalendarSynchronizationWithLockAsync(options.AccountId, cancellationToken, async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PublishCalendarSynchronizationState(
|
||||||
|
options.AccountId,
|
||||||
|
CalendarSynchronizationType.Strict,
|
||||||
|
isSynchronizationInProgress: true,
|
||||||
|
Translator.SyncAction_SynchronizingCalendarMetadata);
|
||||||
|
|
||||||
|
var metadataResult = await SynchronizeCalendarCoreAsync(metadataOptions, cancellationToken, reportState: false).ConfigureAwait(false);
|
||||||
|
if (metadataResult.CompletedState is SynchronizationCompletedState.Failed or SynchronizationCompletedState.Canceled)
|
||||||
|
{
|
||||||
|
return metadataResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishCalendarSynchronizationState(
|
||||||
|
options.AccountId,
|
||||||
|
CalendarSynchronizationType.Strict,
|
||||||
|
isSynchronizationInProgress: true,
|
||||||
|
Translator.SyncAction_SynchronizingCalendarEvents);
|
||||||
|
|
||||||
|
return await SynchronizeCalendarCoreAsync(eventOptions, cancellationToken, reportState: false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
PublishCalendarSynchronizationState(options.AccountId, CalendarSynchronizationType.Strict, isSynchronizationInProgress: false);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<CalendarSynchronizationResult> SynchronizeCalendarCoreAsync(
|
||||||
|
CalendarSynchronizationOptions options,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
bool reportState)
|
||||||
{
|
{
|
||||||
EnsureInitialized();
|
EnsureInitialized();
|
||||||
|
|
||||||
|
if (await IsSynchronizationBlockedByAttentionAsync(options.AccountId).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
_logger.Information("Skipping calendar synchronization for account {AccountId} because it requires credential attention.", options.AccountId);
|
||||||
|
return CalendarSynchronizationResult.Canceled;
|
||||||
|
}
|
||||||
|
|
||||||
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
|
var synchronizer = await GetOrCreateSynchronizerAsync(options.AccountId);
|
||||||
if (synchronizer == null)
|
if (synchronizer == null)
|
||||||
{
|
{
|
||||||
@@ -361,6 +438,15 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
_logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
|
_logger.Information("Starting calendar synchronization for account {AccountId} with type {SyncType}",
|
||||||
options.AccountId, options.Type);
|
options.AccountId, options.Type);
|
||||||
|
|
||||||
|
if (reportState)
|
||||||
|
{
|
||||||
|
PublishCalendarSynchronizationState(
|
||||||
|
options.AccountId,
|
||||||
|
options.Type,
|
||||||
|
isSynchronizationInProgress: true,
|
||||||
|
GetCalendarSynchronizationStatus(options.Type));
|
||||||
|
}
|
||||||
|
|
||||||
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
var accountCancellationSource = _accountSynchronizationCancellationSources.GetOrAdd(options.AccountId, _ => new CancellationTokenSource());
|
||||||
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
@@ -387,6 +473,7 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
catch (AuthenticationAttentionException authEx)
|
catch (AuthenticationAttentionException authEx)
|
||||||
{
|
{
|
||||||
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
_logger.Warning("Account {AccountId} requires attention due to authentication issues", options.AccountId);
|
||||||
|
await SetInvalidCredentialAttentionAsync(authEx.Account).ConfigureAwait(false);
|
||||||
|
|
||||||
// Create app notification for authentication attention
|
// Create app notification for authentication attention
|
||||||
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
|
_notificationBuilder.CreateAttentionRequiredNotification(authEx.Account);
|
||||||
@@ -398,6 +485,13 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
_logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId);
|
_logger.Error(ex, "Calendar synchronization failed for account {AccountId}", options.AccountId);
|
||||||
return CalendarSynchronizationResult.Failed;
|
return CalendarSynchronizationResult.Failed;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (reportState)
|
||||||
|
{
|
||||||
|
PublishCalendarSynchronizationState(options.AccountId, options.Type, isSynchronizationInProgress: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -667,4 +761,69 @@ public class SynchronizationManager : ISynchronizationManager
|
|||||||
throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first.");
|
throw new InvalidOperationException("SynchronizationManager must be initialized before use. Call InitializeAsync first.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SetInvalidCredentialAttentionAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
if (account == null || _accountService == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (persistedAccount == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||||
|
return;
|
||||||
|
|
||||||
|
persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials;
|
||||||
|
await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsSynchronizationBlockedByAttentionAsync(Guid accountId)
|
||||||
|
{
|
||||||
|
if (_accountService == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||||
|
return account?.AttentionReason == AccountAttentionReason.InvalidCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PublishCalendarSynchronizationState(
|
||||||
|
Guid accountId,
|
||||||
|
CalendarSynchronizationType synchronizationType,
|
||||||
|
bool isSynchronizationInProgress,
|
||||||
|
string synchronizationStatus = "")
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new AccountCalendarSynchronizationStateChanged(
|
||||||
|
accountId,
|
||||||
|
synchronizationType,
|
||||||
|
isSynchronizationInProgress,
|
||||||
|
synchronizationStatus));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCalendarSynchronizationStatus(CalendarSynchronizationType synchronizationType)
|
||||||
|
=> synchronizationType switch
|
||||||
|
{
|
||||||
|
CalendarSynchronizationType.CalendarMetadata => Translator.SyncAction_SynchronizingCalendarMetadata,
|
||||||
|
CalendarSynchronizationType.Strict => Translator.SyncAction_SynchronizingCalendarData,
|
||||||
|
_ => Translator.SyncAction_SynchronizingCalendarEvents
|
||||||
|
};
|
||||||
|
|
||||||
|
private async Task<CalendarSynchronizationResult> RunCalendarSynchronizationWithLockAsync(
|
||||||
|
Guid accountId,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Func<Task<CalendarSynchronizationResult>> synchronizationFactory)
|
||||||
|
{
|
||||||
|
var calendarSemaphore = _calendarSynchronizationLocks.GetOrAdd(accountId, _ => new SemaphoreSlim(1, 1));
|
||||||
|
await calendarSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await synchronizationFactory().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
calendarSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Google;
|
||||||
|
using Serilog;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
|
||||||
|
namespace Wino.Core.Synchronizers.Errors.Gmail;
|
||||||
|
|
||||||
|
public class GmailAuthenticationFailedHandler : ISynchronizerErrorHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger = Log.ForContext<GmailAuthenticationFailedHandler>();
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
public GmailAuthenticationFailedHandler(IAccountService accountService)
|
||||||
|
{
|
||||||
|
_accountService = accountService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(SynchronizerErrorContext error)
|
||||||
|
{
|
||||||
|
if (error.Exception is not GoogleApiException googleEx)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var reason = googleEx.Error?.Errors?.FirstOrDefault()?.Reason?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
var message = googleEx.Message?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
|
||||||
|
return googleEx.HttpStatusCode == HttpStatusCode.Unauthorized ||
|
||||||
|
(googleEx.HttpStatusCode == HttpStatusCode.Forbidden &&
|
||||||
|
(reason.Contains("auth") ||
|
||||||
|
reason.Contains("credential") ||
|
||||||
|
message.Contains("invalid credentials") ||
|
||||||
|
message.Contains("insufficient authentication") ||
|
||||||
|
message.Contains("login required")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
|
||||||
|
{
|
||||||
|
_logger.Warning(error.Exception,
|
||||||
|
"Gmail authentication failed for account {AccountName} ({AccountId}). User intervention is required.",
|
||||||
|
error.Account?.Name, error.Account?.Id);
|
||||||
|
|
||||||
|
if (error.Account != null)
|
||||||
|
{
|
||||||
|
await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.Severity = SynchronizerErrorSeverity.AuthRequired;
|
||||||
|
error.Category = SynchronizerErrorCategory.Authentication;
|
||||||
|
error.RetryDelay = null;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistInvalidCredentialAttentionAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (persistedAccount == null || persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||||
|
return;
|
||||||
|
|
||||||
|
persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials;
|
||||||
|
await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MailKit.Security;
|
using MailKit.Security;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
@@ -14,6 +15,12 @@ namespace Wino.Core.Synchronizers.Errors.Imap;
|
|||||||
public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler
|
public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger = Log.ForContext<ImapAuthenticationFailedHandler>();
|
private readonly ILogger _logger = Log.ForContext<ImapAuthenticationFailedHandler>();
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
public ImapAuthenticationFailedHandler(IAccountService accountService)
|
||||||
|
{
|
||||||
|
_accountService = accountService;
|
||||||
|
}
|
||||||
|
|
||||||
public bool CanHandle(SynchronizerErrorContext error)
|
public bool CanHandle(SynchronizerErrorContext error)
|
||||||
{
|
{
|
||||||
@@ -22,12 +29,17 @@ public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler
|
|||||||
(error.ErrorMessage?.Contains("authentication", System.StringComparison.OrdinalIgnoreCase) ?? false);
|
(error.ErrorMessage?.Contains("authentication", System.StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<bool> HandleAsync(SynchronizerErrorContext error)
|
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
|
||||||
{
|
{
|
||||||
_logger.Warning(error.Exception,
|
_logger.Warning(error.Exception,
|
||||||
"IMAP authentication failed for account {AccountName} ({AccountId}). User needs to re-authenticate.",
|
"IMAP authentication failed for account {AccountName} ({AccountId}). User needs to re-authenticate.",
|
||||||
error.Account?.Name, error.Account?.Id);
|
error.Account?.Name, error.Account?.Id);
|
||||||
|
|
||||||
|
if (error.Account != null)
|
||||||
|
{
|
||||||
|
await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark as requiring authentication - this will stop sync and notify user
|
// Mark as requiring authentication - this will stop sync and notify user
|
||||||
error.Severity = SynchronizerErrorSeverity.AuthRequired;
|
error.Severity = SynchronizerErrorSeverity.AuthRequired;
|
||||||
error.Category = SynchronizerErrorCategory.Authentication;
|
error.Category = SynchronizerErrorCategory.Authentication;
|
||||||
@@ -35,6 +47,20 @@ public class ImapAuthenticationFailedHandler : ISynchronizerErrorHandler
|
|||||||
// No point in retrying auth failures - credentials need to be updated
|
// No point in retrying auth failures - credentials need to be updated
|
||||||
error.RetryDelay = null;
|
error.RetryDelay = null;
|
||||||
|
|
||||||
return Task.FromResult(true);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistInvalidCredentialAttentionAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (persistedAccount == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||||
|
return;
|
||||||
|
|
||||||
|
persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials;
|
||||||
|
await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Graph.Models.ODataErrors;
|
||||||
|
using Microsoft.Kiota.Abstractions;
|
||||||
|
using Serilog;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
|
||||||
|
namespace Wino.Core.Synchronizers.Errors.Outlook;
|
||||||
|
|
||||||
|
public class OutlookAuthenticationFailedHandler : ISynchronizerErrorHandler
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger = Log.ForContext<OutlookAuthenticationFailedHandler>();
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
public OutlookAuthenticationFailedHandler(IAccountService accountService)
|
||||||
|
{
|
||||||
|
_accountService = accountService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanHandle(SynchronizerErrorContext error)
|
||||||
|
{
|
||||||
|
if (error.Exception is ApiException apiException)
|
||||||
|
{
|
||||||
|
if (apiException.ResponseStatusCode == 401)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (apiException.ResponseStatusCode == 403)
|
||||||
|
{
|
||||||
|
var message = apiException.Message?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
return message.Contains("access denied") || message.Contains("authentication");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.Exception is ODataError oDataError)
|
||||||
|
{
|
||||||
|
if (oDataError.ResponseStatusCode == 401)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var code = oDataError.Error?.Code?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
var message = oDataError.Error?.Message?.ToLowerInvariant() ?? string.Empty;
|
||||||
|
|
||||||
|
return code.Contains("invalidauthenticationtoken") ||
|
||||||
|
code.Contains("invalidgrant") ||
|
||||||
|
code.Contains("token") ||
|
||||||
|
message.Contains("access token") ||
|
||||||
|
message.Contains("authentication");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
|
||||||
|
{
|
||||||
|
_logger.Warning(error.Exception,
|
||||||
|
"Outlook authentication failed for account {AccountName} ({AccountId}). User intervention is required.",
|
||||||
|
error.Account?.Name, error.Account?.Id);
|
||||||
|
|
||||||
|
if (error.Account != null)
|
||||||
|
{
|
||||||
|
await PersistInvalidCredentialAttentionAsync(error.Account).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
error.Severity = SynchronizerErrorSeverity.AuthRequired;
|
||||||
|
error.Category = SynchronizerErrorCategory.Authentication;
|
||||||
|
error.RetryDelay = null;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PersistInvalidCredentialAttentionAsync(MailAccount account)
|
||||||
|
{
|
||||||
|
var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (persistedAccount == null || persistedAccount.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||||
|
return;
|
||||||
|
|
||||||
|
persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials;
|
||||||
|
await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -603,11 +603,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = await _gmailChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||||
var remotePrimaryCalendarId = GetPrimaryCalendarId(calendarListResponse.Items);
|
var remotePrimaryCalendarId = GetPrimaryCalendarId(calendarListResponse.Items);
|
||||||
var usedCalendarColors = new HashSet<string>(
|
var usedCalendarColors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
localCalendars
|
|
||||||
.Select(a => a.BackgroundColorHex)
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a)),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
List<AccountCalendar> insertedCalendars = new();
|
List<AccountCalendar> insertedCalendars = new();
|
||||||
List<AccountCalendar> updatedCalendars = new();
|
List<AccountCalendar> updatedCalendars = new();
|
||||||
@@ -637,25 +633,25 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
if (existingLocalCalendar == null)
|
if (existingLocalCalendar == null)
|
||||||
{
|
{
|
||||||
// Insert new calendar.
|
// Insert new calendar.
|
||||||
var fallbackColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors);
|
var fallbackColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, calendar.BackgroundColor);
|
||||||
var localCalendar = calendar.AsCalendar(Account.Id, fallbackColor);
|
var localCalendar = calendar.AsCalendar(Account.Id, fallbackColor);
|
||||||
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
if (string.IsNullOrWhiteSpace(localCalendar.BackgroundColorHex) || usedCalendarColors.Contains(localCalendar.BackgroundColorHex))
|
localCalendar.BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, localCalendar.BackgroundColorHex);
|
||||||
localCalendar.BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors);
|
localCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(localCalendar.BackgroundColorHex);
|
||||||
usedCalendarColors.Add(localCalendar.BackgroundColorHex);
|
usedCalendarColors.Add(localCalendar.BackgroundColorHex);
|
||||||
insertedCalendars.Add(localCalendar);
|
insertedCalendars.Add(localCalendar);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing calendar. Right now we only update the name.
|
// Update existing calendar. Right now we only update the name.
|
||||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
var resolvedColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, existingLocalCalendar.BackgroundColorHex);
|
||||||
|
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId) ||
|
||||||
|
!string.Equals(existingLocalCalendar.BackgroundColorHex, resolvedColor, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
existingLocalCalendar.Name = calendar.Summary;
|
existingLocalCalendar.Name = calendar.Summary;
|
||||||
existingLocalCalendar.TimeZone = calendar.TimeZone;
|
existingLocalCalendar.TimeZone = calendar.TimeZone;
|
||||||
if (!string.IsNullOrEmpty(calendar.BackgroundColor))
|
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
||||||
existingLocalCalendar.BackgroundColorHex = calendar.BackgroundColor;
|
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
||||||
if (!string.IsNullOrEmpty(calendar.ForegroundColor))
|
|
||||||
existingLocalCalendar.TextColorHex = calendar.ForegroundColor;
|
|
||||||
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
updatedCalendars.Add(existingLocalCalendar);
|
updatedCalendars.Add(existingLocalCalendar);
|
||||||
@@ -665,6 +661,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
// Remove it from the local folder list to skip additional calendar updates.
|
// Remove it from the local folder list to skip additional calendar updates.
|
||||||
localCalendars.Remove(existingLocalCalendar);
|
localCalendars.Remove(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usedCalendarColors.Add(resolvedColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1455,11 +1455,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
var remoteCalendarsById = remoteCalendars
|
var remoteCalendarsById = remoteCalendars
|
||||||
.GroupBy(c => c.RemoteCalendarId, StringComparer.OrdinalIgnoreCase)
|
.GroupBy(c => c.RemoteCalendarId, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
var usedCalendarColors = new HashSet<string>(
|
var usedCalendarColors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
localCalendars
|
|
||||||
.Select(a => a.BackgroundColorHex)
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a)),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var remotePrimaryCalendarId = remoteCalendars.FirstOrDefault()?.RemoteCalendarId;
|
var remotePrimaryCalendarId = remoteCalendars.FirstOrDefault()?.RemoteCalendarId;
|
||||||
|
|
||||||
@@ -1493,25 +1489,33 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
|||||||
IsPrimary = isPrimary,
|
IsPrimary = isPrimary,
|
||||||
IsSynchronizationEnabled = true,
|
IsSynchronizationEnabled = true,
|
||||||
IsExtended = true,
|
IsExtended = true,
|
||||||
TextColorHex = "#000000",
|
|
||||||
BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors),
|
BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors),
|
||||||
TimeZone = "UTC",
|
TimeZone = "UTC",
|
||||||
SynchronizationDeltaToken = string.Empty
|
SynchronizationDeltaToken = string.Empty
|
||||||
};
|
};
|
||||||
|
|
||||||
|
newCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(newCalendar.BackgroundColorHex);
|
||||||
usedCalendarColors.Add(newCalendar.BackgroundColorHex);
|
usedCalendarColors.Add(newCalendar.BackgroundColorHex);
|
||||||
await _imapChangeProcessor.InsertAccountCalendarAsync(newCalendar).ConfigureAwait(false);
|
await _imapChangeProcessor.InsertAccountCalendarAsync(newCalendar).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resolvedColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, existingLocal.BackgroundColorHex);
|
||||||
var shouldUpdate = !string.Equals(existingLocal.Name, remoteCalendar.Name, StringComparison.Ordinal)
|
var shouldUpdate = !string.Equals(existingLocal.Name, remoteCalendar.Name, StringComparison.Ordinal)
|
||||||
|| existingLocal.IsPrimary != isPrimary;
|
|| existingLocal.IsPrimary != isPrimary
|
||||||
|
|| !string.Equals(existingLocal.BackgroundColorHex, resolvedColor, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (!shouldUpdate)
|
if (!shouldUpdate)
|
||||||
|
{
|
||||||
|
usedCalendarColors.Add(resolvedColor);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
existingLocal.Name = remoteCalendar.Name;
|
existingLocal.Name = remoteCalendar.Name;
|
||||||
existingLocal.IsPrimary = isPrimary;
|
existingLocal.IsPrimary = isPrimary;
|
||||||
|
existingLocal.BackgroundColorHex = resolvedColor;
|
||||||
|
existingLocal.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocal.BackgroundColorHex);
|
||||||
|
usedCalendarColors.Add(existingLocal.BackgroundColorHex);
|
||||||
await _imapChangeProcessor.UpdateAccountCalendarAsync(existingLocal).ConfigureAwait(false);
|
await _imapChangeProcessor.UpdateAccountCalendarAsync(existingLocal).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2333,11 +2333,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
var remotePrimaryCalendarId = await GetPrimaryCalendarIdAsync(calendars.Value, cancellationToken).ConfigureAwait(false);
|
var remotePrimaryCalendarId = await GetPrimaryCalendarIdAsync(calendars.Value, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
var localCalendars = await _outlookChangeProcessor.GetAccountCalendarsAsync(Account.Id).ConfigureAwait(false);
|
||||||
var usedCalendarColors = new HashSet<string>(
|
var usedCalendarColors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
localCalendars
|
|
||||||
.Select(a => a.BackgroundColorHex)
|
|
||||||
.Where(a => !string.IsNullOrWhiteSpace(a)),
|
|
||||||
StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
List<AccountCalendar> insertedCalendars = new();
|
List<AccountCalendar> insertedCalendars = new();
|
||||||
List<AccountCalendar> updatedCalendars = new();
|
List<AccountCalendar> updatedCalendars = new();
|
||||||
@@ -2367,23 +2363,25 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
if (existingLocalCalendar == null)
|
if (existingLocalCalendar == null)
|
||||||
{
|
{
|
||||||
// Insert new calendar.
|
// Insert new calendar.
|
||||||
var fallbackColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors);
|
var fallbackColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, calendar.HexColor);
|
||||||
var localCalendar = calendar.AsCalendar(Account, fallbackColor);
|
var localCalendar = calendar.AsCalendar(Account, fallbackColor);
|
||||||
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
localCalendar.IsPrimary = string.Equals(localCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
if (string.IsNullOrWhiteSpace(localCalendar.BackgroundColorHex) || usedCalendarColors.Contains(localCalendar.BackgroundColorHex))
|
localCalendar.BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, localCalendar.BackgroundColorHex);
|
||||||
localCalendar.BackgroundColorHex = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors);
|
localCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(localCalendar.BackgroundColorHex);
|
||||||
usedCalendarColors.Add(localCalendar.BackgroundColorHex);
|
usedCalendarColors.Add(localCalendar.BackgroundColorHex);
|
||||||
insertedCalendars.Add(localCalendar);
|
insertedCalendars.Add(localCalendar);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Update existing calendar. Right now we only update the name.
|
// Update existing calendar. Right now we only update the name.
|
||||||
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId))
|
var resolvedColor = ColorHelpers.GetDistinctFlatColorHex(usedCalendarColors, existingLocalCalendar.BackgroundColorHex);
|
||||||
|
if (ShouldUpdateCalendar(calendar, existingLocalCalendar, remotePrimaryCalendarId) ||
|
||||||
|
!string.Equals(existingLocalCalendar.BackgroundColorHex, resolvedColor, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
existingLocalCalendar.Name = calendar.Name;
|
existingLocalCalendar.Name = calendar.Name;
|
||||||
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
existingLocalCalendar.IsPrimary = string.Equals(existingLocalCalendar.RemoteCalendarId, remotePrimaryCalendarId, StringComparison.OrdinalIgnoreCase);
|
||||||
if (!string.IsNullOrEmpty(calendar.HexColor))
|
existingLocalCalendar.BackgroundColorHex = resolvedColor;
|
||||||
existingLocalCalendar.BackgroundColorHex = calendar.HexColor;
|
existingLocalCalendar.TextColorHex = ColorHelpers.GetReadableTextColorHex(existingLocalCalendar.BackgroundColorHex);
|
||||||
|
|
||||||
updatedCalendars.Add(existingLocalCalendar);
|
updatedCalendars.Add(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
@@ -2392,6 +2390,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
// Remove it from the local folder list to skip additional calendar updates.
|
// Remove it from the local folder list to skip additional calendar updates.
|
||||||
localCalendars.Remove(existingLocalCalendar);
|
localCalendars.Remove(existingLocalCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usedCalendarColors.Add(resolvedColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,13 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
private void EditAliases()
|
private void EditAliases()
|
||||||
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
|
=> Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void EditImapCalDavSettings()
|
||||||
|
=> Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
Translator.ImapCalDavSettingsPage_TitleEdit,
|
||||||
|
WinoPage.ImapCalDavSettingsPage,
|
||||||
|
ImapCalDavSettingsNavigationContext.CreateForEditMode(Account.Id)));
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SaveChangesAsync()
|
private async Task SaveChangesAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ using Wino.Core.Domain.Models.Accounts;
|
|||||||
using Wino.Core.Domain.Models.AutoDiscovery;
|
using Wino.Core.Domain.Models.AutoDiscovery;
|
||||||
using Wino.Core.Domain.Models.Calendar;
|
using Wino.Core.Domain.Models.Calendar;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
using Wino.Core.Services;
|
using Wino.Core.Services;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
|
using Wino.Messaging.Client.Calendar;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
using Wino.Messaging.Server;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels;
|
namespace Wino.Mail.ViewModels;
|
||||||
|
|
||||||
@@ -289,7 +292,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
PageTitle = Translator.ImapCalDavSettingsPage_TitleEdit;
|
PageTitle = Translator.ImapCalDavSettingsPage_TitleEdit;
|
||||||
await InitializeEditModeAsync(context.AccountId).ConfigureAwait(false);
|
await InitializeEditModeAsync(context.AccountId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +501,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
|
|
||||||
private async Task InitializeEditModeAsync(Guid accountId)
|
private async Task InitializeEditModeAsync(Guid accountId)
|
||||||
{
|
{
|
||||||
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
var account = await _accountService.GetAccountAsync(accountId);
|
||||||
if (account == null)
|
if (account == null)
|
||||||
throw new InvalidOperationException(Translator.Exception_NullAssignedAccount);
|
throw new InvalidOperationException(Translator.Exception_NullAssignedAccount);
|
||||||
|
|
||||||
@@ -768,10 +771,26 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
serverInformation.AccountId = account.Id;
|
serverInformation.AccountId = account.Id;
|
||||||
|
|
||||||
account.ServerInformation = serverInformation;
|
account.ServerInformation = serverInformation;
|
||||||
|
account.AttentionReason = AccountAttentionReason.None;
|
||||||
|
|
||||||
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false);
|
await _accountService.UpdateAccountCustomServerInformationAsync(serverInformation).ConfigureAwait(false);
|
||||||
await _accountService.UpdateAccountAsync(account).ConfigureAwait(false);
|
await _accountService.UpdateAccountAsync(account).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
Type = MailSynchronizationType.FullFolders
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (account.IsCalendarAccessGranted)
|
||||||
|
{
|
||||||
|
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
Type = CalendarSynchronizationType.CalendarEvents
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
_mailDialogService.InfoBarMessage(
|
_mailDialogService.InfoBarMessage(
|
||||||
Translator.IMAPSetupDialog_ValidationSuccess_Title,
|
Translator.IMAPSetupDialog_ValidationSuccess_Title,
|
||||||
Translator.ImapCalDavSettingsPage_SaveSuccessMessage,
|
Translator.ImapCalDavSettingsPage_SaveSuccessMessage,
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ using Wino.Core.Domain.Models;
|
|||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Synchronization;
|
using Wino.Core.Domain.Models.Synchronization;
|
||||||
|
using Wino.Core.Services;
|
||||||
|
using Wino.Mail.ViewModels.Data;
|
||||||
using Wino.Messaging.Client.Accounts;
|
using Wino.Messaging.Client.Accounts;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
using Wino.Messaging.Client.Shell;
|
using Wino.Messaging.Client.Shell;
|
||||||
@@ -81,7 +83,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
private readonly IMailDialogService _dialogService;
|
private readonly IMailDialogService _dialogService;
|
||||||
private readonly IMimeFileService _mimeFileService;
|
private readonly IMimeFileService _mimeFileService;
|
||||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||||
private readonly IUpdateManager _updateManager;
|
|
||||||
private readonly IStoreUpdateService _storeUpdateService;
|
private readonly IStoreUpdateService _storeUpdateService;
|
||||||
|
|
||||||
private readonly INativeAppService _nativeAppService;
|
private readonly INativeAppService _nativeAppService;
|
||||||
@@ -108,7 +109,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IStartupBehaviorService startupBehaviorService,
|
IStartupBehaviorService startupBehaviorService,
|
||||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||||
IUpdateManager updateManager,
|
|
||||||
IStoreUpdateService storeUpdateService)
|
IStoreUpdateService storeUpdateService)
|
||||||
{
|
{
|
||||||
StatePersistenceService = statePersistanceService;
|
StatePersistenceService = statePersistanceService;
|
||||||
@@ -130,7 +130,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
_notificationBuilder = notificationBuilder;
|
_notificationBuilder = notificationBuilder;
|
||||||
_winoRequestDelegator = winoRequestDelegator;
|
_winoRequestDelegator = winoRequestDelegator;
|
||||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||||
_updateManager = updateManager;
|
|
||||||
_storeUpdateService = storeUpdateService;
|
_storeUpdateService = storeUpdateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +234,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var activationContext = parameters as ShellModeActivationContext;
|
var activationContext = parameters as ShellModeActivationContext;
|
||||||
var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true;
|
var shouldRunStartupFlows = (activationContext?.IsInitialActivation ?? true) &&
|
||||||
|
activationContext?.SuppressStartupFlows != true;
|
||||||
var hasExistingAccountMenuItems = MenuItems?.OfType<IAccountMenuItem>().Any() == true;
|
var hasExistingAccountMenuItems = MenuItems?.OfType<IAccountMenuItem>().Any() == true;
|
||||||
|
|
||||||
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
PreferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
@@ -258,7 +258,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
if (shouldRunStartupFlows)
|
if (shouldRunStartupFlows)
|
||||||
{
|
{
|
||||||
await ShowWhatIsNewIfNeededAsync();
|
|
||||||
await MakeSureEnableStartupLaunchAsync();
|
await MakeSureEnableStartupLaunchAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,19 +295,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
FooterItems?.Clear();
|
FooterItems?.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowWhatIsNewIfNeededAsync()
|
|
||||||
{
|
|
||||||
if (!_updateManager.ShouldShowUpdateNotes())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var notes = await _updateManager.GetLatestUpdateNotesAsync();
|
|
||||||
|
|
||||||
if (notes.Sections.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
await _dialogService.ShowWhatIsNewDialogAsync(notes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task MakeSureEnableStartupLaunchAsync()
|
private async Task MakeSureEnableStartupLaunchAsync()
|
||||||
{
|
{
|
||||||
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
if (!_configurationService.Get<bool>(IsActivateStartupLaunchAskedKey, false))
|
||||||
@@ -605,21 +591,75 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task HandleAccountAttentionAsync(MailAccount account)
|
||||||
|
=> FixAccountIssuesAsync(account);
|
||||||
|
|
||||||
|
private void TriggerFullSynchronization(MailAccount account)
|
||||||
|
{
|
||||||
|
Messenger.Send(new NewMailSynchronizationRequested(new MailSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
Type = MailSynchronizationType.FullFolders
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (account.IsCalendarAccessGranted)
|
||||||
|
{
|
||||||
|
Messenger.Send(new NewCalendarSynchronizationRequested(new CalendarSynchronizationOptions
|
||||||
|
{
|
||||||
|
AccountId = account.Id,
|
||||||
|
Type = CalendarSynchronizationType.CalendarEvents
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task FixAccountIssuesAsync(MailAccount account)
|
private async Task FixAccountIssuesAsync(MailAccount account)
|
||||||
{
|
{
|
||||||
// TODO: This area is very unclear. Needs to be rewritten with care.
|
|
||||||
// Fix account issues are expected to not work, but may work for some cases.
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (account.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
if (account.AttentionReason == AccountAttentionReason.InvalidCredentials)
|
||||||
await _accountService.FixTokenIssuesAsync(account.Id);
|
{
|
||||||
else if (account.AttentionReason == AccountAttentionReason.MissingSystemFolderConfiguration)
|
if (account.ProviderType is MailProviderType.Gmail or MailProviderType.Outlook)
|
||||||
await _dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService);
|
{
|
||||||
|
await SynchronizationManager.Instance.HandleAuthorizationAsync(
|
||||||
|
account.ProviderType,
|
||||||
|
account,
|
||||||
|
account.ProviderType == MailProviderType.Gmail);
|
||||||
|
|
||||||
await _accountService.ClearAccountAttentionAsync(account.Id);
|
await _accountService.ClearAccountAttentionAsync(account.Id);
|
||||||
|
|
||||||
_dialogService.InfoBarMessage(Translator.Info_AccountIssueFixFailedTitle, Translator.Info_AccountIssueFixSuccessMessage, InfoBarMessageType.Success);
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.Info_AccountIssueFixSuccessTitle,
|
||||||
|
Translator.Info_AccountIssueFixSuccessMessage,
|
||||||
|
InfoBarMessageType.Success);
|
||||||
|
|
||||||
|
TriggerFullSynchronization(account);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationService.Navigate(WinoPage.SettingsPage, WinoPage.ManageAccountsPage);
|
||||||
|
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
|
Translator.ImapCalDavSettingsPage_TitleEdit,
|
||||||
|
WinoPage.ImapCalDavSettingsPage,
|
||||||
|
ImapCalDavSettingsNavigationContext.CreateForEditMode(account.Id)));
|
||||||
|
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.Info_AccountIssueFixSuccessTitle,
|
||||||
|
Translator.Info_AccountIssueFixImapMessage,
|
||||||
|
InfoBarMessageType.Information);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (account.AttentionReason == AccountAttentionReason.MissingSystemFolderConfiguration)
|
||||||
|
{
|
||||||
|
await _dialogService.HandleSystemFolderConfigurationDialogAsync(account.Id, _folderService);
|
||||||
|
await _accountService.ClearAccountAttentionAsync(account.Id);
|
||||||
|
|
||||||
|
_dialogService.InfoBarMessage(
|
||||||
|
Translator.Info_AccountIssueFixSuccessTitle,
|
||||||
|
Translator.Info_AccountIssueFixSuccessMessage,
|
||||||
|
InfoBarMessageType.Success);
|
||||||
|
|
||||||
|
TriggerFullSynchronization(account);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using CommunityToolkit.Mvvm.Messaging;
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
using Wino.Core.Domain.Models.Updates;
|
||||||
using Wino.Messaging.Client.Navigation;
|
using Wino.Messaging.Client.Navigation;
|
||||||
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Mail.ViewModels;
|
namespace Wino.Mail.ViewModels;
|
||||||
|
|
||||||
public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
||||||
{
|
{
|
||||||
private readonly IUpdateManager _updateManager;
|
private readonly IUpdateManager _updateManager;
|
||||||
|
private readonly IMailDialogService _dialogService;
|
||||||
|
private readonly IWinoAccountDataSyncService _syncService;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
|
public partial List<UpdateNoteSection> UpdateSections { get; set; } = [];
|
||||||
|
|
||||||
public WelcomePageV2ViewModel(IUpdateManager updateManager)
|
[ObservableProperty]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(GetStartedCommand))]
|
||||||
|
[NotifyCanExecuteChangedFor(nameof(ImportFromWinoAccountCommand))]
|
||||||
|
public partial bool IsImportInProgress { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(HasImportStatus))]
|
||||||
|
public partial string ImportStatusMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool HasImportStatus => !string.IsNullOrWhiteSpace(ImportStatusMessage);
|
||||||
|
|
||||||
|
public WelcomePageV2ViewModel(IUpdateManager updateManager,
|
||||||
|
IMailDialogService dialogService,
|
||||||
|
IWinoAccountDataSyncService syncService)
|
||||||
{
|
{
|
||||||
_updateManager = updateManager;
|
_updateManager = updateManager;
|
||||||
|
_dialogService = dialogService;
|
||||||
|
_syncService = syncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -39,11 +59,75 @@ public partial class WelcomePageV2ViewModel : MailBaseViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
|
||||||
private void GetStarted()
|
private void GetStarted()
|
||||||
{
|
{
|
||||||
Messenger.Send(new BreadcrumbNavigationRequested(
|
Messenger.Send(new BreadcrumbNavigationRequested(
|
||||||
Translator.WelcomeWizard_Step2Title,
|
Translator.WelcomeWizard_Step2Title,
|
||||||
WinoPage.ProviderSelectionPage));
|
WinoPage.ProviderSelectionPage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenWelcomeActions))]
|
||||||
|
private async Task ImportFromWinoAccountAsync()
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() => ImportStatusMessage = string.Empty);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var account = await _dialogService.ShowWinoAccountLoginDialogAsync().ConfigureAwait(false);
|
||||||
|
if (account == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteUIThread(() => IsImportInProgress = true);
|
||||||
|
|
||||||
|
var result = await _syncService.ImportAsync(new WinoAccountSyncSelection()).ConfigureAwait(false);
|
||||||
|
if (result.ImportedMailboxCount > 0)
|
||||||
|
{
|
||||||
|
ReportUIChange(new WelcomeImportCompletedMessage(result.ImportedMailboxCount));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ExecuteUIThread(() => ImportStatusMessage = BuildInlineImportMessage(result));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await _dialogService.ShowMessageAsync(ex.Message, Translator.GeneralTitle_Error, WinoCustomMessageDialogIcon.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await ExecuteUIThread(() => IsImportInProgress = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanOpenWelcomeActions() => !IsImportInProgress;
|
||||||
|
|
||||||
|
private static string BuildInlineImportMessage(WinoAccountSyncImportResult result)
|
||||||
|
{
|
||||||
|
var preferencesMessage = result.FailedPreferenceCount > 0
|
||||||
|
? string.Format(Translator.WinoAccount_Management_ImportPartial, result.AppliedPreferenceCount, result.FailedPreferenceCount)
|
||||||
|
: result.HadRemotePreferences
|
||||||
|
? string.Format(Translator.WinoAccount_Management_ImportPreferencesSucceeded, result.AppliedPreferenceCount)
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
if (result.RemoteMailboxCount == 0)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||||
|
? Translator.WelcomeWindow_ImportNoAccountsFound
|
||||||
|
: $"{preferencesMessage} {Translator.WelcomeWindow_ImportNoAccountsFound}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.SkippedDuplicateMailboxCount > 0 && result.ImportedMailboxCount == 0)
|
||||||
|
{
|
||||||
|
var duplicateMessage = string.Format(Translator.WelcomeWindow_ImportDuplicateAccountsSkipped, result.SkippedDuplicateMailboxCount);
|
||||||
|
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||||
|
? duplicateMessage
|
||||||
|
: $"{preferencesMessage} {duplicateMessage}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(preferencesMessage)
|
||||||
|
? Translator.WinoAccount_Management_ImportEmpty
|
||||||
|
: preferencesMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,556 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
//
|
||||||
|
// LottieGen version:
|
||||||
|
// 8.2.250604.1+b02a3ee244
|
||||||
|
//
|
||||||
|
// Command:
|
||||||
|
// LottieGen -Language CSharp -Public -WinUIVersion 2.4 -InputFile sync.json
|
||||||
|
//
|
||||||
|
// Input file:
|
||||||
|
// sync.json (2404 bytes created 20:18+02:00 Apr 4 2026)
|
||||||
|
//
|
||||||
|
// LottieGen source:
|
||||||
|
// http://aka.ms/Lottie
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// ___________________________________________________________
|
||||||
|
// | Object stats | UAP v15 count | UAP v7 count |
|
||||||
|
// |__________________________|_______________|______________|
|
||||||
|
// | All CompositionObjects | 26 | 26 |
|
||||||
|
// |--------------------------+---------------+--------------|
|
||||||
|
// | Expression animators | 1 | 1 |
|
||||||
|
// | KeyFrame animators | 1 | 1 |
|
||||||
|
// | Reference parameters | 1 | 1 |
|
||||||
|
// | Expression operations | 0 | 0 |
|
||||||
|
// |--------------------------+---------------+--------------|
|
||||||
|
// | Animated brushes | - | - |
|
||||||
|
// | Animated gradient stops | - | - |
|
||||||
|
// | ExpressionAnimations | 1 | 1 |
|
||||||
|
// | PathKeyFrameAnimations | - | - |
|
||||||
|
// |--------------------------+---------------+--------------|
|
||||||
|
// | ContainerVisuals | 1 | 1 |
|
||||||
|
// | ShapeVisuals | 1 | 1 |
|
||||||
|
// |--------------------------+---------------+--------------|
|
||||||
|
// | ContainerShapes | 1 | 1 |
|
||||||
|
// | CompositionSpriteShapes | 2 | 2 |
|
||||||
|
// |--------------------------+---------------+--------------|
|
||||||
|
// | Brushes | 1 | 1 |
|
||||||
|
// | Gradient stops | - | - |
|
||||||
|
// | CompositionVisualSurface | - | - |
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
using Microsoft.Graphics.Canvas.Geometry;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using Windows.Graphics;
|
||||||
|
using Windows.UI;
|
||||||
|
using Windows.UI.Composition;
|
||||||
|
|
||||||
|
namespace AnimatedVisuals
|
||||||
|
{
|
||||||
|
// Name: main_libary_shelf_icon_sync
|
||||||
|
// Frame rate: 60 fps
|
||||||
|
// Frame count: 61
|
||||||
|
// Duration: 1016.7 mS
|
||||||
|
sealed class Sync
|
||||||
|
: Microsoft.UI.Xaml.Controls.IAnimatedVisualSource
|
||||||
|
{
|
||||||
|
// Animation duration: 1.017 seconds.
|
||||||
|
internal const long c_durationTicks = 10166666;
|
||||||
|
|
||||||
|
public Microsoft.UI.Xaml.Controls.IAnimatedVisual TryCreateAnimatedVisual(Compositor compositor)
|
||||||
|
{
|
||||||
|
object ignored = null;
|
||||||
|
return TryCreateAnimatedVisual(compositor, out ignored);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Microsoft.UI.Xaml.Controls.IAnimatedVisual TryCreateAnimatedVisual(Compositor compositor, out object diagnostics)
|
||||||
|
{
|
||||||
|
diagnostics = null;
|
||||||
|
|
||||||
|
if (Sync_AnimatedVisual_UAPv15.IsRuntimeCompatible())
|
||||||
|
{
|
||||||
|
var res =
|
||||||
|
new Sync_AnimatedVisual_UAPv15(
|
||||||
|
compositor
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sync_AnimatedVisual_UAPv7.IsRuntimeCompatible())
|
||||||
|
{
|
||||||
|
var res =
|
||||||
|
new Sync_AnimatedVisual_UAPv7(
|
||||||
|
compositor
|
||||||
|
);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of frames in the animation.
|
||||||
|
/// </summary>
|
||||||
|
public double FrameCount => 61d;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the frame rate of the animation.
|
||||||
|
/// </summary>
|
||||||
|
public double Framerate => 60d;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the duration of the animation.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Duration => TimeSpan.FromTicks(10166666);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a zero-based frame number to the corresponding progress value denoting the
|
||||||
|
/// start of the frame.
|
||||||
|
/// </summary>
|
||||||
|
public double FrameToProgress(double frameNumber)
|
||||||
|
{
|
||||||
|
return frameNumber / 61d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a map from marker names to corresponding progress values.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyDictionary<string, double> Markers =>
|
||||||
|
new Dictionary<string, double>
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the color property with the given name, or does nothing if no such property
|
||||||
|
/// exists.
|
||||||
|
/// </summary>
|
||||||
|
public void SetColorProperty(string propertyName, Color value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the scalar property with the given name, or does nothing if no such property
|
||||||
|
/// exists.
|
||||||
|
/// </summary>
|
||||||
|
public void SetScalarProperty(string propertyName, double value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Sync_AnimatedVisual_UAPv15
|
||||||
|
: Microsoft.UI.Xaml.Controls.IAnimatedVisual
|
||||||
|
{
|
||||||
|
const long c_durationTicks = 10166666;
|
||||||
|
readonly Compositor _c;
|
||||||
|
readonly ExpressionAnimation _reusableExpressionAnimation;
|
||||||
|
AnimationController _animationController_0;
|
||||||
|
CompositionColorBrush _colorBrush_AlmostDarkSlateGray_FF2D3846;
|
||||||
|
ContainerVisual _root;
|
||||||
|
|
||||||
|
void BindProperty(
|
||||||
|
CompositionObject target,
|
||||||
|
string animatedPropertyName,
|
||||||
|
string expression,
|
||||||
|
string referenceParameterName,
|
||||||
|
CompositionObject referencedObject)
|
||||||
|
{
|
||||||
|
_reusableExpressionAnimation.ClearAllParameters();
|
||||||
|
_reusableExpressionAnimation.Expression = expression;
|
||||||
|
_reusableExpressionAnimation.SetReferenceParameter(referenceParameterName, referencedObject);
|
||||||
|
target.StartAnimation(animatedPropertyName, _reusableExpressionAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScalarKeyFrameAnimation CreateScalarKeyFrameAnimation(float initialProgress, float initialValue, CompositionEasingFunction initialEasingFunction)
|
||||||
|
{
|
||||||
|
var result = _c.CreateScalarKeyFrameAnimation();
|
||||||
|
result.Duration = TimeSpan.FromTicks(c_durationTicks);
|
||||||
|
result.InsertKeyFrame(initialProgress, initialValue, initialEasingFunction);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionSpriteShape CreateSpriteShape(CompositionGeometry geometry, Matrix3x2 transformMatrix, CompositionBrush fillBrush)
|
||||||
|
{
|
||||||
|
var result = _c.CreateSpriteShape(geometry);
|
||||||
|
result.TransformMatrix = transformMatrix;
|
||||||
|
result.FillBrush = fillBrush;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
AnimationController AnimationController_0()
|
||||||
|
{
|
||||||
|
var result = _animationController_0 = _c.CreateAnimationController();
|
||||||
|
result.Pause();
|
||||||
|
BindProperty(result, "Progress", "_.Progress", "_", _root);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// - - ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
CanvasGeometry Geometry_0()
|
||||||
|
{
|
||||||
|
CanvasGeometry result;
|
||||||
|
using (var builder = new CanvasPathBuilder(null))
|
||||||
|
{
|
||||||
|
builder.SetFilledRegionDetermination(CanvasFilledRegionDetermination.Winding);
|
||||||
|
builder.BeginFigure(new Vector2(11.7449999F, 5.09700012F));
|
||||||
|
builder.AddCubicBezier(new Vector2(11.7449999F, -3.66000009F), new Vector2(4.56699991F, -10.9189997F), new Vector2(-4.25500011F, -10.9189997F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-8.6239996F, -10.9189997F), new Vector2(-12.7040005F, -9.20300007F), new Vector2(-15.7449999F, -6.08900023F));
|
||||||
|
builder.AddLine(new Vector2(-12.8739996F, -3.32599998F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-10.5930004F, -5.66200018F), new Vector2(-7.53200006F, -6.94799995F), new Vector2(-4.25500011F, -6.94799995F));
|
||||||
|
builder.AddCubicBezier(new Vector2(2.36199999F, -6.94799995F), new Vector2(7.74499989F, -1.47099996F), new Vector2(7.74499989F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(3.74499989F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(9.74499989F, 10.9189997F));
|
||||||
|
builder.AddLine(new Vector2(15.7449999F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(11.7449999F, 5.09700012F));
|
||||||
|
builder.EndFigure(CanvasFigureLoop.Closed);
|
||||||
|
result = CanvasGeometry.CreatePath(builder);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// - - ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
CanvasGeometry Geometry_1()
|
||||||
|
{
|
||||||
|
CanvasGeometry result;
|
||||||
|
using (var builder = new CanvasPathBuilder(null))
|
||||||
|
{
|
||||||
|
builder.SetFilledRegionDetermination(CanvasFilledRegionDetermination.Winding);
|
||||||
|
builder.BeginFigure(new Vector2(4.25500011F, 6.94799995F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-2.36199999F, 6.94799995F), new Vector2(-7.74499989F, 1.472F), new Vector2(-7.74499989F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-3.74499989F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-9.74499989F, -10.9189997F));
|
||||||
|
builder.AddLine(new Vector2(-15.7449999F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-11.7449999F, -5.09499979F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-11.7449999F, 3.66199994F), new Vector2(-4.56699991F, 10.9189997F), new Vector2(4.25500011F, 10.9189997F));
|
||||||
|
builder.AddCubicBezier(new Vector2(8.6260004F, 10.9189997F), new Vector2(12.7060003F, 9.20300007F), new Vector2(15.7449999F, 6.08900023F));
|
||||||
|
builder.AddLine(new Vector2(12.8739996F, 3.32500005F));
|
||||||
|
builder.AddCubicBezier(new Vector2(10.5930004F, 5.66099977F), new Vector2(7.53200006F, 6.94799995F), new Vector2(4.25500011F, 6.94799995F));
|
||||||
|
builder.EndFigure(CanvasFigureLoop.Closed);
|
||||||
|
result = CanvasGeometry.CreatePath(builder);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionColorBrush ColorBrush_AlmostDarkSlateGray_FF2D3846()
|
||||||
|
{
|
||||||
|
return _colorBrush_AlmostDarkSlateGray_FF2D3846 = _c.CreateColorBrush(Color.FromArgb(0xFF, 0x2D, 0x38, 0x46));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
CompositionContainerShape ContainerShape()
|
||||||
|
{
|
||||||
|
var result = _c.CreateContainerShape();
|
||||||
|
result.CenterPoint = new Vector2(24F, 24F);
|
||||||
|
var shapes = result.Shapes;
|
||||||
|
// ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
shapes.Add(SpriteShape_0());
|
||||||
|
// ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
shapes.Add(SpriteShape_1());
|
||||||
|
result.StartAnimation("RotationAngleInDegrees", RotationAngleInDegreesScalarAnimation_0_to_360(), AnimationController_0());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
CompositionPathGeometry PathGeometry_0()
|
||||||
|
{
|
||||||
|
return _c.CreatePathGeometry(new CompositionPath(Geometry_0()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
CompositionPathGeometry PathGeometry_1()
|
||||||
|
{
|
||||||
|
return _c.CreatePathGeometry(new CompositionPath(Geometry_1()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Path 1
|
||||||
|
CompositionSpriteShape SpriteShape_0()
|
||||||
|
{
|
||||||
|
// Offset:<28.255, 18.903>
|
||||||
|
var geometry = PathGeometry_0();
|
||||||
|
var result = CreateSpriteShape(geometry, new Matrix3x2(1F, 0F, 0F, 1F, 28.2549992F, 18.9029999F), ColorBrush_AlmostDarkSlateGray_FF2D3846());;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Path 1
|
||||||
|
CompositionSpriteShape SpriteShape_1()
|
||||||
|
{
|
||||||
|
// Offset:<19.745, 29.096>
|
||||||
|
var geometry = PathGeometry_1();
|
||||||
|
var result = CreateSpriteShape(geometry, new Matrix3x2(1F, 0F, 0F, 1F, 19.7450008F, 29.0960007F), _colorBrush_AlmostDarkSlateGray_FF2D3846);;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root of the composition.
|
||||||
|
ContainerVisual Root()
|
||||||
|
{
|
||||||
|
var result = _root = _c.CreateContainerVisual();
|
||||||
|
var propertySet = result.Properties;
|
||||||
|
propertySet.InsertScalar("Progress", 0F);
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
result.Children.InsertAtTop(ShapeVisual_0());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Rotation
|
||||||
|
ScalarKeyFrameAnimation RotationAngleInDegreesScalarAnimation_0_to_360()
|
||||||
|
{
|
||||||
|
// Frame 0.
|
||||||
|
var result = CreateScalarKeyFrameAnimation(0F, 0F, HoldThenStepEasingFunction());
|
||||||
|
// Frame 61.
|
||||||
|
result.InsertKeyFrame(1F, 360F, _c.CreateCubicBezierEasingFunction(new Vector2(0.314999998F, 0F), new Vector2(0.465000004F, 0.861999989F)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
ShapeVisual ShapeVisual_0()
|
||||||
|
{
|
||||||
|
var result = _c.CreateShapeVisual();
|
||||||
|
result.Size = new Vector2(48F, 48F);
|
||||||
|
result.Shapes.Add(ContainerShape());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// RotationAngleInDegrees
|
||||||
|
StepEasingFunction HoldThenStepEasingFunction()
|
||||||
|
{
|
||||||
|
var result = _c.CreateStepEasingFunction();
|
||||||
|
result.IsFinalStepSingleFrame = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Sync_AnimatedVisual_UAPv15(
|
||||||
|
Compositor compositor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_c = compositor;
|
||||||
|
_reusableExpressionAnimation = compositor.CreateExpressionAnimation();
|
||||||
|
Root();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visual RootVisual => _root;
|
||||||
|
public TimeSpan Duration => TimeSpan.FromTicks(c_durationTicks);
|
||||||
|
public Vector2 Size => new Vector2(48F, 48F);
|
||||||
|
void IDisposable.Dispose() => _root?.Dispose();
|
||||||
|
|
||||||
|
internal static bool IsRuntimeCompatible()
|
||||||
|
{
|
||||||
|
return Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Sync_AnimatedVisual_UAPv7
|
||||||
|
: Microsoft.UI.Xaml.Controls.IAnimatedVisual
|
||||||
|
{
|
||||||
|
const long c_durationTicks = 10166666;
|
||||||
|
readonly Compositor _c;
|
||||||
|
readonly ExpressionAnimation _reusableExpressionAnimation;
|
||||||
|
CompositionColorBrush _colorBrush_AlmostDarkSlateGray_FF2D3846;
|
||||||
|
ContainerVisual _root;
|
||||||
|
|
||||||
|
void BindProperty(
|
||||||
|
CompositionObject target,
|
||||||
|
string animatedPropertyName,
|
||||||
|
string expression,
|
||||||
|
string referenceParameterName,
|
||||||
|
CompositionObject referencedObject)
|
||||||
|
{
|
||||||
|
_reusableExpressionAnimation.ClearAllParameters();
|
||||||
|
_reusableExpressionAnimation.Expression = expression;
|
||||||
|
_reusableExpressionAnimation.SetReferenceParameter(referenceParameterName, referencedObject);
|
||||||
|
target.StartAnimation(animatedPropertyName, _reusableExpressionAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScalarKeyFrameAnimation CreateScalarKeyFrameAnimation(float initialProgress, float initialValue, CompositionEasingFunction initialEasingFunction)
|
||||||
|
{
|
||||||
|
var result = _c.CreateScalarKeyFrameAnimation();
|
||||||
|
result.Duration = TimeSpan.FromTicks(c_durationTicks);
|
||||||
|
result.InsertKeyFrame(initialProgress, initialValue, initialEasingFunction);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionSpriteShape CreateSpriteShape(CompositionGeometry geometry, Matrix3x2 transformMatrix, CompositionBrush fillBrush)
|
||||||
|
{
|
||||||
|
var result = _c.CreateSpriteShape(geometry);
|
||||||
|
result.TransformMatrix = transformMatrix;
|
||||||
|
result.FillBrush = fillBrush;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// - - ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
CanvasGeometry Geometry_0()
|
||||||
|
{
|
||||||
|
CanvasGeometry result;
|
||||||
|
using (var builder = new CanvasPathBuilder(null))
|
||||||
|
{
|
||||||
|
builder.SetFilledRegionDetermination(CanvasFilledRegionDetermination.Winding);
|
||||||
|
builder.BeginFigure(new Vector2(11.7449999F, 5.09700012F));
|
||||||
|
builder.AddCubicBezier(new Vector2(11.7449999F, -3.66000009F), new Vector2(4.56699991F, -10.9189997F), new Vector2(-4.25500011F, -10.9189997F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-8.6239996F, -10.9189997F), new Vector2(-12.7040005F, -9.20300007F), new Vector2(-15.7449999F, -6.08900023F));
|
||||||
|
builder.AddLine(new Vector2(-12.8739996F, -3.32599998F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-10.5930004F, -5.66200018F), new Vector2(-7.53200006F, -6.94799995F), new Vector2(-4.25500011F, -6.94799995F));
|
||||||
|
builder.AddCubicBezier(new Vector2(2.36199999F, -6.94799995F), new Vector2(7.74499989F, -1.47099996F), new Vector2(7.74499989F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(3.74499989F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(9.74499989F, 10.9189997F));
|
||||||
|
builder.AddLine(new Vector2(15.7449999F, 5.09700012F));
|
||||||
|
builder.AddLine(new Vector2(11.7449999F, 5.09700012F));
|
||||||
|
builder.EndFigure(CanvasFigureLoop.Closed);
|
||||||
|
result = CanvasGeometry.CreatePath(builder);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// - - ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
CanvasGeometry Geometry_1()
|
||||||
|
{
|
||||||
|
CanvasGeometry result;
|
||||||
|
using (var builder = new CanvasPathBuilder(null))
|
||||||
|
{
|
||||||
|
builder.SetFilledRegionDetermination(CanvasFilledRegionDetermination.Winding);
|
||||||
|
builder.BeginFigure(new Vector2(4.25500011F, 6.94799995F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-2.36199999F, 6.94799995F), new Vector2(-7.74499989F, 1.472F), new Vector2(-7.74499989F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-3.74499989F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-9.74499989F, -10.9189997F));
|
||||||
|
builder.AddLine(new Vector2(-15.7449999F, -5.09499979F));
|
||||||
|
builder.AddLine(new Vector2(-11.7449999F, -5.09499979F));
|
||||||
|
builder.AddCubicBezier(new Vector2(-11.7449999F, 3.66199994F), new Vector2(-4.56699991F, 10.9189997F), new Vector2(4.25500011F, 10.9189997F));
|
||||||
|
builder.AddCubicBezier(new Vector2(8.6260004F, 10.9189997F), new Vector2(12.7060003F, 9.20300007F), new Vector2(15.7449999F, 6.08900023F));
|
||||||
|
builder.AddLine(new Vector2(12.8739996F, 3.32500005F));
|
||||||
|
builder.AddCubicBezier(new Vector2(10.5930004F, 5.66099977F), new Vector2(7.53200006F, 6.94799995F), new Vector2(4.25500011F, 6.94799995F));
|
||||||
|
builder.EndFigure(CanvasFigureLoop.Closed);
|
||||||
|
result = CanvasGeometry.CreatePath(builder);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionColorBrush ColorBrush_AlmostDarkSlateGray_FF2D3846()
|
||||||
|
{
|
||||||
|
return _colorBrush_AlmostDarkSlateGray_FF2D3846 = _c.CreateColorBrush(Color.FromArgb(0xFF, 0x2D, 0x38, 0x46));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
CompositionContainerShape ContainerShape()
|
||||||
|
{
|
||||||
|
var result = _c.CreateContainerShape();
|
||||||
|
result.CenterPoint = new Vector2(24F, 24F);
|
||||||
|
var shapes = result.Shapes;
|
||||||
|
// ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
shapes.Add(SpriteShape_0());
|
||||||
|
// ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
shapes.Add(SpriteShape_1());
|
||||||
|
result.StartAnimation("RotationAngleInDegrees", RotationAngleInDegreesScalarAnimation_0_to_360());
|
||||||
|
var controller = result.TryGetAnimationController("RotationAngleInDegrees");
|
||||||
|
controller.Pause();
|
||||||
|
BindProperty(controller, "Progress", "_.Progress", "_", _root);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// ShapeGroup: Group 2 Offset:<28.255, 18.903>
|
||||||
|
CompositionPathGeometry PathGeometry_0()
|
||||||
|
{
|
||||||
|
return _c.CreatePathGeometry(new CompositionPath(Geometry_0()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// ShapeGroup: Group 1 Offset:<19.745, 29.096>
|
||||||
|
CompositionPathGeometry PathGeometry_1()
|
||||||
|
{
|
||||||
|
return _c.CreatePathGeometry(new CompositionPath(Geometry_1()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Path 1
|
||||||
|
CompositionSpriteShape SpriteShape_0()
|
||||||
|
{
|
||||||
|
// Offset:<28.255, 18.903>
|
||||||
|
var geometry = PathGeometry_0();
|
||||||
|
var result = CreateSpriteShape(geometry, new Matrix3x2(1F, 0F, 0F, 1F, 28.2549992F, 18.9029999F), ColorBrush_AlmostDarkSlateGray_FF2D3846());;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Path 1
|
||||||
|
CompositionSpriteShape SpriteShape_1()
|
||||||
|
{
|
||||||
|
// Offset:<19.745, 29.096>
|
||||||
|
var geometry = PathGeometry_1();
|
||||||
|
var result = CreateSpriteShape(geometry, new Matrix3x2(1F, 0F, 0F, 1F, 19.7450008F, 29.0960007F), _colorBrush_AlmostDarkSlateGray_FF2D3846);;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The root of the composition.
|
||||||
|
ContainerVisual Root()
|
||||||
|
{
|
||||||
|
var result = _root = _c.CreateContainerVisual();
|
||||||
|
var propertySet = result.Properties;
|
||||||
|
propertySet.InsertScalar("Progress", 0F);
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
result.Children.InsertAtTop(ShapeVisual_0());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// Rotation
|
||||||
|
ScalarKeyFrameAnimation RotationAngleInDegreesScalarAnimation_0_to_360()
|
||||||
|
{
|
||||||
|
// Frame 0.
|
||||||
|
var result = CreateScalarKeyFrameAnimation(0F, 0F, HoldThenStepEasingFunction());
|
||||||
|
// Frame 61.
|
||||||
|
result.InsertKeyFrame(1F, 360F, _c.CreateCubicBezierEasingFunction(new Vector2(0.314999998F, 0F), new Vector2(0.465000004F, 0.861999989F)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
ShapeVisual ShapeVisual_0()
|
||||||
|
{
|
||||||
|
var result = _c.CreateShapeVisual();
|
||||||
|
result.Size = new Vector2(48F, 48F);
|
||||||
|
result.Shapes.Add(ContainerShape());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// - - Shape tree root for layer: main_library_shelf_icon_sync Outlines
|
||||||
|
// RotationAngleInDegrees
|
||||||
|
StepEasingFunction HoldThenStepEasingFunction()
|
||||||
|
{
|
||||||
|
var result = _c.CreateStepEasingFunction();
|
||||||
|
result.IsFinalStepSingleFrame = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Sync_AnimatedVisual_UAPv7(
|
||||||
|
Compositor compositor
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_c = compositor;
|
||||||
|
_reusableExpressionAnimation = compositor.CreateExpressionAnimation();
|
||||||
|
Root();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Visual RootVisual => _root;
|
||||||
|
public TimeSpan Duration => TimeSpan.FromTicks(c_durationTicks);
|
||||||
|
public Vector2 Size => new Vector2(48F, 48F);
|
||||||
|
void IDisposable.Dispose() => _root?.Dispose();
|
||||||
|
|
||||||
|
internal static bool IsRuntimeCompatible()
|
||||||
|
{
|
||||||
|
return Windows.Foundation.Metadata.ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+121
-25
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -48,7 +49,8 @@ public partial class App : WinoApplication,
|
|||||||
IRecipient<NewCalendarSynchronizationRequested>,
|
IRecipient<NewCalendarSynchronizationRequested>,
|
||||||
IRecipient<AccountCreatedMessage>,
|
IRecipient<AccountCreatedMessage>,
|
||||||
IRecipient<AccountRemovedMessage>,
|
IRecipient<AccountRemovedMessage>,
|
||||||
IRecipient<GetStartedFromWelcomeRequested>
|
IRecipient<GetStartedFromWelcomeRequested>,
|
||||||
|
IRecipient<WelcomeImportCompletedMessage>
|
||||||
{
|
{
|
||||||
private const int InboxSyncsPerFullSync = 20;
|
private const int InboxSyncsPerFullSync = 20;
|
||||||
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
|
private const string ToggleDefaultModeLaunchArgument = "--mode=toggle-default";
|
||||||
@@ -63,7 +65,7 @@ public partial class App : WinoApplication,
|
|||||||
private bool _isExiting;
|
private bool _isExiting;
|
||||||
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
||||||
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
||||||
private readonly Dictionary<Guid, int> _inboxSyncCounters = [];
|
private readonly ConcurrentDictionary<Guid, int> _inboxSyncCounters = [];
|
||||||
private NativeTrayIcon? _trayIcon;
|
private NativeTrayIcon? _trayIcon;
|
||||||
|
|
||||||
internal bool IsExiting => _isExiting;
|
internal bool IsExiting => _isExiting;
|
||||||
@@ -756,7 +758,9 @@ public partial class App : WinoApplication,
|
|||||||
/// Creates the main window without activating it.
|
/// Creates the main window without activating it.
|
||||||
/// Used for both normal launch and startup task launch (tray only).
|
/// Used for both normal launch and startup task launch (tray only).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args, string? forcedLaunchArguments = null)
|
private void CreateWindow(Microsoft.UI.Xaml.LaunchActivatedEventArgs? args,
|
||||||
|
string? forcedLaunchArguments = null,
|
||||||
|
ShellModeActivationContext? activationContextOverride = null)
|
||||||
{
|
{
|
||||||
LogActivation("Creating main window.");
|
LogActivation("Creating main window.");
|
||||||
|
|
||||||
@@ -769,14 +773,28 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
|
windowManager.SetPrimaryNavigationFrame(WinoWindowKind.Shell, shellWindow.GetMainFrame());
|
||||||
|
|
||||||
|
var navigationService = Services.GetRequiredService<INavigationService>();
|
||||||
|
var defaultMode = _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail;
|
||||||
|
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
|
|
||||||
|
if (activationContextOverride != null)
|
||||||
|
{
|
||||||
|
var targetMode = !string.IsNullOrWhiteSpace(forcedLaunchArguments)
|
||||||
|
? AppModeActivationResolver.Resolve(forcedLaunchArguments, null, null, defaultMode)
|
||||||
|
: TryResolveActivationMode(activationArgs, defaultMode, out var resolvedActivationMode)
|
||||||
|
? resolvedActivationMode
|
||||||
|
: AppModeActivationResolver.Resolve(args?.Arguments, GetCurrentLaunchTileId(), Environment.CommandLine, defaultMode);
|
||||||
|
|
||||||
|
navigationService.ChangeApplicationMode(targetMode, activationContextOverride);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(forcedLaunchArguments))
|
if (!string.IsNullOrWhiteSpace(forcedLaunchArguments))
|
||||||
{
|
{
|
||||||
shellWindow.HandleAppActivation(forcedLaunchArguments);
|
shellWindow.HandleAppActivation(forcedLaunchArguments);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
|
||||||
|
|
||||||
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||||
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
activationArgs.Data is ILaunchActivatedEventArgs launchArgs)
|
||||||
{
|
{
|
||||||
@@ -791,7 +809,7 @@ public partial class App : WinoApplication,
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveActivationMode(activationArgs, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var activationMode))
|
if (TryResolveActivationMode(activationArgs, defaultMode, out var activationMode))
|
||||||
{
|
{
|
||||||
shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode));
|
shellWindow.HandleAppActivation(GetModeLaunchArgument(activationMode));
|
||||||
return;
|
return;
|
||||||
@@ -859,6 +877,7 @@ public partial class App : WinoApplication,
|
|||||||
WeakReferenceMessenger.Default.Register<AccountCreatedMessage>(this);
|
WeakReferenceMessenger.Default.Register<AccountCreatedMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<AccountRemovedMessage>(this);
|
WeakReferenceMessenger.Default.Register<AccountRemovedMessage>(this);
|
||||||
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
WeakReferenceMessenger.Default.Register<GetStartedFromWelcomeRequested>(this);
|
||||||
|
WeakReferenceMessenger.Default.Register<WelcomeImportCompletedMessage>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Receive(NewMailSynchronizationRequested message)
|
public async void Receive(NewMailSynchronizationRequested message)
|
||||||
@@ -882,6 +901,11 @@ public partial class App : WinoApplication,
|
|||||||
syncResult.CompletedState,
|
syncResult.CompletedState,
|
||||||
message.Options.GroupedSynchronizationTrackingId));
|
message.Options.GroupedSynchronizationTrackingId));
|
||||||
|
|
||||||
|
if (syncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
|
||||||
|
{
|
||||||
|
await ClearInvalidCredentialAttentionIfNeededAsync(message.Options.AccountId).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (syncResult.CompletedState == SynchronizationCompletedState.Failed ||
|
if (syncResult.CompletedState == SynchronizationCompletedState.Failed ||
|
||||||
syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted)
|
syncResult.CompletedState == SynchronizationCompletedState.PartiallyCompleted)
|
||||||
{
|
{
|
||||||
@@ -906,7 +930,12 @@ public partial class App : WinoApplication,
|
|||||||
var dialogService = Services.GetRequiredService<IMailDialogService>();
|
var dialogService = Services.GetRequiredService<IMailDialogService>();
|
||||||
dialogService.InfoBarMessage(
|
dialogService.InfoBarMessage(
|
||||||
Translator.Info_SyncFailedTitle,
|
Translator.Info_SyncFailedTitle,
|
||||||
Translator.Exception_FailedToSynchronizeFolders,
|
message.Options.Type switch
|
||||||
|
{
|
||||||
|
CalendarSynchronizationType.CalendarMetadata => Translator.Exception_FailedToSynchronizeCalendarMetadata,
|
||||||
|
CalendarSynchronizationType.Strict => Translator.Exception_FailedToSynchronizeCalendarData,
|
||||||
|
_ => Translator.Exception_FailedToSynchronizeCalendarEvents
|
||||||
|
},
|
||||||
InfoBarMessageType.Error);
|
InfoBarMessageType.Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -942,6 +971,47 @@ public partial class App : WinoApplication,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Receive(WelcomeImportCompletedMessage message)
|
||||||
|
{
|
||||||
|
_hasConfiguredAccounts = message.ImportedMailboxCount > 0;
|
||||||
|
|
||||||
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
|
if (windowManager.GetWindow(WinoWindowKind.Welcome) == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
MainWindow?.DispatcherQueue?.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
if (_preferencesService != null)
|
||||||
|
{
|
||||||
|
_preferencesService.PreferenceChanged -= PreferencesServiceChanged;
|
||||||
|
_preferencesService.PreferenceChanged += PreferencesServiceChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateWindow(
|
||||||
|
null,
|
||||||
|
GetModeLaunchArgument(WinoApplicationMode.Mail),
|
||||||
|
new ShellModeActivationContext
|
||||||
|
{
|
||||||
|
SuppressStartupFlows = true
|
||||||
|
});
|
||||||
|
|
||||||
|
await LoadInitialWinoAccountAsync();
|
||||||
|
CloseWelcomeWindowIfPresent();
|
||||||
|
|
||||||
|
if (MainWindow != null)
|
||||||
|
{
|
||||||
|
await ActivateWindowAsync(MainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
RestartAutoSynchronizationLoop();
|
||||||
|
|
||||||
|
Services.GetRequiredService<IMailDialogService>().InfoBarMessage(
|
||||||
|
Translator.GeneralTitle_Info,
|
||||||
|
Translator.WinoAccount_Management_ImportReloginReminder,
|
||||||
|
InfoBarMessageType.Information);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(AccountRemovedMessage message)
|
public void Receive(AccountRemovedMessage message)
|
||||||
{
|
{
|
||||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||||
@@ -1078,16 +1148,37 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
var currentAccountIds = accounts.Select(a => a.Id).ToHashSet();
|
var currentAccountIds = accounts.Select(a => a.Id).ToHashSet();
|
||||||
_inboxSyncCounters.Keys.Where(a => !currentAccountIds.Contains(a)).ToList().ForEach(a => _inboxSyncCounters.Remove(a));
|
foreach (var staleAccountId in _inboxSyncCounters.Keys.Where(a => !currentAccountIds.Contains(a)).ToList())
|
||||||
|
|
||||||
foreach (var account in accounts)
|
|
||||||
{
|
{
|
||||||
|
_inboxSyncCounters.TryRemove(staleAccountId, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
var synchronizationTasks = accounts
|
||||||
|
.Select(account => ExecuteAutoSynchronizationForAccountAsync(account, cancellationToken))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await Task.WhenAll(synchronizationTasks).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (lockTaken)
|
||||||
|
{
|
||||||
|
_autoSynchronizationSemaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteAutoSynchronizationForAccountAsync(Wino.Core.Domain.Entities.Shared.MailAccount account, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (_synchronizationManager == null)
|
||||||
|
return;
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (_synchronizationManager.IsAccountSynchronizing(account.Id))
|
if (_synchronizationManager.IsAccountSynchronizing(account.Id))
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
var inboxSyncOptions = new MailSynchronizationOptions()
|
var inboxSyncOptions = new MailSynchronizationOptions
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
Type = MailSynchronizationType.InboxOnly
|
Type = MailSynchronizationType.InboxOnly
|
||||||
@@ -1097,12 +1188,13 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
if (inboxSyncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
|
if (inboxSyncResult.CompletedState is SynchronizationCompletedState.Success or SynchronizationCompletedState.PartiallyCompleted)
|
||||||
{
|
{
|
||||||
_inboxSyncCounters.TryAdd(account.Id, 0);
|
await ClearInvalidCredentialAttentionIfNeededAsync(account.Id).ConfigureAwait(false);
|
||||||
_inboxSyncCounters[account.Id]++;
|
|
||||||
|
|
||||||
if (_inboxSyncCounters[account.Id] >= InboxSyncsPerFullSync)
|
var inboxSyncCount = _inboxSyncCounters.AddOrUpdate(account.Id, 1, (_, currentCount) => currentCount + 1);
|
||||||
|
|
||||||
|
if (inboxSyncCount >= InboxSyncsPerFullSync)
|
||||||
{
|
{
|
||||||
var fullSyncOptions = new MailSynchronizationOptions()
|
var fullSyncOptions = new MailSynchronizationOptions
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
Type = MailSynchronizationType.FullFolders
|
Type = MailSynchronizationType.FullFolders
|
||||||
@@ -1114,9 +1206,9 @@ public partial class App : WinoApplication,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!account.IsCalendarAccessGranted)
|
if (!account.IsCalendarAccessGranted)
|
||||||
continue;
|
return;
|
||||||
|
|
||||||
var calendarOptions = new CalendarSynchronizationOptions()
|
var calendarOptions = new CalendarSynchronizationOptions
|
||||||
{
|
{
|
||||||
AccountId = account.Id,
|
AccountId = account.Id,
|
||||||
Type = CalendarSynchronizationType.CalendarMetadata
|
Type = CalendarSynchronizationType.CalendarMetadata
|
||||||
@@ -1124,14 +1216,18 @@ public partial class App : WinoApplication,
|
|||||||
|
|
||||||
await _synchronizationManager.SynchronizeCalendarAsync(calendarOptions, cancellationToken).ConfigureAwait(false);
|
await _synchronizationManager.SynchronizeCalendarAsync(calendarOptions, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
private async Task ClearInvalidCredentialAttentionIfNeededAsync(Guid accountId)
|
||||||
{
|
{
|
||||||
if (lockTaken)
|
if (_accountService == null)
|
||||||
{
|
return;
|
||||||
_autoSynchronizationSemaphore.Release();
|
|
||||||
}
|
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||||
}
|
|
||||||
|
if (account?.AttentionReason != AccountAttentionReason.InvalidCredentials)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _accountService.ClearAccountAttentionAsync(accountId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -15,8 +15,9 @@
|
|||||||
TargetType="Button">
|
TargetType="Button">
|
||||||
<Setter Property="Margin" Value="0" />
|
<Setter Property="Margin" Value="0" />
|
||||||
<Setter Property="Height" Value="32" />
|
<Setter Property="Height" Value="32" />
|
||||||
|
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="Foreground" Value="{ThemeResource SystemColorControlAccentBrush}" />
|
<!--<Setter Property="Foreground" Value="{ThemeResource SystemColorControlAccentBrush}" />-->
|
||||||
|
|
||||||
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
|
||||||
<Setter Property="VerticalAlignment" Value="Center" />
|
<Setter Property="VerticalAlignment" Value="Center" />
|
||||||
@@ -27,7 +28,7 @@
|
|||||||
<Grid Margin="4,0,0,0" Background="Transparent">
|
<Grid Margin="4,0,0,0" Background="Transparent">
|
||||||
<Grid Background="Transparent" ColumnSpacing="16">
|
<Grid Background="Transparent" ColumnSpacing="16">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="64" />
|
<ColumnDefinition Width="128" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Wino.Mail.WinUI.Controls.SyncAnimationControl"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:animatedvisuals="using:Wino.Mail.WinUI.AnimatedVisuals">
|
||||||
|
|
||||||
|
<AnimatedVisualPlayer
|
||||||
|
x:Name="AnimationPlayer"
|
||||||
|
AutoPlay="True"
|
||||||
|
Stretch="Uniform">
|
||||||
|
<animatedvisuals:SyncRefreshAnimation />
|
||||||
|
</AnimatedVisualPlayer>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Wino.Mail.WinUI.Controls;
|
||||||
|
|
||||||
|
public sealed partial class SyncAnimationControl : UserControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty IsPlayingProperty = DependencyProperty.Register(
|
||||||
|
nameof(IsPlaying),
|
||||||
|
typeof(bool),
|
||||||
|
typeof(SyncAnimationControl),
|
||||||
|
new PropertyMetadata(true, OnIsPlayingChanged));
|
||||||
|
|
||||||
|
public bool IsPlaying
|
||||||
|
{
|
||||||
|
get => (bool)GetValue(IsPlayingProperty);
|
||||||
|
set => SetValue(IsPlayingProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SyncAnimationControl()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (IsPlaying)
|
||||||
|
{
|
||||||
|
PlayAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnIsPlayingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
var control = (SyncAnimationControl)d;
|
||||||
|
|
||||||
|
if ((bool)e.NewValue)
|
||||||
|
{
|
||||||
|
control.PlayAnimation();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
control.AnimationPlayer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayAnimation()
|
||||||
|
{
|
||||||
|
#pragma warning disable CS4014 // Fire-and-forget is intentional for looped animation playback.
|
||||||
|
AnimationPlayer.PlayAsync(0, 1, looped: true);
|
||||||
|
#pragma warning restore CS4014
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<ContentDialog
|
|
||||||
x:Class="Wino.Dialogs.WhatIsNewDialog"
|
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
||||||
xmlns:controls="using:Wino.Mail.WinUI.Controls"
|
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
|
||||||
xmlns:domain="using:Wino.Core.Domain"
|
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
HorizontalContentAlignment="Stretch"
|
|
||||||
VerticalContentAlignment="Stretch"
|
|
||||||
Style="{StaticResource WinoDialogStyle}"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
|
|
||||||
<ContentDialog.Resources>
|
|
||||||
<x:Double x:Key="ContentDialogMinWidth">480</x:Double>
|
|
||||||
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
|
|
||||||
<x:Double x:Key="ContentDialogMinHeight">480</x:Double>
|
|
||||||
<x:Double x:Key="ContentDialogMaxHeight">700</x:Double>
|
|
||||||
</ContentDialog.Resources>
|
|
||||||
|
|
||||||
<Grid RowSpacing="16">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<controls:UpdateNotesFlipViewControl x:Name="UpdateNotesControl" Sections="{x:Bind Sections, Mode=OneTime}" />
|
|
||||||
|
|
||||||
<StackPanel
|
|
||||||
Grid.Row="1"
|
|
||||||
HorizontalAlignment="Right"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
Spacing="8">
|
|
||||||
<Button
|
|
||||||
x:Name="GetStartedButton"
|
|
||||||
Click="OnGetStartedClicked"
|
|
||||||
Content="{x:Bind domain:Translator.WhatIsNew_GetStartedButton}"
|
|
||||||
Style="{StaticResource AccentButtonStyle}"
|
|
||||||
Visibility="Collapsed" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
</ContentDialog>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Wino.Core.Domain.Interfaces;
|
|
||||||
using Wino.Core.Domain.Models.Updates;
|
|
||||||
|
|
||||||
namespace Wino.Dialogs;
|
|
||||||
|
|
||||||
public sealed partial class WhatIsNewDialog : ContentDialog
|
|
||||||
{
|
|
||||||
private readonly IUpdateManager _updateManager;
|
|
||||||
|
|
||||||
public List<UpdateNoteSection> Sections { get; }
|
|
||||||
|
|
||||||
private bool _canClose = false;
|
|
||||||
|
|
||||||
public WhatIsNewDialog(UpdateNotes notes, IUpdateManager updateManager)
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
_updateManager = updateManager;
|
|
||||||
Sections = notes.Sections;
|
|
||||||
|
|
||||||
// Show the Get Started button immediately when there is only one page.
|
|
||||||
UpdateNotesControl.SelectedIndexChanged += OnUpdateSectionChanged;
|
|
||||||
UpdateGetStartedButtonVisibility(UpdateNotesControl.SelectedIndex);
|
|
||||||
Closing += OnDialogClosing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnUpdateSectionChanged(object? sender, int selectedIndex)
|
|
||||||
=> UpdateGetStartedButtonVisibility(selectedIndex);
|
|
||||||
|
|
||||||
private void UpdateGetStartedButtonVisibility(int selectedIndex)
|
|
||||||
{
|
|
||||||
GetStartedButton.Visibility = selectedIndex == Sections.Count - 1
|
|
||||||
? Visibility.Visible
|
|
||||||
: Visibility.Collapsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
|
|
||||||
{
|
|
||||||
// Only allow closing when Get Started button was clicked.
|
|
||||||
if (!_canClose)
|
|
||||||
args.Cancel = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnGetStartedClicked(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
GetStartedButton.IsEnabled = false;
|
|
||||||
_updateManager.MarkUpdateNotesAsSeen();
|
|
||||||
_canClose = true;
|
|
||||||
Hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<ContentDialog
|
||||||
|
x:Class="Wino.Dialogs.WinoAccountSyncExportDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:domain="using:Wino.Core.Domain"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
PrimaryButtonClick="ExportClicked"
|
||||||
|
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
|
||||||
|
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Export, Mode=OneTime}"
|
||||||
|
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Close, Mode=OneTime}"
|
||||||
|
Style="{StaticResource WinoDialogStyle}"
|
||||||
|
Title="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_Title, Mode=OneTime}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<ContentDialog.Resources>
|
||||||
|
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
|
||||||
|
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
|
||||||
|
</ContentDialog.Resources>
|
||||||
|
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<TextBlock
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_Description, Mode=OneTime}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<CheckBox
|
||||||
|
x:Name="PreferencesCheckBox"
|
||||||
|
Checked="SelectionChanged"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_IncludePreferences, Mode=OneTime}"
|
||||||
|
IsChecked="True"
|
||||||
|
Unchecked="SelectionChanged" />
|
||||||
|
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<CheckBox
|
||||||
|
x:Name="AccountsCheckBox"
|
||||||
|
Checked="SelectionChanged"
|
||||||
|
Content="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_IncludeAccounts, Mode=OneTime}"
|
||||||
|
IsChecked="True"
|
||||||
|
Unchecked="SelectionChanged" />
|
||||||
|
|
||||||
|
<Border
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||||
|
CornerRadius="10">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock
|
||||||
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_AccountsDisclaimer, Mode=OneTime}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_AccountsRelogin, Mode=OneTime}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
x:Name="ProgressPanel"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_InProgress, Mode=OneTime}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
<ProgressBar IsIndeterminate="True" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentDialog>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
namespace Wino.Dialogs;
|
||||||
|
|
||||||
|
public sealed partial class WinoAccountSyncExportDialog : ContentDialog
|
||||||
|
{
|
||||||
|
private readonly IWinoAccountDataSyncService _syncService;
|
||||||
|
private bool _isBusy;
|
||||||
|
|
||||||
|
public WinoAccountSyncExportDialog(IWinoAccountDataSyncService syncService)
|
||||||
|
{
|
||||||
|
_syncService = syncService;
|
||||||
|
InitializeComponent();
|
||||||
|
UpdateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public WinoAccountSyncExportResult? Result { get; private set; }
|
||||||
|
|
||||||
|
public Exception? FailureException { get; private set; }
|
||||||
|
|
||||||
|
private async void ExportClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
|
{
|
||||||
|
args.Cancel = true;
|
||||||
|
|
||||||
|
if (!HasSelection())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var deferral = args.GetDeferral();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetBusyState(true);
|
||||||
|
FailureException = null;
|
||||||
|
Result = await _syncService.ExportAsync(new WinoAccountSyncSelection(
|
||||||
|
PreferencesCheckBox.IsChecked == true,
|
||||||
|
AccountsCheckBox.IsChecked == true));
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
FailureException = ex;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetBusyState(false);
|
||||||
|
deferral.Complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SelectionChanged(object sender, RoutedEventArgs e)
|
||||||
|
=> UpdateButtonState();
|
||||||
|
|
||||||
|
private void SetBusyState(bool isBusy)
|
||||||
|
{
|
||||||
|
_isBusy = isBusy;
|
||||||
|
ProgressPanel.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
IsSecondaryButtonEnabled = !isBusy;
|
||||||
|
UpdateButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateButtonState()
|
||||||
|
=> IsPrimaryButtonEnabled = !_isBusy && HasSelection();
|
||||||
|
|
||||||
|
private bool HasSelection()
|
||||||
|
=> PreferencesCheckBox.IsChecked == true || AccountsCheckBox.IsChecked == true;
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
IRecipient<CalendarListAdded>,
|
IRecipient<CalendarListAdded>,
|
||||||
IRecipient<CalendarListUpdated>,
|
IRecipient<CalendarListUpdated>,
|
||||||
IRecipient<CalendarListDeleted>,
|
IRecipient<CalendarListDeleted>,
|
||||||
IRecipient<AccountRemovedMessage>
|
IRecipient<AccountRemovedMessage>,
|
||||||
|
IRecipient<AccountUpdatedMessage>,
|
||||||
|
IRecipient<AccountCalendarSynchronizationStateChanged>
|
||||||
{
|
{
|
||||||
private readonly object _calendarStateLock = new();
|
private readonly object _calendarStateLock = new();
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
public partial ReadOnlyObservableGroupedCollection<MailAccount, AccountCalendarViewModel> GroupedCalendars { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial bool IsAnySynchronizationInProgress { get; set; }
|
||||||
|
|
||||||
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
|
public IEnumerable<AccountCalendarViewModel> ActiveCalendars
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -84,6 +89,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
Messenger.Register<CalendarListUpdated>(this);
|
Messenger.Register<CalendarListUpdated>(this);
|
||||||
Messenger.Register<CalendarListDeleted>(this);
|
Messenger.Register<CalendarListDeleted>(this);
|
||||||
Messenger.Register<AccountRemovedMessage>(this);
|
Messenger.Register<AccountRemovedMessage>(this);
|
||||||
|
Messenger.Register<AccountUpdatedMessage>(this);
|
||||||
|
Messenger.Register<AccountCalendarSynchronizationStateChanged>(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e)
|
private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e)
|
||||||
@@ -114,6 +121,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
group.Add(calendar);
|
group.Add(calendar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateAggregateSynchronizationState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,6 +149,8 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
_internalGroupedCalendars.Remove(group);
|
_internalGroupedCalendars.Remove(group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateAggregateSynchronizationState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,4 +351,60 @@ public partial class AccountCalendarStateService : ObservableRecipient,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void Receive(AccountUpdatedMessage message)
|
||||||
|
{
|
||||||
|
if (Dispatcher != null)
|
||||||
|
{
|
||||||
|
await Dispatcher.ExecuteOnUIThread(() => UpdateGroupedAccount(message.Account));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateGroupedAccount(message.Account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void Receive(AccountCalendarSynchronizationStateChanged message)
|
||||||
|
{
|
||||||
|
if (Dispatcher != null)
|
||||||
|
{
|
||||||
|
await Dispatcher.ExecuteOnUIThread(() => UpdateCalendarSynchronizationState(message));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateCalendarSynchronizationState(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateGroupedAccount(MailAccount updatedAccount)
|
||||||
|
{
|
||||||
|
GroupedAccountCalendarViewModel? groupedAccount;
|
||||||
|
lock (_calendarStateLock)
|
||||||
|
{
|
||||||
|
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == updatedAccount.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedAccount?.UpdateAccount(updatedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCalendarSynchronizationState(AccountCalendarSynchronizationStateChanged message)
|
||||||
|
{
|
||||||
|
GroupedAccountCalendarViewModel? groupedAccount;
|
||||||
|
lock (_calendarStateLock)
|
||||||
|
{
|
||||||
|
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == message.AccountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupedAccount == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
groupedAccount.IsSynchronizationInProgress = message.IsSynchronizationInProgress;
|
||||||
|
groupedAccount.SynchronizationStatus = message.SynchronizationStatus;
|
||||||
|
UpdateAggregateSynchronizationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAggregateSynchronizationState()
|
||||||
|
{
|
||||||
|
IsAnySynchronizationInProgress = _internalGroupedAccountCalendars.Any(a => a.IsSynchronizationInProgress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,14 +28,16 @@ namespace Wino.Services;
|
|||||||
public class DialogService : DialogServiceBase, IMailDialogService
|
public class DialogService : DialogServiceBase, IMailDialogService
|
||||||
{
|
{
|
||||||
private readonly IWinoAccountProfileService _winoAccountProfileService;
|
private readonly IWinoAccountProfileService _winoAccountProfileService;
|
||||||
|
private readonly IWinoAccountDataSyncService _winoAccountDataSyncService;
|
||||||
|
|
||||||
public DialogService(INewThemeService themeService,
|
public DialogService(INewThemeService themeService,
|
||||||
IConfigurationService configurationService,
|
IConfigurationService configurationService,
|
||||||
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
IApplicationResourceManager<ResourceDictionary> applicationResourceManager,
|
||||||
IUpdateManager updateManager,
|
IWinoAccountProfileService winoAccountProfileService,
|
||||||
IWinoAccountProfileService winoAccountProfileService) : base(themeService, configurationService, applicationResourceManager, updateManager)
|
IWinoAccountDataSyncService winoAccountDataSyncService) : base(themeService, configurationService, applicationResourceManager)
|
||||||
{
|
{
|
||||||
_winoAccountProfileService = winoAccountProfileService;
|
_winoAccountProfileService = winoAccountProfileService;
|
||||||
|
_winoAccountDataSyncService = winoAccountDataSyncService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
|
||||||
@@ -279,4 +281,21 @@ public class DialogService : DialogServiceBase, IMailDialogService
|
|||||||
|
|
||||||
return dialog.Result;
|
return dialog.Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WinoAccountSyncExportResult?> ShowWinoAccountExportDialogAsync()
|
||||||
|
{
|
||||||
|
var dialog = new WinoAccountSyncExportDialog(_winoAccountDataSyncService)
|
||||||
|
{
|
||||||
|
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
||||||
|
};
|
||||||
|
|
||||||
|
await HandleDialogPresentationAsync(dialog);
|
||||||
|
|
||||||
|
if (dialog.FailureException != null)
|
||||||
|
{
|
||||||
|
throw dialog.FailureException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return dialog.Result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ using Wino.Core.Domain.Interfaces;
|
|||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Common;
|
using Wino.Core.Domain.Models.Common;
|
||||||
using Wino.Core.Domain.Models.Printing;
|
using Wino.Core.Domain.Models.Printing;
|
||||||
using Wino.Core.Domain.Models.Updates;
|
|
||||||
using Wino.Dialogs;
|
using Wino.Dialogs;
|
||||||
using Wino.Mail.WinUI.Dialogs;
|
using Wino.Mail.WinUI.Dialogs;
|
||||||
using Wino.Mail.WinUI.Extensions;
|
using Wino.Mail.WinUI.Extensions;
|
||||||
@@ -31,16 +30,13 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
|
|
||||||
protected INewThemeService ThemeService { get; }
|
protected INewThemeService ThemeService { get; }
|
||||||
protected IConfigurationService ConfigurationService { get; }
|
protected IConfigurationService ConfigurationService { get; }
|
||||||
protected IUpdateManager UpdateManager { get; }
|
|
||||||
|
|
||||||
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
protected IApplicationResourceManager<ResourceDictionary> ApplicationResourceManager { get; }
|
||||||
|
|
||||||
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager, IUpdateManager updateManager)
|
public DialogServiceBase(INewThemeService themeService, IConfigurationService configurationService, IApplicationResourceManager<ResourceDictionary> applicationResourceManager)
|
||||||
{
|
{
|
||||||
ThemeService = themeService;
|
ThemeService = themeService;
|
||||||
ConfigurationService = configurationService;
|
ConfigurationService = configurationService;
|
||||||
ApplicationResourceManager = applicationResourceManager;
|
ApplicationResourceManager = applicationResourceManager;
|
||||||
UpdateManager = updateManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected XamlRoot? GetXamlRoot()
|
protected XamlRoot? GetXamlRoot()
|
||||||
@@ -392,14 +388,4 @@ public class DialogServiceBase : IDialogServiceBase
|
|||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ShowWhatIsNewDialogAsync(UpdateNotes notes)
|
|
||||||
{
|
|
||||||
var dialog = new WhatIsNewDialog(notes, UpdateManager)
|
|
||||||
{
|
|
||||||
RequestedTheme = ThemeService.RootTheme.ToWindowsElementTheme()
|
|
||||||
};
|
|
||||||
|
|
||||||
await HandleDialogPresentationAsync(dialog);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,10 +219,13 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
public bool ChangeApplicationMode(WinoApplicationMode mode)
|
public bool ChangeApplicationMode(WinoApplicationMode mode)
|
||||||
=> ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode));
|
=> ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode));
|
||||||
|
|
||||||
|
public bool ChangeApplicationMode(WinoApplicationMode mode, ShellModeActivationContext activationContext)
|
||||||
|
=> ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode, activationContext));
|
||||||
|
|
||||||
public bool CanGoBack()
|
public bool CanGoBack()
|
||||||
=> ExecuteOnNavigationThread(CanGoBackInternal);
|
=> ExecuteOnNavigationThread(CanGoBackInternal);
|
||||||
|
|
||||||
private bool ChangeApplicationModeInternal(WinoApplicationMode mode, object? activationParameter = null)
|
private bool ChangeApplicationModeInternal(WinoApplicationMode mode, ShellModeActivationContext? activationContext = null)
|
||||||
{
|
{
|
||||||
var coreFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame);
|
var coreFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame);
|
||||||
|
|
||||||
@@ -254,7 +257,8 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
shell.ActivateMode(mode, new ShellModeActivationContext
|
shell.ActivateMode(mode, new ShellModeActivationContext
|
||||||
{
|
{
|
||||||
IsInitialActivation = isInitialShellNavigation,
|
IsInitialActivation = isInitialShellNavigation,
|
||||||
Parameter = activationParameter
|
SuppressStartupFlows = activationContext?.SuppressStartupFlows ?? false,
|
||||||
|
Parameter = activationContext?.Parameter
|
||||||
});
|
});
|
||||||
|
|
||||||
ResetCurrentModeBackStackState();
|
ResetCurrentModeBackStackState();
|
||||||
@@ -280,7 +284,10 @@ public class NavigationService : NavigationServiceBase, INavigationService
|
|||||||
{
|
{
|
||||||
if (_statePersistanceService.ApplicationMode != WinoApplicationMode.Settings)
|
if (_statePersistanceService.ApplicationMode != WinoApplicationMode.Settings)
|
||||||
{
|
{
|
||||||
return ChangeApplicationModeInternal(WinoApplicationMode.Settings, settingsTarget);
|
return ChangeApplicationModeInternal(WinoApplicationMode.Settings, new ShellModeActivationContext
|
||||||
|
{
|
||||||
|
Parameter = settingsTarget
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
page = WinoPage.SettingsPage;
|
page = WinoPage.SettingsPage;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
<ScrollViewer>
|
<ScrollViewer>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
MaxWidth="860"
|
|
||||||
Padding="36,28,36,36"
|
Padding="36,28,36,36"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Stretch"
|
||||||
|
|
||||||
Spacing="24">
|
Spacing="24">
|
||||||
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
|
|||||||
@@ -219,6 +219,11 @@
|
|||||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||||
Text="Manage account settings" />
|
Text="Manage account settings" />
|
||||||
<TextBlock Margin="20,0,0,12" Text="Add new account or manage individidual account settings." />
|
<TextBlock Margin="20,0,0,12" Text="Add new account or manage individidual account settings." />
|
||||||
|
<Button
|
||||||
|
Margin="20,0,0,12"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
Click="ManageAccountsClicked"
|
||||||
|
Content="{x:Bind domain:Translator.MenuManageAccounts}" />
|
||||||
<ListView
|
<ListView
|
||||||
x:Name="AccountsList"
|
x:Name="AccountsList"
|
||||||
IsItemClickEnabled="False"
|
IsItemClickEnabled="False"
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ public sealed partial class SettingOptionsPage : SettingOptionsPageAbstract
|
|||||||
ViewModel.NavigateToAddAccount();
|
ViewModel.NavigateToAddAccount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ManageAccountsClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel.NavigateToManageAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
private void SettingsSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text))
|
if (args.Reason == AutoSuggestionBoxTextChangeReason.UserInput || string.IsNullOrWhiteSpace(sender.Text))
|
||||||
|
|||||||
@@ -130,6 +130,34 @@
|
|||||||
MaxWidth="600"
|
MaxWidth="600"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Spacing="8">
|
Spacing="8">
|
||||||
|
<HyperlinkButton
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Command="{x:Bind ViewModel.ImportFromWinoAccountCommand}"
|
||||||
|
Content="{x:Bind domain:Translator.WelcomeWindow_ImportFromWinoAccount}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
x:Name="ImportProgressPanel"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
Spacing="8"
|
||||||
|
Visibility="{x:Bind ViewModel.IsImportInProgress, Mode=OneWay}">
|
||||||
|
<TextBlock
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.WelcomeWindow_ImportInProgress}" />
|
||||||
|
<ProgressBar IsIndeterminate="True" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock
|
||||||
|
x:Name="ImportStatusTextBlock"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind ViewModel.ImportStatusMessage, Mode=OneWay}"
|
||||||
|
TextAlignment="Center"
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Visibility="{x:Bind ViewModel.HasImportStatus, Mode=OneWay}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
|||||||
@@ -92,17 +92,22 @@
|
|||||||
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
|
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<PathIcon
|
<Button
|
||||||
x:Name="AttentionIcon"
|
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Width="16"
|
|
||||||
Height="16"
|
|
||||||
Margin="8,0"
|
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
x:Load="{x:Bind IsAttentionRequired, Mode=OneWay}"
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
Data="F1 M 2.021484 18.769531 C 1.767578 18.769531 1.52832 18.720703 1.303711 18.623047 C 1.079102 18.525391 0.880534 18.391928 0.708008 18.222656 C 0.535482 18.053385 0.398763 17.856445 0.297852 17.631836 C 0.19694 17.407227 0.146484 17.167969 0.146484 16.914062 C 0.146484 16.614584 0.211589 16.328125 0.341797 16.054688 L 7.695312 1.347656 C 7.851562 1.035156 8.082682 0.784506 8.388672 0.595703 C 8.694661 0.406902 9.023438 0.3125 9.375 0.3125 C 9.726562 0.3125 10.055338 0.406902 10.361328 0.595703 C 10.667317 0.784506 10.898438 1.035156 11.054688 1.347656 L 18.408203 16.054688 C 18.53841 16.328125 18.603516 16.614584 18.603516 16.914062 C 18.603516 17.167969 18.553059 17.407227 18.452148 17.631836 C 18.351236 17.856445 18.216145 18.053385 18.046875 18.222656 C 17.877604 18.391928 17.679035 18.525391 17.451172 18.623047 C 17.223307 18.720703 16.982422 18.769531 16.728516 18.769531 Z M 16.728516 17.519531 C 16.884766 17.519531 17.027994 17.460938 17.158203 17.34375 C 17.28841 17.226562 17.353516 17.086588 17.353516 16.923828 C 17.353516 16.806641 17.330729 16.702475 17.285156 16.611328 L 9.931641 1.904297 C 9.879557 1.793621 9.80306 1.708984 9.702148 1.650391 C 9.601236 1.591797 9.492188 1.5625 9.375 1.5625 C 9.257812 1.5625 9.148763 1.593426 9.047852 1.655273 C 8.946939 1.717123 8.870442 1.800131 8.818359 1.904297 L 1.464844 16.611328 C 1.419271 16.702475 1.396484 16.803387 1.396484 16.914062 C 1.396484 17.083334 1.459961 17.226562 1.586914 17.34375 C 1.713867 17.460938 1.858724 17.519531 2.021484 17.519531 Z M 8.75 11.875 L 8.75 6.875 C 8.75 6.705729 8.811849 6.559245 8.935547 6.435547 C 9.059244 6.31185 9.205729 6.25 9.375 6.25 C 9.544271 6.25 9.690755 6.31185 9.814453 6.435547 C 9.93815 6.559245 10 6.705729 10 6.875 L 10 11.875 C 10 12.044271 9.93815 12.190756 9.814453 12.314453 C 9.690755 12.438151 9.544271 12.5 9.375 12.5 C 9.205729 12.5 9.059244 12.438151 8.935547 12.314453 C 8.811849 12.190756 8.75 12.044271 8.75 11.875 Z M 8.4375 14.375 C 8.4375 14.114584 8.528646 13.893229 8.710938 13.710938 C 8.893229 13.528646 9.114583 13.4375 9.375 13.4375 C 9.635416 13.4375 9.856771 13.528646 10.039062 13.710938 C 10.221354 13.893229 10.3125 14.114584 10.3125 14.375 C 10.3125 14.635417 10.221354 14.856771 10.039062 15.039062 C 9.856771 15.221354 9.635416 15.3125 9.375 15.3125 C 9.114583 15.3125 8.893229 15.221354 8.710938 15.039062 C 8.528646 14.856771 8.4375 14.635417 8.4375 14.375 Z "
|
BorderBrush="{ThemeResource InfoBarWarningSeverityIconBackground}"
|
||||||
Foreground="{ThemeResource InfoBarWarningSeverityIconBackground}" />
|
BorderThickness="1"
|
||||||
|
Click="AttentionIconClicked"
|
||||||
|
Foreground="{ThemeResource InfoBarWarningSeverityIconBackground}"
|
||||||
|
ToolTipService.ToolTip="{x:Bind domain:Translator.Info_AccountAttentionRequiredClickableMessage, Mode=OneWay}"
|
||||||
|
Visibility="{x:Bind IsAttentionRequired, Mode=OneWay}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<PathIcon x:Name="AttentionIcon" Data="F1 M 2.021484 18.769531 C 1.767578 18.769531 1.52832 18.720703 1.303711 18.623047 C 1.079102 18.525391 0.880534 18.391928 0.708008 18.222656 C 0.535482 18.053385 0.398763 17.856445 0.297852 17.631836 C 0.19694 17.407227 0.146484 17.167969 0.146484 16.914062 C 0.146484 16.614584 0.211589 16.328125 0.341797 16.054688 L 7.695312 1.347656 C 7.851562 1.035156 8.082682 0.784506 8.388672 0.595703 C 8.694661 0.406902 9.023438 0.3125 9.375 0.3125 C 9.726562 0.3125 10.055338 0.406902 10.361328 0.595703 C 10.667317 0.784506 10.898438 1.035156 11.054688 1.347656 L 18.408203 16.054688 C 18.53841 16.328125 18.603516 16.614584 18.603516 16.914062 C 18.603516 17.167969 18.553059 17.407227 18.452148 17.631836 C 18.351236 17.856445 18.216145 18.053385 18.046875 18.222656 C 17.877604 18.391928 17.679035 18.525391 17.451172 18.623047 C 17.223307 18.720703 16.982422 18.769531 16.728516 18.769531 Z M 16.728516 17.519531 C 16.884766 17.519531 17.027994 17.460938 17.158203 17.34375 C 17.28841 17.226562 17.353516 17.086588 17.353516 16.923828 C 17.353516 16.806641 17.330729 16.702475 17.285156 16.611328 L 9.931641 1.904297 C 9.879557 1.793621 9.80306 1.708984 9.702148 1.650391 C 9.601236 1.591797 9.492188 1.5625 9.375 1.5625 C 9.257812 1.5625 9.148763 1.593426 9.047852 1.655273 C 8.946939 1.717123 8.870442 1.800131 8.818359 1.904297 L 1.464844 16.611328 C 1.419271 16.702475 1.396484 16.803387 1.396484 16.914062 C 1.396484 17.083334 1.459961 17.226562 1.586914 17.34375 C 1.713867 17.460938 1.858724 17.519531 2.021484 17.519531 Z M 8.75 11.875 L 8.75 6.875 C 8.75 6.705729 8.811849 6.559245 8.935547 6.435547 C 9.059244 6.31185 9.205729 6.25 9.375 6.25 C 9.544271 6.25 9.690755 6.31185 9.814453 6.435547 C 9.93815 6.559245 10 6.705729 10 6.875 L 10 11.875 C 10 12.044271 9.93815 12.190756 9.814453 12.314453 C 9.690755 12.438151 9.544271 12.5 9.375 12.5 C 9.205729 12.5 9.059244 12.438151 8.935547 12.314453 C 8.811849 12.190756 8.75 12.044271 8.75 11.875 Z M 8.4375 14.375 C 8.4375 14.114584 8.528646 13.893229 8.710938 13.710938 C 8.893229 13.528646 9.114583 13.4375 9.375 13.4375 C 9.635416 13.4375 9.856771 13.528646 10.039062 13.710938 C 10.221354 13.893229 10.3125 14.114584 10.3125 14.375 C 10.3125 14.635417 10.221354 14.856771 10.039062 15.039062 C 9.856771 15.221354 9.635416 15.3125 9.375 15.3125 C 9.114583 15.3125 8.893229 15.221354 8.710938 15.039062 C 8.528646 14.856771 8.4375 14.635417 8.4375 14.375 Z " />
|
||||||
|
<TextBlock Text="{x:Bind domain:Translator.Info_AccountAttentionRequiredAction, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<muxc:ProgressRing
|
<muxc:ProgressRing
|
||||||
x:Name="SynchronizationProgressBar"
|
x:Name="SynchronizationProgressBar"
|
||||||
@@ -455,6 +460,7 @@
|
|||||||
Visibility="Collapsed">
|
Visibility="Collapsed">
|
||||||
<Grid x:Name="CalendarPaneContent" Visibility="Collapsed">
|
<Grid x:Name="CalendarPaneContent" Visibility="Collapsed">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
@@ -481,9 +487,31 @@
|
|||||||
Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton, Mode=OneTime}" />
|
Text="{x:Bind domain:Translator.CalendarEventCompose_NewEventButton, Mode=OneTime}" />
|
||||||
</coreControls:WinoNavigationViewItem>
|
</coreControls:WinoNavigationViewItem>
|
||||||
|
|
||||||
|
<coreControls:WinoNavigationViewItem
|
||||||
|
x:Name="SynchronizeCalendarsNavigationItem"
|
||||||
|
Grid.Row="1"
|
||||||
|
Height="50"
|
||||||
|
Margin="0,0,0,12"
|
||||||
|
AutomationProperties.Name="{x:Bind domain:Translator.Buttons_Sync, Mode=OneTime}"
|
||||||
|
IsEnabled="{x:Bind ViewModel.CalendarClient.CanSynchronizeCalendars, Mode=OneWay}"
|
||||||
|
IsTabStop="True"
|
||||||
|
KeyDown="SynchronizeCalendarsNavigationItemKeyDown"
|
||||||
|
SelectsOnInvoked="False"
|
||||||
|
Tapped="SynchronizeCalendarsNavigationItemTapped">
|
||||||
|
<muxc:NavigationViewItem.Icon>
|
||||||
|
<coreControls:WinoFontIcon Icon="Sync" />
|
||||||
|
</muxc:NavigationViewItem.Icon>
|
||||||
|
<TextBlock
|
||||||
|
Margin="0,-2,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="16"
|
||||||
|
Style="{StaticResource FlyoutPickerTitleTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.Buttons_Sync, Mode=OneTime}" />
|
||||||
|
</coreControls:WinoNavigationViewItem>
|
||||||
|
|
||||||
<muxc:CalendarView
|
<muxc:CalendarView
|
||||||
x:Name="VisibleDateRangeCalendarView"
|
x:Name="VisibleDateRangeCalendarView"
|
||||||
Grid.Row="1"
|
Grid.Row="2"
|
||||||
Margin="12,0"
|
Margin="12,0"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
SelectedDatesChanged="VisibleDateRangeCalendarViewSelectedDatesChanged"
|
SelectedDatesChanged="VisibleDateRangeCalendarViewSelectedDatesChanged"
|
||||||
@@ -492,7 +520,7 @@
|
|||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
x:Name="CalendarHostListView"
|
x:Name="CalendarHostListView"
|
||||||
Grid.Row="2"
|
Grid.Row="3"
|
||||||
SelectionMode="None">
|
SelectionMode="None">
|
||||||
<ListView.Header>
|
<ListView.Header>
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -509,6 +537,7 @@
|
|||||||
<muxc:Expander.Header>
|
<muxc:Expander.Header>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
@@ -519,13 +548,29 @@
|
|||||||
IsChecked="{x:Bind IsCheckedState, Mode=TwoWay}"
|
IsChecked="{x:Bind IsCheckedState, Mode=TwoWay}"
|
||||||
IsThreeState="True" />
|
IsThreeState="True" />
|
||||||
|
|
||||||
<TextBlock
|
<Ellipse
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
Margin="8,0,10,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
TextWrapping="Wrap">
|
Fill="{x:Bind AccountColorHex, Converter={StaticResource HexToColorBrushConverter}, Mode=OneWay}" />
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="2"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Spacing="2">
|
||||||
|
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap">
|
||||||
<Run FontWeight="SemiBold" Text="{x:Bind Account.Name}" />
|
<Run FontWeight="SemiBold" Text="{x:Bind Account.Name}" />
|
||||||
<Run FontSize="12" Text=" (" /><Run FontSize="12" Text="{x:Bind Account.Address}" /><Run FontSize="12" Text=")" />
|
<Run FontSize="12" Text=" (" /><Run FontSize="12" Text="{x:Bind Account.Address}" /><Run FontSize="12" Text=")" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
<ProgressBar
|
||||||
|
Height="4"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
ShowPaused="False"
|
||||||
|
ShowError="False"
|
||||||
|
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</muxc:Expander.Header>
|
</muxc:Expander.Header>
|
||||||
<muxc:Expander.Content>
|
<muxc:Expander.Content>
|
||||||
|
|||||||
@@ -277,6 +277,17 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
|||||||
await InvokeNewCalendarEventAsync();
|
await InvokeNewCalendarEventAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void AttentionIconClicked(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not FrameworkElement { DataContext: AccountMenuItem accountMenuItem })
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (ViewModel.MailClient is MailAppShellViewModel mailClient)
|
||||||
|
{
|
||||||
|
await mailClient.HandleAccountAttentionAsync(accountMenuItem.Parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void NewCalendarEventNavigationItemKeyDown(object sender, KeyRoutedEventArgs e)
|
private async void NewCalendarEventNavigationItemKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Key is not (VirtualKey.Enter or VirtualKey.Space))
|
if (e.Key is not (VirtualKey.Enter or VirtualKey.Space))
|
||||||
@@ -289,6 +300,31 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
|||||||
private Task InvokeNewCalendarEventAsync()
|
private Task InvokeNewCalendarEventAsync()
|
||||||
=> ViewModel.CalendarClient.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
=> ViewModel.CalendarClient.HandleNavigationItemInvokedAsync(new NewCalendarEventMenuItem());
|
||||||
|
|
||||||
|
private async void SynchronizeCalendarsNavigationItemTapped(object sender, TappedRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
e.Handled = true;
|
||||||
|
await InvokeCalendarSynchronizationAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SynchronizeCalendarsNavigationItemKeyDown(object sender, KeyRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key is not (VirtualKey.Enter or VirtualKey.Space))
|
||||||
|
return;
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
await InvokeCalendarSynchronizationAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task InvokeCalendarSynchronizationAsync()
|
||||||
|
{
|
||||||
|
if (ViewModel.CalendarClient.SyncCommand.CanExecute(null))
|
||||||
|
{
|
||||||
|
ViewModel.CalendarClient.SyncCommand.Execute(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public void Receive(CalendarDisplayTypeChangedMessage message) => NotifyTitleBarContentChanged();
|
public void Receive(CalendarDisplayTypeChangedMessage message) => NotifyTitleBarContentChanged();
|
||||||
|
|
||||||
public void Receive(AccountCreatedMessage message)
|
public void Receive(AccountCreatedMessage message)
|
||||||
|
|||||||
@@ -200,6 +200,7 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TabbedCommandBar" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Lottie" />
|
||||||
|
|
||||||
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
||||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" />
|
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" />
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Wino.Messaging.UI;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Emitted when calendar synchronization state for an account changes.
|
||||||
|
/// </summary>
|
||||||
|
public record AccountCalendarSynchronizationStateChanged(
|
||||||
|
Guid AccountId,
|
||||||
|
CalendarSynchronizationType SynchronizationType,
|
||||||
|
bool IsSynchronizationInProgress,
|
||||||
|
string SynchronizationStatus = "") : UIMessageBase<AccountCalendarSynchronizationStateChanged>;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Wino.Messaging.UI;
|
||||||
|
|
||||||
|
public record WelcomeImportCompletedMessage(int ImportedMailboxCount) : UIMessageBase<WelcomeImportCompletedMessage>;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Diagnostics;
|
using CommunityToolkit.Diagnostics;
|
||||||
@@ -642,10 +643,11 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
IsExtended = true,
|
IsExtended = true,
|
||||||
RemoteCalendarId = string.Empty,
|
RemoteCalendarId = string.Empty,
|
||||||
TimeZone = string.Empty,
|
TimeZone = string.Empty,
|
||||||
BackgroundColorHex = await GetNextDistinctCalendarColorAsync().ConfigureAwait(false),
|
BackgroundColorHex = await GetNextDistinctCalendarColorAsync().ConfigureAwait(false)
|
||||||
TextColorHex = "#FFFFFF"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
localCalendar.TextColorHex = GetReadableTextColorHex(localCalendar.BackgroundColorHex);
|
||||||
|
|
||||||
await Connection.InsertAsync(localCalendar, typeof(AccountCalendar)).ConfigureAwait(false);
|
await Connection.InsertAsync(localCalendar, typeof(AccountCalendar)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -658,6 +660,16 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
return CalendarColorPalette.GetDistinctColor(usedColors.Select(a => a.BackgroundColorHex));
|
return CalendarColorPalette.GetDistinctColor(usedColors.Select(a => a.BackgroundColorHex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetReadableTextColorHex(string backgroundColorHex)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(backgroundColorHex))
|
||||||
|
return "#FFFFFF";
|
||||||
|
|
||||||
|
var color = ColorTranslator.FromHtml(backgroundColorHex);
|
||||||
|
var luminance = ((0.299 * color.R) + (0.587 * color.G) + (0.114 * color.B)) / 255d;
|
||||||
|
return luminance > 0.6 ? "#111111" : "#FFFFFF";
|
||||||
|
}
|
||||||
|
|
||||||
public async Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair)
|
public async Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair)
|
||||||
{
|
{
|
||||||
foreach (var pair in accountIdOrderPair)
|
foreach (var pair in accountIdOrderPair)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public static class ServicesContainerSetup
|
|||||||
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
services.AddTransient<IKeyboardShortcutService, KeyboardShortcutService>();
|
||||||
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
services.AddSingleton<IWinoAccountApiClient, WinoAccountApiClient>();
|
||||||
services.AddSingleton<IWinoAccountProfileService, WinoAccountProfileService>();
|
services.AddSingleton<IWinoAccountProfileService, WinoAccountProfileService>();
|
||||||
|
services.AddTransient<IWinoAccountDataSyncService, WinoAccountDataSyncService>();
|
||||||
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
services.AddSingleton<IContactPictureFileService, ContactPictureFileService>();
|
||||||
|
|
||||||
services.AddTransient<ICalDavClient, CalDavClient>();
|
services.AddTransient<ICalDavClient, CalDavClient>();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using Wino.Core.Domain.Models.Accounts;
|
|||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
|
|
||||||
namespace Wino.Services;
|
namespace Wino.Services;
|
||||||
|
|
||||||
@@ -159,33 +160,27 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
using var response = await SendAuthorizedAsync(
|
using var response = await SendAuthorizedAsync(
|
||||||
() => CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/settings"),
|
() => CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/settings"),
|
||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
return null;
|
{
|
||||||
|
throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
|
if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.IsSuccessStatusCode)
|
await EnsureSuccessResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||||
return null;
|
|
||||||
|
|
||||||
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default)
|
public async Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
using var response = await SendAuthorizedAsync(
|
using var response = await SendAuthorizedAsync(
|
||||||
() => CreateAuthorizedRequestAsync(
|
() => CreateAuthorizedRequestAsync(
|
||||||
@@ -195,14 +190,54 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
cancellationToken).ConfigureAwait(false);
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (response == null)
|
if (response == null)
|
||||||
return false;
|
|
||||||
|
|
||||||
return response.IsSuccessStatusCode;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
return false;
|
throw new InvalidOperationException("MissingAccessToken");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await EnsureSuccessResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserMailboxSyncListDto> GetMailboxesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var response = await SendAuthorizedAsync(
|
||||||
|
() => CreateAuthorizedRequestAsync(HttpMethod.Get, "api/v1/users/me/mailboxes"),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureSuccessResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var envelope = string.IsNullOrWhiteSpace(payload)
|
||||||
|
? null
|
||||||
|
: JsonSerializer.Deserialize(payload, WinoAccountApiJsonContext.Default.ApiEnvelopeUserMailboxSyncListDto);
|
||||||
|
|
||||||
|
if (envelope?.IsSuccess == true && envelope.Result != null)
|
||||||
|
{
|
||||||
|
return envelope.Result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(ExtractErrorMessage(payload) ?? envelope?.ErrorCode ?? "Mailbox synchronization request failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using var response = await SendAuthorizedAsync(
|
||||||
|
() => CreateAuthorizedRequestAsync(
|
||||||
|
HttpMethod.Put,
|
||||||
|
"api/v1/users/me/mailboxes",
|
||||||
|
() => JsonContent.Create(request, WinoAccountApiJsonContext.Default.ReplaceUserMailboxesRequestDto)),
|
||||||
|
cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (response == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
}
|
||||||
|
|
||||||
|
await EnsureSuccessResponseAsync(response, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WinoAccountApiResult<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
|
private async Task<WinoAccountApiResult<AuthResultDto>> SendAuthRequestAsync<TRequest>(string endpoint, TRequest request, JsonTypeInfo<TRequest> typeInfo, CancellationToken cancellationToken)
|
||||||
@@ -321,6 +356,19 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
return !string.IsNullOrWhiteSpace(value);
|
return !string.IsNullOrWhiteSpace(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task EnsureSuccessResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
ExtractErrorMessage(payload)
|
||||||
|
?? $"HTTP {(int)response.StatusCode} {response.ReasonPhrase}".Trim());
|
||||||
|
}
|
||||||
|
|
||||||
private Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
private Task<ApiEnvelope<TResponse>> SendAuthorizedRequestAsync<TResponse>(string endpoint, JsonTypeInfo<ApiEnvelope<TResponse>> typeInfo, CancellationToken cancellationToken)
|
||||||
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
|
=> SendAuthorizedRequestAsync(HttpMethod.Get, endpoint, typeInfo, cancellationToken);
|
||||||
|
|
||||||
@@ -549,7 +597,9 @@ public sealed class WinoAccountApiClient : IWinoAccountApiClient, IDisposable
|
|||||||
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiStatusResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<AiTextResultDto>))]
|
[JsonSerializable(typeof(ApiEnvelope<AiTextResultDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<WinoStoreCollectionsIdTicketInfo>))]
|
[JsonSerializable(typeof(ApiEnvelope<WinoStoreCollectionsIdTicketInfo>))]
|
||||||
|
[JsonSerializable(typeof(ApiEnvelope<UserMailboxSyncListDto>))]
|
||||||
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
[JsonSerializable(typeof(ApiEnvelope<JsonElement>))]
|
||||||
|
[JsonSerializable(typeof(ReplaceUserMailboxesRequestDto))]
|
||||||
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
internal sealed partial class WinoAccountApiJsonContext : JsonSerializerContext;
|
||||||
|
|
||||||
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
|
internal sealed record SyncStoreEntitlementsRequest(string? StoreIdKey, string? PurchaseIdKey);
|
||||||
|
|||||||
@@ -0,0 +1,289 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CommunityToolkit.Mvvm.Messaging;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
|
using Wino.Messaging.Client.Accounts;
|
||||||
|
|
||||||
|
namespace Wino.Services;
|
||||||
|
|
||||||
|
public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||||
|
{
|
||||||
|
private const int DefaultMaxConcurrentClients = 5;
|
||||||
|
|
||||||
|
private readonly IWinoAccountProfileService _profileService;
|
||||||
|
private readonly IPreferencesService _preferencesService;
|
||||||
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
|
public WinoAccountDataSyncService(
|
||||||
|
IWinoAccountProfileService profileService,
|
||||||
|
IPreferencesService preferencesService,
|
||||||
|
IAccountService accountService)
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
_preferencesService = preferencesService;
|
||||||
|
_accountService = accountService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WinoAccountSyncExportResult> ExportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var exportedMailboxCount = 0;
|
||||||
|
|
||||||
|
if (selection.IncludePreferences)
|
||||||
|
{
|
||||||
|
await _profileService.SaveSettingsAsync(_preferencesService.ExportPreferences(), cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.IncludeAccounts)
|
||||||
|
{
|
||||||
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
|
var request = new ReplaceUserMailboxesRequestDto
|
||||||
|
{
|
||||||
|
Mailboxes = accounts
|
||||||
|
.OrderBy(a => a.Order)
|
||||||
|
.Select(MapMailbox)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
await _profileService.ReplaceMailboxesAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
exportedMailboxCount = request.Mailboxes.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WinoAccountSyncExportResult
|
||||||
|
{
|
||||||
|
IncludedPreferences = selection.IncludePreferences,
|
||||||
|
IncludedAccounts = selection.IncludeAccounts,
|
||||||
|
ExportedMailboxCount = exportedMailboxCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WinoAccountSyncImportResult> ImportAsync(WinoAccountSyncSelection selection, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = new WinoAccountSyncImportResult
|
||||||
|
{
|
||||||
|
IncludedPreferences = selection.IncludePreferences,
|
||||||
|
IncludedAccounts = selection.IncludeAccounts
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selection.IncludePreferences)
|
||||||
|
{
|
||||||
|
var settingsJson = await _profileService.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!string.IsNullOrWhiteSpace(settingsJson))
|
||||||
|
{
|
||||||
|
var (appliedCount, failedCount) = _preferencesService.ImportPreferences(settingsJson);
|
||||||
|
result = new WinoAccountSyncImportResult
|
||||||
|
{
|
||||||
|
IncludedPreferences = result.IncludedPreferences,
|
||||||
|
IncludedAccounts = result.IncludedAccounts,
|
||||||
|
HadRemotePreferences = true,
|
||||||
|
AppliedPreferenceCount = appliedCount,
|
||||||
|
FailedPreferenceCount = failedCount,
|
||||||
|
ImportedMailboxCount = result.ImportedMailboxCount,
|
||||||
|
SkippedDuplicateMailboxCount = result.SkippedDuplicateMailboxCount,
|
||||||
|
RemoteMailboxCount = result.RemoteMailboxCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection.IncludeAccounts)
|
||||||
|
{
|
||||||
|
var mailboxes = await _profileService.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
var orderedMailboxes = mailboxes.Mailboxes
|
||||||
|
.OrderBy(a => a.SortOrder)
|
||||||
|
.ThenBy(a => a.Address, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var localAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
|
var existingKeys = localAccounts
|
||||||
|
.Select(CreateMailboxKey)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var importedMailboxCount = 0;
|
||||||
|
var skippedDuplicateMailboxCount = 0;
|
||||||
|
|
||||||
|
foreach (var mailbox in orderedMailboxes)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var mailboxKey = CreateMailboxKey(mailbox.Address, mailbox.ProviderType);
|
||||||
|
if (!existingKeys.Add(mailboxKey))
|
||||||
|
{
|
||||||
|
skippedDuplicateMailboxCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var account = CreateImportedAccount(mailbox);
|
||||||
|
var serverInformation = CreateImportedServerInformation(mailbox, account.Id);
|
||||||
|
|
||||||
|
await _accountService.CreateAccountAsync(account, serverInformation).ConfigureAwait(false);
|
||||||
|
await _accountService.CreateRootAliasAsync(account.Id, account.Address).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (account.ProviderType == MailProviderType.IMAP4)
|
||||||
|
{
|
||||||
|
var persistedAccount = await _accountService.GetAccountAsync(account.Id).ConfigureAwait(false);
|
||||||
|
if (persistedAccount != null && persistedAccount.AttentionReason != AccountAttentionReason.InvalidCredentials)
|
||||||
|
{
|
||||||
|
persistedAccount.AttentionReason = AccountAttentionReason.InvalidCredentials;
|
||||||
|
await _accountService.UpdateAccountAsync(persistedAccount).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
importedMailboxCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importedMailboxCount > 0)
|
||||||
|
{
|
||||||
|
WeakReferenceMessenger.Default.Send(new AccountsMenuRefreshRequested(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new WinoAccountSyncImportResult
|
||||||
|
{
|
||||||
|
IncludedPreferences = result.IncludedPreferences,
|
||||||
|
IncludedAccounts = result.IncludedAccounts,
|
||||||
|
HadRemotePreferences = result.HadRemotePreferences,
|
||||||
|
AppliedPreferenceCount = result.AppliedPreferenceCount,
|
||||||
|
FailedPreferenceCount = result.FailedPreferenceCount,
|
||||||
|
ImportedMailboxCount = importedMailboxCount,
|
||||||
|
SkippedDuplicateMailboxCount = skippedDuplicateMailboxCount,
|
||||||
|
RemoteMailboxCount = orderedMailboxes.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await RepairStartupEntityAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserMailboxSyncItemDto MapMailbox(MailAccount account)
|
||||||
|
{
|
||||||
|
var serverInformation = account.ProviderType == MailProviderType.IMAP4
|
||||||
|
? account.ServerInformation
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return new UserMailboxSyncItemDto
|
||||||
|
{
|
||||||
|
Address = account.Address ?? string.Empty,
|
||||||
|
ProviderType = (int)account.ProviderType,
|
||||||
|
SpecialImapProvider = (int)account.SpecialImapProvider,
|
||||||
|
AccountName = account.Name,
|
||||||
|
SenderName = account.SenderName,
|
||||||
|
AccountColorHex = account.AccountColorHex,
|
||||||
|
SortOrder = account.Order,
|
||||||
|
IsCalendarAccessGranted = account.IsCalendarAccessGranted,
|
||||||
|
CalendarSupportMode = serverInformation != null ? (int)serverInformation.CalendarSupportMode : 0,
|
||||||
|
IncomingServer = serverInformation?.IncomingServer,
|
||||||
|
IncomingServerPort = serverInformation?.IncomingServerPort,
|
||||||
|
IncomingServerUsername = serverInformation?.IncomingServerUsername,
|
||||||
|
IncomingServerSocketOption = serverInformation != null ? (int?)serverInformation.IncomingServerSocketOption : null,
|
||||||
|
IncomingAuthenticationMethod = serverInformation != null ? (int?)serverInformation.IncomingAuthenticationMethod : null,
|
||||||
|
OutgoingServer = serverInformation?.OutgoingServer,
|
||||||
|
OutgoingServerPort = serverInformation?.OutgoingServerPort,
|
||||||
|
OutgoingServerUsername = serverInformation?.OutgoingServerUsername,
|
||||||
|
OutgoingServerSocketOption = serverInformation != null ? (int?)serverInformation.OutgoingServerSocketOption : null,
|
||||||
|
OutgoingAuthenticationMethod = serverInformation != null ? (int?)serverInformation.OutgoingAuthenticationMethod : null,
|
||||||
|
CalDavServiceUrl = serverInformation?.CalDavServiceUrl,
|
||||||
|
CalDavUsername = serverInformation?.CalDavUsername,
|
||||||
|
ProxyServer = serverInformation?.ProxyServer,
|
||||||
|
ProxyServerPort = serverInformation?.ProxyServerPort,
|
||||||
|
MaxConcurrentClients = serverInformation?.MaxConcurrentClients
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MailAccount CreateImportedAccount(UserMailboxSyncItemDto mailbox)
|
||||||
|
{
|
||||||
|
var providerType = (MailProviderType)mailbox.ProviderType;
|
||||||
|
|
||||||
|
return new MailAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Address = mailbox.Address.Trim(),
|
||||||
|
Name = string.IsNullOrWhiteSpace(mailbox.AccountName) ? mailbox.Address.Trim() : mailbox.AccountName.Trim(),
|
||||||
|
SenderName = string.IsNullOrWhiteSpace(mailbox.SenderName) ? mailbox.Address.Trim() : mailbox.SenderName.Trim(),
|
||||||
|
ProviderType = providerType,
|
||||||
|
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
|
||||||
|
AccountColorHex = mailbox.AccountColorHex?.Trim(),
|
||||||
|
Base64ProfilePictureData = string.Empty,
|
||||||
|
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
|
||||||
|
SynchronizationDeltaIdentifier = string.Empty,
|
||||||
|
CalendarSynchronizationDeltaIdentifier = string.Empty,
|
||||||
|
AttentionReason = AccountAttentionReason.InvalidCredentials
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CustomServerInformation? CreateImportedServerInformation(UserMailboxSyncItemDto mailbox, Guid accountId)
|
||||||
|
{
|
||||||
|
var providerType = (MailProviderType)mailbox.ProviderType;
|
||||||
|
if (providerType != MailProviderType.IMAP4)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CustomServerInformation
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccountId = accountId,
|
||||||
|
Address = mailbox.Address.Trim(),
|
||||||
|
IncomingServer = mailbox.IncomingServer?.Trim(),
|
||||||
|
IncomingServerPort = mailbox.IncomingServerPort?.Trim(),
|
||||||
|
IncomingServerUsername = mailbox.IncomingServerUsername?.Trim(),
|
||||||
|
IncomingServerPassword = string.Empty,
|
||||||
|
IncomingServerSocketOption = mailbox.IncomingServerSocketOption is int incomingSocketOption
|
||||||
|
? (ImapConnectionSecurity)incomingSocketOption
|
||||||
|
: ImapConnectionSecurity.Auto,
|
||||||
|
IncomingAuthenticationMethod = mailbox.IncomingAuthenticationMethod is int incomingAuthMethod
|
||||||
|
? (ImapAuthenticationMethod)incomingAuthMethod
|
||||||
|
: ImapAuthenticationMethod.Auto,
|
||||||
|
OutgoingServer = mailbox.OutgoingServer?.Trim(),
|
||||||
|
OutgoingServerPort = mailbox.OutgoingServerPort?.Trim(),
|
||||||
|
OutgoingServerUsername = mailbox.OutgoingServerUsername?.Trim(),
|
||||||
|
OutgoingServerPassword = string.Empty,
|
||||||
|
OutgoingServerSocketOption = mailbox.OutgoingServerSocketOption is int outgoingSocketOption
|
||||||
|
? (ImapConnectionSecurity)outgoingSocketOption
|
||||||
|
: ImapConnectionSecurity.Auto,
|
||||||
|
OutgoingAuthenticationMethod = mailbox.OutgoingAuthenticationMethod is int outgoingAuthMethod
|
||||||
|
? (ImapAuthenticationMethod)outgoingAuthMethod
|
||||||
|
: ImapAuthenticationMethod.Auto,
|
||||||
|
CalDavServiceUrl = mailbox.CalDavServiceUrl?.Trim(),
|
||||||
|
CalDavUsername = mailbox.CalDavUsername?.Trim(),
|
||||||
|
CalDavPassword = string.Empty,
|
||||||
|
CalendarSupportMode = (ImapCalendarSupportMode)mailbox.CalendarSupportMode,
|
||||||
|
ProxyServer = mailbox.ProxyServer?.Trim(),
|
||||||
|
ProxyServerPort = mailbox.ProxyServerPort?.Trim(),
|
||||||
|
MaxConcurrentClients = mailbox.MaxConcurrentClients.GetValueOrDefault(DefaultMaxConcurrentClients)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RepairStartupEntityAsync()
|
||||||
|
{
|
||||||
|
if (!_preferencesService.StartupEntityId.HasValue)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var startupEntityId = _preferencesService.StartupEntityId.Value;
|
||||||
|
var accounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
||||||
|
var accountIds = accounts.Select(a => a.Id);
|
||||||
|
var mergedInboxIds = accounts.Where(a => a.MergedInboxId.HasValue).Select(a => a.MergedInboxId!.Value);
|
||||||
|
|
||||||
|
if (accountIds.Concat(mergedInboxIds).Contains(startupEntityId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_preferencesService.StartupEntityId = accounts.FirstOrDefault()?.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateMailboxKey(MailAccount account)
|
||||||
|
=> CreateMailboxKey(account.Address, (int)account.ProviderType);
|
||||||
|
|
||||||
|
private static string CreateMailboxKey(string? address, int providerType)
|
||||||
|
=> $"{address?.Trim().ToLowerInvariant()}|{providerType}";
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ using Wino.Core.Domain.Models.Accounts;
|
|||||||
using Wino.Mail.Api.Contracts.Ai;
|
using Wino.Mail.Api.Contracts.Ai;
|
||||||
using Wino.Mail.Api.Contracts.Auth;
|
using Wino.Mail.Api.Contracts.Auth;
|
||||||
using Wino.Mail.Api.Contracts.Common;
|
using Wino.Mail.Api.Contracts.Common;
|
||||||
|
using Wino.Mail.Api.Contracts.Users;
|
||||||
using Wino.Messaging.UI;
|
using Wino.Messaging.UI;
|
||||||
|
|
||||||
namespace Wino.Services;
|
namespace Wino.Services;
|
||||||
@@ -285,6 +286,38 @@ public sealed class WinoAccountProfileService : BaseDatabaseService, IWinoAccoun
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetSettingsAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false)
|
||||||
|
?? throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
|
||||||
|
return await _apiClient.GetSettingsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveSettingsAsync(string settingsJson, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false)
|
||||||
|
?? throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
|
||||||
|
await _apiClient.SaveSettingsAsync(settingsJson, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<UserMailboxSyncListDto> GetMailboxesAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false)
|
||||||
|
?? throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
|
||||||
|
return await _apiClient.GetMailboxesAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReplaceMailboxesAsync(ReplaceUserMailboxesRequestDto request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
_ = await GetAuthenticatedAccountAsync(cancellationToken).ConfigureAwait(false)
|
||||||
|
?? throw new InvalidOperationException("MissingAccessToken");
|
||||||
|
|
||||||
|
await _apiClient.ReplaceMailboxesAsync(request, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default)
|
public async Task<bool> ProcessBillingCallbackAsync(Uri callbackUri, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await _billingCallbackLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _billingCallbackLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user