mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-06-09 15:52:37 -06:00
Merge 2464610b05 into acac8f7bbb
This commit is contained in:
commit
e37cb748b7
176
DiscordChatExporter.Cli/Commands/DeleteMessagesCommand.cs
Normal file
176
DiscordChatExporter.Cli/Commands/DeleteMessagesCommand.cs
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Binding;
|
||||
using CliFx.Infrastructure;
|
||||
using DiscordChatExporter.Cli.Commands.Base;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Exceptions;
|
||||
using PowerKit.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Cli.Commands;
|
||||
|
||||
[Command("deletemessages", Description = "Deletes messages from a channel.")]
|
||||
public partial class DeleteMessagesCommand : DiscordCommandBase
|
||||
{
|
||||
[CommandOption(
|
||||
"channel",
|
||||
'c',
|
||||
Description = "Channel ID. Note: You can only delete your own messages."
|
||||
)]
|
||||
public required Snowflake ChannelId { get; set; }
|
||||
|
||||
[CommandOption(
|
||||
"before",
|
||||
Description = "Limit to messages sent before this date (formatted using the current culture)."
|
||||
)]
|
||||
public DateTimeOffset? Before { get; set; }
|
||||
|
||||
[CommandOption(
|
||||
"after",
|
||||
Description = "Limit to messages sent after this date (formatted using the current culture)."
|
||||
)]
|
||||
public DateTimeOffset? After { get; set; }
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
await base.ExecuteAsync(console);
|
||||
|
||||
var cancellationToken = console.RegisterCancellationHandler();
|
||||
|
||||
// Get current user
|
||||
await console.Output.WriteLineAsync("Getting current user...");
|
||||
var currentUser = await Discord.GetCurrentUserAsync(cancellationToken);
|
||||
await console.Output.WriteLineAsync($"Authenticated as: {currentUser.FullName}");
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
// Resolve the channel
|
||||
await console.Output.WriteLineAsync("Resolving channel...");
|
||||
var channel = await Discord.GetChannelAsync(ChannelId, cancellationToken);
|
||||
|
||||
// Warning message
|
||||
using (console.WithForegroundColor(ConsoleColor.Yellow))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
"WARNING: This will delete your messages from the channel."
|
||||
);
|
||||
await console.Output.WriteLineAsync("Messages from other users will be skipped.");
|
||||
}
|
||||
|
||||
await console.Output.WriteLineAsync();
|
||||
await console.Output.WriteLineAsync($"Channel: {channel.Name}");
|
||||
|
||||
if (After is not null)
|
||||
await console.Output.WriteLineAsync($"After: {After:g}");
|
||||
|
||||
if (Before is not null)
|
||||
await console.Output.WriteLineAsync($"Before: {Before:g}");
|
||||
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
// Count user's messages
|
||||
await console.Output.WriteLineAsync("Counting your messages...");
|
||||
|
||||
var beforeSnowflake = Before?.Pipe(Snowflake.FromDate);
|
||||
var afterSnowflake = After?.Pipe(Snowflake.FromDate);
|
||||
|
||||
var userMessageIds = new List<Snowflake>();
|
||||
await foreach (
|
||||
var message in Discord.GetMessagesAsync(
|
||||
ChannelId,
|
||||
afterSnowflake,
|
||||
beforeSnowflake,
|
||||
null,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
if (message.Author.Id == currentUser.Id)
|
||||
{
|
||||
userMessageIds.Add(message.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var totalUserMessages = userMessageIds.Count;
|
||||
await console.Output.WriteLineAsync($"Found {totalUserMessages} of your messages");
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
if (totalUserMessages == 0)
|
||||
{
|
||||
using (console.WithForegroundColor(ConsoleColor.Yellow))
|
||||
{
|
||||
await console.Output.WriteLineAsync("No messages to delete.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete messages
|
||||
await console.Output.WriteLineAsync("Deleting messages...");
|
||||
|
||||
var successCount = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
foreach (var messageId in userMessageIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await Discord.DeleteMessageAsync(
|
||||
ChannelId,
|
||||
messageId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (deleted)
|
||||
{
|
||||
successCount++;
|
||||
using (console.WithForegroundColor(ConsoleColor.Green))
|
||||
{
|
||||
await console.Output.WriteAsync("\r");
|
||||
await console.Output.WriteAsync(
|
||||
$"✓ Deleted {successCount} / {totalUserMessages} messages "
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
failedCount++;
|
||||
}
|
||||
|
||||
// Discord's rate limit headers are handled automatically by DeleteMessageAsync
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
failedCount++;
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
await console.Output.WriteAsync("\r");
|
||||
await console.Output.WriteAsync(
|
||||
$"✗ Error | Deleted: {successCount} / {totalUserMessages}, Failed: {failedCount} "
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the progress line
|
||||
await console.Output.WriteLineAsync();
|
||||
await console.Output.WriteLineAsync();
|
||||
|
||||
// Report results
|
||||
await console.Output.WriteLineAsync("Deletion complete!");
|
||||
|
||||
using (console.WithForegroundColor(ConsoleColor.Green))
|
||||
{
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Successfully deleted: {successCount} / {totalUserMessages} message(s)"
|
||||
);
|
||||
}
|
||||
|
||||
if (failedCount > 0)
|
||||
{
|
||||
using (console.WithForegroundColor(ConsoleColor.Red))
|
||||
{
|
||||
await console.Output.WriteLineAsync($"Failed to delete: {failedCount} message(s)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +220,12 @@ public class DiscordClient(
|
|||
return response?.Pipe(User.Parse);
|
||||
}
|
||||
|
||||
public async ValueTask<User> GetCurrentUserAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await GetJsonResponseAsync("users/@me", cancellationToken);
|
||||
return User.Parse(response);
|
||||
}
|
||||
|
||||
public async IAsyncEnumerable<Guild> GetUserGuildsAsync(
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default
|
||||
)
|
||||
|
|
@ -820,4 +826,96 @@ public class DiscordClient(
|
|||
yield break;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<HttpResponseMessage> DeleteResponseAsync(
|
||||
string url,
|
||||
TokenKind tokenKind,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
return await Http.ResponseResiliencePipeline.ExecuteAsync(
|
||||
async innerCancellationToken =>
|
||||
{
|
||||
using var request = new HttpRequestMessage(
|
||||
HttpMethod.Delete,
|
||||
new Uri(_baseUri, url)
|
||||
);
|
||||
|
||||
request.Headers.TryAddWithoutValidation(
|
||||
"Authorization",
|
||||
tokenKind == TokenKind.Bot ? $"Bot {token}" : token
|
||||
);
|
||||
|
||||
var response = await Http.Client.SendAsync(
|
||||
request,
|
||||
HttpCompletionOption.ResponseHeadersRead,
|
||||
innerCancellationToken
|
||||
);
|
||||
|
||||
if (rateLimitPreference.IsRespectedFor(tokenKind))
|
||||
{
|
||||
var remainingRequestCount = response
|
||||
.Headers.TryGetValue("X-RateLimit-Remaining")
|
||||
?.Pipe(s => int.ParseOrNull(s, CultureInfo.InvariantCulture));
|
||||
|
||||
var resetAfterDelay = response
|
||||
.Headers.TryGetValue("X-RateLimit-Reset-After")
|
||||
?.Pipe(s => double.ParseOrNull(s, CultureInfo.InvariantCulture))
|
||||
?.Pipe(TimeSpan.FromSeconds);
|
||||
|
||||
if (remainingRequestCount <= 0 && resetAfterDelay is not null)
|
||||
{
|
||||
var delay = (resetAfterDelay.Value + TimeSpan.FromSeconds(1)).Clamp(
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(60)
|
||||
);
|
||||
|
||||
await Task.Delay(delay, innerCancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
private async ValueTask<HttpResponseMessage> DeleteResponseAsync(
|
||||
string url,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
await DeleteResponseAsync(
|
||||
url,
|
||||
await ResolveTokenKindAsync(cancellationToken),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
public async ValueTask<bool> DeleteMessageAsync(
|
||||
Snowflake channelId,
|
||||
Snowflake messageId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
using var response = await DeleteResponseAsync(
|
||||
$"channels/{channelId}/messages/{messageId}",
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
// 204 No Content = successfully deleted
|
||||
if (response.StatusCode == HttpStatusCode.NoContent)
|
||||
return true;
|
||||
|
||||
// 403 Forbidden = not authorized to delete (e.g., someone else's message)
|
||||
// 404 Not Found = message already deleted or doesn't exist
|
||||
if (
|
||||
response.StatusCode == HttpStatusCode.Forbidden
|
||||
|| response.StatusCode == HttpStatusCode.NotFound
|
||||
)
|
||||
return false;
|
||||
|
||||
// For other errors, throw an exception
|
||||
throw new DiscordChatExporterException(
|
||||
$"Failed to delete message #{messageId} in channel #{channelId}: {response.StatusCode}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public partial class App : Application, IDisposable
|
|||
services.AddTransient<MainViewModel>();
|
||||
services.AddTransient<DashboardViewModel>();
|
||||
services.AddTransient<ExportSetupViewModel>();
|
||||
services.AddTransient<DeleteSetupViewModel>();
|
||||
services.AddTransient<MessageBoxViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ public partial class ViewManager
|
|||
MainViewModel => new MainView(),
|
||||
DashboardViewModel => new DashboardView(),
|
||||
ExportSetupViewModel => new ExportSetupView(),
|
||||
DeleteSetupViewModel => new DeleteSetupView(),
|
||||
MessageBoxViewModel => new MessageBoxView(),
|
||||
SettingsViewModel => new SettingsView(),
|
||||
_ => null,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,19 @@ public class ViewModelManager(IServiceProvider services, LocalizationManager loc
|
|||
return viewModel;
|
||||
}
|
||||
|
||||
public DeleteSetupViewModel GetDeleteSetupViewModel(
|
||||
Guild guild,
|
||||
IReadOnlyList<Channel> channels
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<DeleteSetupViewModel>();
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public MessageBoxViewModel GetMessageBoxViewModel(
|
||||
string title,
|
||||
string message,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public partial class LocalizationManager
|
|||
[nameof(PullGuildsTooltip)] = "Pull available servers and channels (Enter)",
|
||||
[nameof(SettingsTooltip)] = "Settings",
|
||||
[nameof(LastMessageSentTooltip)] = "Last message sent:",
|
||||
[nameof(ExportTooltip)] = "Export selected channels",
|
||||
[nameof(DeleteMessagesTooltip)] = "Delete messages from selected channels",
|
||||
[nameof(TokenPlaceholderText)] = "Token",
|
||||
// Token instructions (personal account)
|
||||
[nameof(TokenPersonalHeader)] = "To get the token for your personal account:",
|
||||
|
|
@ -123,6 +125,16 @@ public partial class LocalizationManager
|
|||
"Download assets to this directory. If not specified, the asset directory path will be derived from the output path.",
|
||||
[nameof(AdvancedOptionsTooltip)] = "Toggle advanced options",
|
||||
[nameof(ExportButton)] = "EXPORT",
|
||||
// Delete Setup
|
||||
[nameof(DeleteWarningTitle)] =
|
||||
"⚠ WARNING: This will permanently delete messages from the channel.",
|
||||
[nameof(DeleteWarningSubtext)] =
|
||||
"You can only delete your own messages. Messages from other users will be skipped.",
|
||||
[nameof(DeleteAfterDateTooltip)] = "Only delete messages sent after this date",
|
||||
[nameof(DeleteBeforeDateTooltip)] = "Only delete messages sent before this date",
|
||||
[nameof(DeleteAfterTimeTooltip)] = "Only delete messages sent after this time",
|
||||
[nameof(DeleteBeforeTimeTooltip)] = "Only delete messages sent before this time",
|
||||
[nameof(DeleteButton)] = "DELETE",
|
||||
// Common buttons
|
||||
[nameof(CloseButton)] = "CLOSE",
|
||||
[nameof(CancelButton)] = "CANCEL",
|
||||
|
|
@ -152,5 +164,7 @@ public partial class LocalizationManager
|
|||
[nameof(ErrorPullingChannelsTitle)] = "Error pulling channels",
|
||||
[nameof(ErrorExportingTitle)] = "Error exporting channel(s)",
|
||||
[nameof(SuccessfulExportMessage)] = "Successfully exported {0} channel(s)",
|
||||
[nameof(ErrorDeletingTitle)] = "Error deleting messages",
|
||||
[nameof(SuccessfulDeletionMessage)] = "Deleted {0} message(s), {1} failed",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ public partial class LocalizationManager
|
|||
[nameof(PullGuildsTooltip)] = "Charger les serveurs et canaux disponibles (Entrée)",
|
||||
[nameof(SettingsTooltip)] = "Paramètres",
|
||||
[nameof(LastMessageSentTooltip)] = "Dernier message envoyé :",
|
||||
[nameof(ExportTooltip)] = "Exporter les canaux sélectionnés",
|
||||
[nameof(DeleteMessagesTooltip)] = "Supprimer les messages des canaux sélectionnés",
|
||||
[nameof(TokenPlaceholderText)] = "Token",
|
||||
// Token instructions (personal account)
|
||||
[nameof(TokenPersonalHeader)] = "Obtenir le token pour votre compte personnel :",
|
||||
|
|
@ -125,6 +127,20 @@ public partial class LocalizationManager
|
|||
"Télécharger les ressources dans ce dossier. Si non spécifié, le chemin sera dérivé du chemin de sortie.",
|
||||
[nameof(AdvancedOptionsTooltip)] = "Basculer les options avancées",
|
||||
[nameof(ExportButton)] = "EXPORTER",
|
||||
// Delete Setup
|
||||
[nameof(DeleteWarningTitle)] =
|
||||
"⚠ AVERTISSEMENT : Cette action supprimera définitivement les messages du canal.",
|
||||
[nameof(DeleteWarningSubtext)] =
|
||||
"Vous ne pouvez supprimer que vos propres messages. Les messages des autres utilisateurs seront ignorés.",
|
||||
[nameof(DeleteAfterDateTooltip)] =
|
||||
"Supprimer uniquement les messages envoyés après cette date",
|
||||
[nameof(DeleteBeforeDateTooltip)] =
|
||||
"Supprimer uniquement les messages envoyés avant cette date",
|
||||
[nameof(DeleteAfterTimeTooltip)] =
|
||||
"Supprimer uniquement les messages envoyés après cette heure",
|
||||
[nameof(DeleteBeforeTimeTooltip)] =
|
||||
"Supprimer uniquement les messages envoyés avant cette heure",
|
||||
[nameof(DeleteButton)] = "SUPPRIMER",
|
||||
// Common buttons
|
||||
[nameof(CloseButton)] = "FERMER",
|
||||
[nameof(CancelButton)] = "ANNULER",
|
||||
|
|
@ -154,5 +170,7 @@ public partial class LocalizationManager
|
|||
[nameof(ErrorPullingChannelsTitle)] = "Erreur lors du chargement des canaux",
|
||||
[nameof(ErrorExportingTitle)] = "Erreur lors de l'exportation des canaux",
|
||||
[nameof(SuccessfulExportMessage)] = "{0} canal(-aux) exporté(s) avec succès",
|
||||
[nameof(ErrorDeletingTitle)] = "Erreur lors de la suppression des messages",
|
||||
[nameof(SuccessfulDeletionMessage)] = "{0} message(s) supprimé(s), {1} échec(s)",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ public partial class LocalizationManager
|
|||
[nameof(PullGuildsTooltip)] = "Verfügbare Server und Kanäle laden (Enter)",
|
||||
[nameof(SettingsTooltip)] = "Einstellungen",
|
||||
[nameof(LastMessageSentTooltip)] = "Letzte Nachricht gesendet:",
|
||||
[nameof(ExportTooltip)] = "Ausgewählte Kanäle exportieren",
|
||||
[nameof(DeleteMessagesTooltip)] = "Nachrichten aus ausgewählten Kanälen löschen",
|
||||
[nameof(TokenPlaceholderText)] = "Token",
|
||||
// Token instructions (personal account)
|
||||
[nameof(TokenPersonalHeader)] = "Token für Ihr persönliches Konto abrufen:",
|
||||
|
|
@ -129,6 +131,20 @@ public partial class LocalizationManager
|
|||
"Assets in dieses Verzeichnis herunterladen. Wenn nicht angegeben, wird der Asset-Verzeichnispfad vom Ausgabepfad abgeleitet.",
|
||||
[nameof(AdvancedOptionsTooltip)] = "Erweiterte Optionen umschalten",
|
||||
[nameof(ExportButton)] = "EXPORTIEREN",
|
||||
// Delete Setup
|
||||
[nameof(DeleteWarningTitle)] =
|
||||
"⚠ WARNUNG: Dadurch werden Nachrichten dauerhaft aus dem Kanal gelöscht.",
|
||||
[nameof(DeleteWarningSubtext)] =
|
||||
"Sie können nur Ihre eigenen Nachrichten löschen. Nachrichten anderer Benutzer werden übersprungen.",
|
||||
[nameof(DeleteAfterDateTooltip)] =
|
||||
"Nur Nachrichten löschen, die nach diesem Datum gesendet wurden",
|
||||
[nameof(DeleteBeforeDateTooltip)] =
|
||||
"Nur Nachrichten löschen, die vor diesem Datum gesendet wurden",
|
||||
[nameof(DeleteAfterTimeTooltip)] =
|
||||
"Nur Nachrichten löschen, die nach dieser Uhrzeit gesendet wurden",
|
||||
[nameof(DeleteBeforeTimeTooltip)] =
|
||||
"Nur Nachrichten löschen, die vor dieser Uhrzeit gesendet wurden",
|
||||
[nameof(DeleteButton)] = "LÖSCHEN",
|
||||
// Common buttons
|
||||
[nameof(CloseButton)] = "SCHLIESSEN",
|
||||
[nameof(CancelButton)] = "ABBRECHEN",
|
||||
|
|
@ -158,5 +174,7 @@ public partial class LocalizationManager
|
|||
[nameof(ErrorPullingChannelsTitle)] = "Fehler beim Laden der Kanäle",
|
||||
[nameof(ErrorExportingTitle)] = "Fehler beim Exportieren der Kanäle",
|
||||
[nameof(SuccessfulExportMessage)] = "{0} Kanal/-äle erfolgreich exportiert",
|
||||
[nameof(ErrorDeletingTitle)] = "Fehler beim Löschen der Nachrichten",
|
||||
[nameof(SuccessfulDeletionMessage)] = "{0} Nachricht(en) gelöscht, {1} fehlgeschlagen",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public partial class LocalizationManager
|
|||
[nameof(PullGuildsTooltip)] = "Cargar servidores y canales disponibles (Enter)",
|
||||
[nameof(SettingsTooltip)] = "Ajustes",
|
||||
[nameof(LastMessageSentTooltip)] = "Último mensaje enviado:",
|
||||
[nameof(ExportTooltip)] = "Exportar canales seleccionados",
|
||||
[nameof(DeleteMessagesTooltip)] = "Eliminar mensajes de los canales seleccionados",
|
||||
[nameof(TokenPlaceholderText)] = "Token",
|
||||
// Token instructions (personal account)
|
||||
[nameof(TokenPersonalHeader)] = "Cómo obtener el token para tu cuenta personal:",
|
||||
|
|
@ -123,6 +125,20 @@ public partial class LocalizationManager
|
|||
"Descargar recursos en este directorio. Si no se especifica, la ruta se derivará de la ruta de salida.",
|
||||
[nameof(AdvancedOptionsTooltip)] = "Alternar opciones avanzadas",
|
||||
[nameof(ExportButton)] = "EXPORTAR",
|
||||
// Delete Setup
|
||||
[nameof(DeleteWarningTitle)] =
|
||||
"⚠ ADVERTENCIA: Esto eliminará permanentemente los mensajes del canal.",
|
||||
[nameof(DeleteWarningSubtext)] =
|
||||
"Solo puedes eliminar tus propios mensajes. Se omitirán los mensajes de otros usuarios.",
|
||||
[nameof(DeleteAfterDateTooltip)] =
|
||||
"Eliminar solo los mensajes enviados después de esta fecha",
|
||||
[nameof(DeleteBeforeDateTooltip)] =
|
||||
"Eliminar solo los mensajes enviados antes de esta fecha",
|
||||
[nameof(DeleteAfterTimeTooltip)] =
|
||||
"Eliminar solo los mensajes enviados después de esta hora",
|
||||
[nameof(DeleteBeforeTimeTooltip)] =
|
||||
"Eliminar solo los mensajes enviados antes de esta hora",
|
||||
[nameof(DeleteButton)] = "ELIMINAR",
|
||||
// Common buttons
|
||||
[nameof(CloseButton)] = "CERRAR",
|
||||
[nameof(CancelButton)] = "CANCELAR",
|
||||
|
|
@ -152,5 +168,7 @@ public partial class LocalizationManager
|
|||
[nameof(ErrorPullingChannelsTitle)] = "Error al cargar canales",
|
||||
[nameof(ErrorExportingTitle)] = "Error al exportar canal(es)",
|
||||
[nameof(SuccessfulExportMessage)] = "{0} canal(es) exportado(s) con éxito",
|
||||
[nameof(ErrorDeletingTitle)] = "Error al eliminar mensajes",
|
||||
[nameof(SuccessfulDeletionMessage)] = "{0} mensaje(s) eliminado(s), {1} fallido(s)",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ public partial class LocalizationManager
|
|||
[nameof(PullGuildsTooltip)] = "Завантажити доступні сервери та канали (Enter)",
|
||||
[nameof(SettingsTooltip)] = "Налаштування",
|
||||
[nameof(LastMessageSentTooltip)] = "Останнє повідомлення:",
|
||||
[nameof(ExportTooltip)] = "Експортувати вибрані канали",
|
||||
[nameof(DeleteMessagesTooltip)] = "Видалити повідомлення з вибраних каналів",
|
||||
[nameof(TokenPlaceholderText)] = "Токен",
|
||||
// Token instructions (personal account)
|
||||
[nameof(TokenPersonalHeader)] = "Як отримати токен для персонального акаунту:",
|
||||
|
|
@ -123,6 +125,19 @@ public partial class LocalizationManager
|
|||
"Завантажувати ресурси до цієї директорії. Якщо не вказано, шлях до директорії ресурсів буде визначено з шляху збереження.",
|
||||
[nameof(AdvancedOptionsTooltip)] = "Перемкнути розширені параметри",
|
||||
[nameof(ExportButton)] = "ЕКСПОРТУВАТИ",
|
||||
// Delete Setup
|
||||
[nameof(DeleteWarningTitle)] = "⚠ УВАГА: Це назавжди видалить повідомлення з каналу.",
|
||||
[nameof(DeleteWarningSubtext)] =
|
||||
"Ви можете видаляти лише власні повідомлення. Повідомлення інших користувачів буде пропущено.",
|
||||
[nameof(DeleteAfterDateTooltip)] =
|
||||
"Видаляти лише повідомлення, надіслані після цієї дати",
|
||||
[nameof(DeleteBeforeDateTooltip)] =
|
||||
"Видаляти лише повідомлення, надіслані до цієї дати",
|
||||
[nameof(DeleteAfterTimeTooltip)] =
|
||||
"Видаляти лише повідомлення, надіслані після цього часу",
|
||||
[nameof(DeleteBeforeTimeTooltip)] =
|
||||
"Видаляти лише повідомлення, надіслані до цього часу",
|
||||
[nameof(DeleteButton)] = "ВИДАЛИТИ",
|
||||
// Common buttons
|
||||
[nameof(CloseButton)] = "ЗАКРИТИ",
|
||||
[nameof(CancelButton)] = "СКАСУВАТИ",
|
||||
|
|
@ -151,5 +166,7 @@ public partial class LocalizationManager
|
|||
[nameof(ErrorPullingChannelsTitle)] = "Помилка завантаження каналів",
|
||||
[nameof(ErrorExportingTitle)] = "Помилка експорту каналу(-ів)",
|
||||
[nameof(SuccessfulExportMessage)] = "Успішно експортовано {0} канал(-ів)",
|
||||
[nameof(ErrorDeletingTitle)] = "Помилка видалення повідомлень",
|
||||
[nameof(SuccessfulDeletionMessage)] = "Видалено {0} повідомлення(-ь), {1} не вдалося",
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ public partial class LocalizationManager
|
|||
public string PullGuildsTooltip => Get();
|
||||
public string SettingsTooltip => Get();
|
||||
public string LastMessageSentTooltip => Get();
|
||||
public string ExportTooltip => Get();
|
||||
public string DeleteMessagesTooltip => Get();
|
||||
public string TokenPlaceholderText => Get();
|
||||
|
||||
// Token instructions (personal account)
|
||||
|
|
@ -141,6 +143,16 @@ public partial class LocalizationManager
|
|||
public string AdvancedOptionsTooltip => Get();
|
||||
public string ExportButton => Get();
|
||||
|
||||
// ---- Delete Setup ----
|
||||
|
||||
public string DeleteWarningTitle => Get();
|
||||
public string DeleteWarningSubtext => Get();
|
||||
public string DeleteAfterDateTooltip => Get();
|
||||
public string DeleteBeforeDateTooltip => Get();
|
||||
public string DeleteAfterTimeTooltip => Get();
|
||||
public string DeleteBeforeTimeTooltip => Get();
|
||||
public string DeleteButton => Get();
|
||||
|
||||
// ---- Common buttons ----
|
||||
|
||||
public string CloseButton => Get();
|
||||
|
|
@ -162,4 +174,6 @@ public partial class LocalizationManager
|
|||
public string ErrorPullingChannelsTitle => Get();
|
||||
public string ErrorExportingTitle => Get();
|
||||
public string SuccessfulExportMessage => Get();
|
||||
public string ErrorDeletingTitle => Get();
|
||||
public string SuccessfulDeletionMessage => Get();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,11 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
),
|
||||
SelectedChannels.WatchProperty(
|
||||
o => o.Count,
|
||||
_ => ExportCommand.NotifyCanExecuteChanged()
|
||||
_ =>
|
||||
{
|
||||
ExportCommand.NotifyCanExecuteChanged();
|
||||
DeleteMessagesCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -66,6 +70,7 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
[NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(DeleteMessagesCommand))]
|
||||
public partial bool IsBusy { get; set; }
|
||||
|
||||
public LocalizationManager LocalizationManager { get; }
|
||||
|
|
@ -324,6 +329,129 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
}
|
||||
}
|
||||
|
||||
private bool CanDeleteMessages() =>
|
||||
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanDeleteMessages))]
|
||||
private async Task DeleteMessagesAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var currentUser = await _discord.GetCurrentUserAsync();
|
||||
|
||||
var dialog = _viewModelManager.GetDeleteSetupViewModel(
|
||||
SelectedGuild,
|
||||
SelectedChannels.Select(c => c.Channel).ToArray()
|
||||
);
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
var channelProgressPairs = dialog
|
||||
.Channels!.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() })
|
||||
.ToArray();
|
||||
|
||||
var totalDeletedCount = 0;
|
||||
var totalFailedCount = 0;
|
||||
|
||||
await Parallel.ForEachAsync(
|
||||
channelProgressPairs,
|
||||
new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit),
|
||||
},
|
||||
async (pair, cancellationToken) =>
|
||||
{
|
||||
var channel = pair.Channel;
|
||||
var progress = pair.Progress;
|
||||
|
||||
try
|
||||
{
|
||||
var userMessageIds = new List<Snowflake>();
|
||||
await foreach (
|
||||
var message in _discord.GetMessagesAsync(
|
||||
channel.Id,
|
||||
dialog.After?.Pipe(Snowflake.FromDate),
|
||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||
progress,
|
||||
cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
if (message.Author.Id == currentUser.Id)
|
||||
userMessageIds.Add(message.Id);
|
||||
}
|
||||
|
||||
if (userMessageIds.Count == 0)
|
||||
return;
|
||||
|
||||
var successCount = 0;
|
||||
var failedCount = 0;
|
||||
|
||||
foreach (var messageId in userMessageIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var deleted = await _discord.DeleteMessageAsync(
|
||||
channel.Id,
|
||||
messageId,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (deleted)
|
||||
successCount++;
|
||||
else
|
||||
failedCount++;
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
failedCount++;
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Add(ref totalDeletedCount, successCount);
|
||||
Interlocked.Add(ref totalFailedCount, failedCount);
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
progress.ReportCompletion();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
_snackbarManager.Notify(
|
||||
string.Format(
|
||||
LocalizationManager.SuccessfulDeletionMessage,
|
||||
totalDeletedCount,
|
||||
totalFailedCount
|
||||
)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelManager.GetMessageBoxViewModel(
|
||||
LocalizationManager.ErrorDeletingTitle,
|
||||
ex.ToString()
|
||||
);
|
||||
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Localization;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public partial class DeleteSetupViewModel(LocalizationManager localizationManager)
|
||||
: DialogViewModelBase
|
||||
{
|
||||
public LocalizationManager LocalizationManager { get; } = localizationManager;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Guild? Guild { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsSingleChannel))]
|
||||
public partial IReadOnlyList<Channel>? Channels { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsAfterDateSet))]
|
||||
[NotifyPropertyChangedFor(nameof(After))]
|
||||
public partial DateTimeOffset? AfterDate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan? AfterTime { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsBeforeDateSet))]
|
||||
[NotifyPropertyChangedFor(nameof(Before))]
|
||||
public partial DateTimeOffset? BeforeDate { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial TimeSpan? BeforeTime { get; set; }
|
||||
|
||||
public bool IsSingleChannel => Channels?.Count == 1;
|
||||
|
||||
public bool IsAfterDateSet => AfterDate is not null;
|
||||
|
||||
public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);
|
||||
|
||||
public bool IsBeforeDateSet => BeforeDate is not null;
|
||||
|
||||
public DateTimeOffset? Before => BeforeDate?.Add(BeforeTime ?? TimeSpan.Zero);
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
Close(true);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Close(null);
|
||||
}
|
||||
}
|
||||
|
|
@ -284,24 +284,47 @@
|
|||
</ScrollViewer>
|
||||
</Panel>
|
||||
|
||||
<!-- Export button -->
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
Margin="32,24"
|
||||
Padding="0"
|
||||
<!-- Action buttons -->
|
||||
<StackPanel
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource MaterialSecondaryMidBrush}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
|
||||
IsVisible="{Binding $self.IsEffectivelyEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="32"
|
||||
Height="32"
|
||||
Kind="Download" />
|
||||
</Button>
|
||||
Margin="32,24"
|
||||
Orientation="Horizontal"
|
||||
Spacing="16">
|
||||
<!-- Delete button -->
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
Padding="0"
|
||||
Background="#FFE74C3C"
|
||||
Command="{Binding DeleteMessagesCommand}"
|
||||
Foreground="White"
|
||||
IsVisible="{Binding $self.IsEffectivelyEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DeleteMessagesTooltip}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="32"
|
||||
Height="32"
|
||||
Kind="Delete" />
|
||||
</Button>
|
||||
|
||||
<!-- Export button -->
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
Padding="0"
|
||||
Background="{DynamicResource MaterialSecondaryMidBrush}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
|
||||
IsVisible="{Binding $self.IsEffectivelyEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.ExportTooltip}">
|
||||
<materialIcons:MaterialIcon
|
||||
Width="32"
|
||||
Height="32"
|
||||
Kind="Download" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
|
|
|||
180
DiscordChatExporter.Gui/Views/Dialogs/DeleteSetupView.axaml
Normal file
180
DiscordChatExporter.Gui/Views/Dialogs/DeleteSetupView.axaml
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<UserControl
|
||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.DeleteSetupView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||
xmlns:materialAssists="clr-namespace:Material.Styles.Assists;assembly=Material.Styles"
|
||||
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
|
||||
x:Name="UserControl"
|
||||
Width="380"
|
||||
x:DataType="dialogs:DeleteSetupViewModel"
|
||||
Loaded="UserControl_OnLoaded">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- Guild/channel info -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
ColumnDefinitions="Auto,*">
|
||||
<!-- Guild icon -->
|
||||
<Ellipse
|
||||
Grid.Column="0"
|
||||
Width="32"
|
||||
Height="32">
|
||||
<Ellipse.Fill>
|
||||
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{Binding Guild.IconUrl}" />
|
||||
</Ellipse.Fill>
|
||||
</Ellipse>
|
||||
|
||||
<!-- Channel count (for multiple channels) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
IsVisible="{Binding !IsSingleChannel}"
|
||||
TextTrimming="CharacterEllipsis">
|
||||
<Run Text="{Binding Channels.Count, FallbackValue=0, Mode=OneWay}" />
|
||||
<Run Text="{Binding LocalizationManager.ChannelsSelectedText}" />
|
||||
</TextBlock>
|
||||
|
||||
<!-- Category and channel name (for single channel) -->
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="19"
|
||||
FontWeight="Light"
|
||||
IsVisible="{Binding IsSingleChannel}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip.Tip="{Binding Channels[0], Converter={x:Static converters:ChannelToHierarchicalNameStringConverter.Instance}}">
|
||||
<TextBlock IsVisible="{Binding !!Channels[0].Parent}">
|
||||
<Run Text="{Binding Channels[0].Parent.Name, Mode=OneWay}" />
|
||||
<Run Text="/" />
|
||||
</TextBlock>
|
||||
<Run FontWeight="SemiBold" Text="{Binding Channels[0].Name, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Padding="0,8"
|
||||
BorderBrush="{DynamicResource MaterialDividerBrush}"
|
||||
BorderThickness="0,1">
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<!-- Warning message -->
|
||||
<Border
|
||||
Margin="16,8"
|
||||
Padding="12"
|
||||
Background="#33FFA500"
|
||||
BorderBrush="#FFFFA500"
|
||||
BorderThickness="2"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#FFFFA500"
|
||||
TextWrapping="Wrap">
|
||||
<Run Text="{Binding LocalizationManager.DeleteWarningTitle}" />
|
||||
<LineBreak />
|
||||
<Run FontWeight="Normal" Text="{Binding LocalizationManager.DeleteWarningSubtext}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
|
||||
<!-- Date limits -->
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="*,*">
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="16,8,8,8"
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.AfterDateLabel}"
|
||||
SelectedDate="{Binding AfterDate}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DeleteAfterDateTooltip}">
|
||||
<DatePicker.Styles>
|
||||
<Style Selector="DatePicker">
|
||||
<Style Selector="^ /template/ TextBox#DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</DatePicker.Styles>
|
||||
</DatePicker>
|
||||
<DatePicker
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="8,8,16,8"
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.BeforeDateLabel}"
|
||||
SelectedDate="{Binding BeforeDate}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DeleteBeforeDateTooltip}">
|
||||
<DatePicker.Styles>
|
||||
<Style Selector="DatePicker">
|
||||
<Style Selector="^ /template/ TextBox#DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</DatePicker.Styles>
|
||||
</DatePicker>
|
||||
|
||||
<!-- Time limits -->
|
||||
<TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="16,8,8,8"
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.AfterTimeLabel}"
|
||||
ClockIdentifier="{x:Static utils:Internationalization.AvaloniaClockIdentifier}"
|
||||
IsEnabled="{Binding IsAfterDateSet}"
|
||||
SelectedTime="{Binding AfterTime}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DeleteAfterTimeTooltip}">
|
||||
<TimePicker.Styles>
|
||||
<Style Selector="TimePicker">
|
||||
<Style Selector="^ /template/ TextBox#PART_DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TimePicker.Styles>
|
||||
</TimePicker>
|
||||
<TimePicker
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Margin="8,8,16,8"
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.BeforeTimeLabel}"
|
||||
ClockIdentifier="{x:Static utils:Internationalization.AvaloniaClockIdentifier}"
|
||||
IsEnabled="{Binding IsBeforeDateSet}"
|
||||
SelectedTime="{Binding BeforeTime}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DeleteBeforeTimeTooltip}">
|
||||
<TimePicker.Styles>
|
||||
<Style Selector="TimePicker">
|
||||
<Style Selector="^ /template/ TextBox#PART_DisplayTextBox">
|
||||
<Setter Property="Theme" Value="{DynamicResource FilledTextBox}" />
|
||||
</Style>
|
||||
</Style>
|
||||
</TimePicker.Styles>
|
||||
</TimePicker>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid
|
||||
Grid.Row="2"
|
||||
Margin="16"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Command="{Binding ConfirmCommand}"
|
||||
Content="{Binding LocalizationManager.DeleteButton}"
|
||||
IsDefault="True"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Margin="16,0,0,0"
|
||||
Command="{Binding CancelCommand}"
|
||||
Content="{Binding LocalizationManager.CancelButton}"
|
||||
IsCancel="True"
|
||||
Theme="{DynamicResource MaterialOutlineButton}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Views.Dialogs;
|
||||
|
||||
public partial class DeleteSetupView : UserControl
|
||||
{
|
||||
public DeleteSetupView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
private void UserControl_OnLoaded(object? sender, RoutedEventArgs args)
|
||||
{
|
||||
// Nothing to do on load for now
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue