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.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Calendar.ViewModels.Data;
@@ -32,7 +33,7 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
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)
{
@@ -59,13 +60,11 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
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;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanSynchronize), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
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]
public partial string SynchronizationStatus { get; set; } = string.Empty;
public bool CanSynchronize => !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()
{
if (_isExternalPropChangeBlocked) return;
if (_isExternalPropChangeBlocked)
return;
_isExternalPropChangeBlocked = true;
@@ -113,17 +147,13 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
partial void OnIsCheckedStateChanged(bool? oldValue, bool? newValue)
{
if (_isExternalPropChangeBlocked) return;
// Update is triggered by user on the three-state checkbox.
// We should not report all changes one by one.
if (_isExternalPropChangeBlocked)
return;
_isExternalPropChangeBlocked = true;
if (newValue == null)
{
// Only primary calendars must be checked.
foreach (var calendar in AccountCalendars)
{
UpdateCalendarCheckedState(calendar, calendar.IsPrimary);
@@ -138,7 +168,6 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
}
_isExternalPropChangeBlocked = false;
CollectiveSelectionStateChanged?.Invoke(this, EventArgs.Empty);
}
@@ -146,22 +175,17 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
{
var currentValue = accountCalendarViewModel.IsChecked;
if (currentValue == newValue && !ignoreValueCheck) return;
if (currentValue == newValue && !ignoreValueCheck)
return;
accountCalendarViewModel.IsChecked = newValue;
// No need to report.
if (_isExternalPropChangeBlocked == true) return;
if (_isExternalPropChangeBlocked)
return;
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)
@@ -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.Shared;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.Interfaces;
public interface IAccountMenuItem : IMenuItem
{
bool IsEnabled { get; set; }
bool IsSynchronizationInProgress { get; set; }
/// <summary>
/// Calculated synchronization progress percentage (0-100). -1 for indeterminate.
/// Calculated synchronization progress percentage (0-100).
/// </summary>
double SynchronizationProgress { get; }
/// <summary>
/// Progress value clamped for XAML progress controls.
/// </summary>
double SynchronizationProgressValue { get; }
/// <summary>
/// Total items to sync. 0 for indeterminate progress.
/// </summary>
@@ -30,6 +37,7 @@ public interface IAccountMenuItem : IMenuItem
int UnreadItemCount { get; set; }
IEnumerable<MailAccount> HoldingAccounts { get; }
void ApplySynchronizationProgress(AccountSynchronizationProgress progress);
void UpdateAccount(MailAccount account);
}
@@ -41,6 +41,11 @@ public interface ISynchronizationManager
/// </summary>
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>
/// Queues a mail action request to the corresponding account's synchronizer with optional synchronization triggering.
/// </summary>
+28 -23
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
@@ -6,6 +6,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.MenuItems;
@@ -14,18 +15,22 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
[ObservableProperty]
private int unreadItemCount;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate), nameof(SynchronizationProgress), nameof(SynchronizationProgressValue))]
public partial bool IsSynchronizationInProgress { get; set; }
/// <summary>
/// Total items to sync. 0 means indeterminate progress.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSynchronizationProgressVisible), nameof(SynchronizationProgress), nameof(IsProgressIndeterminate))]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; }
/// <summary>
/// Remaining items to sync.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress))]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
/// <summary>
@@ -39,31 +44,22 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
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
{
get
{
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0)
return -1; // Indeterminate
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
return Math.Clamp(((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100, 0, 100);
}
}
/// <summary>
/// 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;
public double SynchronizationProgressValue => SynchronizationProgress;
/// <summary>
/// Whether progress should be indeterminate (when total is 0 but there's still synchronization happening).
/// </summary>
public bool IsProgressIndeterminate => TotalItemsToSync == 0 && RemainingItemsToSync == 0 && IsSynchronizationProgressVisible;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
public Guid AccountId => Parameter.Id;
@@ -77,7 +73,6 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
if (SetProperty(ref attentionReason, value))
{
OnPropertyChanged(nameof(IsAttentionRequired));
UpdateFixAccountIssueMenuItem();
}
}
@@ -108,6 +103,17 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
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)
{
Parameter = account;
@@ -118,7 +124,8 @@ public partial class AccountMenuItem : MenuItemBase<MailAccount, MenuItemBase<IM
OnPropertyChanged(nameof(AccountColorHex));
OnPropertyChanged(nameof(IsAttentionRequired));
if (SubMenuItems == null) return;
if (SubMenuItems == null)
return;
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))
{
// Add fix issue item if not exists.
SubMenuItems.Insert(0, new FixAccountIssuesMenuItem(Parameter, this));
}
else
{
// Remove existing if issue is resolved.
var fixAccountIssueItem = SubMenuItems.FirstOrDefault(a => a is FixAccountIssuesMenuItem);
if (fixAccountIssueItem != null)
@@ -1,9 +1,10 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Core.Domain.MenuItems;
@@ -16,50 +17,37 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
[ObservableProperty]
private int unreadItemCount;
/// <summary>
/// Total items to sync across all merged accounts.
/// </summary>
[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; }
/// <summary>
/// Remaining items to sync across all merged accounts.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(SynchronizationProgressValue), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
/// <summary>
/// Current synchronization status message.
/// </summary>
[ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty;
/// <summary>
/// Calculated synchronization progress for merged accounts.
/// </summary>
public double SynchronizationProgress
{
get
{
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0)
return -1; // Indeterminate
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
}
/// <summary>
/// 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;
public double SynchronizationProgressValue => SynchronizationProgress;
/// <summary>
/// Whether progress should be indeterminate.
/// </summary>
public bool IsProgressIndeterminate => TotalItemsToSync == 0 && IsSynchronizationProgressVisible;
public bool IsSynchronizationProgressVisible => IsSynchronizationInProgress;
public bool IsProgressIndeterminate => IsSynchronizationInProgress && TotalItemsToSync <= 0;
[ObservableProperty]
private string mergedAccountName;
@@ -78,22 +66,33 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
UnreadItemCount = SubMenuItems.OfType<IAccountMenuItem>().Sum(a => a.UnreadItemCount);
}
/// <summary>
/// Aggregates synchronization progress from all child account menu items.
/// </summary>
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);
RemainingItemsToSync = accountMenuItems.Sum(a => a.RemainingItemsToSync);
IsSynchronizationInProgress = activeAccountMenuItems.Any();
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
SynchronizationStatus = accountMenuItems.FirstOrDefault(a => !string.IsNullOrEmpty(a.SynchronizationStatus))?.SynchronizationStatus ?? string.Empty;
public void ApplySynchronizationProgress(AccountSynchronizationProgress progress)
{
if (progress == null)
return;
IsSynchronizationInProgress = progress.IsInProgress;
TotalItemsToSync = progress.TotalUnits;
RemainingItemsToSync = progress.RemainingUnits;
SynchronizationStatus = progress.Status ?? string.Empty;
}
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_SynchronizingCalendarEvents": "Synchronizing calendar events",
"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)",
"CalendarAllDayEventSummary": "all-day events",
"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.
/// Replaces the old WinoServerConnectionManager functionality.
/// </summary>
public class SynchronizationManager : ISynchronizationManager
public class SynchronizationManager : ISynchronizationManager, IRecipient<AccountSynchronizerStateChanged>
{
private static readonly Lazy<SynchronizationManager> _instance = new(() => new SynchronizationManager());
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, CancellationTokenSource> _accountSynchronizationCancellationSources = 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 ILogger _logger = Log.ForContext<SynchronizationManager>();
@@ -41,6 +43,7 @@ public class SynchronizationManager : ISynchronizationManager
private INotificationBuilder _notificationBuilder;
private bool _isInitialized = false;
private bool _isRegisteredForProgressMessages;
private SynchronizationManager() { }
@@ -73,6 +76,11 @@ public class SynchronizationManager : ISynchronizationManager
// DO NOT create synchronizers here to avoid requiring window handles during initialization.
// Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync.
if (!_isRegisteredForProgressMessages)
{
WeakReferenceMessenger.Default.Register<AccountSynchronizerStateChanged>(this);
_isRegisteredForProgressMessages = true;
}
_isInitialized = true;
_logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily.");
@@ -219,6 +227,21 @@ public class SynchronizationManager : ISynchronizationManager
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>
/// 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.
@@ -651,6 +674,9 @@ public class SynchronizationManager : ISynchronizationManager
_logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId);
}
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
return Task.CompletedTask;
}
@@ -679,6 +705,9 @@ public class SynchronizationManager : ISynchronizationManager
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
}
}
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
}
/// <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()
{
if (!_isInitialized)
@@ -804,17 +860,67 @@ public class SynchronizationManager : ISynchronizationManager
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(
Guid accountId,
CalendarSynchronizationType synchronizationType,
bool isSynchronizationInProgress,
string synchronizationStatus = "")
{
WeakReferenceMessenger.Default.Send(new AccountCalendarSynchronizationStateChanged(
PublishSynchronizationProgress(new AccountSynchronizationProgress(
accountId,
synchronizationType,
SynchronizationProgressCategory.Calendar,
isSynchronizationInProgress,
synchronizationStatus));
isSynchronizationInProgress,
0,
0,
0,
synchronizationStatus,
isSynchronizationInProgress ? AccountSynchronizerState.Synchronizing : AccountSynchronizerState.Idle));
}
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 ConcurrentQueue<SynchronizationIssue> _capturedSynchronizationIssues = new();
protected readonly IMessenger Messenger;
protected SynchronizationProgressCategory CurrentSynchronizationProgressCategory { get; set; } = SynchronizationProgressCategory.Mail;
public MailAccount Account { get; }
@@ -44,7 +45,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
value,
TotalItemsToSync,
RemainingItemsToSync,
SynchronizationStatus));
SynchronizationStatus,
CurrentSynchronizationProgressCategory));
}
}
@@ -75,8 +77,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
{
get
{
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0)
return -1; // Indeterminate
if (TotalItemsToSync <= 0)
return 0;
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
@@ -118,7 +120,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
State,
TotalItemsToSync,
RemainingItemsToSync,
SynchronizationStatus));
SynchronizationStatus,
CurrentSynchronizationProgressCategory));
}
/// <summary>
+14 -2
View File
@@ -22,6 +22,7 @@ using Microsoft.IdentityModel.Tokens;
using MimeKit;
using MoreLinq;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
@@ -388,7 +389,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
} while (!string.IsNullOrEmpty(pageToken));
_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);
@@ -521,8 +522,16 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
.Where(c => c.IsSynchronizationEnabled)
.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
{
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
@@ -602,6 +611,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
}
catch (OperationCanceledException)
{
@@ -624,6 +634,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
if (!errorContext.CanContinueSync)
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 MimeKit;
using Serilog;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
@@ -1192,8 +1193,16 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
var periodStartUtc = DateTimeOffset.UtcNow.AddYears(-1);
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();
if (!remoteCalendarsById.TryGetValue(localCalendar.RemoteCalendarId, out var remoteCalendar))
@@ -1265,6 +1274,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
localCalendar.SynchronizationDeltaToken = remoteToken;
await _imapChangeProcessor.UpdateAccountCalendarAsync(localCalendar).ConfigureAwait(false);
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
}
return CalendarSynchronizationResult.Empty;
+13 -1
View File
@@ -2320,8 +2320,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
// 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
{
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
@@ -2440,6 +2448,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
}
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
}
catch (OperationCanceledException)
{
@@ -2462,6 +2472,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
if (!errorContext.CanContinueSync)
throw;
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
}
}
@@ -147,6 +147,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
if (shouldExecuteRequests && changeRequestQueue.Any())
{
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
@@ -264,6 +265,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
// Set indeterminate progress for initial state
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
UpdateSyncProgress(0, 0, "Synchronizing...");
State = AccountSynchronizerState.Synchronizing;
@@ -388,6 +390,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
if (shouldExecuteRequests)
{
calendarRequestsWereExecuting = true;
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
State = AccountSynchronizerState.ExecutingRequests;
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
@@ -482,6 +485,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
await Task.Delay(maxExecutionDelay, cancellationToken);
}
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
var synchronizationResult = await SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
return FinalizeCalendarResult(synchronizationResult);
}
+37 -10
View File
@@ -38,7 +38,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
IRecipient<MergedInboxRenamed>,
IRecipient<LanguageChanged>,
IRecipient<AccountMenuItemsReordered>,
IRecipient<AccountSynchronizerStateChanged>,
IRecipient<AccountSynchronizationProgressUpdatedMessage>,
IRecipient<NavigateAppPreferencesRequested>,
IRecipient<AccountFolderConfigurationUpdated>,
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()
{
// First clear all account menu items.
@@ -185,9 +203,13 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
foreach (var mergedAccount in mergedAccounts)
{
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(() =>
{
MenuItems.Add(mergedAccountMenuItem);
@@ -196,9 +218,12 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
}
else
{
var accountMenuItem = new AccountMenuItem(account, null);
ApplySynchronizationProgress(accountMenuItem, SynchronizationProgressCategory.Mail);
await ExecuteUIThread(() =>
{
MenuItems.Add(new AccountMenuItem(account, null));
MenuItems.Add(accountMenuItem);
});
initializedAccountIds.Add(account.Id);
@@ -1194,17 +1219,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
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;
await ExecuteUIThread(() =>
{
accountMenuItem.TotalItemsToSync = message.TotalItemsToSync;
accountMenuItem.RemainingItemsToSync = message.RemainingItemsToSync;
accountMenuItem.SynchronizationStatus = message.SynchronizationStatus;
accountMenuItem.ApplySynchronizationProgress(progress);
// If this account is part of a merged inbox, update the merged inbox progress as well
if (accountMenuItem.ParentMenuItem is MergedAccountMenuItem mergedAccountMenuItem)
@@ -1231,7 +1258,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
Messenger.Register<MergedInboxRenamed>(this);
Messenger.Register<LanguageChanged>(this);
Messenger.Register<AccountMenuItemsReordered>(this);
Messenger.Register<AccountSynchronizerStateChanged>(this);
Messenger.Register<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Register<NavigateAppPreferencesRequested>(this);
Messenger.Register<AccountFolderConfigurationUpdated>(this);
}
@@ -1248,7 +1275,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
Messenger.Unregister<MergedInboxRenamed>(this);
Messenger.Unregister<LanguageChanged>(this);
Messenger.Unregister<AccountMenuItemsReordered>(this);
Messenger.Unregister<AccountSynchronizerStateChanged>(this);
Messenger.Unregister<AccountSynchronizationProgressUpdatedMessage>(this);
Messenger.Unregister<NavigateAppPreferencesRequested>(this);
Messenger.Unregister<AccountFolderConfigurationUpdated>(this);
}
@@ -8,7 +8,9 @@ using CommunityToolkit.Mvvm.Messaging;
using Wino.Calendar.ViewModels.Data;
using Wino.Calendar.ViewModels.Interfaces;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Messaging.Client.Calendar;
using Wino.Messaging.UI;
@@ -25,7 +27,7 @@ public partial class AccountCalendarStateService : ObservableRecipient,
IRecipient<CalendarListDeleted>,
IRecipient<AccountRemovedMessage>,
IRecipient<AccountUpdatedMessage>,
IRecipient<AccountCalendarSynchronizationStateChanged>
IRecipient<AccountSynchronizationProgressUpdatedMessage>
{
private readonly object _calendarStateLock = new();
@@ -90,7 +92,7 @@ public partial class AccountCalendarStateService : ObservableRecipient,
Messenger.Register<CalendarListDeleted>(this);
Messenger.Register<AccountRemovedMessage>(this);
Messenger.Register<AccountUpdatedMessage>(this);
Messenger.Register<AccountCalendarSynchronizationStateChanged>(this);
Messenger.Register<AccountSynchronizationProgressUpdatedMessage>(this);
}
private void SingleGroupCalendarCollectiveStateChanged(object? sender, EventArgs e)
@@ -105,6 +107,15 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{
groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
try
{
groupedAccountCalendar.ApplySynchronizationProgress(SynchronizationManager.Instance.GetSynchronizationProgress(
groupedAccountCalendar.Account.Id,
SynchronizationProgressCategory.Calendar));
}
catch (InvalidOperationException)
{
}
_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)
{
await Dispatcher.ExecuteOnUIThread(() => UpdateCalendarSynchronizationState(message));
await Dispatcher.ExecuteOnUIThread(() => UpdateCalendarSynchronizationState(message.Progress));
}
else
{
UpdateCalendarSynchronizationState(message);
UpdateCalendarSynchronizationState(message.Progress);
}
}
@@ -387,19 +401,18 @@ public partial class AccountCalendarStateService : ObservableRecipient,
groupedAccount?.UpdateAccount(updatedAccount);
}
private void UpdateCalendarSynchronizationState(AccountCalendarSynchronizationStateChanged message)
private void UpdateCalendarSynchronizationState(Wino.Core.Domain.Models.Synchronization.AccountSynchronizationProgress progress)
{
GroupedAccountCalendarViewModel? groupedAccount;
lock (_calendarStateLock)
{
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == message.AccountId);
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == progress.AccountId);
}
if (groupedAccount == null)
return;
groupedAccount.IsSynchronizationInProgress = message.IsSynchronizationInProgress;
groupedAccount.SynchronizationStatus = message.SynchronizationStatus;
groupedAccount.ApplySynchronizationProgress(progress);
UpdateAggregateSynchronizationState();
}
+40 -16
View File
@@ -65,7 +65,13 @@
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="10" />
</Grid.RowDefinitions>
<TextBlock
x:Name="AccountNameTextblock"
FontWeight="{x:Bind helpers:XamlHelpers.GetFontWeightByChildSelectedState(IsSelected), Mode=OneWay}"
@@ -75,22 +81,23 @@
TextTrimming="CharacterEllipsis" />
<TextBlock
Grid.Row="1"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Parameter.Address, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
TextTrimming="CharacterEllipsis" />
<TextBlock
x:Name="SyncStatusText"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind SynchronizationStatus, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
</StackPanel>
<ProgressBar
Grid.Row="2"
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}" />
</Grid>
<Button
Grid.Column="1"
@@ -117,9 +124,9 @@
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
x:Load="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
IsActive="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}" />
</Grid>
</controls:AccountNavigationItem>
</DataTemplate>
@@ -292,6 +299,15 @@
TextTrimming="CharacterEllipsis"
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
FontSize="12"
MaxLines="1"
@@ -564,12 +580,20 @@
<Run FontWeight="SemiBold" Text="{x:Bind Account.Name}" />
<Run FontSize="12" Text=" (" /><Run FontSize="12" Text="{x:Bind Account.Address}" /><Run FontSize="12" Text=")" />
</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
Height="4"
IsIndeterminate="True"
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
ShowError="False"
ShowPaused="False"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
Value="{x:Bind SynchronizationProgressValue, Mode=OneWay}" />
</StackPanel>
</Grid>
</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.UI;
@@ -21,6 +22,7 @@ namespace Wino.Messaging;
[JsonSerializable(typeof(AccountSynchronizationCompleted))]
[JsonSerializable(typeof(RefreshUnreadCountsMessage))]
[JsonSerializable(typeof(AccountSynchronizerStateChanged))]
[JsonSerializable(typeof(AccountSynchronizationProgress))]
[JsonSerializable(typeof(AccountSynchronizationProgressUpdatedMessage))]
[JsonSerializable(typeof(AccountFolderConfigurationUpdated))]
[JsonSerializable(typeof(CopyAuthURLRequested))]
@@ -1,8 +1,9 @@
using System;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Messaging.UI;
/// <summary>
/// Reports back the account synchronization progress.
/// </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="RemainingItemsToSync">Remaining items to sync</param>
/// <param name="SynchronizationStatus">Current synchronization status message</param>
/// <param name="ProgressCategory">Synchronization category that emitted the update</param>
public record AccountSynchronizerStateChanged(
Guid AccountId,
AccountSynchronizerState NewState,
int TotalItemsToSync = 0,
int RemainingItemsToSync = 0,
string SynchronizationStatus = "") : UIMessageBase<AccountSynchronizerStateChanged>;
string SynchronizationStatus = "",
SynchronizationProgressCategory ProgressCategory = SynchronizationProgressCategory.Mail) : UIMessageBase<AccountSynchronizerStateChanged>;