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,89 +1,111 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DiscordChatExporter.Core.Discord;
using DiscordChatExporter.Core.Discord.Data;
using DiscordChatExporter.Core.Exporting;
using DiscordChatExporter.Core.Exporting.Filtering;
using DiscordChatExporter.Core.Exporting.Partitioning;
using DiscordChatExporter.Core.Utils.Extensions;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Services;
using DiscordChatExporter.Gui.ViewModels.Framework;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
public class ExportSetupViewModel : DialogScreen
public partial class ExportSetupViewModel(
DialogManager dialogManager,
SettingsService settingsService
) : DialogViewModelBase
{
private readonly DialogManager _dialogManager;
private readonly SettingsService _settingsService;
[ObservableProperty]
private Guild? _guild;
public Guild? Guild { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsSingleChannel))]
private IReadOnlyList<Channel>? _channels;
public IReadOnlyList<Channel>? Channels { get; set; }
[ObservableProperty]
private string? _outputPath;
[ObservableProperty]
private ExportFormat _selectedFormat;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsAfterDateSet))]
[NotifyPropertyChangedFor(nameof(After))]
private DateTimeOffset? _afterDate;
[ObservableProperty]
private TimeSpan? _afterTime;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsBeforeDateSet))]
[NotifyPropertyChangedFor(nameof(Before))]
private DateTimeOffset? _beforeDate;
[ObservableProperty]
private TimeSpan? _beforeTime;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(PartitionLimit))]
private string? _partitionLimitValue;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(MessageFilter))]
private string? _messageFilterValue;
[ObservableProperty]
private bool _shouldFormatMarkdown;
[ObservableProperty]
private bool _shouldDownloadAssets;
[ObservableProperty]
private bool _shouldReuseAssets;
[ObservableProperty]
private string? _assetsDirPath;
[ObservableProperty]
private bool _isAdvancedSectionDisplayed;
public bool IsSingleChannel => Channels?.Count == 1;
public string? OutputPath { get; set; }
public IReadOnlyList<ExportFormat> AvailableFormats { get; } = Enum.GetValues<ExportFormat>();
public ExportFormat SelectedFormat { get; set; }
// This date/time abomination is required because we use separate controls to set these
public DateTimeOffset? AfterDate { get; set; }
public bool IsAfterDateSet => AfterDate is not null;
public TimeSpan? AfterTime { get; set; }
public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);
public DateTimeOffset? BeforeDate { get; set; }
public bool IsBeforeDateSet => BeforeDate is not null;
public TimeSpan? BeforeTime { get; set; }
public DateTimeOffset? Before => BeforeDate?.Add(BeforeTime ?? TimeSpan.Zero);
public string? PartitionLimitValue { get; set; }
public PartitionLimit PartitionLimit =>
!string.IsNullOrWhiteSpace(PartitionLimitValue)
? PartitionLimit.Parse(PartitionLimitValue)
: PartitionLimit.Null;
public string? MessageFilterValue { get; set; }
public MessageFilter MessageFilter =>
!string.IsNullOrWhiteSpace(MessageFilterValue)
? MessageFilter.Parse(MessageFilterValue)
: MessageFilter.Null;
public bool ShouldFormatMarkdown { get; set; }
public bool ShouldDownloadAssets { get; set; }
public bool ShouldReuseAssets { get; set; }
public string? AssetsDirPath { get; set; }
public bool IsAdvancedSectionDisplayed { get; set; }
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
[RelayCommand]
private void Initialize()
{
_dialogManager = dialogManager;
_settingsService = settingsService;
// Persist preferences
SelectedFormat = _settingsService.LastExportFormat;
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
MessageFilterValue = _settingsService.LastMessageFilterValue;
ShouldFormatMarkdown = _settingsService.LastShouldFormatMarkdown;
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
ShouldReuseAssets = _settingsService.LastShouldReuseAssets;
AssetsDirPath = _settingsService.LastAssetsDirPath;
SelectedFormat = settingsService.LastExportFormat;
PartitionLimitValue = settingsService.LastPartitionLimitValue;
MessageFilterValue = settingsService.LastMessageFilterValue;
ShouldFormatMarkdown = settingsService.LastShouldFormatMarkdown;
ShouldDownloadAssets = settingsService.LastShouldDownloadAssets;
ShouldReuseAssets = settingsService.LastShouldReuseAssets;
AssetsDirPath = settingsService.LastAssetsDirPath;
// Show the "advanced options" section by default if any
// of the advanced options are set to non-default values.
@@ -97,9 +119,8 @@ public class ExportSetupViewModel : DialogScreen
|| !string.IsNullOrWhiteSpace(AssetsDirPath);
}
public void ToggleAdvancedSection() => IsAdvancedSectionDisplayed = !IsAdvancedSectionDisplayed;
public void ShowOutputPathPrompt()
[RelayCommand]
private async Task ShowOutputPathPromptAsync()
{
if (IsSingleChannel)
{
@@ -112,33 +133,43 @@ public class ExportSetupViewModel : DialogScreen
);
var extension = SelectedFormat.GetFileExtension();
var filter = $"{extension.ToUpperInvariant()} files|*.{extension}";
var path = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
var path = await dialogManager.PromptSaveFilePathAsync(
[
new FilePickerFileType($"{extension.ToUpperInvariant()} file")
{
Patterns = [$"*.{extension}"]
}
],
defaultFileName
);
if (!string.IsNullOrWhiteSpace(path))
OutputPath = path;
}
else
{
var path = _dialogManager.PromptDirectoryPath();
var path = await dialogManager.PromptDirectoryPathAsync();
if (!string.IsNullOrWhiteSpace(path))
OutputPath = path;
}
}
public void ShowAssetsDirPathPrompt()
[RelayCommand]
private async Task ShowAssetsDirPathPromptAsync()
{
var path = _dialogManager.PromptDirectoryPath();
var path = await dialogManager.PromptDirectoryPathAsync();
if (!string.IsNullOrWhiteSpace(path))
AssetsDirPath = path;
}
public void Confirm()
[RelayCommand]
private async Task ConfirmAsync()
{
// Prompt the output path if it's not set yet
// Prompt the output path if it hasn't been set yet
if (string.IsNullOrWhiteSpace(OutputPath))
{
ShowOutputPathPrompt();
await ShowOutputPathPromptAsync();
// If the output path is still not set, cancel the export
if (string.IsNullOrWhiteSpace(OutputPath))
@@ -146,31 +177,14 @@ public class ExportSetupViewModel : DialogScreen
}
// Persist preferences
_settingsService.LastExportFormat = SelectedFormat;
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
_settingsService.LastMessageFilterValue = MessageFilterValue;
_settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
_settingsService.LastShouldReuseAssets = ShouldReuseAssets;
_settingsService.LastAssetsDirPath = AssetsDirPath;
settingsService.LastExportFormat = SelectedFormat;
settingsService.LastPartitionLimitValue = PartitionLimitValue;
settingsService.LastMessageFilterValue = MessageFilterValue;
settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
settingsService.LastShouldReuseAssets = ShouldReuseAssets;
settingsService.LastAssetsDirPath = AssetsDirPath;
Close(true);
}
}
public static class ExportSetupViewModelExtensions
{
public static ExportSetupViewModel CreateExportSetupViewModel(
this IViewModelFactory factory,
Guild guild,
IReadOnlyList<Channel> channels
)
{
var viewModel = factory.CreateExportSetupViewModel();
viewModel.Guild = guild;
viewModel.Channels = channels;
return viewModel;
}
}

