Migrate to Avalonia (#1220)

This commit is contained in:
Oleksii Holub
2024-04-27 04:17:46 +03:00
committed by GitHub
parent 74f99b4e59
commit b9c1c47474
89 changed files with 2467 additions and 2810 deletions

View File

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