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.
DiscordChatExporter/DiscordChatExporter.Gui/ViewModels/Components/DashboardViewModel.cs

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);
}
}