some changes for progress
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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(() =>
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user