some changes for progress

This commit is contained in:
Burak Kaan Köse
2025-10-31 01:41:51 +01:00
parent 4bf8f8b3d3
commit 3cc1d10b87
8 changed files with 170 additions and 53 deletions
@@ -20,14 +20,14 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
/// Total items to sync across all merged accounts.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress))]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
public partial int TotalItemsToSync { get; set; }
/// <summary>
/// Remaining items to sync across all merged accounts.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress))]
[NotifyPropertyChangedFor(nameof(SynchronizationProgress), nameof(IsSynchronizationProgressVisible), nameof(IsProgressIndeterminate))]
public partial int RemainingItemsToSync { get; set; }
/// <summary>
@@ -50,6 +50,17 @@ public partial class MergedAccountMenuItem : MenuItemBase<MergedInbox, IMenuItem
}
}
/// <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;
/// <summary>
/// Whether progress should be indeterminate.
/// </summary>
public bool IsProgressIndeterminate => TotalItemsToSync == 0 && IsSynchronizationProgressVisible;
[ObservableProperty]
private string mergedAccountName;
+30 -3
View File
@@ -154,6 +154,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
if (shouldSynchronizeFolders)
{
_logger.Information("Synchronizing folders for {Name}", Account.Name);
UpdateSyncProgress(0, 0, "Synchronizing folders...");
try
{
@@ -169,6 +170,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
}
_logger.Information("Synchronizing folders for {Name} is completed", Account.Name);
UpdateSyncProgress(0, 0, "Folders synchronized");
}
// There is no specific folder synchronization in Gmail.
@@ -186,24 +188,30 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
if (Account.SynchronizationStatus == InitialSynchronizationStatus.None)
{
UpdateSyncProgress(0, 0, "Fetching email IDs...");
await FetchAllEmailIdsAsync().ConfigureAwait(false);
await CompleteAccountSyncStatusAsync();
UpdateSyncProgress(0, 0, "Email IDs fetched");
}
if (!string.IsNullOrEmpty(Account.SynchronizationDeltaIdentifier))
{
UpdateSyncProgress(0, 0, "Synchronizing changes...");
await SynchronizeDeltaAsync(options, cancellationToken).ConfigureAwait(false);
await CompleteAccountSyncStatusAsync();
UpdateSyncProgress(0, 0, "Changes synchronized");
}
if (Account.SynchronizationStatus == InitialSynchronizationStatus.IdsFetched)
{
UpdateSyncProgress(0, 0, "Processing email metadata...");
await ProcessEmailMetadataFromQueueAsync(cancellationToken);
UpdateSyncProgress(0, 0, "Email metadata processed");
}
if (Account.SynchronizationStatus == InitialSynchronizationStatus.Completed)
{
UpdateSyncProgress(0, 0, "Synchronization completed");
}
return MailSynchronizationResult.Completed(new List<MailCopy>());
@@ -220,12 +228,15 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
await _gmailChangeProcessor.ClearMailItemQueueAsync(Account.Id).ConfigureAwait(false);
var totalFetched = 0;
string? pageToken = null;
string pageToken = null;
var pageCount = 0;
do
{
try
{
pageCount++;
// Use maximum page size of 500 for efficiency
var request = _gmailService.Users.Messages.List("me");
request.MaxResults = 500;
@@ -248,6 +259,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
await _gmailChangeProcessor.AddMailItemQueueItemsAsync(queueEntries1).ConfigureAwait(false);
totalFetched += queueEntries1.Count();
// Update progress - we don't know total count, so show indeterminate with status
UpdateSyncProgress(0, 0, $"Fetched {totalFetched} email IDs (page {pageCount})");
}
pageToken = response.NextPageToken;
@@ -261,8 +275,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
continue; // Retry the same page
}
} while (!string.IsNullOrEmpty(pageToken));
// Final update with total count
UpdateSyncProgress(0, 0, $"Fetched {totalFetched} email IDs total");
}
catch (Exception ex)
catch (Exception)
{
throw;
}
@@ -327,6 +344,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{
// Get total count for progress tracking
var totalInQueue = await _gmailChangeProcessor.GetMailItemQueueCountAsync(Account.Id).ConfigureAwait(false);
var processedCount = 0;
try
{
@@ -365,6 +383,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{
queueItem.IsProcessed = true;
queueItem.ProcessedAt = DateTime.UtcNow;
processedCount++;
}
}
catch (Exception)
@@ -376,11 +395,19 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
queueItem.ProcessedAt = null;
queueItem.FailedCount++;
totalFailed++;
processedCount++; // Count failed items as processed for progress
}
}
await _gmailChangeProcessor.UpdateMailItemQueueAsync(mailItemQueue).ConfigureAwait(false);
// Update progress based on processed items
if (totalInQueue > 0)
{
var remainingItems = totalInQueue - processedCount;
UpdateSyncProgress(totalInQueue, remainingItems, $"Processing emails: {processedCount}/{totalInQueue}");
}
// If too many failures, pause to avoid hitting rate limits
if (totalFailed > 85) await Task.Delay(TimeSpan.FromSeconds(10));
}
@@ -514,9 +514,10 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
// Group items by their grouping key and add them in a single UI thread call
if (itemsToAdd.Count > 0)
{
var groupedItems = itemsToAdd
// Pre-compute grouping on background thread to reduce UI thread work
var groupedItems = await Task.Run(() => itemsToAdd
.GroupBy(GetGroupingKey)
.ToDictionary(g => g.Key, g => g.ToList());
.ToDictionary(g => g.Key, g => g.ToList())).ConfigureAwait(false);
await ExecuteUIThread(() =>
{
+32 -5
View File
@@ -571,7 +571,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
return;
}
var viewModels = PrepareMailViewModels(items);
var viewModels = await PrepareMailViewModelsAsync(items).ConfigureAwait(false);
await MailCollection.AddRangeAsync(viewModels, false);
await ExecuteUIThread(() => { IsInitializingFolder = false; });
@@ -738,9 +738,19 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
private List<MailItemViewModel> PrepareMailViewModels(IEnumerable<MailCopy> mailItems)
private async Task<List<MailItemViewModel>> PrepareMailViewModelsAsync(IEnumerable<MailCopy> mailItems, CancellationToken cancellationToken = default)
{
return mailItems.Select(a => new MailItemViewModel(a)).ToList();
// Run ViewModel creation on background thread to avoid blocking UI
return await Task.Run(() =>
{
var viewModels = new List<MailItemViewModel>();
foreach (var mailItem in mailItems)
{
cancellationToken.ThrowIfCancellationRequested();
viewModels.Add(new MailItemViewModel(mailItem));
}
return viewModels;
}, cancellationToken).ConfigureAwait(false);
}
[RelayCommand]
@@ -764,7 +774,11 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (ActiveFolder == null)
return;
await ExecuteUIThread(() => { IsInitializingFolder = true; });
await ExecuteUIThread(() => {
IsInitializingFolder = true;
// Show initial loading progress
UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, "Loading emails...");
});
// Folder is changed during initialization.
// Just cancel the existing one and wait for new initialization.
@@ -855,11 +869,21 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
// Update progress: Creating view models
await ExecuteUIThread(() => {
UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, $"Processing {items.Count} emails...");
});
// Here they are already threaded if needed.
// We don't need to insert them one by one.
// Just create VMs and do bulk insert.
var viewModels = PrepareMailViewModels(items);
var viewModels = await PrepareMailViewModelsAsync(items, cancellationToken).ConfigureAwait(false);
// Update progress: Adding to collection
await ExecuteUIThread(() => {
UpdateBarMessage(InfoBarMessageType.Information, ActiveFolder.FolderName, "Finalizing...");
});
await MailCollection.AddRangeAsync(viewModels, clearIdCache: true);
@@ -895,6 +919,9 @@ public partial class MailListPageViewModel : MailBaseViewModel,
OnPropertyChanged(nameof(CanSynchronize));
NotifyItemFoundState();
// Clear the loading message after completion
IsBarOpen = false;
});
}
}
+50 -14
View File
@@ -76,12 +76,25 @@
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Parameter.Address, Mode=OneWay}"
TextTrimming="CharacterEllipsis" />
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}, Mode=OneWay}" />
<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>
<PathIcon
x:Name="AttentionIcon"
Grid.Column="2"
Grid.Column="1"
Width="16"
Height="16"
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
x:Load="{x:Bind IsAttentionRequired, Mode=OneWay}"
@@ -90,18 +103,15 @@
<muxc:ProgressRing
x:Name="SynchronizationProgressBar"
Grid.ColumnSpan="3"
Width="10"
Height="10"
HorizontalAlignment="Right"
Grid.Column="2"
Width="16"
Height="16"
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{ThemeResource AppBarItemBackgroundThemeBrush}"
Foreground="{ThemeResource AppBarItemForegroundThemeBrush}"
IsActive="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
IsIndeterminate="{x:Bind IsProgressIndeterminate}"
Maximum="100"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
Value="{x:Bind SynchronizationProgress, Mode=OneWay}" />
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
</Grid>
</controls:AccountNavigationItem>
</DataTemplate>
@@ -251,6 +261,10 @@
</coreControls:WinoNavigationViewItem.Icon>
<Grid Height="50">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center" Spacing="0">
<TextBlock
x:Name="AccountNameTextblock"
@@ -262,12 +276,34 @@
<TextBlock
FontSize="12"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis">
<Run Text="{x:Bind MergedAccountCount}" /><Run Text="{x:Bind domain:Translator.MenuMergedAccountItemAccountsSuffix}" />
Text="{x:Bind SynchronizationStatus, Mode=OneWay}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
<TextBlock
FontSize="12"
MaxLines="1"
Style="{StaticResource CaptionTextBlockStyle}"
TextTrimming="CharacterEllipsis"
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(IsSynchronizationProgressVisible), Mode=OneWay}">
<Run Text="{x:Bind MergedAccountCount, Mode=OneWay}" /><Run Text="{x:Bind domain:Translator.MenuMergedAccountItemAccountsSuffix}" />
</TextBlock>
</StackPanel>
<muxc:ProgressRing
x:Name="SynchronizationProgressBar"
Grid.Column="1"
Width="16"
Height="16"
Margin="8,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsActive="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}"
IsIndeterminate="{x:Bind IsProgressIndeterminate, Mode=OneWay}"
Visibility="{x:Bind IsSynchronizationProgressVisible, Mode=OneWay}" />
</Grid>
</controls:AccountNavigationItem>
</DataTemplate>
@@ -95,7 +95,7 @@
</DataTemplate>
</Page.Resources>
<Grid>
<Grid MaxWidth="700">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
+4 -5
View File
@@ -1,7 +1,6 @@
using System.IO;
using System.Threading.Tasks;
using SQLite;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
@@ -44,6 +43,10 @@ public class DatabaseService : IDatabaseService
private async Task CreateTablesAsync()
{
//typeof(AccountCalendar),
// typeof(CalendarEventAttendee),
// typeof(CalendarItem),
// typeof(Reminder),
await Connection.CreateTablesAsync(CreateFlags.None,
typeof(MailCopy),
typeof(MailItemFolder),
@@ -54,10 +57,6 @@ public class DatabaseService : IDatabaseService
typeof(MergedInbox),
typeof(MailAccountPreferences),
typeof(MailAccountAlias),
typeof(AccountCalendar),
typeof(CalendarEventAttendee),
typeof(CalendarItem),
typeof(Reminder),
typeof(Thumbnail),
typeof(KeyboardShortcut),
typeof(MailItemQueue)
+37 -21
View File
@@ -252,33 +252,35 @@ public class MailService : BaseDatabaseService, IMailService
return [.. mails];
}
// Include other mails in the same threads
// Include other mails in the same threads - batch process to reduce DB calls
var expandedMails = new List<MailCopy>(mails);
var processedThreadIds = new HashSet<string>();
var uniqueThreadIds = mails
.Where(m => !string.IsNullOrEmpty(m.ThreadId))
.Select(m => m.ThreadId)
.Distinct()
.ToList();
foreach (var mail in mails)
if (uniqueThreadIds.Count > 0)
{
if (!string.IsNullOrEmpty(mail.ThreadId) && !processedThreadIds.Contains(mail.ThreadId))
// Get all thread mails in a single DB call
var existingMailIds = expandedMails.Select(m => m.Id).ToHashSet();
var allThreadMails = await GetMailsByThreadIdsAsync(uniqueThreadIds, existingMailIds).ConfigureAwait(false);
if (allThreadMails?.Count > 0)
{
processedThreadIds.Add(mail.ThreadId);
// Get all other mails with the same ThreadId that are not already in the result
var existingMailIds = expandedMails.Select(m => m.Id).ToHashSet();
var threadMails = await GetMailsByThreadIdAsync(mail.ThreadId, existingMailIds).ConfigureAwait(false);
if (threadMails?.Any() == true)
// Process thread mails in parallel to improve performance
var tasks = allThreadMails.Select(async threadMail =>
{
// Load assigned properties for the thread mails
foreach (var threadMail in threadMails)
{
await LoadAssignedPropertiesWithCacheAsync(threadMail, folderCache, accountCache, contactCache).ConfigureAwait(false);
}
await LoadAssignedPropertiesWithCacheAsync(threadMail, folderCache, accountCache, contactCache).ConfigureAwait(false);
return threadMail;
});
// Remove items that have no assigned account or folder
threadMails.RemoveAll(a => a.AssignedAccount == null || a.AssignedFolder == null);
expandedMails.AddRange(threadMails);
}
var processedThreadMails = await Task.WhenAll(tasks).ConfigureAwait(false);
// Filter out items with no assigned account or folder
var validThreadMails = processedThreadMails.Where(m => m.AssignedAccount != null && m.AssignedFolder != null);
expandedMails.AddRange(validThreadMails);
}
cancellationToken.ThrowIfCancellationRequested();
@@ -301,6 +303,20 @@ public class MailService : BaseDatabaseService, IMailService
return await Connection.QueryAsync<MailCopy>(query);
}
private async Task<List<MailCopy>> GetMailsByThreadIdsAsync(List<string> threadIds, HashSet<string> excludeMailIds)
{
if (threadIds?.Count == 0)
return [];
var query = new Query("MailCopy")
.WhereIn("ThreadId", threadIds)
.WhereNotIn("Id", excludeMailIds)
.SelectRaw("MailCopy.*")
.GetRawQuery();
return await Connection.QueryAsync<MailCopy>(query).ConfigureAwait(false);
}
/// <summary>
/// This method should used for operations with multiple mailItems. Don't use this for single mail items.
/// Called method should provide own instances for caches.