From 4461a53f8baea17cd93ec3ff56915b601a3a7d6d Mon Sep 17 00:00:00 2001 From: Kornelius Rohrschneider Date: Tue, 28 Oct 2025 02:34:21 +0100 Subject: [PATCH] Added an export progress logger An export progress logger has been added to the exporter. It provides a consistent way to log the entire process of an export, meaning all status updates of each of the exported channels. It also allows to count the different results of the individual channel exports and can provide a summary of them. There are two subclasses: The ConsoleProgressLogger logs the status updates with colors on the console while the SnackbarProgressLogger logs them in the GUI snackbar. Previously, two exceptions had been used to signalize "warnings" and "errors", and respective messages had been displayed (GUI) respectively collected and displayed after the export finished (CLI) upon catching them. Individual counts had been used to display an export summary message after the export finished. The new logging system has numerous advantages: - It allows logging status updates during a channel export (without having to abort it). Therefore, it also allows for informational messages, success messages and (non-urgent) warning messages to be logged. - It doesn't use exceptions in situations without any need for exceptional handling, resulting in cleaner code. - It logs all these messages instantly on both the CLI and GUI, instead of only displaying the log messages after the export finished. - It removes duplicated code structures between the CLI and GUI parts; replacing individual logging / counting code with a general consistent logger / counter, which improves the code quality. - This also makes it handle the logging much more consistently: Not only are the messages logged in the general export codebase, but their appearance and formatting are now defined together in a single place, making the logging system much more consistent and maintainable. Using the new system, many informational messages and warning messages have been added to the export process. This particularly includes messages about the handling of existing export files that had been added provisionally with Console.WriteLine. The existing messages have also been made more consistent with each other. Furthermore, the logging of success messages has been added to the exporter. This allows the user to better watch the progress in the GUI and solves the problem that the progress hadn't been watchable in the CLI if parallel exports had been enabled. The export summary message has also been massively improved: While previously, it had just contained the number of channels that have been successfully exported, the handling of existing export files has added other possible export results, specifically the channel having already been exported and being appended or overwritten successfully, and the channel having already been exported and not having been appended as there are no new messages. Therefore, the export summary has been changed to contain the number of channel exports that resulted in each occurring export result. This also means that it now contains the number of empty new channel exports (which had previously not been included). --- .../Commands/Base/ExportCommandBase.cs | 69 ++---------- .../Utils/Extensions/ConsoleExtensions.cs | 102 ++++++++++++++++- .../Exceptions/ChannelEmptyException.cs | 3 - .../Exporting/ChannelExporter.cs | 67 +++++++---- .../Exporting/Logging/ExportResult.cs | 32 ++++++ .../Exporting/Logging/ProgressLogger.cs | 105 ++++++++++++++++++ .../Framework/SnackbarManager.cs | 102 +++++++++++++++++ .../Components/DashboardViewModel.cs | 27 ++--- 8 files changed, 405 insertions(+), 102 deletions(-) delete mode 100644 DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs create mode 100644 DiscordChatExporter.Core/Exporting/Logging/ExportResult.cs create mode 100644 DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index 8573f4b6..be427b7b 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -229,16 +229,15 @@ public abstract class ExportCommandBase : DiscordCommandBase } // Export - var errorsByChannel = new ConcurrentDictionary(); - var warningsByChannel = new ConcurrentDictionary(); - await console.Output.WriteLineAsync($"Exporting {unwrappedChannels.Count} channel(s)..."); - await console - .CreateProgressTicker() + var (progressTicker, logger) = console.CreateProgressTicker(); + await progressTicker .HideCompleted( // When exporting multiple channels in parallel, hide the completed tasks // because it gets hard to visually parse them as they complete out of order. // https://github.com/Tyrrrz/DiscordChatExporter/issues/1124 + // They are logged above the active tasks instead, so that the user can still + // see the entire progress. ParallelLimit > 1 ) .StartAsync(async ctx => @@ -282,6 +281,8 @@ public abstract class ExportCommandBase : DiscordCommandBase ); await Exporter.ExportChannelAsync( + logger, + ParallelLimit > 1, request, progress.ToPercentageBased(), innerCancellationToken @@ -289,71 +290,19 @@ public abstract class ExportCommandBase : DiscordCommandBase } ); } - catch (ChannelEmptyException ex) - { - warningsByChannel[channel] = ex.Message; - } catch (DiscordChatExporterException ex) when (!ex.IsFatal) { - errorsByChannel[channel] = ex.Message; + logger.LogError(null, ex.Message); } } ); }); - // Print the result - using (console.WithForegroundColor(ConsoleColor.White)) - { - await console.Output.WriteLineAsync( - $"Successfully exported {unwrappedChannels.Count - errorsByChannel.Count} channel(s)." - ); - } - - // Print warnings - if (warningsByChannel.Any()) - { - await console.Output.WriteLineAsync(); - - using (console.WithForegroundColor(ConsoleColor.Yellow)) - { - await console.Error.WriteLineAsync( - "Warnings reported for the following channel(s):" - ); - } - - foreach (var (channel, message) in warningsByChannel) - { - await console.Error.WriteAsync($"{channel.GetHierarchicalName()}: "); - using (console.WithForegroundColor(ConsoleColor.Yellow)) - await console.Error.WriteLineAsync(message); - } - - await console.Error.WriteLineAsync(); - } - - // Print errors - if (errorsByChannel.Any()) - { - await console.Output.WriteLineAsync(); - - using (console.WithForegroundColor(ConsoleColor.Red)) - { - await console.Error.WriteLineAsync("Failed to export the following channel(s):"); - } - - foreach (var (channel, message) in errorsByChannel) - { - await console.Error.WriteAsync($"{channel.GetHierarchicalName()}: "); - using (console.WithForegroundColor(ConsoleColor.Red)) - await console.Error.WriteLineAsync(message); - } - - await console.Error.WriteLineAsync(); - } + logger.PrintExportSummary(FileExistsHandling); // Fail the command only if ALL channels failed to export. // If only some channels failed to export, it's okay. - if (errorsByChannel.Count >= unwrappedChannels.Count) + if (logger.AllFailed()) throw new CommandException("Export failed."); } diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 4509026b..45e560ff 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; using CliFx.Infrastructure; +using DiscordChatExporter.Core.Exporting; +using DiscordChatExporter.Core.Exporting.Logging; using Spectre.Console; namespace DiscordChatExporter.Cli.Utils.Extensions; @@ -20,9 +22,11 @@ internal static class ConsoleExtensions public static Status CreateStatusTicker(this IConsole console) => console.CreateAnsiConsole().Status().AutoRefresh(true); - public static Progress CreateProgressTicker(this IConsole console) => - console - .CreateAnsiConsole() + public static (Progress, ConsoleProgressLogger) CreateProgressTicker(this IConsole console) + { + var ansiConsole = console.CreateAnsiConsole(); + var logger = new ConsoleProgressLogger(ansiConsole); + var progress = ansiConsole .Progress() .AutoClear(false) .AutoRefresh(true) @@ -32,6 +36,8 @@ internal static class ConsoleExtensions new ProgressBarColumn(), new PercentageColumn() ); + return (progress, logger); + } public static async ValueTask StartTaskAsync( this ProgressContext context, @@ -59,3 +65,93 @@ internal static class ConsoleExtensions } } } + +/// +/// The ConsoleProgressLogger is a subclass that logs the status updates of the exported +/// channels with colors on the console. +/// It also provides a way to print the generated export summary on the console. +/// +/// The console that the progress information and summary is logged on. +public class ConsoleProgressLogger(IAnsiConsole console) : ProgressLogger +{ + /// + /// The ConsoleProgressLogger logs the success message to the console. + public override void LogSuccess(ExportRequest request, string message) + { + LogMessage("SUCCESS", "[green]", request, message); + } + + /// + /// The ConsoleProgressLogger logs the informational message to the console. + public override void LogInfo(ExportRequest request, string message) + { + LogMessage("INFO", "[default]", request, message); + } + + /// + /// The ConsoleProgressLogger logs the warning message to the console. + public override void LogWarning(ExportRequest request, string message) + { + LogMessage("WARNING", "[yellow]", request, message); + } + + /// + /// The ConsoleProgressLogger logs the error message to the console. + public override void LogError(ExportRequest? request, string message) + { + IncrementCounter(ExportResult.ExportError); + LogMessage("ERROR", "[red]", request, message); + } + + /// + /// Logs the given message of the given category about the current channel export with the given color to the + /// console. + /// + /// The category of the message that should be logged. + /// The color in which the message should be logged. + /// The request specifying the current channel export. + /// The message about the current channel export that should be logged. + private void LogMessage(string category, string color, ExportRequest? request, string message) + { + var paddedCategory = (category + ":").PadRight(10); + + var channelInfo = ""; + if (request != null) + channelInfo = + request.Guild.Name + " / " + request.Channel.GetHierarchicalName() + " | "; + + var logMessage = $"{color}{paddedCategory}{channelInfo}{message}[/]"; + console.MarkupLine(logMessage); + } + + /// + /// Prints a summary on all previously logged exports and their respective results to the console. + /// + /// The file exists handling of the export whose summary should be printed. + public void PrintExportSummary(FileExistsHandling updateType) + { + var exportSummary = GetExportSummary(updateType); + exportSummary.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessMessage); + exportSummary.TryGetValue( + ExportResult.NewExportSuccessEmpty, + out var newExportSuccessEmptyMessage + ); + exportSummary.TryGetValue( + ExportResult.UpdateExportSuccess, + out var updateExportSuccessMessage + ); + exportSummary.TryGetValue(ExportResult.UpdateExportSkip, out var updateExportSkipMessage); + exportSummary.TryGetValue(ExportResult.ExportError, out var exportErrorMessage); + + if (newExportSuccessMessage != null) + console.MarkupLine($"[green]{newExportSuccessMessage}[/]"); + if (newExportSuccessEmptyMessage != null) + console.MarkupLine($"[default]{newExportSuccessEmptyMessage}[/]"); + if (updateExportSuccessMessage != null) + console.MarkupLine($"[green]{updateExportSuccessMessage}[/]"); + if (updateExportSkipMessage != null) + console.MarkupLine($"[default]{updateExportSkipMessage}[/]"); + if (exportErrorMessage != null) + console.MarkupLine($"[red]{exportErrorMessage}[/]"); + } +} diff --git a/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs b/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs deleted file mode 100644 index 42e6c787..00000000 --- a/DiscordChatExporter.Core/Exceptions/ChannelEmptyException.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace DiscordChatExporter.Core.Exceptions; - -public class ChannelEmptyException(string message) : DiscordChatExporterException(message); diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 6a307970..4a434a97 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using DiscordChatExporter.Core.Discord; using DiscordChatExporter.Core.Discord.Data; using DiscordChatExporter.Core.Exceptions; +using DiscordChatExporter.Core.Exporting.Logging; using Gress; namespace DiscordChatExporter.Core.Exporting; @@ -12,6 +13,8 @@ namespace DiscordChatExporter.Core.Exporting; public class ChannelExporter(DiscordClient discord) { public async ValueTask ExportChannelAsync( + ProgressLogger logger, + bool logSuccess, ExportRequest request, IProgress? progress = null, CancellationToken cancellationToken = default @@ -20,26 +23,29 @@ public class ChannelExporter(DiscordClient discord) // Forum channels don't have messages, they are just a list of threads if (request.Channel.Kind == ChannelKind.GuildForum) { - throw new DiscordChatExporterException( - $"Channel '{request.Channel.Name}' " - + $"of guild '{request.Guild.Name}' " - + $"is a forum and cannot be exported directly. " - + "You need to pull its threads and export them individually." + // TODO: The GUI apparently has no thread inclusion setting + logger.LogError( + request, + "This channel is a forum and cannot be exported. " + + "Did you forget to turn on thread inclusion?" ); + return; } var currentPartitionIndex = 0; + var exportExists = false; // TODO: Maybe add a way to search for old files after a username change if (File.Exists(request.OutputFilePath)) { + exportExists = true; // TODO: Maybe add an "Ask" option in the future switch (request.FileExistsHandling) { case FileExistsHandling.Abort: - Console.WriteLine("Channel aborted"); + logger.LogError(request, "Aborted export due to existing export files"); return; case FileExistsHandling.Overwrite: - Console.WriteLine("Removing old files"); + logger.LogWarning(request, "Removing existing export files"); MessageExporter.RemoveExistingFiles(request.OutputFilePath); break; case FileExistsHandling.Append: @@ -53,12 +59,15 @@ public class ChannelExporter(DiscordClient discord) if (!request.Channel.MayHaveMessagesAfter(request.LastPriorMessage.Value)) { - Console.WriteLine("Download already up to date"); + logger.IncrementCounter(ExportResult.UpdateExportSkip); + logger.LogInfo(request, "Existing export already up to date"); return; } - Console.WriteLine( - "Downloading data after " + lastMessageSnowflake.Value.ToDate() + logger.LogInfo( + request, + "Appending existing export starting at " + + lastMessageSnowflake.Value.ToDate() ); currentPartitionIndex = MessageExporter.GetPartitionCount( request.OutputFilePath @@ -83,11 +92,10 @@ public class ChannelExporter(DiscordClient discord) // Check if the channel is empty if (request.Channel.IsEmpty) { - throw new ChannelEmptyException( - $"Channel '{request.Channel.Name}' " - + $"of guild '{request.Guild.Name}' " - + $"does not contain any messages; an empty file will be created." - ); + logger.IncrementCounter(ExportResult.NewExportSuccess); + logger.IncrementCounter(ExportResult.NewExportSuccessEmpty); + logger.LogInfo(request, "The channel does not contain any messages"); + return; } // Check if the 'before' and 'after' boundaries are valid @@ -102,11 +110,13 @@ public class ChannelExporter(DiscordClient discord) ) ) { - throw new ChannelEmptyException( - $"Channel '{request.Channel.Name}' " - + $"of guild '{request.Guild.Name}' " - + $"does not contain any messages within the specified period; an empty file will be created." + logger.IncrementCounter(ExportResult.NewExportSuccess); + logger.IncrementCounter(ExportResult.NewExportSuccessEmpty); + logger.LogWarning( + request, + "The channel does not contain any messages within the specified period" ); + return; } await foreach ( @@ -141,5 +151,24 @@ public class ChannelExporter(DiscordClient discord) ); } } + + if (!exportExists) + { + logger.IncrementCounter(ExportResult.NewExportSuccess); + if (logSuccess) + logger.LogSuccess(request, "Successfully exported the channel"); + } + else if (request.FileExistsHandling == FileExistsHandling.Append) + { + logger.IncrementCounter(ExportResult.UpdateExportSuccess); + if (logSuccess) + logger.LogSuccess(request, "Successfully appended the channel export"); + } + else + { + logger.IncrementCounter(ExportResult.UpdateExportSuccess); + if (logSuccess) + logger.LogSuccess(request, "Successfully overwrote the channel export"); + } } } diff --git a/DiscordChatExporter.Core/Exporting/Logging/ExportResult.cs b/DiscordChatExporter.Core/Exporting/Logging/ExportResult.cs new file mode 100644 index 00000000..053e9449 --- /dev/null +++ b/DiscordChatExporter.Core/Exporting/Logging/ExportResult.cs @@ -0,0 +1,32 @@ +namespace DiscordChatExporter.Core.Exporting.Logging; + +/// +/// Represents a possible result of the export of a single channel +/// +public enum ExportResult +{ + /// + /// The channel is a new channel that has been exported successfully. + /// + NewExportSuccess, + + /// + /// The channel is a new empty channel that has been exported successfully. + /// + NewExportSuccessEmpty, + + /// + /// The channel is a channel that had already been exported and has been appended or overwritten successfully. + /// + UpdateExportSuccess, + + /// + /// The channel is a channel that had already been exported and hasn't been appended as there are no new messages. + /// + UpdateExportSkip, + + /// + /// The channel couldn't be exported successfully. + /// + ExportError, +} diff --git a/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs b/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs new file mode 100644 index 00000000..3a35e6d8 --- /dev/null +++ b/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs @@ -0,0 +1,105 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace DiscordChatExporter.Core.Exporting.Logging; + +/// +/// The ProgressLogger provides a consistent way to log the entire progress of an export, meaning all status updates of +/// each of the exported channels. +/// It also allows to count the different results of the individual channel exports and can provide a summary of them. +/// +public abstract class ProgressLogger +{ + private readonly ConcurrentDictionary _counters = []; + + /// + /// Increments the internal counter of the given export result in a thread-safe way. + /// + /// The export result whose counter should be incremented. + public void IncrementCounter(ExportResult exportResult) + { + _counters.AddOrUpdate(exportResult, 1, (_, currentCount) => currentCount + 1); + } + + /// + /// Generates and returns a summary on all previously logged exports and their respective results. + /// The summary is returned as one string for each export result that occurred at least once. + /// + /// The file exists handling of the export whose summary should be returned. + /// A summary on all previously logged exports and their respective results. + protected Dictionary GetExportSummary(FileExistsHandling updateType) + { + _counters.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessCount); + _counters.TryGetValue( + ExportResult.NewExportSuccessEmpty, + out var newExportSuccessEmptyCount + ); + _counters.TryGetValue(ExportResult.UpdateExportSuccess, out var updateExportSuccessCount); + _counters.TryGetValue(ExportResult.UpdateExportSkip, out var updateExportSkipCount); + _counters.TryGetValue(ExportResult.ExportError, out var exportErrorCount); + + Dictionary exportSummary = []; + if (newExportSuccessCount > 0) + exportSummary[ExportResult.NewExportSuccess] = + $"Successfully exported {newExportSuccessCount} new channel(s)."; + if (newExportSuccessEmptyCount > 0) + exportSummary[ExportResult.NewExportSuccessEmpty] = + $"{newExportSuccessEmptyCount} of those channel(s) has / have been empty."; + if (updateExportSuccessCount > 0) + exportSummary[ExportResult.UpdateExportSuccess] = + "Successfully " + + (updateType == FileExistsHandling.Append ? "appended" : "overrode") + + $" {updateExportSuccessCount} existing channel export(s)."; + if (updateExportSkipCount > 0) + exportSummary[ExportResult.UpdateExportSkip] = + $"Skipped {updateExportSkipCount} existing up-to-date channel export(s)"; + if (exportErrorCount > 0) + exportSummary[ExportResult.ExportError] = + $"Failed to export {exportErrorCount} channel(s) (see prior error message(s))."; + + return exportSummary; + } + + /// + /// Returns whether all exports have failed. + /// If exports have been skipped as there are no new messages to append, this still returns true if no export + /// finished successfully and at least one failed. + /// + /// Whether all exports have failed. + public bool AllFailed() + { + _counters.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessCount); + _counters.TryGetValue(ExportResult.UpdateExportSuccess, out var updateExportSuccessCount); + _counters.TryGetValue(ExportResult.ExportError, out var exportErrorCount); + return newExportSuccessCount + updateExportSuccessCount == 0 && exportErrorCount > 0; + } + + /// + /// Logs the success of the current channel export. + /// + /// The request specifying the current channel export + /// The success message about the current channel export that should be logged. + public abstract void LogSuccess(ExportRequest request, string message); + + /// + /// Logs an informational message about the current channel export. + /// + /// The request specifying the current channel export. + /// The informational message about the current channel export that should be logged. + public abstract void LogInfo(ExportRequest request, string message); + + /// + /// Logs a warning message about the current channel export. + /// + /// The request specifying the current channel export. + /// The warning message about the current channel export that should be logged. + public abstract void LogWarning(ExportRequest request, string message); + + /// + /// Logs an error message about the current channel export. + /// If this is called, the count of channels that failed is automatically increased. + /// + /// The request specifying the current channel export. + /// The error message about the current channel export that should be logged. + public abstract void LogError(ExportRequest request, string message); +} diff --git a/DiscordChatExporter.Gui/Framework/SnackbarManager.cs b/DiscordChatExporter.Gui/Framework/SnackbarManager.cs index a4af9f5d..41edecb5 100644 --- a/DiscordChatExporter.Gui/Framework/SnackbarManager.cs +++ b/DiscordChatExporter.Gui/Framework/SnackbarManager.cs @@ -1,5 +1,7 @@ using System; using Avalonia.Threading; +using DiscordChatExporter.Core.Exporting; +using DiscordChatExporter.Core.Exporting.Logging; using Material.Styles.Controls; using Material.Styles.Models; @@ -32,3 +34,103 @@ public class SnackbarManager DispatcherPriority.Normal ); } + +/// +/// The SnackbarProgressLogger is a subclass that logs the status updates of the exported +/// channels in the GUI snackbar. +/// +/// +/// The snackbar manager that's used to control the snackbar and add log messages to it. +/// +public class SnackbarProgressLogger(SnackbarManager snackbarManager) : ProgressLogger +{ + /// + /// The SnackbarProgressLogger logs the success message in the GUI snackbar. + public override void LogSuccess(ExportRequest request, string message) + { + LogMessage("SUCCESS", request, message); + } + + /// + /// The SnackbarProgressLogger logs the informational message in the GUI snackbar. + public override void LogInfo(ExportRequest request, string message) + { + LogMessage("INFO", request, message); + } + + /// + /// The SnackbarProgressLogger logs the warning message in the GUI snackbar. + public override void LogWarning(ExportRequest request, string message) + { + LogMessage("WARNING", request, message); + } + + /// + /// The SnackbarProgressLogger logs the error message in the GUI snackbar. + public override void LogError(ExportRequest? request, string message) + { + IncrementCounter(ExportResult.ExportError); + // TODO: Maybe highlight error messages with a red background + LogMessage("ERROR", request, message, TimeSpan.FromSeconds(10)); + } + + /// + /// Logs the given message of the given category about the current channel export in the GUI snackbar. + /// + /// The category of the message that should be logged. + /// The request specifying the current channel export. + /// The message about the current channel export that should be logged. + /// + /// The duration the message should be displayed in the snackbar. + /// If the given value is null, the default duration is used. + /// + private void LogMessage( + string category, + ExportRequest? request, + string message, + TimeSpan? duration = null + ) + { + var channelInfo = ""; + if (request != null) + channelInfo = + request.Guild.Name + " / " + request.Channel.GetHierarchicalName() + " | "; + + var logMessage = $"{category}: {channelInfo}{message}"; + snackbarManager.Notify(logMessage, duration); + } + + /// + /// Prints a summary on all previously logged exports and their respective results in the GUI snackbar. + /// + /// The file exists handling of the export whose summary should be printed. + public void PrintExportSummary(FileExistsHandling updateType) + { + var exportSummary = GetExportSummary(updateType); + exportSummary.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessMessage); + exportSummary.TryGetValue( + ExportResult.NewExportSuccessEmpty, + out var newExportSuccessEmptyMessage + ); + exportSummary.TryGetValue( + ExportResult.UpdateExportSuccess, + out var updateExportSuccessMessage + ); + exportSummary.TryGetValue(ExportResult.UpdateExportSkip, out var updateExportSkipMessage); + exportSummary.TryGetValue(ExportResult.ExportError, out var exportErrorMessage); + + var summaryString = ""; + if (newExportSuccessMessage != null) + summaryString += newExportSuccessMessage + "\n"; + if (newExportSuccessEmptyMessage != null) + summaryString += newExportSuccessEmptyMessage + "\n"; + if (updateExportSuccessMessage != null) + summaryString += updateExportSuccessMessage + "\n"; + if (updateExportSkipMessage != null) + summaryString += updateExportSkipMessage + "\n"; + if (exportErrorMessage != null) + summaryString += exportErrorMessage + "\n"; + + snackbarManager.Notify(summaryString.TrimEnd(), TimeSpan.FromSeconds(15)); + } +} diff --git a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs index f5ead982..052007d5 100644 --- a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs @@ -242,13 +242,12 @@ public partial class DashboardViewModel : ViewModelBase return; var exporter = new ChannelExporter(_discord); + var logger = new SnackbarProgressLogger(_snackbarManager); var channelProgressPairs = dialog .Channels!.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() }) .ToArray(); - var successfulExportCount = 0; - await Parallel.ForEachAsync( channelProgressPairs, new ParallelOptions @@ -280,17 +279,17 @@ public partial class DashboardViewModel : ViewModelBase _settingsService.IsUtcNormalizationEnabled ); - await exporter.ExportChannelAsync(request, progress, cancellationToken); - - Interlocked.Increment(ref successfulExportCount); - } - catch (ChannelEmptyException ex) - { - _snackbarManager.Notify(ex.Message.TrimEnd('.')); + await exporter.ExportChannelAsync( + logger, + true, + request, + progress, + cancellationToken + ); } catch (DiscordChatExporterException ex) when (!ex.IsFatal) { - _snackbarManager.Notify(ex.Message.TrimEnd('.')); + logger.LogError(null, ex.Message.TrimEnd('.')); } finally { @@ -299,13 +298,7 @@ public partial class DashboardViewModel : ViewModelBase } ); - // Notify of the overall completion - if (successfulExportCount > 0) - { - _snackbarManager.Notify( - $"Successfully exported {successfulExportCount} channel(s)" - ); - } + logger.PrintExportSummary(_settingsService.FileExistsHandling); } catch (Exception ex) {