mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-04-10 21:32:05 +00:00
Migrate to Avalonia (#1220)
This commit is contained in:
88
DiscordChatExporter.Gui/Framework/DialogManager.cs
Normal file
88
DiscordChatExporter.Gui/Framework/DialogManager.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using Avalonia;
|
||||
using Avalonia.Platform.Storage;
|
||||
using DialogHostAvalonia;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class DialogManager : IDisposable
|
||||
{
|
||||
private readonly AsyncNonKeyedLocker _dialogLock = new();
|
||||
|
||||
public async Task<T?> ShowDialogAsync<T>(DialogViewModelBase<T> dialog)
|
||||
{
|
||||
using (await _dialogLock.LockAsync())
|
||||
{
|
||||
await DialogHost.Show(
|
||||
dialog,
|
||||
// It's fine to await in a void method here because it's an event handler
|
||||
// ReSharper disable once AsyncVoidLambda
|
||||
async (object _, DialogOpenedEventArgs args) =>
|
||||
{
|
||||
await dialog.WaitForCloseAsync();
|
||||
|
||||
try
|
||||
{
|
||||
args.Session.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Dialog host is already processing a close operation
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return dialog.DialogResult;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> PromptSaveFilePathAsync(
|
||||
IReadOnlyList<FilePickerFileType>? fileTypes = null,
|
||||
string defaultFilePath = ""
|
||||
)
|
||||
{
|
||||
var topLevel =
|
||||
Application.Current?.ApplicationLifetime?.TryGetTopLevel()
|
||||
?? throw new ApplicationException("Could not find the top-level visual element.");
|
||||
|
||||
var file = await topLevel.StorageProvider.SaveFilePickerAsync(
|
||||
new FilePickerSaveOptions
|
||||
{
|
||||
FileTypeChoices = fileTypes,
|
||||
SuggestedFileName = defaultFilePath,
|
||||
DefaultExtension = Path.GetExtension(defaultFilePath).TrimStart('.')
|
||||
}
|
||||
);
|
||||
|
||||
return file?.Path.LocalPath;
|
||||
}
|
||||
|
||||
public async Task<string?> PromptDirectoryPathAsync(string defaultDirPath = "")
|
||||
{
|
||||
var topLevel =
|
||||
Application.Current?.ApplicationLifetime?.TryGetTopLevel()
|
||||
?? throw new ApplicationException("Could not find the top-level visual element.");
|
||||
|
||||
var startLocation = await topLevel.StorageProvider.TryGetFolderFromPathAsync(
|
||||
defaultDirPath
|
||||
);
|
||||
|
||||
var folderPickResult = await topLevel.StorageProvider.OpenFolderPickerAsync(
|
||||
new FolderPickerOpenOptions
|
||||
{
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = startLocation
|
||||
}
|
||||
);
|
||||
|
||||
return folderPickResult.FirstOrDefault()?.Path.LocalPath;
|
||||
}
|
||||
|
||||
public void Dispose() => _dialogLock.Dispose();
|
||||
}
|
||||
25
DiscordChatExporter.Gui/Framework/DialogVIewModelBase.cs
Normal file
25
DiscordChatExporter.Gui/Framework/DialogVIewModelBase.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public abstract partial class DialogViewModelBase<T> : ViewModelBase
|
||||
{
|
||||
private readonly TaskCompletionSource<T> _closeTcs =
|
||||
new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
[ObservableProperty]
|
||||
private T? _dialogResult;
|
||||
|
||||
[RelayCommand]
|
||||
protected void Close(T dialogResult)
|
||||
{
|
||||
DialogResult = dialogResult;
|
||||
_closeTcs.TrySetResult(dialogResult);
|
||||
}
|
||||
|
||||
public async Task<T> WaitForCloseAsync() => await _closeTcs.Task;
|
||||
}
|
||||
|
||||
public abstract class DialogViewModelBase : DialogViewModelBase<bool?>;
|
||||
34
DiscordChatExporter.Gui/Framework/SnackbarManager.cs
Normal file
34
DiscordChatExporter.Gui/Framework/SnackbarManager.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Avalonia.Threading;
|
||||
using Material.Styles.Controls;
|
||||
using Material.Styles.Models;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class SnackbarManager
|
||||
{
|
||||
private readonly TimeSpan _defaultDuration = TimeSpan.FromSeconds(5);
|
||||
|
||||
public void Notify(string message, TimeSpan? duration = null) =>
|
||||
SnackbarHost.Post(
|
||||
new SnackbarModel(message, duration ?? _defaultDuration),
|
||||
null,
|
||||
DispatcherPriority.Normal
|
||||
);
|
||||
|
||||
public void Notify(
|
||||
string message,
|
||||
string actionText,
|
||||
Action actionHandler,
|
||||
TimeSpan? duration = null
|
||||
) =>
|
||||
SnackbarHost.Post(
|
||||
new SnackbarModel(
|
||||
message,
|
||||
duration ?? _defaultDuration,
|
||||
new SnackbarButtonModel { Text = actionText, Action = actionHandler }
|
||||
),
|
||||
null,
|
||||
DispatcherPriority.Normal
|
||||
);
|
||||
}
|
||||
18
DiscordChatExporter.Gui/Framework/UserControl.cs
Normal file
18
DiscordChatExporter.Gui/Framework/UserControl.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class UserControl<TDataContext> : UserControl
|
||||
{
|
||||
public new TDataContext DataContext
|
||||
{
|
||||
get =>
|
||||
base.DataContext is TDataContext dataContext
|
||||
? dataContext
|
||||
: throw new InvalidCastException(
|
||||
$"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
|
||||
);
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
}
|
||||
37
DiscordChatExporter.Gui/Framework/ViewManager.cs
Normal file
37
DiscordChatExporter.Gui/Framework/ViewManager.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public partial class ViewManager
|
||||
{
|
||||
public Control? TryBindView(ViewModelBase viewModel)
|
||||
{
|
||||
var name = viewModel
|
||||
.GetType()
|
||||
.FullName?.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
|
||||
var type = Type.GetType(name);
|
||||
if (type is null)
|
||||
return null;
|
||||
|
||||
if (Activator.CreateInstance(type) is not Control view)
|
||||
return null;
|
||||
|
||||
view.DataContext ??= viewModel;
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class ViewManager : IDataTemplate
|
||||
{
|
||||
bool IDataTemplate.Match(object? data) => data is ViewModelBase;
|
||||
|
||||
Control? ITemplate<object?, Control?>.Build(object? data) =>
|
||||
data is ViewModelBase viewModel ? TryBindView(viewModel) : null;
|
||||
}
|
||||
19
DiscordChatExporter.Gui/Framework/ViewModelBase.cs
Normal file
19
DiscordChatExporter.Gui/Framework/ViewModelBase.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public abstract class ViewModelBase : ObservableObject, IDisposable
|
||||
{
|
||||
~ViewModelBase() => Dispose(false);
|
||||
|
||||
protected void OnAllPropertiesChanged() => OnPropertyChanged(string.Empty);
|
||||
|
||||
protected virtual void Dispose(bool disposing) { }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
53
DiscordChatExporter.Gui/Framework/ViewModelManager.cs
Normal file
53
DiscordChatExporter.Gui/Framework/ViewModelManager.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class ViewModelManager(IServiceProvider services)
|
||||
{
|
||||
public MainViewModel CreateMainViewModel() => services.GetRequiredService<MainViewModel>();
|
||||
|
||||
public DashboardViewModel CreateDashboardViewModel() =>
|
||||
services.GetRequiredService<DashboardViewModel>();
|
||||
|
||||
public ExportSetupViewModel CreateExportSetupViewModel(
|
||||
Guild guild,
|
||||
IReadOnlyList<Channel> channels
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<ExportSetupViewModel>();
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
string title,
|
||||
string message,
|
||||
string? okButtonText,
|
||||
string? cancelButtonText
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<MessageBoxViewModel>();
|
||||
|
||||
viewModel.Title = title;
|
||||
viewModel.Message = message;
|
||||
viewModel.DefaultButtonText = okButtonText;
|
||||
viewModel.CancelButtonText = cancelButtonText;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
||||
public MessageBoxViewModel CreateMessageBoxViewModel(string title, string message) =>
|
||||
CreateMessageBoxViewModel(title, message, "CLOSE", null);
|
||||
|
||||
public SettingsViewModel CreateSettingsViewModel() =>
|
||||
services.GetRequiredService<SettingsViewModel>();
|
||||
}
|
||||
18
DiscordChatExporter.Gui/Framework/Window.cs
Normal file
18
DiscordChatExporter.Gui/Framework/Window.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Framework;
|
||||
|
||||
public class Window<TDataContext> : Window
|
||||
{
|
||||
public new TDataContext DataContext
|
||||
{
|
||||
get =>
|
||||
base.DataContext is TDataContext dataContext
|
||||
? dataContext
|
||||
: throw new InvalidCastException(
|
||||
$"DataContext is null or not of the expected type '{typeof(TDataContext).FullName}'."
|
||||
);
|
||||
set => base.DataContext = value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user