Files
Wino-Mail/Wino.Core/Synchronizers/BaseSynchronizer.cs
T
Burak Kaan Köse ebc35c3de8 Event creation.
2026-03-07 17:13:48 +01:00

238 lines
8.2 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Net.Http;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Requests.Bundles;
using Wino.Messaging.UI;
namespace Wino.Core.Synchronizers;
public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject, IBaseSynchronizer
{
protected SemaphoreSlim synchronizationSemaphore = new(1);
protected CancellationToken activeSynchronizationCancellationToken;
protected List<IRequestBase> changeRequestQueue = [];
private readonly ConcurrentDictionary<Guid, byte> _pendingMailOperationIds = new();
private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new();
protected readonly IMessenger Messenger;
public MailAccount Account { get; }
private AccountSynchronizerState state;
public AccountSynchronizerState State
{
get { return state; }
set
{
state = value;
// Send state changed message with current progress information
Messenger.Send(new AccountSynchronizerStateChanged(
Account.Id,
value,
TotalItemsToSync,
RemainingItemsToSync,
SynchronizationStatus));
}
}
/// <summary>
/// Current synchronization status message.
/// </summary>
[ObservableProperty]
public partial string SynchronizationStatus { get; set; } = string.Empty;
/// <summary>
/// Total items to download/sync in current operation.
/// 0 means no active download or indeterminate progress.
/// </summary>
[ObservableProperty]
public partial int TotalItemsToSync { get; set; }
/// <summary>
/// Remaining items to download/sync in current operation.
/// </summary>
[ObservableProperty]
public partial int RemainingItemsToSync { get; set; }
/// <summary>
/// Calculated progress percentage (0-100) based on TotalItemsToSync and RemainingItemsToSync.
/// Returns -1 for indeterminate progress (when both are 0).
/// </summary>
public double SynchronizationProgress
{
get
{
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0)
return -1; // Indeterminate
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
}
}
protected BaseSynchronizer(MailAccount account, IMessenger messenger)
{
Account = account;
Messenger = messenger ?? WeakReferenceMessenger.Default;
}
/// <summary>
/// Resets synchronization progress to default state.
/// </summary>
protected void ResetSyncProgress()
{
TotalItemsToSync = 0;
RemainingItemsToSync = 0;
SynchronizationStatus = string.Empty;
OnPropertyChanged(nameof(SynchronizationProgress));
}
/// <summary>
/// Updates synchronization progress with current item counts.
/// </summary>
/// <param name="total">Total items to sync</param>
/// <param name="remaining">Remaining items to sync</param>
/// <param name="status">Optional status message</param>
protected void UpdateSyncProgress(int total, int remaining, string status = "")
{
TotalItemsToSync = total;
RemainingItemsToSync = remaining;
SynchronizationStatus = status;
OnPropertyChanged(nameof(SynchronizationProgress));
// Send progress update message
Messenger.Send(new AccountSynchronizerStateChanged(
Account.Id,
State,
TotalItemsToSync,
RemainingItemsToSync,
SynchronizationStatus));
}
/// <summary>
/// Queues a single request to be executed in the next synchronization.
/// </summary>
/// <param name="request">Request to execute.</param>
public void QueueRequest(IRequestBase request)
{
changeRequestQueue.Add(request);
TrackQueuedRequest(request);
}
public bool HasPendingOperation(Guid mailUniqueId) => _pendingMailOperationIds.ContainsKey(mailUniqueId);
public IReadOnlyCollection<Guid> GetPendingOperationUniqueIds() => _pendingMailOperationIds.Keys.ToArray();
public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId);
protected void TrackQueuedRequest(IRequestBase request)
{
if (request is IMailActionRequest mailActionRequest)
{
_pendingMailOperationIds.TryAdd(mailActionRequest.Item.UniqueId, 0);
}
if (request is ICalendarActionRequest calendarActionRequest)
{
if (calendarActionRequest.LocalCalendarItemId.HasValue)
{
_pendingCalendarOperationIds.TryAdd(calendarActionRequest.LocalCalendarItemId.Value, 0);
}
}
}
protected void UntrackProcessedRequest(IRequestBase request)
{
if (request is IMailActionRequest mailActionRequest)
{
_pendingMailOperationIds.TryRemove(mailActionRequest.Item.UniqueId, out _);
}
if (request is ICalendarActionRequest calendarActionRequest)
{
if (calendarActionRequest.LocalCalendarItemId.HasValue)
{
_pendingCalendarOperationIds.TryRemove(calendarActionRequest.LocalCalendarItemId.Value, out _);
}
}
}
protected void UntrackProcessedRequests(IEnumerable<IRequestBase> requests)
{
foreach (var request in requests)
UntrackProcessedRequest(request);
}
/// <summary>
/// Runs existing queued requests in the queue.
/// </summary>
/// <param name="batchedRequests">Batched requests to execute. Integrator methods will only receive batched requests.</param>
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(List<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// </summary>
protected async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
var profileInformation = await GetProfileInformationAsync();
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
if (!string.IsNullOrEmpty(profileInformation.AccountAddress))
{
Account.Address = profileInformation.AccountAddress;
}
}
return profileInformation;
}
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> GetProfilePictureBase64EncodedAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url).ConfigureAwait(false);
var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
return Convert.ToBase64String(byteContent);
}
public List<IRequestBundle<TBaseRequest>> ForEachRequest<TWinoRequestType>(IEnumerable<TWinoRequestType> requests,
Func<TWinoRequestType, TBaseRequest> action)
where TWinoRequestType : IRequestBase
{
List<IRequestBundle<TBaseRequest>> ret = [];
foreach (var request in requests)
ret.Add(new HttpRequestBundle<TBaseRequest>(action(request), request, request));
return ret;
}
}