View File

@@ -1,49 +1,29 @@
using DiscordChatExporter.Gui.ViewModels.Framework;
using CommunityToolkit.Mvvm.ComponentModel;
using DiscordChatExporter.Gui.Framework;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
public class MessageBoxViewModel : DialogScreen
public partial class MessageBoxViewModel : DialogViewModelBase
{
public string? Title { get; set; }
[ObservableProperty]
private string? _title = "Title";
public string? Message { get; set; }
[ObservableProperty]
private string? _message = "Message";
public bool IsOkButtonVisible { get; set; } = true;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsDefaultButtonVisible))]
[NotifyPropertyChangedFor(nameof(ButtonsCount))]
private string? _defaultButtonText = "OK";
public string? OkButtonText { get; set; }
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsCancelButtonVisible))]
[NotifyPropertyChangedFor(nameof(ButtonsCount))]
private string? _cancelButtonText = "Cancel";
public bool IsCancelButtonVisible { get; set; }
public bool IsDefaultButtonVisible => !string.IsNullOrWhiteSpace(DefaultButtonText);
public string? CancelButtonText { get; set; }
public bool IsCancelButtonVisible => !string.IsNullOrWhiteSpace(CancelButtonText);
public int ButtonsCount => (IsOkButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);
}
public static class MessageBoxViewModelExtensions
{
public static MessageBoxViewModel CreateMessageBoxViewModel(
this IViewModelFactory factory,
string title,
string message,
string? okButtonText,
string? cancelButtonText
)
{
var viewModel = factory.CreateMessageBoxViewModel();
viewModel.Title = title;
viewModel.Message = message;
viewModel.IsOkButtonVisible = !string.IsNullOrWhiteSpace(okButtonText);
viewModel.OkButtonText = okButtonText;
viewModel.IsCancelButtonVisible = !string.IsNullOrWhiteSpace(cancelButtonText);
viewModel.CancelButtonText = cancelButtonText;
return viewModel;
}
public static MessageBoxViewModel CreateMessageBoxViewModel(
this IViewModelFactory factory,
string title,
string message
) => factory.CreateMessageBoxViewModel(title, message, "CLOSE", null);
public int ButtonsCount => (IsDefaultButtonVisible ? 1 : 0) + (IsCancelButtonVisible ? 1 : 0);
}

