Updated synchronization progress implementation.

This commit is contained in:
Burak Kaan Köse
2026-04-11 12:57:51 +02:00
parent 40318ef99c
commit 5cb49efeb4
20 changed files with 444 additions and 145 deletions
@@ -1,10 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Calendar.ViewModels.Data; namespace Wino.Calendar.ViewModels.Data;
@@ -32,7 +33,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
AccountCalendars.CollectionChanged += CalendarListUpdated; AccountCalendars.CollectionChanged += CalendarListUpdated;
} }
private void CalendarListUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) private void CalendarListUpdated(object sender, NotifyCollectionChangedEventArgs e)
{ {
if (e.Action == NotifyCollectionChangedAction.Add) if (e.Action == NotifyCollectionChangedAction.Add)
{ {
@@ -59,13 +60,11 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{ {
if (sender is AccountCalendarViewModel viewModel) if (sender is AccountCalendarViewModel viewModel &&
e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
{ {
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked)) ManageIsCheckedState();
{ UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
ManageIsCheckedState();
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
}
} }
} }
@@ -79,19 +78,54 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
public partial string AccountColorHex { get; set; } = string.Empty; public partial string AccountColorHex { get; set; } = string.Empty;
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
public partial bool IsSynchronizationInProgress { get; set; } public partial bool IsSynchronizationInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
[ObservableProperty] [ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty; public partial string SynchronizationStatus { get; set; } = string.Empty;
public bool CanSynchronize => !IsSynchronizationInProgress; public bool CanSynchronize => !IsSynchronizationInProgress;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress; public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
private bool _isExternalPropChangeBlocked = false; public double SynchronizationProgress
{
get
{
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
}
public double SynchronizationProgressValue => SynchronizationProgress;
private bool _isExternalPropChangeBlocked;
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
{
if (progress == null || progress.AccountId != Account.Id)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
}
private void ManageIsCheckedState() private void ManageIsCheckedState()
{ {
if (_isExternalPropChangeBlocked) return; if (_isExternalPropChangeBlocked)
return;
_isExternalPropChangeBlocked = true; _isExternalPropChangeBlocked = true;
@@ -113,17 +147,13 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue) partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
{ {
if (_isExternalPropChangeBlocked) return; if (_isExternalPropChangeBlocked)
return;
// Update is triggered by user on the three-state checkbox.
// We should not report all changes one by one.
_isExternalPropChangeBlocked = true; _isExternalPropChangeBlocked = true;
if (newValue == null) if (newValue == null)
{ {
// Only primary calendars must be checked.
foreach (var calendar in AccountCalendars) foreach (var calendar in AccountCalendars)
{ {
UpdateCalendarCheckedState(calendar, calendar.IsPrimary); UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
@@ -138,7 +168,6 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
} }
_isExternalPropChangeBlocked = false; _isExternalPropChangeBlocked = false;
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty); CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
} }
@@ -146,22 +175,17 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
{ {
var currentValue = accountCalendarViewModel.IsChecked; var currentValue = accountCalendarViewModel.IsChecked;
if (currentValue == newValue && !ignoreValueCheck) return; if (currentValue == newValue && !ignoreValueCheck)
return;
accountCalendarViewModel.IsChecked = newValue; accountCalendarViewModel.IsChecked = newValue;
// No need to report. if (_isExternalPropChangeBlocked)
if (_isExternalPropChangeBlocked == true) return; return;
CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel); CalendarSelectionStateChanged?.Invoke(this, accountCalendarViewModel);
} }
partial void OnIsSynchronizationInProgressChanged(bool value)
{
OnPropertyChanged(nameof(CanSynchronize));
OnPropertyChanged(nameof(IsSynchronizationProgressVisible));
}
public void UpdateAccount(MailAccount updatedAccount) public void UpdateAccount(MailAccount updatedAccount)
{ {
if (updatedAccount == null || updatedAccount.Id != Account.Id) if (updatedAccount == null || updatedAccount.Id != Account.Id)
@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Enums;
public enum SynchronizationProgressCategory
{
Mail,
Calendar
}
@@ -1,18 +1,25 @@
using System.Collections.Generic; using System.Collections.Generic;
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.Models.Synchronization;
namespace Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Interfaces;
public interface IAccountMenuItem : IMenuItem public interface IAccountMenuItem : IMenuItem
{ {
bool IsEnabled { get; set; } bool IsEnabled { get; set; }
bool IsSynchronizationInProgress { get; set; }
/// <summary> /// <summary>
/// Calculated synchronization progress percentage (0-100). -1 for indeterminate. /// Calculated synchronization progress percentage (0-100).
/// </summary> /// </summary>
double SynchronizationProgress { get; } double SynchronizationProgress { get; }
/// <summary>
/// Progress value clamped for XAML progress controls.
/// </summary>
double SynchronizationProgressValue { get; }
/// <summary> /// <summary>
/// Total items to sync. 0 for indeterminate progress. /// Total items to sync. 0 for indeterminate progress.
/// </summary> /// </summary>
@@ -30,6 +37,7 @@ public interface IAccountMenuItem : IMenuItem
int UnreadItemCount { get; set; } int UnreadItemCount { get; set; }
IEnumerable<MailAccount> HoldingAccounts { get; } IEnumerable<MailAccount> HoldingAccounts { get; }
void ApplySynchronizationProgress(AccountSynchronizationProgress progress);
void UpdateAccount(MailAccount account); void UpdateAccount(MailAccount account);
} }
@@ -41,6 +41,11 @@ public interface ISynchronizationManager
/// </summary> /// </summary>
bool IsAccountSynchronizing(Guid accountId); bool IsAccountSynchronizing(Guid accountId);
/// <summary>
/// Gets the latest centralized synchronization progress snapshot for the given account and category.
/// </summary>
AccountSynchronizationProgress GetSynchronizationProgress(Guid accountId, SynchronizationProgressCategory category);
/// <summary> /// <summary>
/// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering. /// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering.
/// </summary> /// </summary>
+28 -23
View File
@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@@ -6,6 +6,7 @@ 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.Folders; using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.MenuItems; namespace Wino.Core.Domain.MenuItems;
@@ -14,18 +15,22 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
[ObservableProperty] [ObservableProperty]
private int unreadItemCount; private int unreadItemCount;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate), nameof(SynchronizationProgress), nameof(SynchronizationProgressValue))]
public partial bool IsSynchronizationInProgress { get; set; }
/// <summary> /// <summary>
/// Total items to sync. 0 means indeterminate progress. /// Total items to sync. 0 means indeterminate progress.
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(SynchronizationProgress), nameof(IsProgressIndeterminate))] [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; } public partial int TotalItemsToSync { get; set; }
/// <summary> /// <summary>
/// Remaining items to sync. /// Remaining items to sync.
/// </summary> /// </summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress))] [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; } public partial int RemainingItemsToSync { get; set; }
/// <summary> /// <summary>
@@ -39,31 +44,22 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
public bool IsAttentionRequired => AttentionReason != AccountAttentionReason.None; public bool IsAttentionRequired => AttentionReason != AccountAttentionReason.None;
/// <summary>
/// Calculates synchronization progress percentage (0-100).
/// Returns -1 for indeterminate progress when TotalItemsToSync is 0.
/// </summary>
public double SynchronizationProgress public double SynchronizationProgress
{ {
get get
{ {
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) if (TotalItemsToSync <= 0)
return -1; // Indeterminate return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; return Math.Clamp(((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100, 0, 100);
} }
} }
/// <summary> public double SynchronizationProgressValue => SynchronizationProgress;
/// Whether synchronization progress should be visible.
/// Visible when there's active synchronization (TotalItemsToSync > 0 or RemainingItemsToSync > 0).
/// </summary>
public bool IsSynchronizationProgressVisible => TotalItemsToSync > 0 || RemainingItemsToSync > 0;
/// <summary> public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
/// Whether progress should be indeterminate (when total is 0 but there's still synchronization happening).
/// </summary> public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public bool IsProgressIndeterminate => TotalItemsToSync == 0 && RemainingItemsToSync == 0 && IsSynchronizationProgressVisible;
public Guid AccountId => Parameter.Id; public Guid AccountId => Parameter.Id;
@@ -77,7 +73,6 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
if (SetProperty(ref attentionReason, value)) if (SetProperty(ref attentionReason, value))
{ {
OnPropertyChanged(nameof(IsAttentionRequired)); OnPropertyChanged(nameof(IsAttentionRequired));
UpdateFixAccountIssueMenuItem(); UpdateFixAccountIssueMenuItem();
} }
} }
@@ -108,6 +103,17 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
UpdateAccount(account); UpdateAccount(account);
} }
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
{
if (progress == null || progress.AccountId != AccountId)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
}
public void UpdateAccount(MailAccount account) public void UpdateAccount(MailAccount account)
{ {
Parameter = account; Parameter = account;
@@ -118,7 +124,8 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
OnPropertyChanged(nameof(AccountColorHex)); OnPropertyChanged(nameof(AccountColorHex));
OnPropertyChanged(nameof(IsAttentionRequired)); OnPropertyChanged(nameof(IsAttentionRequired));
if (SubMenuItems == null) return; if (SubMenuItems == null)
return;
foreach (var item in SubMenuItems) foreach (var item in SubMenuItems)
{ {
@@ -133,12 +140,10 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
{ {
if (AttentionReason != AccountAttentionReason.None && !SubMenuItems.Any(a => a is FixAccountIssuesMenuItem)) if (AttentionReason != AccountAttentionReason.None && !SubMenuItems.Any(a => a is FixAccountIssuesMenuItem))
{ {
// Add fix issue item if not exists.
SubMenuItems.Insert(0, new FixAccountIssuesMenuItem(Parameter, this)); SubMenuItems.Insert(0, new FixAccountIssuesMenuItem(Parameter, this));
} }
else else
{ {
// Remove existing if issue is resolved.
var fixAccountIssueItem = SubMenuItems.FirstOrDefault(a => a is FixAccountIssuesMenuItem); var fixAccountIssueItem = SubMenuItems.FirstOrDefault(a => a is FixAccountIssuesMenuItem);
if (fixAccountIssueItem != null) if (fixAccountIssueItem != null)
@@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
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.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.MenuItems; namespace Wino.Core.Domain.MenuItems;
@@ -16,50 +17,37 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
[ObservableProperty] [ObservableProperty]
private int unreadItemCount; private int unreadItemCount;
/// <summary>
/// Total items to sync across all merged accounts.
/// </summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] [NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate), nameof(SynchronizationProgress), nameof(SynchronizationProgressValue))]
public partial bool IsSynchronizationInProgress { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; } public partial int TotalItemsToSync { get; set; }
/// <summary>
/// Remaining items to sync across all merged accounts.
/// </summary>
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))] [NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; } public partial int RemainingItemsToSync { get; set; }
/// <summary>
/// Current synchronization status message.
/// </summary>
[ObservableProperty] [ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty; public partial string SynchronizationStatus { get; set; } = string.Empty;
/// <summary>
/// Calculated synchronization progress for merged accounts.
/// </summary>
public double SynchronizationProgress public double SynchronizationProgress
{ {
get get
{ {
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) if (TotalItemsToSync <= 0)
return -1; // Indeterminate return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
} }
} }
/// <summary> public double SynchronizationProgressValue => SynchronizationProgress;
/// Whether synchronization progress should be visible.
/// Visible when there's active synchronization (TotalItemsToSync > 0 or RemainingItemsToSync > 0).
/// </summary>
public bool IsSynchronizationProgressVisible => TotalItemsToSync > 0 || RemainingItemsToSync > 0;
/// <summary> public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
/// Whether progress should be indeterminate.
/// </summary> public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public bool IsProgressIndeterminate => TotalItemsToSync == 0 && IsSynchronizationProgressVisible;
[ObservableProperty] [ObservableProperty]
private string mergedAccountName; private string mergedAccountName;
@@ -78,22 +66,33 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
UnreadItemCount = SubMenuItems.OfType<IAccountMenuItem>().Sum(a => a.UnreadItemCount); UnreadItemCount = SubMenuItems.OfType<IAccountMenuItem>().Sum(a => a.UnreadItemCount);
} }
/// <summary>
/// Aggregates synchronization progress from all child account menu items.
/// </summary>
public void RefreshSynchronizationProgress() public void RefreshSynchronizationProgress()
{ {
var accountMenuItems = SubMenuItems.OfType<IAccountMenuItem>().ToList(); var activeAccountMenuItems = SubMenuItems
.OfType<IAccountMenuItem>()
.Where(a => a.IsSynchronizationInProgress)
.ToList();
TotalItemsToSync = accountMenuItems.Sum(a => a.TotalItemsToSync); IsSynchronizationInProgress = activeAccountMenuItems.Any();
RemainingItemsToSync = accountMenuItems.Sum(a => a.RemainingItemsToSync); TotalItemsToSync = activeAccountMenuItems.Sum(a => a.TotalItemsToSync);
RemainingItemsToSync = activeAccountMenuItems.Sum(a => a.RemainingItemsToSync);
SynchronizationStatus = activeAccountMenuItems
.Select(a => a.SynchronizationStatus)
.FirstOrDefault(s => !string.IsNullOrWhiteSpace(s)) ?? string.Empty;
}
// Use first non-empty status message public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
SynchronizationStatus = accountMenuItems.FirstOrDefault(a => !string.IsNullOrEmpty(a.SynchronizationStatus))?.SynchronizationStatus ?? string.Empty; {
if (progress == null)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
} }
public void UpdateAccount(MailAccount account) public void UpdateAccount(MailAccount account)
{ {
} }
} }
@@ -0,0 +1,30 @@
using System;
using Wino.Core.Domain.Enums;
namespace Wino.Core.Domain.Models.Synchronization;
public record AccountSynchronizationProgress(
Guid AccountId,
SynchronizationProgressCategory Category,
bool IsInProgress,
bool IsIndeterminate,
double ProgressPercentage,
int TotalUnits,
int RemainingUnits,
string Status,
AccountSynchronizerState State)
{
public int CompletedUnits => Math.Max(0, TotalUnits - RemainingUnits);
public static AccountSynchronizationProgress Idle(Guid accountId, SynchronizationProgressCategory category)
=> new(
accountId,
category,
false,
false,
0,
0,
0,
string.Empty,
AccountSynchronizerState.Idle);
}
@@ -108,6 +108,11 @@
"SyncAction_SynchronizingCalendarData": "Synchronizing calendar data", "SyncAction_SynchronizingCalendarData": "Synchronizing calendar data",
"SyncAction_SynchronizingCalendarEvents": "Synchronizing calendar events", "SyncAction_SynchronizingCalendarEvents": "Synchronizing calendar events",
"SyncAction_SynchronizingCalendarMetadata": "Synchronizing calendar metadata", "SyncAction_SynchronizingCalendarMetadata": "Synchronizing calendar metadata",
"SynchronizationProgress_ApplyingChanges": "Applying changes",
"SynchronizationProgress_CalendarInProgress": "Calendar sync in progress",
"SynchronizationProgress_CalendarPercent": "Calendar sync {0}%",
"SynchronizationProgress_MailInProgress": "Mail sync in progress",
"SynchronizationProgress_MailPercent": "Mail sync {0}%",
"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",
+110 -4
View File
@@ -23,7 +23,7 @@ namespace Wino.Core.Services;
/// Singleton manager that handles synchronizer instances and operations for all accounts. /// Singleton manager that handles synchronizer instances and operations for all accounts.
/// Replaces the old WinoServerConnectionManager functionality. /// Replaces the old WinoServerConnectionManager functionality.
/// </summary> /// </summary>
public class SynchronizationManager : ISynchronizationManager public class SynchronizationManager : ISynchronizationManager, IRecipient<AccountSynchronizerStateChanged>
{ {
private static readonly Lazy<SynchronizationManager> _instance = new(() => new SynchronizationManager()); private static readonly Lazy<SynchronizationManager> _instance = new(() => new SynchronizationManager());
public static SynchronizationManager Instance => _instance.Value; public static SynchronizationManager Instance => _instance.Value;
@@ -31,6 +31,8 @@ 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 ConcurrentDictionary<Guid, SemaphoreSlim> _calendarSynchronizationLocks = new();
private readonly ConcurrentDictionary<Guid, AccountSynchronizationProgress> _mailSynchronizationProgress = new();
private readonly ConcurrentDictionary<Guid, AccountSynchronizationProgress> _calendarSynchronizationProgress = 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>();
@@ -41,6 +43,7 @@ public class SynchronizationManager : ISynchronizationManager
private INotificationBuilder _notificationBuilder; private INotificationBuilder _notificationBuilder;
private bool _isInitialized = false; private bool _isInitialized = false;
private bool _isRegisteredForProgressMessages;
private SynchronizationManager() { } private SynchronizationManager() { }
@@ -73,6 +76,11 @@ public class SynchronizationManager : ISynchronizationManager
// DO NOT create synchronizers here to avoid requiring window handles during initialization. // DO NOT create synchronizers here to avoid requiring window handles during initialization.
// Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync. // Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync.
if (!_isRegisteredForProgressMessages)
{
WeakReferenceMessenger.Default.Register<AccountSynchronizerStateChanged>(this);
_isRegisteredForProgressMessages = true;
}
_isInitialized = true; _isInitialized = true;
_logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily."); _logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily.");
@@ -219,6 +227,21 @@ public class SynchronizationManager : ISynchronizationManager
return false; return false;
} }
public AccountSynchronizationProgress GetSynchronizationProgress(Guid accountId, SynchronizationProgressCategory category)
{
EnsureInitialized();
return category switch
{
SynchronizationProgressCategory.Calendar => _calendarSynchronizationProgress.TryGetValue(accountId, out var calendarProgress)
? calendarProgress
: AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar),
_ => _mailSynchronizationProgress.TryGetValue(accountId, out var mailProgress)
? mailProgress
: AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail)
};
}
/// <summary> /// <summary>
/// Queues a request to the corresponding account's synchronizer with optional synchronization triggering. /// Queues a request to the corresponding account's synchronizer with optional synchronization triggering.
/// Automatically determines whether to trigger mail or calendar synchronization based on the request type. /// Automatically determines whether to trigger mail or calendar synchronization based on the request type.
@@ -651,6 +674,9 @@ public class SynchronizationManager : ISynchronizationManager
_logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId); _logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId);
} }
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -679,6 +705,9 @@ public class SynchronizationManager : ISynchronizationManager
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId); _logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
} }
} }
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
} }
/// <summary> /// <summary>
@@ -770,6 +799,33 @@ public class SynchronizationManager : ISynchronizationManager
} }
} }
public void Receive(AccountSynchronizerStateChanged message)
{
var totalUnits = Math.Max(0, message.TotalItemsToSync);
var remainingUnits = totalUnits > 0
? Math.Clamp(message.RemainingItemsToSync, 0, totalUnits)
: 0;
var isInProgress = message.NewState != AccountSynchronizerState.Idle;
var isIndeterminate = isInProgress && totalUnits <= 0;
var progressPercentage = totalUnits > 0
? ((double)(totalUnits - remainingUnits) / totalUnits) * 100
: 0;
var progress = new AccountSynchronizationProgress(
message.AccountId,
message.ProgressCategory,
isInProgress,
isIndeterminate,
progressPercentage,
totalUnits,
remainingUnits,
BuildSynchronizationStatus(message.ProgressCategory, message.NewState, totalUnits, progressPercentage, message.SynchronizationStatus),
message.NewState);
PublishSynchronizationProgress(progress);
}
private void EnsureInitialized() private void EnsureInitialized()
{ {
if (!_isInitialized) if (!_isInitialized)
@@ -804,17 +860,67 @@ public class SynchronizationManager : ISynchronizationManager
return account?.AttentionReason == AccountAttentionReason.InvalidCredentials; return account?.AttentionReason == AccountAttentionReason.InvalidCredentials;
} }
private void PublishSynchronizationProgress(AccountSynchronizationProgress progress)
{
var normalized = progress.IsInProgress
? progress
: AccountSynchronizationProgress.Idle(progress.AccountId, progress.Category);
var cache = normalized.Category == SynchronizationProgressCategory.Calendar
? _calendarSynchronizationProgress
: _mailSynchronizationProgress;
cache.AddOrUpdate(normalized.AccountId, normalized, (_, _) => normalized);
WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(normalized));
}
private static string BuildSynchronizationStatus(
SynchronizationProgressCategory category,
AccountSynchronizerState state,
int totalUnits,
double progressPercentage,
string rawStatus)
{
if (state == AccountSynchronizerState.Idle)
return string.Empty;
if (state == AccountSynchronizerState.ExecutingRequests)
return Translator.SynchronizationProgress_ApplyingChanges;
if (totalUnits > 0)
{
var roundedProgress = (int)Math.Round(progressPercentage, MidpointRounding.AwayFromZero);
return category == SynchronizationProgressCategory.Calendar
? string.Format(Translator.SynchronizationProgress_CalendarPercent, roundedProgress)
: string.Format(Translator.SynchronizationProgress_MailPercent, roundedProgress);
}
if (category == SynchronizationProgressCategory.Calendar && !string.IsNullOrWhiteSpace(rawStatus))
return rawStatus;
return category == SynchronizationProgressCategory.Calendar
? Translator.SynchronizationProgress_CalendarInProgress
: Translator.SynchronizationProgress_MailInProgress;
}
private void PublishCalendarSynchronizationState( private void PublishCalendarSynchronizationState(
Guid accountId, Guid accountId,
CalendarSynchronizationType synchronizationType, CalendarSynchronizationType synchronizationType,
bool isSynchronizationInProgress, bool isSynchronizationInProgress,
string synchronizationStatus = "") string synchronizationStatus = "")
{ {
WeakReferenceMessenger.Default.Send(new AccountCalendarSynchronizationStateChanged( PublishSynchronizationProgress(new AccountSynchronizationProgress(
accountId, accountId,
synchronizationType, SynchronizationProgressCategory.Calendar,
isSynchronizationInProgress, isSynchronizationInProgress,
synchronizationStatus)); isSynchronizationInProgress,
0,
0,
0,
synchronizationStatus,
isSynchronizationInProgress ? AccountSynchronizerState.Synchronizing : AccountSynchronizerState.Idle));
} }
private static string GetCalendarSynchronizationStatus(CalendarSynchronizationType synchronizationType) private static string GetCalendarSynchronizationStatus(CalendarSynchronizationType synchronizationType)
+7 -4
View File
@@ -27,6 +27,7 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new(); private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new();
private readonly ConcurrentQueue<SynchronizationIssue> _capturedSynchronizationIssues = new(); private readonly ConcurrentQueue<SynchronizationIssue> _capturedSynchronizationIssues = new();
protected readonly IMessenger Messenger; protected readonly IMessenger Messenger;
protected SynchronizationProgressCategory CurrentSynchronizationProgressCategory { get; set; } = SynchronizationProgressCategory.Mail;
public MailAccount Account { get; } public MailAccount Account { get; }
@@ -44,7 +45,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
value, value,
TotalItemsToSync, TotalItemsToSync,
RemainingItemsToSync, RemainingItemsToSync,
SynchronizationStatus)); SynchronizationStatus,
CurrentSynchronizationProgressCategory));
} }
} }
@@ -75,8 +77,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
{ {
get get
{ {
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0) if (TotalItemsToSync <= 0)
return -1; // Indeterminate return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100; return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
} }
@@ -118,7 +120,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
State, State,
TotalItemsToSync, TotalItemsToSync,
RemainingItemsToSync, RemainingItemsToSync,
SynchronizationStatus)); SynchronizationStatus,
CurrentSynchronizationProgressCategory));
} }
/// <summary> /// <summary>
+14 -2
View File
@@ -22,6 +22,7 @@ using Microsoft.IdentityModel.Tokens;
using MimeKit; using MimeKit;
using MoreLinq; using MoreLinq;
using Serilog; using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
@@ -388,7 +389,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
} while (!string.IsNullOrEmpty(pageToken)); } while (!string.IsNullOrEmpty(pageToken));
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded); _logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
UpdateSyncProgress(0, 0, $"Downloaded {totalMessagesDownloaded} messages"); UpdateSyncProgress(totalFolders, 0, Translator.SyncAction_SynchronizingAccount);
} }
_logger.Information("Initial sync completed. Downloaded {Count} unique messages for {Name}", downloadedMessageIds.Count, Account.Name); _logger.Information("Initial sync completed. Downloaded {Count} unique messages for {Name}", downloadedMessageIds.Count, Account.Name);
@@ -521,8 +522,16 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
.Where(c => c.IsSynchronizationEnabled) .Where(c => c.IsSynchronizationEnabled)
.ToList(); .ToList();
foreach (var calendar in localCalendars) var totalCalendars = localCalendars.Count;
if (totalCalendars > 0)
{ {
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
}
for (int i = 0; i < totalCalendars; i++)
{
var calendar = localCalendars[i];
try try
{ {
var request = _calendarService.Events.List(calendar.RemoteCalendarId); var request = _calendarService.Events.List(calendar.RemoteCalendarId);
@@ -602,6 +611,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
} }
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false); await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -624,6 +634,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
if (!errorContext.CanContinueSync) if (!errorContext.CanContinueSync)
throw; throw;
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
} }
} }
+11 -1
View File
@@ -11,6 +11,7 @@ using MailKit.Net.Imap;
using MailKit.Search; using MailKit.Search;
using MimeKit; using MimeKit;
using Serilog; using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
@@ -1192,8 +1193,16 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
var periodStartUtc = DateTimeOffset.UtcNow.AddYears(-1); var periodStartUtc = DateTimeOffset.UtcNow.AddYears(-1);
var periodEndUtc = DateTimeOffset.UtcNow.AddYears(2); var periodEndUtc = DateTimeOffset.UtcNow.AddYears(2);
foreach (var localCalendar in localCalendars) var totalCalendars = localCalendars.Count;
if (totalCalendars > 0)
{ {
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
}
for (int i = 0; i < totalCalendars; i++)
{
var localCalendar = localCalendars[i];
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
if (!remoteCalendarsById.TryGetValue(localCalendar.RemoteCalendarId, out var remoteCalendar)) if (!remoteCalendarsById.TryGetValue(localCalendar.RemoteCalendarId, out var remoteCalendar))
@@ -1265,6 +1274,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
localCalendar.SynchronizationDeltaToken = remoteToken; localCalendar.SynchronizationDeltaToken = remoteToken;
await _imapChangeProcessor.UpdateAccountCalendarAsync(localCalendar).ConfigureAwait(false); await _imapChangeProcessor.UpdateAccountCalendarAsync(localCalendar).ConfigureAwait(false);
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
} }
return CalendarSynchronizationResult.Empty; return CalendarSynchronizationResult.Empty;
+13 -1
View File
@@ -2320,8 +2320,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
// TODO: Maybe we can batch each calendar? // TODO: Maybe we can batch each calendar?
foreach (var calendar in localCalendars) var totalCalendars = localCalendars.Count;
if (totalCalendars > 0)
{ {
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
}
for (int i = 0; i < totalCalendars; i++)
{
var calendar = localCalendars[i];
try try
{ {
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken); bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
@@ -2440,6 +2448,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false); await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
} }
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -2462,6 +2472,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
if (!errorContext.CanContinueSync) if (!errorContext.CanContinueSync)
throw; throw;
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
} }
} }
@@ -147,6 +147,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
if (shouldExecuteRequests && changeRequestQueue.Any()) if (shouldExecuteRequests && changeRequestQueue.Any())
{ {
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
State = AccountSynchronizerState.ExecutingRequests; State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new(); List<IRequestBundle<TBaseRequest>> nativeRequests = new();
@@ -264,6 +265,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken); await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
// Set indeterminate progress for initial state // Set indeterminate progress for initial state
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
UpdateSyncProgress(0, 0, "Synchronizing..."); UpdateSyncProgress(0, 0, "Synchronizing...");
State = AccountSynchronizerState.Synchronizing; State = AccountSynchronizerState.Synchronizing;
@@ -388,6 +390,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
if (shouldExecuteRequests) if (shouldExecuteRequests)
{ {
calendarRequestsWereExecuting = true; calendarRequestsWereExecuting = true;
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
State = AccountSynchronizerState.ExecutingRequests; State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new(); List<IRequestBundle<TBaseRequest>> nativeRequests = new();
@@ -482,6 +485,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
await Task.Delay(maxExecutionDelay, cancellationToken); await Task.Delay(maxExecutionDelay, cancellationToken);
} }
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
var synchronizationResult = await SynchronizeCalendarEventsInternalAsync(options, cancellationToken); var synchronizationResult = await SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
return FinalizeCalendarResult(synchronizationResult); return FinalizeCalendarResult(synchronizationResult);
} }
+37 -10
View File
@@ -38,7 +38,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<MergedInboxRenamed>, IRecipient<MergedInboxRenamed>,
IRecipient<LanguageChanged>, IRecipient<LanguageChanged>,
IRecipient<AccountMenuItemsReordered>, IRecipient<AccountMenuItemsReordered>,
IRecipient<AccountSynchronizerStateChanged>, IRecipient<AccountSynchronizationProgressUpdatedMessage>,
IRecipient<NavigateAppPreferencesRequested>, IRecipient<NavigateAppPreferencesRequested>,
IRecipient<AccountFolderConfigurationUpdated>, IRecipient<AccountFolderConfigurationUpdated>,
IRecipient<AccountRemovedMessage>, IRecipient<AccountRemovedMessage>,
@@ -157,6 +157,24 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}); });
} }
private static void ApplySynchronizationProgress(IAccountMenuItem accountMenuItem, SynchronizationProgressCategory category)
{
AccountSynchronizationProgress progress;
try
{
progress = SynchronizationManager.Instance.GetSynchronizationProgress(
accountMenuItem.HoldingAccounts.First().Id,
category);
}
catch (InvalidOperationException)
{
return;
}
accountMenuItem.ApplySynchronizationProgress(progress);
}
private async Task LoadAccountsAsync() private async Task LoadAccountsAsync()
{ {
// First clear all account menu items. // First clear all account menu items.
@@ -185,9 +203,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
foreach (var mergedAccount in mergedAccounts) foreach (var mergedAccount in mergedAccounts)
{ {
initializedAccountIds.Add(mergedAccount.Id); initializedAccountIds.Add(mergedAccount.Id);
mergedAccountMenuItem.SubMenuItems.Add(new AccountMenuItem(mergedAccount, mergedAccountMenuItem)); var accountMenuItem = new AccountMenuItem(mergedAccount, mergedAccountMenuItem);
ApplySynchronizationProgress(accountMenuItem, SynchronizationProgressCategory.Mail);
mergedAccountMenuItem.SubMenuItems.Add(accountMenuItem);
} }
mergedAccountMenuItem.RefreshSynchronizationProgress();
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
MenuItems.Add(mergedAccountMenuItem); MenuItems.Add(mergedAccountMenuItem);
@@ -196,9 +218,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
} }
else else
{ {
var accountMenuItem = new AccountMenuItem(account, null);
ApplySynchronizationProgress(accountMenuItem, SynchronizationProgressCategory.Mail);
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
MenuItems.Add(new AccountMenuItem(account, null)); MenuItems.Add(accountMenuItem);
}); });
initializedAccountIds.Add(account.Id); initializedAccountIds.Add(account.Id);
@@ -1194,17 +1219,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
UpdateFolderCollection(mailItemFolder); UpdateFolderCollection(mailItemFolder);
} }
public async void Receive(AccountSynchronizerStateChanged message) public async void Receive(AccountSynchronizationProgressUpdatedMessage message)
{ {
var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(message.AccountId); var progress = message.Progress;
if (progress.Category != SynchronizationProgressCategory.Mail)
return;
var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(progress.AccountId);
if (accountMenuItem == null) return; if (accountMenuItem == null) return;
await ExecuteUIThread(() => await ExecuteUIThread(() =>
{ {
accountMenuItem.TotalItemsToSync = message.TotalItemsToSync; accountMenuItem.ApplySynchronizationProgress(progress);
accountMenuItem.RemainingItemsToSync = message.RemainingItemsToSync;
accountMenuItem.SynchronizationStatus = message.SynchronizationStatus;
// If this account is part of a merged inbox, update the merged inbox progress as well // If this account is part of a merged inbox, update the merged inbox progress as well
if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem) if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
@@ -1231,7 +1258,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
Messenger.Register<MergedInboxRenamed>(this); Messenger.Register<MergedInboxRenamed>(this);
Messenger.Register<LanguageChanged>(this); Messenger.Register<LanguageChanged>(this);
Messenger.Register<AccountMenuItemsReordered>(this); Messenger.Register<AccountMenuItemsReordered>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this); Messenger.Register<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Register<NavigateAppPreferencesRequested>(this); Messenger.Register<NavigateAppPreferencesRequested>(this);
Messenger.Register<AccountFolderConfigurationUpdated>(this); Messenger.Register<AccountFolderConfigurationUpdated>(this);
} }
@@ -1248,7 +1275,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
Messenger.Unregister<MergedInboxRenamed>(this); Messenger.Unregister<MergedInboxRenamed>(this);
Messenger.Unregister<LanguageChanged>(this); Messenger.Unregister<LanguageChanged>(this);
Messenger.Unregister<AccountMenuItemsReordered>(this); Messenger.Unregister<AccountMenuItemsReordered>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this); Messenger.Unregister<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Unregister<NavigateAppPreferencesRequested>(this); Messenger.Unregister<NavigateAppPreferencesRequested>(this);
Messenger.Unregister<AccountFolderConfigurationUpdated>(this); Messenger.Unregister<AccountFolderConfigurationUpdated>(this);
} }
@@ -8,7 +8,9 @@ using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Data; using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces; using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Messaging.Client.Calendar; using Wino.Messaging.Client.Calendar;
using Wino.Messaging.UI; using Wino.Messaging.UI;
@@ -25,7 +27,7 @@ public partial class AccountCalendarStateService : ObservableRecipient,
IRecipient<CalendarListDeleted>, IRecipient<CalendarListDeleted>,
IRecipient<AccountRemovedMessage>, IRecipient<AccountRemovedMessage>,
IRecipient<AccountUpdatedMessage>, IRecipient<AccountUpdatedMessage>,
IRecipient<AccountCalendarSynchronizationStateChanged> IRecipient<AccountSynchronizationProgressUpdatedMessage>
{ {
private readonly object _calendarStateLock = new(); private readonly object _calendarStateLock = new();
@@ -90,7 +92,7 @@ public partial class AccountCalendarStateService : ObservableRecipient,
Messenger.Register<CalendarListDeleted>(this); Messenger.Register<CalendarListDeleted>(this);
Messenger.Register<AccountRemovedMessage>(this); Messenger.Register<AccountRemovedMessage>(this);
Messenger.Register<AccountUpdatedMessage>(this); Messenger.Register<AccountUpdatedMessage>(this);
Messenger.Register<AccountCalendarSynchronizationStateChanged>(this); Messenger.Register<AccountSynchronizationProgressUpdatedMessage>(this);
} }
private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e) private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e)
@@ -105,6 +107,15 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{ {
groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged; groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged; groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
try
{
groupedAccountCalendar.ApplySynchronizationProgress(SynchronizationManager.Instance.GetSynchronizationProgress(
groupedAccountCalendar.Account.Id,
SynchronizationProgressCategory.Calendar));
}
catch (InvalidOperationException)
{
}
_internalGroupedAccountCalendars.Add(groupedAccountCalendar); _internalGroupedAccountCalendars.Add(groupedAccountCalendar);
@@ -364,15 +375,18 @@ public partial class AccountCalendarStateService : ObservableRecipient,
} }
} }
public async void Receive(AccountCalendarSynchronizationStateChanged message) public async void Receive(AccountSynchronizationProgressUpdatedMessage message)
{ {
if (message.Progress.Category != SynchronizationProgressCategory.Calendar)
return;
if (Dispatcher != null) if (Dispatcher != null)
{ {
await Dispatcher.ExecuteOnUIThread(() => UpdateCalendarSynchronizationState(message)); await Dispatcher.ExecuteOnUIThread(() => UpdateCalendarSynchronizationState(message.Progress));
} }
else else
{ {
UpdateCalendarSynchronizationState(message); UpdateCalendarSynchronizationState(message.Progress);
} }
} }
@@ -387,19 +401,18 @@ public partial class AccountCalendarStateService : ObservableRecipient,
groupedAccount?.UpdateAccount(updatedAccount); groupedAccount?.UpdateAccount(updatedAccount);
} }
private void UpdateCalendarSynchronizationState(AccountCalendarSynchronizationStateChanged message) private void UpdateCalendarSynchronizationState(Wino.Core.Domain.Models.Synchronization.AccountSynchronizationProgress progress)
{ {
GroupedAccountCalendarViewModel? groupedAccount; GroupedAccountCalendarViewModel? groupedAccount;
lock (_calendarStateLock) lock (_calendarStateLock)
{ {
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == message.AccountId); groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == progress.AccountId);
} }
if (groupedAccount == null) if (groupedAccount == null)
return; return;
groupedAccount.IsSynchronizationInProgress = message.IsSynchronizationInProgress; groupedAccount.ApplySynchronizationProgress(progress);
groupedAccount.SynchronizationStatus = message.SynchronizationStatus;
UpdateAggregateSynchronizationState(); UpdateAggregateSynchronizationState();
} }
+40 -16
View File
@@ -65,7 +65,13 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center"> <Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<TextBlock <TextBlock
x:Name="AccountNameTextblock" x:Name="AccountNameTextblock"
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}" FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}"
@@ -75,22 +81,23 @@
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<TextBlock <TextBlock
Grid.Row="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1" MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}" Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Parameter.Address, Mode=OneWay}" Text="{x:Bind Parameter.Address, Mode=OneWay}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis" />
Visibility="{x:Bind IsSynchronizationProgressVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
<TextBlock <ProgressBar
x:Name="SyncStatusText" Grid.Row="2"
Foreground="{ThemeResource TextFillColorSecondaryBrush}" Height="3"
MaxLines="1" Margin="0,4,0,0"
Style="{StaticResource CaptionTextBlockStyle}" IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
Text="{x:Bind SynchronizationStatus, Mode=OneWay}" ShowError="False"
TextTrimming="CharacterEllipsis" ShowPaused="False"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" /> Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
</StackPanel> Value="{x:Bind SynchronizationProgressValue, Mode=OneWay}" />
</Grid>
<Button <Button
Grid.Column="1" Grid.Column="1"
@@ -117,9 +124,9 @@
Margin="8,0" Margin="8,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
x:Load="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
IsActive="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" IsActive="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}" IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}" />
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
</Grid> </Grid>
</controls:AccountNavigationItem> </controls:AccountNavigationItem>
</DataTemplate> </DataTemplate>
@@ -292,6 +299,15 @@
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" /> Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
<ProgressBar
Height="3"
Margin="0,4,0,0"
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
Value="{x:Bind SynchronizationProgressValue, Mode=OneWay}" />
<TextBlock <TextBlock
FontSize="12" FontSize="12"
MaxLines="1" MaxLines="1"
@@ -564,12 +580,20 @@
<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>
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind SynchronizationStatus, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
<ProgressBar <ProgressBar
Height="4" Height="4"
IsIndeterminate="True" IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
ShowError="False" ShowError="False"
ShowPaused="False" ShowPaused="False"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" /> Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
Value="{x:Bind SynchronizationProgressValue, Mode=OneWay}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</muxc:Expander.Header> </muxc:Expander.Header>
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Server; using Wino.Messaging.Server;
using Wino.Messaging.UI; using Wino.Messaging.UI;
@@ -21,6 +22,7 @@ namespace Wino.Messaging;
[JsonSerializable(typeof(AccountSynchronizationCompleted))] [JsonSerializable(typeof(AccountSynchronizationCompleted))]
[JsonSerializable(typeof(RefreshUnreadCountsMessage))] [JsonSerializable(typeof(RefreshUnreadCountsMessage))]
[JsonSerializable(typeof(AccountSynchronizerStateChanged))] [JsonSerializable(typeof(AccountSynchronizerStateChanged))]
[JsonSerializable(typeof(AccountSynchronizationProgress))]
[JsonSerializable(typeof(AccountSynchronizationProgressUpdatedMessage))] [JsonSerializable(typeof(AccountSynchronizationProgressUpdatedMessage))]
[JsonSerializable(typeof(AccountFolderConfigurationUpdated))] [JsonSerializable(typeof(AccountFolderConfigurationUpdated))]
[JsonSerializable(typeof(CopyAuthURLRequested))] [JsonSerializable(typeof(CopyAuthURLRequested))]
@@ -1,8 +1,9 @@
using System; using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Messaging.UI; namespace Wino.Messaging.UI;
/// <summary> /// <summary>
/// Reports back the account synchronization progress. /// Reports back the account synchronization progress.
/// </summary> /// </summary>
public record AccountSynchronizationProgressUpdatedMessage(Guid AccountId, double Progress) : UIMessageBase<AccountSynchronizationProgressUpdatedMessage>; public record AccountSynchronizationProgressUpdatedMessage(AccountSynchronizationProgress Progress)
: UIMessageBase<AccountSynchronizationProgressUpdatedMessage>;
@@ -11,9 +11,11 @@ namespace Wino.Messaging.UI;
/// <param name="TotalItemsToSync">Total items to sync (0 for indeterminate)</param> /// <param name="TotalItemsToSync">Total items to sync (0 for indeterminate)</param>
/// <param name="RemainingItemsToSync">Remaining items to sync</param> /// <param name="RemainingItemsToSync">Remaining items to sync</param>
/// <param name="SynchronizationStatus">Current synchronization status message</param> /// <param name="SynchronizationStatus">Current synchronization status message</param>
/// <param name="ProgressCategory">Synchronization category that emitted the update</param>
public record AccountSynchronizerStateChanged( public record AccountSynchronizerStateChanged(
Guid AccountId, Guid AccountId,
AccountSynchronizerState NewState, AccountSynchronizerState NewState,
int TotalItemsToSync = 0, int TotalItemsToSync = 0,
int RemainingItemsToSync = 0, int RemainingItemsToSync = 0,
string SynchronizationStatus = "") : UIMessageBase<AccountSynchronizerStateChanged>; string SynchronizationStatus = "",
SynchronizationProgressCategory ProgressCategory = SynchronizationProgressCategory.Mail) : UIMessageBase<AccountSynchronizerStateChanged>;