diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 07e70fa6..b1e090cc 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -14,6 +14,7 @@ using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Exporting; using DiscordChatExporter.Core.Exporting.Filtering; using DiscordChatExporter.Core.Exporting.Partitioning; +using Gress; namespace DiscordChatExporter.Cli.Commands.Base; @@ -78,8 +79,7 @@ public abstract class ExportCommandBase : TokenCommandBase { try { - await progressContext.StartTaskAsync( - $"{channel.Category.Name} / {channel.Name}", + await progressContext.StartTaskAsync($"{channel.Category.Name} / {channel.Name}", async progress => { var guild = await Discord.GetGuildAsync(channel.GuildId, innerCancellationToken); @@ -98,7 +98,11 @@ public abstract class ExportCommandBase : TokenCommandBase DateFormat ); - await Exporter.ExportChannelAsync(request, progress, innerCancellationToken); + await Exporter.ExportChannelAsync( + request, + progress.ToPercentageBased(), + innerCancellationToken + ); } ); } diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index 0a328a61..5d1e173b 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -8,7 +8,7 @@ - + diff --git a/DiscordChatExporter.Core/Discord/DiscordClient.cs b/DiscordChatExporter.Core/Discord/DiscordClient.cs index 10c98693..9115b6c6 100644 --- a/DiscordChatExporter.Core/Discord/DiscordClient.cs +++ b/DiscordChatExporter.Core/Discord/DiscordClient.cs @@ -12,6 +12,7 @@ using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Utils; using DiscordChatExporter.Core.Utils.Extensions; +using Gress; using JsonExtensions.Http; using JsonExtensions.Reading; @@ -273,7 +274,7 @@ public class DiscordClient Snowflake channelId, Snowflake? after = null, Snowflake? before = null, - IProgress? progress = null, + IProgress? progress = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Get the last message in the specified range. @@ -322,16 +323,13 @@ public class DiscordClient var exportedDuration = (message.Timestamp - firstMessage.Timestamp).Duration(); var totalDuration = (lastMessage.Timestamp - firstMessage.Timestamp).Duration(); - if (totalDuration > TimeSpan.Zero) - { - progress.Report(exportedDuration / totalDuration); - } - // Avoid division by zero if all messages have the exact same timestamp - // (which may be the case if there's only one message in the channel) - else - { - progress.Report(1); - } + progress.Report(Percentage.FromFraction( + // Avoid division by zero if all messages have the exact same timestamp + // (which may be the case if there's only one message in the channel) + totalDuration > TimeSpan.Zero + ? exportedDuration / totalDuration + : 1 + )); } yield return message; diff --git a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj index 0757c987..d07394f9 100644 --- a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj +++ b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj @@ -5,6 +5,7 @@ + diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index e6dcb7df..5d40ee37 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -8,6 +8,7 @@ using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Discord.Data.Common; using DiscordChatExporter.Core.Exceptions; using DiscordChatExporter.Core.Utils.Extensions; +using Gress; namespace DiscordChatExporter.Core.Exporting; @@ -19,7 +20,7 @@ public class ChannelExporter public async ValueTask ExportChannelAsync( ExportRequest request, - IProgress? progress = null, + IProgress? progress = null, CancellationToken cancellationToken = default) { // Build context diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index 42074a0e..2bf4d0c0 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -13,7 +13,7 @@ - + diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index c9cc78a3..b3a5709c 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -13,6 +13,7 @@ using DiscordChatExporter.Gui.Utils; using DiscordChatExporter.Gui.ViewModels.Dialogs; using DiscordChatExporter.Gui.ViewModels.Framework; using Gress; +using Gress.Completable; using MaterialDesignThemes.Wpf; using Stylet; @@ -25,11 +26,13 @@ public class RootViewModel : Screen private readonly SettingsService _settingsService; private readonly UpdateService _updateService; + private readonly AutoResetProgressMuxer _progressMuxer; + private DiscordClient? _discord; - public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); + public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5)); - public IProgressManager ProgressManager { get; } = new ProgressManager(); + public ProgressContainer Progress { get; } = new(); public bool IsBusy { get; private set; } @@ -62,17 +65,14 @@ public class RootViewModel : Screen DisplayName = $"{App.Name} v{App.VersionString}"; - // Update busy state when progress manager changes - ProgressManager.Bind(o => o.IsActive, (_, _) => - IsBusy = ProgressManager.IsActive + _progressMuxer = Progress.CreateMuxer().WithAutoReset(); + + this.Bind(o => o.IsBusy, (_, _) => + IsProgressIndeterminate = IsBusy && Progress.Current.Fraction is <= 0 or >= 1 ); - ProgressManager.Bind(o => o.IsActive, (_, _) => - IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1 - ); - - ProgressManager.Bind(o => o.Progress, (_, _) => - IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1 + Progress.Bind(o => o.Current, (_, _) => + IsProgressIndeterminate = IsBusy && Progress.Current.Fraction is <= 0 or >= 1 ); } @@ -147,7 +147,8 @@ public class RootViewModel : Screen public async void PopulateGuildsAndChannels() { - using var operation = ProgressManager.CreateOperation(); + IsBusy = true; + var progress = _progressMuxer.CreateInput(); try { @@ -183,6 +184,11 @@ public class RootViewModel : Screen await _dialogManager.ShowDialogAsync(dialog); } + finally + { + progress.ReportCompletion(); + IsBusy = false; + } } public bool CanExportChannels => @@ -194,6 +200,8 @@ public class RootViewModel : Screen public async void ExportChannels() { + IsBusy = true; + try { if (_discord is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any()) @@ -205,18 +213,22 @@ public class RootViewModel : Screen var exporter = new ChannelExporter(_discord); - var operations = ProgressManager.CreateOperations(dialog.Channels!.Count); + var progresses = Enumerable + .Range(0, dialog.Channels!.Count) + .Select(_ => _progressMuxer.CreateInput()) + .ToArray(); + var successfulExportCount = 0; await Parallel.ForEachAsync( - dialog.Channels.Zip(operations), + dialog.Channels.Zip(progresses), new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit) }, async (tuple, cancellationToken) => { - var (channel, operation) = tuple; + var (channel, progress) = tuple; try { @@ -234,7 +246,7 @@ public class RootViewModel : Screen _settingsService.DateFormat ); - await exporter.ExportChannelAsync(request, operation, cancellationToken); + await exporter.ExportChannelAsync(request, progress, cancellationToken); Interlocked.Increment(ref successfulExportCount); } @@ -244,7 +256,7 @@ public class RootViewModel : Screen } finally { - operation.Dispose(); + progress.ReportCompletion(); } } ); @@ -262,5 +274,9 @@ public class RootViewModel : Screen await _dialogManager.ShowDialogAsync(dialog); } + finally + { + IsBusy = false; + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index 034c4f4f..ed593420 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -1,7 +1,16 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + - + @@ -46,7 +46,7 @@ - + @@ -54,8 +54,8 @@ @@ -66,74 +66,74 @@ + Width="24" /> + x:Name="TokenValueTextBox" /> + Value="{Binding Progress.Current.Fraction, Mode=OneWay}" />