From 90ed829375d5fea0943d46b91dbe5d77d52be2c1 Mon Sep 17 00:00:00 2001 From: Kornelius Rohrschneider Date: Thu, 30 Oct 2025 00:04:37 +0100 Subject: [PATCH] Added existing export search Previously, an existing export could only be detected if it existed at the current file target path. However, if the name of the channel, the channel parent or the guild has changed or if the default file name formatting has changed, the existing export's file name would be different, and it could therefore not be detected. Therefore, the option to explicitly search for the existing export in the target directory has been added. If it's activated (and there's no existing export at the current file target path), all file names in the target directory will be compared to a regex that matches any file name the channel export (with the same date range) might have had in the past. If several existing exports have been detected, an error is logged and the channel export is aborted. Otherwise, it continues as before (only additionally moving the existing export files to the to the new file paths if they should be appended). Whether this new option is activated is currently hardcoded. The enum FileExistsHandling has been renamed to ExportExistsHandling to clarify that the setting applies to existing exports in general. If file names of a directory are needed, they are collected lazily and stored for future use in other channel exports to avoid unnecessary I/O operations. --- .../Commands/Base/ExportCommandBase.cs | 8 +- .../Utils/Extensions/ConsoleExtensions.cs | 2 +- .../Exporting/ChannelExporter.cs | 256 ++++++++++++++---- ...stsHandling.cs => ExportExistsHandling.cs} | 2 +- .../Exporting/ExportRequest.cs | 64 ++++- .../Exporting/Logging/ProgressLogger.cs | 4 +- .../Exporting/MessageExporter.cs | 35 ++- .../Framework/SnackbarManager.cs | 2 +- .../Services/SettingsService.cs | 2 +- .../Components/DashboardViewModel.cs | 7 +- .../ViewModels/Dialogs/SettingsViewModel.cs | 10 +- .../Views/Dialogs/SettingsView.axaml | 6 +- 12 files changed, 312 insertions(+), 86 deletions(-) rename DiscordChatExporter.Core/Exporting/{FileExistsHandling.cs => ExportExistsHandling.cs} (95%) diff --git a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs index be427b7b..354d9e0e 100644 --- a/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs +++ b/DiscordChatExporter.Cli/Commands/Base/ExportCommandBase.cs @@ -118,7 +118,7 @@ public abstract class ExportCommandBase : DiscordCommandBase "prev-export", Description = "What the exporter should do if the channel had already been exported." )] - public FileExistsHandling FileExistsHandling { get; init; } = FileExistsHandling.Abort; + public ExportExistsHandling ExportExistsHandling { get; init; } = ExportExistsHandling.Abort; [Obsolete("This option doesn't do anything. Kept for backwards compatibility.")] [CommandOption( @@ -230,6 +230,7 @@ public abstract class ExportCommandBase : DiscordCommandBase // Export await console.Output.WriteLineAsync($"Exporting {unwrappedChannels.Count} channel(s)..."); + var outputDirFilesDict = new ConcurrentDictionary(); var (progressTicker, logger) = console.CreateProgressTicker(); await progressTicker .HideCompleted( @@ -270,7 +271,7 @@ public abstract class ExportCommandBase : DiscordCommandBase ExportFormat, After, Before, - FileExistsHandling, + ExportExistsHandling, PartitionLimit, MessageFilter, ShouldFormatMarkdown, @@ -284,6 +285,7 @@ public abstract class ExportCommandBase : DiscordCommandBase logger, ParallelLimit > 1, request, + outputDirFilesDict, progress.ToPercentageBased(), innerCancellationToken ); @@ -298,7 +300,7 @@ public abstract class ExportCommandBase : DiscordCommandBase ); }); - logger.PrintExportSummary(FileExistsHandling); + logger.PrintExportSummary(ExportExistsHandling); // Fail the command only if ALL channels failed to export. // If only some channels failed to export, it's okay. diff --git a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs index 45e560ff..dd132e8a 100644 --- a/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs +++ b/DiscordChatExporter.Cli/Utils/Extensions/ConsoleExtensions.cs @@ -128,7 +128,7 @@ public class ConsoleProgressLogger(IAnsiConsole console) : ProgressLogger /// 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) + public void PrintExportSummary(ExportExistsHandling updateType) { var exportSummary = GetExportSummary(updateType); exportSummary.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessMessage); diff --git a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs index 9a8aa37c..2348b5c7 100644 --- a/DiscordChatExporter.Core/Exporting/ChannelExporter.cs +++ b/DiscordChatExporter.Core/Exporting/ChannelExporter.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Concurrent; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using DiscordChatExporter.Core.Discord; @@ -16,6 +18,7 @@ public class ChannelExporter(DiscordClient discord) ProgressLogger logger, bool logSuccess, ExportRequest request, + ConcurrentDictionary outputDirFilesDict, IProgress? progress = null, CancellationToken cancellationToken = default ) @@ -32,62 +35,27 @@ public class ChannelExporter(DiscordClient discord) 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: - logger.LogError(request, "Aborted export due to existing export files"); - return; - case FileExistsHandling.Overwrite: - logger.LogWarning(request, "Removing existing export files"); - MessageExporter.RemoveExistingFiles(request.OutputFilePath); - break; - case FileExistsHandling.Append: - var lastMessageSnowflake = MessageExporter.GetLastMessageSnowflake( - request.OutputFilePath, - request.Format - ); - if (lastMessageSnowflake != null) - { - if (!request.Channel.MayHaveMessagesAfter(lastMessageSnowflake.Value)) - { - logger.IncrementCounter(ExportResult.UpdateExportSkip); - logger.LogInfo(request, "Existing export already up to date"); - return; - } - request.LastPriorMessage = lastMessageSnowflake.Value; - logger.LogInfo( - request, - "Appending existing export starting at " - + lastMessageSnowflake.Value.ToDate() - ); - } - else - { - if (request.Channel.IsEmpty) - { - logger.IncrementCounter(ExportResult.UpdateExportSkip); - logger.LogInfo(request, "Existing empty export already up to date"); - return; - } - logger.LogInfo(request, "Appending existing empty export."); - } - currentPartitionIndex = MessageExporter.GetPartitionCount( - request.OutputFilePath - ); - break; - default: - throw new InvalidOperationException( - $"Unknown FileExistsHandling value '{request.FileExistsHandling}'." - ); - } - } + // TODO: Add a way for the user to choose the setting + var searchForExistingExport = true; + if ( + !DetectExistingExport( + request, + logger, + searchForExistingExport, + outputDirFilesDict, + out var existingExportFile + ) + ) + return; + if ( + !HandleExistingExport( + request, + logger, + existingExportFile, + out var currentPartitionIndex + ) + ) + return; // Build context var context = new ExportContext(discord, request); @@ -95,7 +63,10 @@ public class ChannelExporter(DiscordClient discord) // Initialize the exporter before further checks to ensure the file is created even if // an exception is thrown after this point. - await using var messageExporter = new MessageExporter(context, currentPartitionIndex); + await using var messageExporter = new MessageExporter( + context, + currentPartitionIndex!.Value + ); // Check if the channel is empty if (request.Channel.IsEmpty) @@ -160,13 +131,13 @@ public class ChannelExporter(DiscordClient discord) } } - if (!exportExists) + if (existingExportFile == null) { logger.IncrementCounter(ExportResult.NewExportSuccess); if (logSuccess) logger.LogSuccess(request, "Successfully exported the channel"); } - else if (request.FileExistsHandling == FileExistsHandling.Append) + else if (request.ExportExistsHandling == ExportExistsHandling.Append) { logger.IncrementCounter(ExportResult.UpdateExportSuccess); if (logSuccess) @@ -179,4 +150,171 @@ public class ChannelExporter(DiscordClient discord) logger.LogSuccess(request, "Successfully overwrote the channel export"); } } + + /// + /// Detects whether an existing export of the given request exists. + /// + /// The request specifying the current channel export. + /// The logger that's used to log progress updates about the export. + /// + /// If false, it will only be detected whether an existing export exists at the current target file path. + /// This means that an existing export won't be detected if the name of the channel, the channel parent or the + /// guild has changed or if the default file name formatting has changed. + /// If true, the entire directory will be searched for an existing export file of the given request (if there is + /// none at the current target file path). + /// + /// + /// A thread-safe dictionary mapping lists of filenames to the directory they're in. + /// If the directory files are needed, they will be collected lazily and stored for future use in other channel + /// exports. + /// + /// + /// The absolute base file path of the existing export of this request, if one has been detected. Null otherwise. + /// + /// + /// Whether the export should continue normally (true) or return (false). + /// This is true if no, or exactly one, existing export of this request has been detected, and false if + /// several ones have been detected. + /// + private static bool DetectExistingExport( + ExportRequest request, + ProgressLogger logger, + bool searchForExistingExport, + ConcurrentDictionary outputDirFilesDict, + out string? existingExportFile + ) + { + existingExportFile = null; + + if (File.Exists(request.OutputFilePath)) + { + existingExportFile = request.OutputFilePath; + return true; + } + if (!searchForExistingExport) + return true; + + // Look for an existing export under a different file name + var outputFileRegex = request.GetDefaultOutputFileNameRegex(); + var outputDirFiles = outputDirFilesDict.GetOrAdd( + request.OutputDirPath, + outputDirPath => + Directory + .GetFiles(outputDirPath) + .Select(Path.GetFileName) + .Select(fileName => fileName!) + .ToArray() + ); + var regexFiles = outputDirFiles + .Where(fileName => outputFileRegex.IsMatch(fileName)) + .ToArray(); + if (regexFiles.Length == 0) + return true; + if (regexFiles.Length > 1) + { + logger.LogError( + request, + "Found multiple existing channel exports under different file names: " + + string.Join(", ", regexFiles) + + "." + ); + return false; + } + + logger.LogInfo( + request, + "Found existing channel export under file name " + regexFiles[0] + "." + ); + existingExportFile = Path.Combine(request.OutputDirPath, regexFiles[0]); + return true; + } + + /// + /// Handles the existing export files of the current request according to the set file exists handling. + /// + /// The request specifying the current channel export. + /// The logger that's used to log progress updates about the export. + /// + /// The absolute base file path of the existing export of this request, if one has been detected. + /// If this is null, the function will immediately return. + /// + /// + /// The index of the current export partition the newly exported messages should be written to. + /// + /// + /// Whether the export should continue normally (true) or return (false). + /// This is false both if the export should be aborted and if the export is already up to date, and true otherwise. + /// + /// + private static bool HandleExistingExport( + ExportRequest request, + ProgressLogger logger, + string? existingExportFile, + out int? currentPartitionIndex + ) + { + currentPartitionIndex = null; + + if (existingExportFile == null) + { + currentPartitionIndex = 0; + return true; + } + + // TODO: Maybe add an "Ask" option in the future + switch (request.ExportExistsHandling) + { + case ExportExistsHandling.Abort: + logger.LogError(request, "Aborted export due to existing export files"); + return false; + case ExportExistsHandling.Overwrite: + logger.LogWarning(request, "Removing existing export files"); + MessageExporter.RemoveExistingExport(existingExportFile); + currentPartitionIndex = 0; + return true; + case ExportExistsHandling.Append: + if (existingExportFile != request.OutputFilePath) + { + logger.LogInfo(request, "Moving existing export files to the new file names"); + MessageExporter.MoveExistingExport(existingExportFile, request.OutputFilePath); + } + + var lastMessageSnowflake = MessageExporter.GetLastMessageSnowflake( + request.OutputFilePath, + request.Format + ); + if (lastMessageSnowflake != null) + { + if (!request.Channel.MayHaveMessagesAfter(lastMessageSnowflake.Value)) + { + logger.IncrementCounter(ExportResult.UpdateExportSkip); + logger.LogInfo(request, "Existing export already up to date"); + return false; + } + request.LastPriorMessage = lastMessageSnowflake.Value; + logger.LogInfo( + request, + "Appending existing export starting at " + + lastMessageSnowflake.Value.ToDate() + ); + } + else + { + if (request.Channel.IsEmpty) + { + logger.IncrementCounter(ExportResult.UpdateExportSkip); + logger.LogInfo(request, "Existing empty export already up to date"); + return false; + } + logger.LogInfo(request, "Appending existing empty export."); + } + + currentPartitionIndex = MessageExporter.GetPartitionCount(request.OutputFilePath); + return true; + default: + throw new InvalidOperationException( + $"Unknown FileExistsHandling value '{request.ExportExistsHandling}'." + ); + } + } } diff --git a/DiscordChatExporter.Core/Exporting/FileExistsHandling.cs b/DiscordChatExporter.Core/Exporting/ExportExistsHandling.cs similarity index 95% rename from DiscordChatExporter.Core/Exporting/FileExistsHandling.cs rename to DiscordChatExporter.Core/Exporting/ExportExistsHandling.cs index d6cfd41c..9c6bf911 100644 --- a/DiscordChatExporter.Core/Exporting/FileExistsHandling.cs +++ b/DiscordChatExporter.Core/Exporting/ExportExistsHandling.cs @@ -3,7 +3,7 @@ namespace DiscordChatExporter.Core.Exporting; /// /// Represents the setting on how to handle the export of a channel that has already been exported. /// -public enum FileExistsHandling +public enum ExportExistsHandling { /// /// If a channel had previously been exported, its export will be aborted. diff --git a/DiscordChatExporter.Core/Exporting/ExportRequest.cs b/DiscordChatExporter.Core/Exporting/ExportRequest.cs index d3dcba70..5abe25fb 100644 --- a/DiscordChatExporter.Core/Exporting/ExportRequest.cs +++ b/DiscordChatExporter.Core/Exporting/ExportRequest.cs @@ -30,7 +30,7 @@ public partial class ExportRequest public Snowflake? Before { get; } - public FileExistsHandling FileExistsHandling { get; } + public ExportExistsHandling ExportExistsHandling { get; } public Snowflake? LastPriorMessage { get; set; } @@ -58,7 +58,7 @@ public partial class ExportRequest ExportFormat format, Snowflake? after, Snowflake? before, - FileExistsHandling fileExistsHandling, + ExportExistsHandling exportExistsHandling, PartitionLimit partitionLimit, MessageFilter messageFilter, bool shouldFormatMarkdown, @@ -73,7 +73,7 @@ public partial class ExportRequest Format = format; After = after; Before = before; - FileExistsHandling = fileExistsHandling; + ExportExistsHandling = exportExistsHandling; PartitionLimit = partitionLimit; MessageFilter = messageFilter; ShouldFormatMarkdown = shouldFormatMarkdown; @@ -104,6 +104,7 @@ public partial class ExportRequest Snowflake? before = null ) { + // Do not change this without adding the new version to the corresponding regex below var buffer = new StringBuilder(); // Guild name @@ -154,6 +155,63 @@ public partial class ExportRequest return PathEx.EscapeFileName(buffer.ToString()); } + /// + /// Returns a regex that matches any default file name this channel export might have had in the past. + /// This can be used to detect existing exports of this channel with a different guild, parent and / or channel + /// name. + /// This only matches existing exports with the same date range as the current export. + /// + /// A regex that matches any default file name this channel might have had in the past. + public Regex GetDefaultOutputFileNameRegex() + { + // While this code looks similar to GetDefaultOutputFileName, the two functions are intentionally independent + // Even if the default output file name gets changed, the previous default file names should still be matched + // by this; the new version should just be added additionally to this regex + var buffer = new StringBuilder(); + + // Guild name + buffer.Append(".*?"); + + // Parent name + if (Channel.Parent is not null) + buffer.Append(" - ").Append(".*?"); + + // Channel name and ID + buffer + .Append(" - ") + .Append(".*?") + .Append(' ') + .Append("\\[") + .Append(Channel.Id) + .Append("\\]"); + + // Date range + if (After is not null || Before is not null) + { + buffer.Append(' ').Append("\\("); + if (After is not null && Before is not null) + { + buffer.Append( + $"{After.Value.ToDate():yyyy-MM-dd} to {Before.Value.ToDate():yyyy-MM-dd}" + ); + } + else if (After is not null) + { + buffer.Append($"after {After.Value.ToDate():yyyy-MM-dd}"); + } + else if (Before is not null) + { + buffer.Append($"before {Before.Value.ToDate():yyyy-MM-dd}"); + } + buffer.Append("\\)"); + } + + // File extension + buffer.Append("\\.").Append(Format.GetFileExtension()); + + return new Regex(buffer.ToString()); + } + private static string FormatPath( string path, Guild guild, diff --git a/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs b/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs index 3a35e6d8..c638c3d0 100644 --- a/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs +++ b/DiscordChatExporter.Core/Exporting/Logging/ProgressLogger.cs @@ -27,7 +27,7 @@ public abstract class ProgressLogger /// /// 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) + protected Dictionary GetExportSummary(ExportExistsHandling updateType) { _counters.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessCount); _counters.TryGetValue( @@ -48,7 +48,7 @@ public abstract class ProgressLogger if (updateExportSuccessCount > 0) exportSummary[ExportResult.UpdateExportSuccess] = "Successfully " - + (updateType == FileExistsHandling.Append ? "appended" : "overrode") + + (updateType == ExportExistsHandling.Append ? "appended" : "overrode") + $" {updateExportSuccessCount} existing channel export(s)."; if (updateExportSkipCount > 0) exportSummary[ExportResult.UpdateExportSkip] = diff --git a/DiscordChatExporter.Core/Exporting/MessageExporter.cs b/DiscordChatExporter.Core/Exporting/MessageExporter.cs index fe23b7b8..fdc46c9e 100644 --- a/DiscordChatExporter.Core/Exporting/MessageExporter.cs +++ b/DiscordChatExporter.Core/Exporting/MessageExporter.cs @@ -109,7 +109,7 @@ internal partial class MessageExporter if (File.Exists(filePath)) { throw new InvalidOperationException( - "Error: An exported file already exists. This should never happen." + "Error: The target export file already exists. This should never happen." ); } @@ -184,10 +184,9 @@ internal partial class MessageExporter /// /// The path of the first partition of the Discord channel export that should be removed. /// - public static void RemoveExistingFiles(string baseFilePath) + public static void RemoveExistingExport(string baseFilePath) { - var currentPartition = 0; - while (true) + for (var currentPartition = 0; ; currentPartition++) { var currentFilePath = GetPartitionFilePath(baseFilePath, currentPartition); if (File.Exists(currentFilePath)) @@ -198,7 +197,33 @@ internal partial class MessageExporter { return; } - currentPartition++; + } + } + + /// + /// Moves all partitions of the previously exported Discord channel with the given old base file path to the given + /// new base file path. + /// + /// + /// The old path of the first partition of the Discord channel export that should be moved. + /// + /// + /// The new path to which the first partition of the Discord channel export should be moved. + /// + public static void MoveExistingExport(string oldBaseFilePath, string newBaseFilePath) + { + for (var currentPartition = 0; ; currentPartition++) + { + var currentOldFilePath = GetPartitionFilePath(oldBaseFilePath, currentPartition); + if (File.Exists(currentOldFilePath)) + { + var currentNewFilePath = GetPartitionFilePath(newBaseFilePath, currentPartition); + File.Move(currentOldFilePath, currentNewFilePath); + } + else + { + return; + } } } diff --git a/DiscordChatExporter.Gui/Framework/SnackbarManager.cs b/DiscordChatExporter.Gui/Framework/SnackbarManager.cs index 41edecb5..0e502c26 100644 --- a/DiscordChatExporter.Gui/Framework/SnackbarManager.cs +++ b/DiscordChatExporter.Gui/Framework/SnackbarManager.cs @@ -104,7 +104,7 @@ public class SnackbarProgressLogger(SnackbarManager snackbarManager) : ProgressL /// 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) + public void PrintExportSummary(ExportExistsHandling updateType) { var exportSummary = GetExportSummary(updateType); exportSummary.TryGetValue(ExportResult.NewExportSuccess, out var newExportSuccessMessage); diff --git a/DiscordChatExporter.Gui/Services/SettingsService.cs b/DiscordChatExporter.Gui/Services/SettingsService.cs index c1a618d0..a56da7e1 100644 --- a/DiscordChatExporter.Gui/Services/SettingsService.cs +++ b/DiscordChatExporter.Gui/Services/SettingsService.cs @@ -37,7 +37,7 @@ public partial class SettingsService() public partial ThreadInclusionMode ThreadInclusionMode { get; set; } [ObservableProperty] - public partial FileExistsHandling FileExistsHandling { get; set; } + public partial ExportExistsHandling ExportExistsHandling { get; set; } [ObservableProperty] public partial string? Locale { get; set; } diff --git a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs index 052007d5..2f9761b0 100644 --- a/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -243,6 +244,7 @@ public partial class DashboardViewModel : ViewModelBase var exporter = new ChannelExporter(_discord); var logger = new SnackbarProgressLogger(_snackbarManager); + var outputDirFilesDict = new ConcurrentDictionary(); var channelProgressPairs = dialog .Channels!.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() }) @@ -269,7 +271,7 @@ public partial class DashboardViewModel : ViewModelBase dialog.SelectedFormat, dialog.After?.Pipe(timestamp => Snowflake.FromDate(timestamp, true)), dialog.Before?.Pipe(timestamp => Snowflake.FromDate(timestamp)), - _settingsService.FileExistsHandling, + _settingsService.ExportExistsHandling, dialog.PartitionLimit, dialog.MessageFilter, dialog.ShouldFormatMarkdown, @@ -283,6 +285,7 @@ public partial class DashboardViewModel : ViewModelBase logger, true, request, + outputDirFilesDict, progress, cancellationToken ); @@ -298,7 +301,7 @@ public partial class DashboardViewModel : ViewModelBase } ); - logger.PrintExportSummary(_settingsService.FileExistsHandling); + logger.PrintExportSummary(_settingsService.ExportExistsHandling); } catch (Exception ex) { diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs index c9dd1e61..8d96f0d1 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs @@ -62,13 +62,13 @@ public class SettingsViewModel : DialogViewModelBase set => _settingsService.ThreadInclusionMode = value; } - public IReadOnlyList AvailableFileExistHandlingOptions { get; } = - Enum.GetValues(); + public IReadOnlyList AvailableExportExistsHandlingOptions { get; } = + Enum.GetValues(); - public FileExistsHandling FileExistsHandling + public ExportExistsHandling ExportExistsHandling { - get => _settingsService.FileExistsHandling; - set => _settingsService.FileExistsHandling = value; + get => _settingsService.ExportExistsHandling; + set => _settingsService.ExportExistsHandling = value; } // These items have to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected diff --git a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml index adce95e2..b1cc5abf 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/SettingsView.axaml @@ -132,7 +132,7 @@ - + + ItemsSource="{Binding AvailableExportExistsHandlingOptions}" + SelectedItem="{Binding ExportExistsHandling}" />