Updated synchronization progress implementation.
This commit is contained in:
@@ -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,15 +60,13 @@ public partial class GroupedAccountCalendarViewModel : ObservableObject
|
||||
|
||||
private void CalendarPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (sender is AccountCalendarViewModel viewModel)
|
||||
{
|
||||
if (e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
|
||||
if (sender is AccountCalendarViewModel viewModel &&
|
||||
e.PropertyName == nameof(AccountCalendarViewModel.IsChecked))
|
||||
{
|
||||
ManageIsCheckedState();
|
||||
UpdateCalendarCheckedState(viewModel, viewModel.IsChecked, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsExpanded { get; set; } = 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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user