mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-03-17 12:22:32 +00:00
C#10ify
This commit is contained in:
@@ -10,129 +10,128 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class ExportSetupViewModel : DialogScreen
|
||||
{
|
||||
public class ExportSetupViewModel : DialogScreen
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
public Guild? Guild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? Channels { get; set; }
|
||||
|
||||
public bool IsSingleChannel => Channels is null || Channels.Count == 1;
|
||||
|
||||
public string? OutputPath { get; set; }
|
||||
|
||||
public IReadOnlyList<ExportFormat> AvailableFormats =>
|
||||
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
|
||||
|
||||
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 ShouldDownloadMedia { get; set; }
|
||||
|
||||
// Whether to show the "advanced options" by default when the dialog opens.
|
||||
// This is active if any of the advanced options are set to non-default values.
|
||||
public bool IsAdvancedSectionDisplayedByDefault =>
|
||||
After != default ||
|
||||
Before != default ||
|
||||
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
||||
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
|
||||
ShouldDownloadMedia != default;
|
||||
|
||||
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
|
||||
{
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
public Guild? Guild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? Channels { get; set; }
|
||||
|
||||
public bool IsSingleChannel => Channels is null || Channels.Count == 1;
|
||||
|
||||
public string? OutputPath { get; set; }
|
||||
|
||||
public IReadOnlyList<ExportFormat> AvailableFormats =>
|
||||
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
|
||||
|
||||
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 ShouldDownloadMedia { get; set; }
|
||||
|
||||
// Whether to show the "advanced options" by default when the dialog opens.
|
||||
// This is active if any of the advanced options are set to non-default values.
|
||||
public bool IsAdvancedSectionDisplayedByDefault =>
|
||||
After != default ||
|
||||
Before != default ||
|
||||
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
||||
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
|
||||
ShouldDownloadMedia != default;
|
||||
|
||||
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
|
||||
{
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
|
||||
// Persist preferences
|
||||
SelectedFormat = _settingsService.LastExportFormat;
|
||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||
ShouldDownloadMedia = _settingsService.LastShouldDownloadMedia;
|
||||
}
|
||||
|
||||
public void Confirm()
|
||||
{
|
||||
// Persist preferences
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
|
||||
|
||||
// If single channel - prompt file path
|
||||
if (Channels is not null && IsSingleChannel)
|
||||
{
|
||||
var channel = Channels.Single();
|
||||
var defaultFileName = ExportRequest.GetDefaultOutputFileName(
|
||||
Guild!,
|
||||
channel,
|
||||
SelectedFormat,
|
||||
After?.Pipe(Snowflake.FromDate),
|
||||
Before?.Pipe(Snowflake.FromDate)
|
||||
);
|
||||
|
||||
// Filter
|
||||
var ext = SelectedFormat.GetFileExtension();
|
||||
var filter = $"{ext.ToUpperInvariant()} files|*.{ext}";
|
||||
|
||||
OutputPath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
|
||||
}
|
||||
// If multiple channels - prompt dir path
|
||||
else
|
||||
{
|
||||
OutputPath = _dialogManager.PromptDirectoryPath();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(OutputPath))
|
||||
return;
|
||||
|
||||
Close(true);
|
||||
}
|
||||
// Persist preferences
|
||||
SelectedFormat = _settingsService.LastExportFormat;
|
||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||
ShouldDownloadMedia = _settingsService.LastShouldDownloadMedia;
|
||||
}
|
||||
|
||||
public static class ExportSetupViewModelExtensions
|
||||
public void Confirm()
|
||||
{
|
||||
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
|
||||
Guild guild, IReadOnlyList<Channel> channels)
|
||||
// Persist preferences
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
|
||||
|
||||
// If single channel - prompt file path
|
||||
if (Channels is not null && IsSingleChannel)
|
||||
{
|
||||
var viewModel = factory.CreateExportSetupViewModel();
|
||||
var channel = Channels.Single();
|
||||
var defaultFileName = ExportRequest.GetDefaultOutputFileName(
|
||||
Guild!,
|
||||
channel,
|
||||
SelectedFormat,
|
||||
After?.Pipe(Snowflake.FromDate),
|
||||
Before?.Pipe(Snowflake.FromDate)
|
||||
);
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
// Filter
|
||||
var ext = SelectedFormat.GetFileExtension();
|
||||
var filter = $"{ext.ToUpperInvariant()} files|*.{ext}";
|
||||
|
||||
return viewModel;
|
||||
OutputPath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
|
||||
}
|
||||
// If multiple channels - prompt dir path
|
||||
else
|
||||
{
|
||||
OutputPath = _dialogManager.PromptDirectoryPath();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(OutputPath))
|
||||
return;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,26 @@
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class MessageBoxViewModel : DialogScreen
|
||||
{
|
||||
public class MessageBoxViewModel : DialogScreen
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
|
||||
public static class MessageBoxViewModelExtensions
|
||||
{
|
||||
public static MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
this IViewModelFactory factory,
|
||||
string title,
|
||||
string message)
|
||||
{
|
||||
public string? Title { get; set; }
|
||||
var viewModel = factory.CreateMessageBoxViewModel();
|
||||
|
||||
public string? Message { get; set; }
|
||||
}
|
||||
viewModel.Title = title;
|
||||
viewModel.Message = message;
|
||||
|
||||
public static class MessageBoxViewModelExtensions
|
||||
{
|
||||
public static MessageBoxViewModel CreateMessageBoxViewModel(
|
||||
this IViewModelFactory factory,
|
||||
string title,
|
||||
string message)
|
||||
{
|
||||
var viewModel = factory.CreateMessageBoxViewModel();
|
||||
|
||||
viewModel.Title = title;
|
||||
viewModel.Message = message;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
return viewModel;
|
||||
}
|
||||
}
|
||||
@@ -2,49 +2,48 @@
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
public class SettingsViewModel : DialogScreen
|
||||
{
|
||||
public class SettingsViewModel : DialogScreen
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
public bool IsAutoUpdateEnabled
|
||||
{
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
public bool IsAutoUpdateEnabled
|
||||
{
|
||||
get => _settingsService.IsAutoUpdateEnabled;
|
||||
set => _settingsService.IsAutoUpdateEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsDarkModeEnabled
|
||||
{
|
||||
get => _settingsService.IsDarkModeEnabled;
|
||||
set => _settingsService.IsDarkModeEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsTokenPersisted
|
||||
{
|
||||
get => _settingsService.IsTokenPersisted;
|
||||
set => _settingsService.IsTokenPersisted = value;
|
||||
}
|
||||
|
||||
public string DateFormat
|
||||
{
|
||||
get => _settingsService.DateFormat;
|
||||
set => _settingsService.DateFormat = value;
|
||||
}
|
||||
|
||||
public int ParallelLimit
|
||||
{
|
||||
get => _settingsService.ParallelLimit;
|
||||
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||
}
|
||||
|
||||
public bool ShouldReuseMedia
|
||||
{
|
||||
get => _settingsService.ShouldReuseMedia;
|
||||
set => _settingsService.ShouldReuseMedia = value;
|
||||
}
|
||||
|
||||
public SettingsViewModel(SettingsService settingsService) =>
|
||||
_settingsService = settingsService;
|
||||
get => _settingsService.IsAutoUpdateEnabled;
|
||||
set => _settingsService.IsAutoUpdateEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsDarkModeEnabled
|
||||
{
|
||||
get => _settingsService.IsDarkModeEnabled;
|
||||
set => _settingsService.IsDarkModeEnabled = value;
|
||||
}
|
||||
|
||||
public bool IsTokenPersisted
|
||||
{
|
||||
get => _settingsService.IsTokenPersisted;
|
||||
set => _settingsService.IsTokenPersisted = value;
|
||||
}
|
||||
|
||||
public string DateFormat
|
||||
{
|
||||
get => _settingsService.DateFormat;
|
||||
set => _settingsService.DateFormat = value;
|
||||
}
|
||||
|
||||
public int ParallelLimit
|
||||
{
|
||||
get => _settingsService.ParallelLimit;
|
||||
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||
}
|
||||
|
||||
public bool ShouldReuseMedia
|
||||
{
|
||||
get => _settingsService.ShouldReuseMedia;
|
||||
set => _settingsService.ShouldReuseMedia = value;
|
||||
}
|
||||
|
||||
public SettingsViewModel(SettingsService settingsService) =>
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
@@ -6,58 +6,57 @@ using Microsoft.Win32;
|
||||
using Ookii.Dialogs.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public class DialogManager
|
||||
{
|
||||
public class DialogManager
|
||||
private readonly IViewManager _viewManager;
|
||||
|
||||
public DialogManager(IViewManager viewManager)
|
||||
{
|
||||
private readonly IViewManager _viewManager;
|
||||
_viewManager = viewManager;
|
||||
}
|
||||
|
||||
public DialogManager(IViewManager viewManager)
|
||||
public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen)
|
||||
{
|
||||
var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
|
||||
|
||||
void OnDialogOpened(object? sender, DialogOpenedEventArgs openArgs)
|
||||
{
|
||||
_viewManager = viewManager;
|
||||
}
|
||||
|
||||
public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen)
|
||||
{
|
||||
var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
|
||||
|
||||
void OnDialogOpened(object? sender, DialogOpenedEventArgs openArgs)
|
||||
void OnScreenClosed(object? o, EventArgs closeArgs)
|
||||
{
|
||||
void OnScreenClosed(object? o, EventArgs closeArgs)
|
||||
{
|
||||
openArgs.Session.Close();
|
||||
dialogScreen.Closed -= OnScreenClosed;
|
||||
}
|
||||
|
||||
dialogScreen.Closed += OnScreenClosed;
|
||||
openArgs.Session.Close();
|
||||
dialogScreen.Closed -= OnScreenClosed;
|
||||
}
|
||||
|
||||
await DialogHost.Show(view, OnDialogOpened);
|
||||
|
||||
return dialogScreen.DialogResult;
|
||||
dialogScreen.Closed += OnScreenClosed;
|
||||
}
|
||||
|
||||
public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
|
||||
await DialogHost.Show(view, OnDialogOpened);
|
||||
|
||||
return dialogScreen.DialogResult;
|
||||
}
|
||||
|
||||
public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = filter,
|
||||
AddExtension = true,
|
||||
FileName = defaultFilePath,
|
||||
DefaultExt = Path.GetExtension(defaultFilePath)
|
||||
};
|
||||
Filter = filter,
|
||||
AddExtension = true,
|
||||
FileName = defaultFilePath,
|
||||
DefaultExt = Path.GetExtension(defaultFilePath)
|
||||
};
|
||||
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
|
||||
public string? PromptDirectoryPath(string defaultDirPath = "")
|
||||
public string? PromptDirectoryPath(string defaultDirPath = "")
|
||||
{
|
||||
var dialog = new VistaFolderBrowserDialog
|
||||
{
|
||||
var dialog = new VistaFolderBrowserDialog
|
||||
{
|
||||
SelectedPath = defaultDirPath
|
||||
};
|
||||
SelectedPath = defaultDirPath
|
||||
};
|
||||
|
||||
return dialog.ShowDialog() == true ? dialog.SelectedPath : null;
|
||||
}
|
||||
return dialog.ShowDialog() == true ? dialog.SelectedPath : null;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
using System;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public abstract class DialogScreen<T> : PropertyChangedBase
|
||||
{
|
||||
public abstract class DialogScreen<T> : PropertyChangedBase
|
||||
public T? DialogResult { get; private set; }
|
||||
|
||||
public event EventHandler? Closed;
|
||||
|
||||
public void Close(T dialogResult)
|
||||
{
|
||||
public T? DialogResult { get; private set; }
|
||||
|
||||
public event EventHandler? Closed;
|
||||
|
||||
public void Close(T dialogResult)
|
||||
{
|
||||
DialogResult = dialogResult;
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
DialogResult = dialogResult;
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DialogScreen : DialogScreen<bool?>
|
||||
{
|
||||
}
|
||||
public abstract class DialogScreen : DialogScreen<bool?>
|
||||
{
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
// Used to instantiate new view models while making use of dependency injection
|
||||
public interface IViewModelFactory
|
||||
{
|
||||
// Used to instantiate new view models while making use of dependency injection
|
||||
public interface IViewModelFactory
|
||||
{
|
||||
ExportSetupViewModel CreateExportSetupViewModel();
|
||||
ExportSetupViewModel CreateExportSetupViewModel();
|
||||
|
||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||
|
||||
SettingsViewModel CreateSettingsViewModel();
|
||||
}
|
||||
SettingsViewModel CreateSettingsViewModel();
|
||||
}
|
||||
@@ -16,247 +16,246 @@ using Gress;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels
|
||||
namespace DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
public class RootViewModel : Screen
|
||||
{
|
||||
public class RootViewModel : Screen
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
|
||||
public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
||||
|
||||
public IProgressManager ProgressManager { get; } = new ProgressManager();
|
||||
|
||||
public bool IsBusy { get; private set; }
|
||||
|
||||
public bool IsProgressIndeterminate { get; private set; }
|
||||
|
||||
public bool IsBotToken { get; set; }
|
||||
|
||||
public string? TokenValue { get; set; }
|
||||
|
||||
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
||||
|
||||
public IReadOnlyList<Guild>? AvailableGuilds => GuildChannelMap?.Keys.ToArray();
|
||||
|
||||
public Guild? SelectedGuild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? AvailableChannels => SelectedGuild is not null
|
||||
? GuildChannelMap?[SelectedGuild]
|
||||
: null;
|
||||
|
||||
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||
|
||||
public RootViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService)
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
|
||||
public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5));
|
||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||
|
||||
public IProgressManager ProgressManager { get; } = new ProgressManager();
|
||||
// Update busy state when progress manager changes
|
||||
ProgressManager.Bind(o => o.IsActive, (_, _) =>
|
||||
IsBusy = ProgressManager.IsActive
|
||||
);
|
||||
|
||||
public bool IsBusy { get; private set; }
|
||||
ProgressManager.Bind(o => o.IsActive, (_, _) =>
|
||||
IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1
|
||||
);
|
||||
|
||||
public bool IsProgressIndeterminate { get; private set; }
|
||||
ProgressManager.Bind(o => o.Progress, (_, _) =>
|
||||
IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1
|
||||
);
|
||||
}
|
||||
|
||||
public bool IsBotToken { get; set; }
|
||||
|
||||
public string? TokenValue { get; set; }
|
||||
|
||||
private IReadOnlyDictionary<Guild, IReadOnlyList<Channel>>? GuildChannelMap { get; set; }
|
||||
|
||||
public IReadOnlyList<Guild>? AvailableGuilds => GuildChannelMap?.Keys.ToArray();
|
||||
|
||||
public Guild? SelectedGuild { get; set; }
|
||||
|
||||
public IReadOnlyList<Channel>? AvailableChannels => SelectedGuild is not null
|
||||
? GuildChannelMap?[SelectedGuild]
|
||||
: null;
|
||||
|
||||
public IReadOnlyList<Channel>? SelectedChannels { get; set; }
|
||||
|
||||
public RootViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService)
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
var updateVersion = await _updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
|
||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
|
||||
await _updateService.PrepareUpdateAsync(updateVersion);
|
||||
|
||||
// Update busy state when progress manager changes
|
||||
ProgressManager.Bind(o => o.IsActive, (_, _) =>
|
||||
IsBusy = ProgressManager.IsActive
|
||||
);
|
||||
|
||||
ProgressManager.Bind(o => o.IsActive, (_, _) =>
|
||||
IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1
|
||||
);
|
||||
|
||||
ProgressManager.Bind(o => o.Progress, (_, _) =>
|
||||
IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress is <= 0 or >= 1
|
||||
Notifications.Enqueue(
|
||||
"Update has been downloaded and will be installed when you exit",
|
||||
"INSTALL NOW", () =>
|
||||
{
|
||||
_updateService.FinalizeUpdate(true);
|
||||
RequestClose();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
catch
|
||||
{
|
||||
try
|
||||
{
|
||||
var updateVersion = await _updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
// Failure to update shouldn't crash the application
|
||||
Notifications.Enqueue("Failed to perform application update");
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
|
||||
await _updateService.PrepareUpdateAsync(updateVersion);
|
||||
protected override async void OnViewLoaded()
|
||||
{
|
||||
base.OnViewLoaded();
|
||||
|
||||
Notifications.Enqueue(
|
||||
"Update has been downloaded and will be installed when you exit",
|
||||
"INSTALL NOW", () =>
|
||||
{
|
||||
_updateService.FinalizeUpdate(true);
|
||||
RequestClose();
|
||||
}
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failure to update shouldn't crash the application
|
||||
Notifications.Enqueue("Failed to perform application update");
|
||||
}
|
||||
_settingsService.Load();
|
||||
|
||||
if (_settingsService.LastToken is not null)
|
||||
{
|
||||
IsBotToken = _settingsService.LastToken.Kind == AuthTokenKind.Bot;
|
||||
TokenValue = _settingsService.LastToken.Value;
|
||||
}
|
||||
|
||||
protected override async void OnViewLoaded()
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
{
|
||||
base.OnViewLoaded();
|
||||
|
||||
_settingsService.Load();
|
||||
|
||||
if (_settingsService.LastToken is not null)
|
||||
{
|
||||
IsBotToken = _settingsService.LastToken.Kind == AuthTokenKind.Bot;
|
||||
TokenValue = _settingsService.LastToken.Value;
|
||||
}
|
||||
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
{
|
||||
App.SetDarkTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
await CheckForUpdatesAsync();
|
||||
App.SetDarkTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
base.OnClose();
|
||||
await CheckForUpdatesAsync();
|
||||
}
|
||||
|
||||
_settingsService.Save();
|
||||
_updateService.FinalizeUpdate(false);
|
||||
protected override void OnClose()
|
||||
{
|
||||
base.OnClose();
|
||||
|
||||
_settingsService.Save();
|
||||
_updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
public async void ShowSettings()
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateSettingsViewModel();
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
||||
|
||||
public bool CanPopulateGuildsAndChannels =>
|
||||
!IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
|
||||
|
||||
public async void PopulateGuildsAndChannels()
|
||||
{
|
||||
using var operation = ProgressManager.CreateOperation();
|
||||
|
||||
try
|
||||
{
|
||||
var tokenValue = TokenValue?.Trim('"', ' ');
|
||||
if (string.IsNullOrWhiteSpace(tokenValue))
|
||||
return;
|
||||
|
||||
var token = new AuthToken(
|
||||
IsBotToken ? AuthTokenKind.Bot : AuthTokenKind.User,
|
||||
tokenValue
|
||||
);
|
||||
|
||||
_settingsService.LastToken = token;
|
||||
|
||||
var discord = new DiscordClient(token);
|
||||
|
||||
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||
{
|
||||
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
||||
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
||||
}
|
||||
|
||||
GuildChannelMap = guildChannelMap;
|
||||
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async void ShowSettings()
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateSettingsViewModel();
|
||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Error pulling guilds and channels",
|
||||
ex.ToString()
|
||||
);
|
||||
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.GitHubProjectWikiUrl);
|
||||
public bool CanExportChannels =>
|
||||
!IsBusy && SelectedGuild is not null && SelectedChannels is not null && SelectedChannels.Any();
|
||||
|
||||
public bool CanPopulateGuildsAndChannels =>
|
||||
!IsBusy && !string.IsNullOrWhiteSpace(TokenValue);
|
||||
|
||||
public async void PopulateGuildsAndChannels()
|
||||
public async void ExportChannels()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var operation = ProgressManager.CreateOperation();
|
||||
var token = _settingsService.LastToken;
|
||||
if (token is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
try
|
||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
var exporter = new ChannelExporter(token);
|
||||
|
||||
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
|
||||
var successfulExportCount = 0;
|
||||
|
||||
await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple =>
|
||||
{
|
||||
var tokenValue = TokenValue?.Trim('"', ' ');
|
||||
if (string.IsNullOrWhiteSpace(tokenValue))
|
||||
return;
|
||||
var (channel, operation) = tuple;
|
||||
|
||||
var token = new AuthToken(
|
||||
IsBotToken ? AuthTokenKind.Bot : AuthTokenKind.User,
|
||||
tokenValue
|
||||
);
|
||||
|
||||
_settingsService.LastToken = token;
|
||||
|
||||
var discord = new DiscordClient(token);
|
||||
|
||||
var guildChannelMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
|
||||
await foreach (var guild in discord.GetUserGuildsAsync())
|
||||
try
|
||||
{
|
||||
var channels = await discord.GetGuildChannelsAsync(guild.Id);
|
||||
guildChannelMap[guild] = channels.Where(c => c.IsTextChannel).ToArray();
|
||||
var request = new ExportRequest(
|
||||
dialog.Guild!,
|
||||
channel!,
|
||||
dialog.OutputPath!,
|
||||
dialog.SelectedFormat,
|
||||
dialog.After?.Pipe(Snowflake.FromDate),
|
||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||
dialog.PartitionLimit,
|
||||
dialog.MessageFilter,
|
||||
dialog.ShouldDownloadMedia,
|
||||
_settingsService.ShouldReuseMedia,
|
||||
_settingsService.DateFormat
|
||||
);
|
||||
|
||||
await exporter.ExportChannelAsync(request, operation);
|
||||
|
||||
Interlocked.Increment(ref successfulExportCount);
|
||||
}
|
||||
|
||||
GuildChannelMap = guildChannelMap;
|
||||
SelectedGuild = guildChannelMap.Keys.FirstOrDefault();
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Error pulling guilds and channels",
|
||||
ex.ToString()
|
||||
);
|
||||
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanExportChannels =>
|
||||
!IsBusy && SelectedGuild is not null && SelectedChannels is not null && SelectedChannels.Any();
|
||||
|
||||
public async void ExportChannels()
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = _settingsService.LastToken;
|
||||
if (token is null || SelectedGuild is null || SelectedChannels is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
return;
|
||||
|
||||
var exporter = new ChannelExporter(token);
|
||||
|
||||
var operations = ProgressManager.CreateOperations(dialog.Channels!.Count);
|
||||
var successfulExportCount = 0;
|
||||
|
||||
await dialog.Channels.Zip(operations).ParallelForEachAsync(async tuple =>
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
var (channel, operation) = tuple;
|
||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
operation.Dispose();
|
||||
}
|
||||
}, Math.Max(1, _settingsService.ParallelLimit));
|
||||
|
||||
try
|
||||
{
|
||||
var request = new ExportRequest(
|
||||
dialog.Guild!,
|
||||
channel!,
|
||||
dialog.OutputPath!,
|
||||
dialog.SelectedFormat,
|
||||
dialog.After?.Pipe(Snowflake.FromDate),
|
||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||
dialog.PartitionLimit,
|
||||
dialog.MessageFilter,
|
||||
dialog.ShouldDownloadMedia,
|
||||
_settingsService.ShouldReuseMedia,
|
||||
_settingsService.DateFormat
|
||||
);
|
||||
// Notify of overall completion
|
||||
if (successfulExportCount > 0)
|
||||
Notifications.Enqueue($"Successfully exported {successfulExportCount} channel(s)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Error exporting channel(s)",
|
||||
ex.ToString()
|
||||
);
|
||||
|
||||
await exporter.ExportChannelAsync(request, operation);
|
||||
|
||||
Interlocked.Increment(ref successfulExportCount);
|
||||
}
|
||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||
{
|
||||
Notifications.Enqueue(ex.Message.TrimEnd('.'));
|
||||
}
|
||||
finally
|
||||
{
|
||||
operation.Dispose();
|
||||
}
|
||||
}, Math.Max(1, _settingsService.ParallelLimit));
|
||||
|
||||
// Notify of overall completion
|
||||
if (successfulExportCount > 0)
|
||||
Notifications.Enqueue($"Successfully exported {successfulExportCount} channel(s)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Error exporting channel(s)",
|
||||
ex.ToString()
|
||||
);
|
||||
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
await _dialogManager.ShowDialogAsync(dialog);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user