You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
337 lines
10 KiB
337 lines
10 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using DiscordChatExporter.Core.Discord;
|
|
using DiscordChatExporter.Core.Discord.Data;
|
|
using DiscordChatExporter.Core.Exceptions;
|
|
using DiscordChatExporter.Core.Exporting;
|
|
using DiscordChatExporter.Core.Utils.Extensions;
|
|
using DiscordChatExporter.Gui.Framework;
|
|
using DiscordChatExporter.Gui.Models;
|
|
using DiscordChatExporter.Gui.Services;
|
|
using DiscordChatExporter.Gui.Utils;
|
|
using DiscordChatExporter.Gui.Utils.Extensions;
|
|
using Gress;
|
|
using Gress.Completable;
|
|
|
|
namespace DiscordChatExporter.Gui.ViewModels.Components;
|
|
|
|
public partial class DashboardViewModel : ViewModelBase
|
|
{
|
|
private readonly ViewModelManager _viewModelManager;
|
|
private readonly SnackbarManager _snackbarManager;
|
|
private readonly DialogManager _dialogManager;
|
|
private readonly SettingsService _settingsService;
|
|
|
|
private readonly DisposableCollector _eventRoot = new();
|
|
private readonly AutoResetProgressMuxer _progressMuxer;
|
|
|
|
private DiscordClient? _discord;
|
|
|
|
[ObservableProperty]
|
|
[NotifyPropertyChangedFor(nameof(IsProgressIndeterminate))]
|
|
[NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
|
private bool _isBusy;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(PullGuildsCommand))]
|
|
private string? _token;
|
|
|
|
[ObservableProperty]
|
|
private IReadOnlyList<Guild>? _availableGuilds;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(PullChannelsCommand))]
|
|
[NotifyCanExecuteChangedFor(nameof(ExportCommand))]
|
|
private Guild? _selectedGuild;
|
|
|
|
[ObservableProperty]
|
|
private IReadOnlyList<ChannelNode>? _availableChannels;
|
|
|
|
public DashboardViewModel(
|
|
ViewModelManager viewModelManager,
|
|
DialogManager dialogManager,
|
|
SnackbarManager snackbarManager,
|
|
SettingsService settingsService
|
|
)
|
|
{
|
|
_viewModelManager = viewModelManager;
|
|
_dialogManager = dialogManager;
|
|
_snackbarManager = snackbarManager;
|
|
_settingsService = settingsService;
|
|
|
|
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
|
|
|
_eventRoot.Add(
|
|
Progress.WatchProperty(
|
|
o => o.Current,
|
|
() => OnPropertyChanged(nameof(IsProgressIndeterminate))
|
|
)
|
|
);
|
|
|
|
_eventRoot.Add(
|
|
SelectedChannels.WatchProperty(
|
|
o => o.Count,
|
|
() => ExportCommand.NotifyCanExecuteChanged()
|
|
)
|
|
);
|
|
}
|
|
|
|
public ProgressContainer<Percentage> Progress { get; } = new();
|
|
|
|
public bool IsProgressIndeterminate => IsBusy && Progress.Current.Fraction is <= 0 or >= 1;
|
|
|
|
public ObservableCollection<ChannelNode> SelectedChannels { get; } = [];
|
|
|
|
[RelayCommand]
|
|
private void Initialize()
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))
|
|
Token = _settingsService.LastToken;
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task ShowSettingsAsync() =>
|
|
await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());
|
|
|
|
[RelayCommand]
|
|
private void ShowHelp() => ProcessEx.StartShellExecute(Program.DocumentationUrl);
|
|
|
|
private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanPullGuilds))]
|
|
private async Task PullGuildsAsync()
|
|
{
|
|
IsBusy = true;
|
|
var progress = _progressMuxer.CreateInput();
|
|
|
|
try
|
|
{
|
|
var token = Token?.Trim('"', ' ');
|
|
if (string.IsNullOrWhiteSpace(token))
|
|
return;
|
|
|
|
AvailableGuilds = null;
|
|
SelectedGuild = null;
|
|
AvailableChannels = null;
|
|
SelectedChannels.Clear();
|
|
|
|
_discord = new DiscordClient(token);
|
|
_settingsService.LastToken = token;
|
|
|
|
var guilds = await _discord.GetUserGuildsAsync();
|
|
|
|
AvailableGuilds = guilds;
|
|
SelectedGuild = guilds.FirstOrDefault();
|
|
|
|
await PullChannelsAsync();
|
|
}
|
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
|
{
|
|
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
|
"Error pulling guilds",
|
|
ex.ToString()
|
|
);
|
|
|
|
await _dialogManager.ShowDialogAsync(dialog);
|
|
}
|
|
finally
|
|
{
|
|
progress.ReportCompletion();
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
private bool CanPullChannels() => !IsBusy && _discord is not null && SelectedGuild is not null;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanPullChannels))]
|
|
private async Task PullChannelsAsync()
|
|
{
|
|
IsBusy = true;
|
|
var progress = _progressMuxer.CreateInput();
|
|
|
|
try
|
|
{
|
|
if (_discord is null || SelectedGuild is null)
|
|
return;
|
|
|
|
AvailableChannels = null;
|
|
SelectedChannels.Clear();
|
|
|
|
var channels = new List<Channel>();
|
|
|
|
// Regular channels
|
|
await foreach (var channel in _discord.GetGuildChannelsAsync(SelectedGuild.Id))
|
|
channels.Add(channel);
|
|
|
|
// Threads
|
|
if (_settingsService.ThreadInclusionMode != ThreadInclusionMode.None)
|
|
{
|
|
await foreach (
|
|
var thread in _discord.GetGuildThreadsAsync(
|
|
SelectedGuild.Id,
|
|
_settingsService.ThreadInclusionMode == ThreadInclusionMode.All
|
|
)
|
|
)
|
|
{
|
|
channels.Add(thread);
|
|
}
|
|
}
|
|
|
|
// Build a hierarchy of channels
|
|
var channelTree = ChannelNode.BuildTree(
|
|
channels
|
|
.OrderByDescending(c => c.IsDirect ? c.LastMessageId : null)
|
|
.ThenBy(c => c.Position)
|
|
.ToArray()
|
|
);
|
|
|
|
AvailableChannels = channelTree;
|
|
SelectedChannels.Clear();
|
|
}
|
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
|
{
|
|
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
|
"Error pulling channels",
|
|
ex.ToString()
|
|
);
|
|
|
|
await _dialogManager.ShowDialogAsync(dialog);
|
|
}
|
|
finally
|
|
{
|
|
progress.ReportCompletion();
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
private bool CanExport() =>
|
|
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();
|
|
|
|
[RelayCommand(CanExecute = nameof(CanExport))]
|
|
private async Task ExportAsync()
|
|
{
|
|
IsBusy = true;
|
|
|
|
try
|
|
{
|
|
if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())
|
|
return;
|
|
|
|
var dialog = _viewModelManager.CreateExportSetupViewModel(
|
|
SelectedGuild,
|
|
SelectedChannels.Select(c => c.Channel).ToArray()
|
|
);
|
|
|
|
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
|
return;
|
|
|
|
var exporter = new ChannelExporter(_discord);
|
|
|
|
var channelProgressPairs = dialog
|
|
.Channels!.Select(c => new { Channel = c, Progress = _progressMuxer.CreateInput() })
|
|
.ToArray();
|
|
|
|
var successfulExportCount = 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 request = new ExportRequest(
|
|
dialog.Guild!,
|
|
channel,
|
|
dialog.OutputPath!,
|
|
dialog.AssetsDirPath,
|
|
dialog.SelectedFormat,
|
|
dialog.After?.Pipe(Snowflake.FromDate),
|
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
|
dialog.PartitionLimit,
|
|
dialog.MessageFilter,
|
|
dialog.ShouldFormatMarkdown,
|
|
dialog.ShouldDownloadAssets,
|
|
dialog.ShouldReuseAssets,
|
|
_settingsService.Locale,
|
|
_settingsService.IsUtcNormalizationEnabled
|
|
);
|
|
|
|
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
|
|
|
Interlocked.Increment(ref successfulExportCount);
|
|
}
|
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
|
{
|
|
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
|
}
|
|
finally
|
|
{
|
|
progress.ReportCompletion();
|
|
}
|
|
}
|
|
);
|
|
|
|
// Notify of the overall completion
|
|
if (successfulExportCount > 0)
|
|
{
|
|
_snackbarManager.Notify(
|
|
$"Successfully exported {successfulExportCount} channel(s)"
|
|
);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
|
"Error exporting channel(s)",
|
|
ex.ToString()
|
|
);
|
|
|
|
await _dialogManager.ShowDialogAsync(dialog);
|
|
}
|
|
finally
|
|
{
|
|
IsBusy = false;
|
|
}
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app");
|
|
|
|
[RelayCommand]
|
|
private void OpenDiscordDeveloperPortal() =>
|
|
ProcessEx.StartShellExecute("https://discord.com/developers/applications");
|
|
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_eventRoot.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
}
|