View File

@@ -2,30 +2,43 @@
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Core.Utils.Extensions;
using DiscordChatExporter.Gui.Framework;
using DiscordChatExporter.Gui.Models;
using DiscordChatExporter.Gui.Services;
using DiscordChatExporter.Gui.ViewModels.Framework;
using DiscordChatExporter.Gui.Utils;
using DiscordChatExporter.Gui.Utils.Extensions;
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
public class SettingsViewModel(SettingsService settingsService) : DialogScreen
public class SettingsViewModel : DialogViewModelBase
{
private readonly SettingsService _settingsService;
private readonly DisposableCollector _eventRoot = new();
public SettingsViewModel(SettingsService settingsService)
{
_settingsService = settingsService;
_eventRoot.Add(_settingsService.WatchAllProperties(OnAllPropertiesChanged));
}
public bool IsAutoUpdateEnabled
{
get => settingsService.IsAutoUpdateEnabled;
set => settingsService.IsAutoUpdateEnabled = value;
get => _settingsService.IsAutoUpdateEnabled;
set => _settingsService.IsAutoUpdateEnabled = value;
}
public bool IsDarkModeEnabled
{
get => settingsService.IsDarkModeEnabled;
set => settingsService.IsDarkModeEnabled = value;
get => _settingsService.IsDarkModeEnabled;
set => _settingsService.IsDarkModeEnabled = value;
}
public bool IsTokenPersisted
{
get => settingsService.IsTokenPersisted;
set => settingsService.IsTokenPersisted = value;
get => _settingsService.IsTokenPersisted;
set => _settingsService.IsTokenPersisted = value;
}
public IReadOnlyList<ThreadInclusionMode> AvailableThreadInclusions { get; } =
@@ -33,13 +46,13 @@ public class SettingsViewModel(SettingsService settingsService) : DialogScreen
public ThreadInclusionMode ThreadInclusionMode
{
get => settingsService.ThreadInclusionMode;
set => settingsService.ThreadInclusionMode = value;
get => _settingsService.ThreadInclusionMode;
set => _settingsService.ThreadInclusionMode = value;
}
// These items have to be non-nullable because WPF ComboBox doesn't allow a null value to be selected
public IReadOnlyList<string> AvailableLocales { get; } = new[]
{
// These items have to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected
public IReadOnlyList<string> AvailableLocales { get; } =
[
// Current locale (maps to null downstream)
"",
// Locales supported by the Discord app
@@ -72,25 +85,35 @@ public class SettingsViewModel(SettingsService settingsService) : DialogScreen
"ja-JP",
"zh-TW",
"ko-KR"
}.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
];
// This has to be non-nullable because WPF ComboBox doesn't allow a null value to be selected
// This has to be non-nullable because Avalonia ComboBox doesn't allow a null value to be selected
public string Locale
{
get => settingsService.Locale ?? "";
get => _settingsService.Locale ?? "";
// Important to reduce empty strings to nulls, because empty strings don't correspond to valid cultures
set => settingsService.Locale = value.NullIfWhiteSpace();
set => _settingsService.Locale = value.NullIfWhiteSpace();
}
public bool IsUtcNormalizationEnabled
{
get => settingsService.IsUtcNormalizationEnabled;
set => settingsService.IsUtcNormalizationEnabled = value;
get => _settingsService.IsUtcNormalizationEnabled;
set => _settingsService.IsUtcNormalizationEnabled = value;
}
public int ParallelLimit
{
get => settingsService.ParallelLimit;
set => settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
get => _settingsService.ParallelLimit;
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_eventRoot.Dispose();
}
base.Dispose(disposing);
}
}