mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-03-16 20:02:42 +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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using AsyncKeyedLock;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public class DialogManager(IViewManager viewManager) : IDisposable
|
||||
{
|
||||
private readonly AsyncNonKeyedLocker _dialogLock = new();
|
||||
|
||||
public async ValueTask<T?> ShowDialogAsync<T>(DialogScreen<T> dialogScreen)
|
||||
{
|
||||
var view = viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen);
|
||||
|
||||
void OnDialogOpened(object? openSender, DialogOpenedEventArgs openArgs)
|
||||
{
|
||||
void OnScreenClosed(object? closeSender, EventArgs closeArgs)
|
||||
{
|
||||
try
|
||||
{
|
||||
openArgs.Session.Close();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Race condition: dialog is already being closed
|
||||
}
|
||||
|
||||
dialogScreen.Closed -= OnScreenClosed;
|
||||
}
|
||||
dialogScreen.Closed += OnScreenClosed;
|
||||
}
|
||||
|
||||
using (await _dialogLock.LockAsync())
|
||||
{
|
||||
await DialogHost.Show(view, OnDialogOpened);
|
||||
return dialogScreen.DialogResult;
|
||||
}
|
||||
}
|
||||
|
||||
public string? PromptSaveFilePath(string filter = "All files|*.*", string defaultFilePath = "")
|
||||
{
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = filter,
|
||||
AddExtension = true,
|
||||
FileName = defaultFilePath,
|
||||
DefaultExt = Path.GetExtension(defaultFilePath)
|
||||
};
|
||||
|
||||
return dialog.ShowDialog() == true ? dialog.FileName : null;
|
||||
}
|
||||
|
||||
public string? PromptDirectoryPath(string defaultDirPath = "")
|
||||
{
|
||||
var dialog = new OpenFolderDialog { InitialDirectory = defaultDirPath };
|
||||
return dialog.ShowDialog() == true ? dialog.FolderName : null;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dialogLock.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
public abstract class DialogScreen<T> : PropertyChangedBase
|
||||
{
|
||||
public T? DialogResult { get; private set; }
|
||||
|
||||
public event EventHandler? Closed;
|
||||
|
||||
public void Close(T dialogResult)
|
||||
{
|
||||
DialogResult = dialogResult;
|
||||
Closed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class DialogScreen : DialogScreen<bool?>;
|
||||
@@ -1,16 +0,0 @@
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
|
||||
// Used to instantiate new view models while making use of dependency injection
|
||||
public interface IViewModelFactory
|
||||
{
|
||||
DashboardViewModel CreateDashboardViewModel();
|
||||
|
||||
ExportSetupViewModel CreateExportSetupViewModel();
|
||||
|
||||
MessageBoxViewModel CreateMessageBoxViewModel();
|
||||
|
||||
SettingsViewModel CreateSettingsViewModel();
|
||||
}
|
||||
127
DiscordChatExporter.Gui/ViewModels/MainViewModel.cs
Normal file
127
DiscordChatExporter.Gui/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Gui.Framework;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.Utils.Extensions;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
public partial class MainViewModel(
|
||||
ViewModelManager viewModelManager,
|
||||
DialogManager dialogManager,
|
||||
SnackbarManager snackbarManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService
|
||||
) : ViewModelBase
|
||||
{
|
||||
public string Title { get; } = $"{Program.Name} v{Program.VersionString}";
|
||||
|
||||
public DashboardViewModel Dashboard { get; } = viewModelManager.CreateDashboardViewModel();
|
||||
|
||||
private async Task ShowUkraineSupportMessageAsync()
|
||||
{
|
||||
if (!settingsService.IsUkraineSupportMessageEnabled)
|
||||
return;
|
||||
|
||||
var dialog = viewModelManager.CreateMessageBoxViewModel(
|
||||
"Thank you for supporting Ukraine!",
|
||||
"""
|
||||
As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.
|
||||
|
||||
Click LEARN MORE to find ways that you can help.
|
||||
""",
|
||||
"LEARN MORE",
|
||||
"CLOSE"
|
||||
);
|
||||
|
||||
// Disable this message in the future
|
||||
settingsService.IsUkraineSupportMessageEnabled = false;
|
||||
settingsService.Save();
|
||||
|
||||
if (await dialogManager.ShowDialogAsync(dialog) == true)
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
|
||||
private async Task CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var updateVersion = await updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
|
||||
snackbarManager.Notify($"Downloading update to {Program.Name} v{updateVersion}...");
|
||||
await updateService.PrepareUpdateAsync(updateVersion);
|
||||
|
||||
snackbarManager.Notify(
|
||||
"Update has been downloaded and will be installed when you exit",
|
||||
"INSTALL NOW",
|
||||
() =>
|
||||
{
|
||||
updateService.FinalizeUpdate(true);
|
||||
|
||||
if (Application.Current?.ApplicationLifetime?.TryShutdown(2) != true)
|
||||
Environment.Exit(2);
|
||||
}
|
||||
);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failure to update shouldn't crash the application
|
||||
snackbarManager.Notify("Failed to perform application update");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
// Reset settings (needed to resolve the default dark mode setting)
|
||||
settingsService.Reset();
|
||||
|
||||
// Load settings
|
||||
settingsService.Load();
|
||||
|
||||
// Set the correct theme
|
||||
if (settingsService.IsDarkModeEnabled)
|
||||
App.SetDarkTheme();
|
||||
else
|
||||
App.SetLightTheme();
|
||||
|
||||
await ShowUkraineSupportMessageAsync();
|
||||
await CheckForUpdatesAsync();
|
||||
|
||||
// App has just been updated, display the changelog
|
||||
if (
|
||||
settingsService.LastAppVersion is not null
|
||||
&& settingsService.LastAppVersion != Program.Version
|
||||
)
|
||||
{
|
||||
snackbarManager.Notify(
|
||||
$"Successfully updated to {Program.Name} v{Program.VersionString}",
|
||||
"WHAT'S NEW",
|
||||
() => ProcessEx.StartShellExecute(Program.LatestReleaseUrl)
|
||||
);
|
||||
|
||||
settingsService.LastAppVersion = Program.Version;
|
||||
settingsService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// Save settings
|
||||
settingsService.Save();
|
||||
|
||||
// Finalize pending updates
|
||||
updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
namespace DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
|
||||
public record NotificationMessage(string Text);
|
||||
@@ -1,147 +0,0 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Gui.Services;
|
||||
using DiscordChatExporter.Gui.Utils;
|
||||
using DiscordChatExporter.Gui.ViewModels.Components;
|
||||
using DiscordChatExporter.Gui.ViewModels.Dialogs;
|
||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||
using DiscordChatExporter.Gui.ViewModels.Messages;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace DiscordChatExporter.Gui.ViewModels;
|
||||
|
||||
public class RootViewModel : Screen, IHandle<NotificationMessage>, IDisposable
|
||||
{
|
||||
private readonly IViewModelFactory _viewModelFactory;
|
||||
private readonly DialogManager _dialogManager;
|
||||
private readonly SettingsService _settingsService;
|
||||
private readonly UpdateService _updateService;
|
||||
|
||||
public SnackbarMessageQueue Notifications { get; } = new(TimeSpan.FromSeconds(5));
|
||||
|
||||
public DashboardViewModel Dashboard { get; }
|
||||
|
||||
public RootViewModel(
|
||||
IViewModelFactory viewModelFactory,
|
||||
IEventAggregator eventAggregator,
|
||||
DialogManager dialogManager,
|
||||
SettingsService settingsService,
|
||||
UpdateService updateService
|
||||
)
|
||||
{
|
||||
_viewModelFactory = viewModelFactory;
|
||||
_dialogManager = dialogManager;
|
||||
_settingsService = settingsService;
|
||||
_updateService = updateService;
|
||||
|
||||
eventAggregator.Subscribe(this);
|
||||
|
||||
Dashboard = _viewModelFactory.CreateDashboardViewModel();
|
||||
|
||||
DisplayName = $"{App.Name} v{App.VersionString}";
|
||||
}
|
||||
|
||||
private async Task ShowUkraineSupportMessageAsync()
|
||||
{
|
||||
if (!_settingsService.IsUkraineSupportMessageEnabled)
|
||||
return;
|
||||
|
||||
var dialog = _viewModelFactory.CreateMessageBoxViewModel(
|
||||
"Thank you for supporting Ukraine!",
|
||||
"""
|
||||
As Russia wages a genocidal war against my country, I'm grateful to everyone who continues to stand with Ukraine in our fight for freedom.
|
||||
|
||||
Click LEARN MORE to find ways that you can help.
|
||||
""",
|
||||
"LEARN MORE",
|
||||
"CLOSE"
|
||||
);
|
||||
|
||||
// Disable this message in the future
|
||||
_settingsService.IsUkraineSupportMessageEnabled = false;
|
||||
_settingsService.Save();
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) == true)
|
||||
ProcessEx.StartShellExecute("https://tyrrrz.me/ukraine?source=discordchatexporter");
|
||||
}
|
||||
|
||||
private async ValueTask CheckForUpdatesAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var updateVersion = await _updateService.CheckForUpdatesAsync();
|
||||
if (updateVersion is null)
|
||||
return;
|
||||
|
||||
Notifications.Enqueue($"Downloading update to {App.Name} v{updateVersion}...");
|
||||
await _updateService.PrepareUpdateAsync(updateVersion);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
public async void OnViewFullyLoaded()
|
||||
{
|
||||
await ShowUkraineSupportMessageAsync();
|
||||
await CheckForUpdatesAsync();
|
||||
}
|
||||
|
||||
protected override void OnViewLoaded()
|
||||
{
|
||||
base.OnViewLoaded();
|
||||
|
||||
_settingsService.Load();
|
||||
|
||||
// Sync the theme with settings
|
||||
if (_settingsService.IsDarkModeEnabled)
|
||||
{
|
||||
App.SetDarkTheme();
|
||||
}
|
||||
else
|
||||
{
|
||||
App.SetLightTheme();
|
||||
}
|
||||
|
||||
// App has just been updated, display the changelog
|
||||
if (
|
||||
_settingsService.LastAppVersion is not null
|
||||
&& _settingsService.LastAppVersion != App.Version
|
||||
)
|
||||
{
|
||||
Notifications.Enqueue(
|
||||
$"Successfully updated to {App.Name} v{App.VersionString}",
|
||||
"WHAT'S NEW",
|
||||
() => ProcessEx.StartShellExecute(App.LatestReleaseUrl)
|
||||
);
|
||||
|
||||
_settingsService.LastAppVersion = App.Version;
|
||||
_settingsService.Save();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClose()
|
||||
{
|
||||
base.OnClose();
|
||||
|
||||
_settingsService.Save();
|
||||
_updateService.FinalizeUpdate(false);
|
||||
}
|
||||
|
||||
public void Handle(NotificationMessage message) => Notifications.Enqueue(message.Text);
|
||||
|
||||
public void Dispose() => Notifications.Dispose();
|
||||
}
|
||||
Reference in New Issue
Block a user