mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-03-20 22:01:29 +00:00
Migrate to Avalonia (#1220)
This commit is contained in:
@@ -1,104 +1,113 @@
|
||||
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.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
using Gress;
|
||||
using Gress.Completable;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Components;
|
||||
|
||||
public class DashboardViewModel : PropertyChangedBase
|
||||
public partial class DashboardViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
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;
|
||||
|
||||
public bool IsBusy { get; private set; }
|
||||
[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 string? Token { get; set; }
|
||||
public ObservableCollection<ChannelNode> SelectedChannels { get; } = [];
|
||||
|
||||
public IReadOnlyList<Guild>? AvailableGuilds { get; private set; }
|
||||
|
||||
public Guild? SelectedGuild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? AvailableChannels { get; private set; }
|
||||
|
||||
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||
|
||||
public DashboardViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService
|
||||
)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_eventAggregator = eventAggregator;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
_progressMuxer = Progress.CreateMuxer().WithAutoReset();
|
||||
|
||||
this.Bind(o => o.IsBusy, (_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate));
|
||||
|
||||
Progress.Bind(
|
||||
o => o.Current,
|
||||
(_, _) => NotifyOfPropertyChange(() => IsProgressIndeterminate)
|
||||
);
|
||||
|
||||
this.Bind(
|
||||
o => o.SelectedGuild,
|
||||
(_, _) =>
|
||||
{
|
||||
// Reset channels when the selected guild changes, to avoid jitter
|
||||
// due to the channels being asynchronously loaded.
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
|
||||
// Pull channels for the selected guild
|
||||
// (ideally this should be called inside `PullGuilds()`,
|
||||
// but Stylet doesn't support async commands)
|
||||
PullChannels();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void OnViewLoaded()
|
||||
[RelayCommand]
|
||||
private void Initialize()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))
|
||||
Token = _settingsService.LastToken;
|
||||
}
|
||||
|
||||
public async void ShowSettings() =>
|
||||
await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateSettingsViewModel());
|
||||
[RelayCommand]
|
||||
private async Task ShowSettingsAsync() =>
|
||||
await _dialogManager.ShowDialogAsync(_viewModelManager.CreateSettingsViewModel());
|
||||
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.DocumentationUrl);
|
||||
[RelayCommand]
|
||||
private void ShowHelp() => ProcessEx.StartShellExecute(Program.DocumentationUrl);
|
||||
|
||||
public bool CanPullGuilds => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
private bool CanPullGuilds() => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||
|
||||
public async void PullGuilds()
|
||||
[RelayCommand(CanExecute = nameof(CanPullGuilds))]
|
||||
private async Task PullGuildsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
var progress = _progressMuxer.CreateInput();
|
||||
@@ -112,7 +121,7 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
AvailableGuilds = null;
|
||||
SelectedGuild = null;
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
SelectedChannels.Clear();
|
||||
|
||||
_discord = new DiscordClient(token);
|
||||
_settingsService.LastToken = token;
|
||||
@@ -121,14 +130,16 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
|
||||
AvailableGuilds = guilds;
|
||||
SelectedGuild = guilds.FirstOrDefault();
|
||||
|
||||
await PullChannelsAsync();
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error pulling guilds",
|
||||
ex.ToString()
|
||||
);
|
||||
@@ -142,9 +153,10 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanPullChannels => !IsBusy && _discord is not null && SelectedGuild is not null;
|
||||
private bool CanPullChannels() => !IsBusy && _discord is not null && SelectedGuild is not null;
|
||||
|
||||
public async void PullChannels()
|
||||
[RelayCommand(CanExecute = nameof(CanPullChannels))]
|
||||
private async Task PullChannelsAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
var progress = _progressMuxer.CreateInput();
|
||||
@@ -155,18 +167,13 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
return;
|
||||
|
||||
AvailableChannels = null;
|
||||
SelectedChannels = null;
|
||||
SelectedChannels.Clear();
|
||||
|
||||
var channels = new List<Channel>();
|
||||
|
||||
// Regular channels
|
||||
await foreach (var channel in _discord.GetGuildChannelsAsync(SelectedGuild.Id))
|
||||
{
|
||||
if (channel.IsCategory)
|
||||
continue;
|
||||
|
||||
channels.Add(channel);
|
||||
}
|
||||
|
||||
// Threads
|
||||
if (_settingsService.ThreadInclusionMode != ThreadInclusionMode.None)
|
||||
@@ -182,16 +189,24 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
}
|
||||
|
||||
AvailableChannels = channels;
|
||||
SelectedChannels = null;
|
||||
// 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)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error pulling channels",
|
||||
ex.ToString()
|
||||
);
|
||||
@@ -205,30 +220,24 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanExport =>
|
||||
!IsBusy
|
||||
&& _discord is not null
|
||||
&& SelectedGuild is not null
|
||||
&& SelectedChannels?.Any() is true;
|
||||
private bool CanExport() =>
|
||||
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();
|
||||
|
||||
public async void Export()
|
||||
[RelayCommand(CanExecute = nameof(CanExport))]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
try
|
||||
{
|
||||
if (
|
||||
_discord is null
|
||||
|| SelectedGuild is null
|
||||
|| SelectedChannels is null
|
||||
|| !SelectedChannels.Any()
|
||||
)
|
||||
if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(
|
||||
var dialog = _viewModelManager.CreateExportSetupViewModel(
|
||||
SelectedGuild,
|
||||
SelectedChannels
|
||||
SelectedChannels.Select(c => c.Channel).ToArray()
|
||||
);
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
@@ -276,7 +285,7 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
_eventAggregator.Publish(new NotificationMessage(ex.Message.TrimEnd('.')));
|
||||
_snackbarManager.Notify(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -288,16 +297,14 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
// Notify of the overall completion
|
||||
if (successfulExportCount > 0)
|
||||
{
|
||||
_eventAggregator.Publish(
|
||||
new NotificationMessage(
|
||||
$"Successfully exported {successfulExportCount} channel(s)"
|
||||
)
|
||||
_snackbarManager.Notify(
|
||||
$"Successfully exported {successfulExportCount} channel(s)"
|
||||
);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
var dialog = _viewModelManager.CreateMessageBoxViewModel(
|
||||
"Error exporting channel(s)",
|
||||
ex.ToString()
|
||||
);
|
||||
@@ -310,8 +317,20 @@ public class DashboardViewModel : PropertyChangedBase
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app");
|
||||
[RelayCommand]
|
||||
private void OpenDiscord() => ProcessEx.StartShellExecute("https://discord.com/app");
|
||||
|
||||
public void OpenDiscordDeveloperPortal() =>
|
||||
[RelayCommand]
|
||||
private void OpenDiscordDeveloperPortal() =>
|
||||
ProcessEx.StartShellExecute("https://discord.com/developers/applications");
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_eventRoot.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user