From 0d3510222e736d9770e66fc1da5bbf8bac0ea46d Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Thu, 29 Nov 2018 19:18:44 +0200 Subject: [PATCH] Migrate to Stylet and refactor view/view-model framework --- DiscordChatExporter.Cli/Container.cs | 32 +- .../DiscordChatExporter.Cli.csproj | 5 +- .../Verbs/ExportChatVerb.cs | 7 +- .../Verbs/GetChannelsVerb.cs | 3 +- .../Verbs/GetDirectMessageChannelsVerb.cs | 3 +- .../Verbs/GetGuildsVerb.cs | 3 +- .../DiscordChatExporter.Core.csproj | 6 +- .../Services/DataService.cs | 2 +- .../Services/ExportService.cs | 6 +- .../Services/IDataService.cs | 34 --- .../Services/IExportService.cs | 10 - .../Services/ISettingsService.cs | 19 -- .../Services/IUpdateService.cs | 14 - .../Services/SettingsService.cs | 2 +- .../Services/UpdateService.cs | 37 ++- DiscordChatExporter.Gui/App.xaml | 20 +- DiscordChatExporter.Gui/App.xaml.cs | 11 +- DiscordChatExporter.Gui/Bootstrapper.cs | 34 +++ DiscordChatExporter.Gui/Container.cs | 36 --- .../ExportFormatToStringConverter.cs | 6 +- .../DiscordChatExporter.Gui.csproj | 53 ++-- DiscordChatExporter.Gui/FodyWeavers.xml | 4 + DiscordChatExporter.Gui/FodyWeavers.xsd | 49 +++ .../Messages/ShowExportSetupMessage.cs | 17 -- .../Messages/ShowNotificationMessage.cs | 25 -- .../Messages/ShowSettingsMessage.cs | 6 - .../Messages/StartExportMessage.cs | 31 -- .../Dialogs/ExportSetupViewModel.cs | 77 +++++ .../{ => Dialogs}/SettingsViewModel.cs | 10 +- .../ViewModels/ExportSetupViewModel.cs | 113 ------- .../ViewModels/Framework/DialogManager.cs | 58 ++++ .../ViewModels/Framework/DialogScreen.cs | 26 ++ .../ViewModels/Framework/IViewModelFactory.cs | 12 + .../ViewModels/IExportSetupViewModel.cs | 21 -- .../ViewModels/IMainViewModel.cs | 29 -- .../ViewModels/ISettingsViewModel.cs | 10 - .../ViewModels/MainViewModel.cs | 280 ------------------ .../ViewModels/RootViewModel.cs | 227 ++++++++++++++ .../ExportSetupView.xaml} | 48 ++- .../Views/Dialogs/ExportSetupView.xaml.cs | 10 + .../SettingsView.xaml} | 19 +- .../Views/Dialogs/SettingsView.xaml.cs | 10 + .../Views/ExportSetupDialog.xaml.cs | 44 --- .../Views/MainWindow.xaml.cs | 35 --- .../Views/{MainWindow.xaml => RootView.xaml} | 55 ++-- .../Views/RootView.xaml.cs | 10 + .../Views/SettingsDialog.xaml.cs | 10 - DiscordChatExporter.Gui/app.config | 11 - Readme.md | 3 +- 49 files changed, 672 insertions(+), 921 deletions(-) delete mode 100644 DiscordChatExporter.Core/Services/IDataService.cs delete mode 100644 DiscordChatExporter.Core/Services/IExportService.cs delete mode 100644 DiscordChatExporter.Core/Services/ISettingsService.cs delete mode 100644 DiscordChatExporter.Core/Services/IUpdateService.cs create mode 100644 DiscordChatExporter.Gui/Bootstrapper.cs delete mode 100644 DiscordChatExporter.Gui/Container.cs create mode 100644 DiscordChatExporter.Gui/FodyWeavers.xml create mode 100644 DiscordChatExporter.Gui/FodyWeavers.xsd delete mode 100644 DiscordChatExporter.Gui/Messages/ShowExportSetupMessage.cs delete mode 100644 DiscordChatExporter.Gui/Messages/ShowNotificationMessage.cs delete mode 100644 DiscordChatExporter.Gui/Messages/ShowSettingsMessage.cs delete mode 100644 DiscordChatExporter.Gui/Messages/StartExportMessage.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs rename DiscordChatExporter.Gui/ViewModels/{ => Dialogs}/SettingsViewModel.cs (70%) delete mode 100644 DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/Framework/DialogScreen.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs delete mode 100644 DiscordChatExporter.Gui/ViewModels/IExportSetupViewModel.cs delete mode 100644 DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs delete mode 100644 DiscordChatExporter.Gui/ViewModels/ISettingsViewModel.cs delete mode 100644 DiscordChatExporter.Gui/ViewModels/MainViewModel.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/RootViewModel.cs rename DiscordChatExporter.Gui/Views/{ExportSetupDialog.xaml => Dialogs/ExportSetupView.xaml} (64%) create mode 100644 DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml.cs rename DiscordChatExporter.Gui/Views/{SettingsDialog.xaml => Dialogs/SettingsView.xaml} (65%) create mode 100644 DiscordChatExporter.Gui/Views/Dialogs/SettingsView.xaml.cs delete mode 100644 DiscordChatExporter.Gui/Views/ExportSetupDialog.xaml.cs delete mode 100644 DiscordChatExporter.Gui/Views/MainWindow.xaml.cs rename DiscordChatExporter.Gui/Views/{MainWindow.xaml => RootView.xaml} (87%) create mode 100644 DiscordChatExporter.Gui/Views/RootView.xaml.cs delete mode 100644 DiscordChatExporter.Gui/Views/SettingsDialog.xaml.cs delete mode 100644 DiscordChatExporter.Gui/app.config diff --git a/DiscordChatExporter.Cli/Container.cs b/DiscordChatExporter.Cli/Container.cs index dea35154..091c60e4 100644 --- a/DiscordChatExporter.Cli/Container.cs +++ b/DiscordChatExporter.Cli/Container.cs @@ -1,26 +1,24 @@ -using CommonServiceLocator; -using DiscordChatExporter.Core.Services; -using GalaSoft.MvvmLight.Ioc; +using DiscordChatExporter.Core.Services; +using StyletIoC; namespace DiscordChatExporter.Cli { - public class Container + public static class Container { - public Container() - { - ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); - SimpleIoc.Default.Reset(); + public static IContainer Instance { get; } - // Services - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - } - - public T Resolve(string key = null) + static Container() { - return ServiceLocator.Current.GetInstance(key); + var builder = new StyletIoCBuilder(); + + // Autobind services in the .Core assembly + builder.Autobind(typeof(DataService).Assembly); + + // Bind settings as singleton + builder.Bind().ToSelf().InSingletonScope(); + + // Set instance + Instance = builder.BuildContainer(); } } } \ No newline at end of file diff --git a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj index 3dcbc662..ad39ac0e 100644 --- a/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj +++ b/DiscordChatExporter.Cli/DiscordChatExporter.Cli.csproj @@ -10,9 +10,8 @@ - - - + + diff --git a/DiscordChatExporter.Cli/Verbs/ExportChatVerb.cs b/DiscordChatExporter.Cli/Verbs/ExportChatVerb.cs index 653d5017..25524d7f 100644 --- a/DiscordChatExporter.Cli/Verbs/ExportChatVerb.cs +++ b/DiscordChatExporter.Cli/Verbs/ExportChatVerb.cs @@ -18,10 +18,9 @@ namespace DiscordChatExporter.Cli.Verbs public override async Task ExecuteAsync() { // Get services - var container = new Container(); - var settingsService = container.Resolve(); - var dataService = container.Resolve(); - var exportService = container.Resolve(); + var settingsService = Container.Instance.Get(); + var dataService = Container.Instance.Get(); + var exportService = Container.Instance.Get(); // Configure settings if (Options.DateFormat.IsNotBlank()) diff --git a/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs index 36cdd6a7..69e7ed89 100644 --- a/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs +++ b/DiscordChatExporter.Cli/Verbs/GetChannelsVerb.cs @@ -18,8 +18,7 @@ namespace DiscordChatExporter.Cli.Verbs public override async Task ExecuteAsync() { // Get data service - var container = new Container(); - var dataService = container.Resolve(); + var dataService = Container.Instance.Get(); // Get channels var channels = await dataService.GetGuildChannelsAsync(Options.GetToken(), Options.GuildId); diff --git a/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs index 020a8142..6731d015 100644 --- a/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs +++ b/DiscordChatExporter.Cli/Verbs/GetDirectMessageChannelsVerb.cs @@ -16,8 +16,7 @@ namespace DiscordChatExporter.Cli.Verbs public override async Task ExecuteAsync() { // Get data service - var container = new Container(); - var dataService = container.Resolve(); + var dataService = Container.Instance.Get(); // Get channels var channels = await dataService.GetDirectMessageChannelsAsync(Options.GetToken()); diff --git a/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs b/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs index 309e1aec..79fa4ad8 100644 --- a/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs +++ b/DiscordChatExporter.Cli/Verbs/GetGuildsVerb.cs @@ -16,8 +16,7 @@ namespace DiscordChatExporter.Cli.Verbs public override async Task ExecuteAsync() { // Get data service - var container = new Container(); - var dataService = container.Resolve(); + var dataService = Container.Instance.Get(); // Get guilds var guilds = await dataService.GetUserGuildsAsync(Options.GetToken()); diff --git a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj index 0e566987..3768b087 100644 --- a/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj +++ b/DiscordChatExporter.Core/DiscordChatExporter.Core.csproj @@ -21,10 +21,10 @@ - - + + - + diff --git a/DiscordChatExporter.Core/Services/DataService.cs b/DiscordChatExporter.Core/Services/DataService.cs index 452ca850..5b1f6689 100644 --- a/DiscordChatExporter.Core/Services/DataService.cs +++ b/DiscordChatExporter.Core/Services/DataService.cs @@ -13,7 +13,7 @@ using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Services { - public partial class DataService : IDataService, IDisposable + public partial class DataService : IDisposable { private readonly HttpClient _httpClient = new HttpClient(); diff --git a/DiscordChatExporter.Core/Services/ExportService.cs b/DiscordChatExporter.Core/Services/ExportService.cs index 896b9746..055e6be5 100644 --- a/DiscordChatExporter.Core/Services/ExportService.cs +++ b/DiscordChatExporter.Core/Services/ExportService.cs @@ -7,11 +7,11 @@ using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Services { - public partial class ExportService : IExportService + public partial class ExportService { - private readonly ISettingsService _settingsService; + private readonly SettingsService _settingsService; - public ExportService(ISettingsService settingsService) + public ExportService(SettingsService settingsService) { _settingsService = settingsService; } diff --git a/DiscordChatExporter.Core/Services/IDataService.cs b/DiscordChatExporter.Core/Services/IDataService.cs deleted file mode 100644 index 0822d237..00000000 --- a/DiscordChatExporter.Core/Services/IDataService.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Services -{ - public interface IDataService - { - Task GetGuildAsync(AuthToken token, string guildId); - - Task GetChannelAsync(AuthToken token, string channelId); - - Task> GetUserGuildsAsync(AuthToken token); - - Task> GetDirectMessageChannelsAsync(AuthToken token); - - Task> GetGuildChannelsAsync(AuthToken token, string guildId); - - Task> GetGuildRolesAsync(AuthToken token, string guildId); - - Task> GetChannelMessagesAsync(AuthToken token, string channelId, - DateTime? from = null, DateTime? to = null, IProgress progress = null); - - Task GetMentionablesAsync(AuthToken token, string guildId, - IEnumerable messages); - - Task GetChatLogAsync(AuthToken token, Guild guild, Channel channel, - DateTime? from = null, DateTime? to = null, IProgress progress = null); - - Task GetChatLogAsync(AuthToken token, string channelId, - DateTime? from = null, DateTime? to = null, IProgress progress = null); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/IExportService.cs b/DiscordChatExporter.Core/Services/IExportService.cs deleted file mode 100644 index f3d07382..00000000 --- a/DiscordChatExporter.Core/Services/IExportService.cs +++ /dev/null @@ -1,10 +0,0 @@ -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Services -{ - public interface IExportService - { - void ExportChatLog(ChatLog chatLog, string filePath, ExportFormat format, - int? partitionLimit = null); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/ISettingsService.cs b/DiscordChatExporter.Core/Services/ISettingsService.cs deleted file mode 100644 index ec50c89f..00000000 --- a/DiscordChatExporter.Core/Services/ISettingsService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Core.Services -{ - public interface ISettingsService - { - bool IsAutoUpdateEnabled { get; set; } - - string DateFormat { get; set; } - int MessageGroupLimit { get; set; } - - AuthToken LastToken { get; set; } - ExportFormat LastExportFormat { get; set; } - int? LastPartitionLimit { get; set; } - - void Load(); - void Save(); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/IUpdateService.cs b/DiscordChatExporter.Core/Services/IUpdateService.cs deleted file mode 100644 index 7e9d071f..00000000 --- a/DiscordChatExporter.Core/Services/IUpdateService.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace DiscordChatExporter.Core.Services -{ - public interface IUpdateService - { - bool NeedRestart { get; set; } - - Task CheckPrepareUpdateAsync(); - - void FinalizeUpdate(); - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Core/Services/SettingsService.cs b/DiscordChatExporter.Core/Services/SettingsService.cs index 11762796..611dba7e 100644 --- a/DiscordChatExporter.Core/Services/SettingsService.cs +++ b/DiscordChatExporter.Core/Services/SettingsService.cs @@ -3,7 +3,7 @@ using Tyrrrz.Settings; namespace DiscordChatExporter.Core.Services { - public class SettingsService : SettingsManager, ISettingsService + public class SettingsService : SettingsManager { public bool IsAutoUpdateEnabled { get; set; } = true; diff --git a/DiscordChatExporter.Core/Services/UpdateService.cs b/DiscordChatExporter.Core/Services/UpdateService.cs index e0cf78ec..f2c66b91 100644 --- a/DiscordChatExporter.Core/Services/UpdateService.cs +++ b/DiscordChatExporter.Core/Services/UpdateService.cs @@ -5,23 +5,20 @@ using Onova.Services; namespace DiscordChatExporter.Core.Services { - public class UpdateService : IUpdateService + public class UpdateService { - private readonly ISettingsService _settingsService; - private readonly IUpdateManager _manager; + private readonly SettingsService _settingsService; + + private readonly IUpdateManager _updateManager = new UpdateManager( + new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"), + new ZipPackageExtractor()); private Version _updateVersion; - private bool _updateFinalized; + private bool _updaterLaunched; - public bool NeedRestart { get; set; } - - public UpdateService(ISettingsService settingsService) + public UpdateService(SettingsService settingsService) { _settingsService = settingsService; - - _manager = new UpdateManager( - new GithubPackageResolver("Tyrrrz", "DiscordChatExporter", "DiscordChatExporter.zip"), - new ZipPackageExtractor()); } public async Task CheckPrepareUpdateAsync() @@ -31,33 +28,33 @@ namespace DiscordChatExporter.Core.Services return null; // Cleanup leftover files - _manager.Cleanup(); + _updateManager.Cleanup(); // Check for updates - var check = await _manager.CheckForUpdatesAsync(); + var check = await _updateManager.CheckForUpdatesAsync(); if (!check.CanUpdate) return null; // Prepare the update - if (!_manager.IsUpdatePrepared(check.LastVersion)) - await _manager.PrepareUpdateAsync(check.LastVersion); + if (!_updateManager.IsUpdatePrepared(check.LastVersion)) + await _updateManager.PrepareUpdateAsync(check.LastVersion); return _updateVersion = check.LastVersion; } - public void FinalizeUpdate() + public void FinalizeUpdate(bool needRestart) { // Check if an update is pending if (_updateVersion == null) return; - // Check if the update has already been finalized - if (_updateFinalized) + // Check if the updater has already been launched + if (_updaterLaunched) return; // Launch the updater - _manager.LaunchUpdater(_updateVersion, NeedRestart); - _updateFinalized = true; + _updateManager.LaunchUpdater(_updateVersion, needRestart); + _updaterLaunched = true; } } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/App.xaml b/DiscordChatExporter.Gui/App.xaml index de9d06c4..b571f865 100644 --- a/DiscordChatExporter.Gui/App.xaml +++ b/DiscordChatExporter.Gui/App.xaml @@ -2,16 +2,19 @@ x:Class="DiscordChatExporter.Gui.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters" xmlns:local="clr-namespace:DiscordChatExporter.Gui" - DispatcherUnhandledException="App_OnDispatcherUnhandledException" - StartupUri="Views/MainWindow.xaml"> + xmlns:s="https://github.com/canton7/Stylet"> - + + + + + + + - @@ -110,11 +113,6 @@ - - - - - - + \ No newline at end of file diff --git a/DiscordChatExporter.Gui/App.xaml.cs b/DiscordChatExporter.Gui/App.xaml.cs index d5158a8b..381f7344 100644 --- a/DiscordChatExporter.Gui/App.xaml.cs +++ b/DiscordChatExporter.Gui/App.xaml.cs @@ -1,13 +1,6 @@ -using System.Windows; -using System.Windows.Threading; - -namespace DiscordChatExporter.Gui +namespace DiscordChatExporter.Gui { public partial class App - { - private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs args) - { - MessageBox.Show(args.Exception.ToString(), "Error occured", MessageBoxButton.OK, MessageBoxImage.Error); - } + { } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Bootstrapper.cs b/DiscordChatExporter.Gui/Bootstrapper.cs new file mode 100644 index 00000000..9bd43a72 --- /dev/null +++ b/DiscordChatExporter.Gui/Bootstrapper.cs @@ -0,0 +1,34 @@ +using System.Windows; +using System.Windows.Threading; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Gui.ViewModels; +using DiscordChatExporter.Gui.ViewModels.Framework; +using Stylet; +using StyletIoC; + +namespace DiscordChatExporter.Gui +{ + public class Bootstrapper : Bootstrapper + { + protected override void ConfigureIoC(IStyletIoCBuilder builder) + { + base.ConfigureIoC(builder); + + // Autobind services in the .Core assembly + builder.Autobind(typeof(DataService).Assembly); + + // Bind settings as singleton + builder.Bind().ToSelf().InSingletonScope(); + + // Bind view model factory + builder.Bind().ToAbstractFactory(); + } + + protected override void OnUnhandledException(DispatcherUnhandledExceptionEventArgs e) + { + base.OnUnhandledException(e); + + MessageBox.Show(e.Exception.ToString(), "Error occured", MessageBoxButton.OK, MessageBoxImage.Error); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Container.cs b/DiscordChatExporter.Gui/Container.cs deleted file mode 100644 index f47cc474..00000000 --- a/DiscordChatExporter.Gui/Container.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CommonServiceLocator; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Gui.ViewModels; -using GalaSoft.MvvmLight.Ioc; - -namespace DiscordChatExporter.Gui -{ - public class Container - { - public IExportSetupViewModel ExportSetupViewModel => Resolve(); - public IMainViewModel MainViewModel => Resolve(); - public ISettingsViewModel SettingsViewModel => Resolve(); - - public Container() - { - ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default); - SimpleIoc.Default.Reset(); - - // Services - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - SimpleIoc.Default.Register(); - - // View models - SimpleIoc.Default.Register(true); - SimpleIoc.Default.Register(true); - SimpleIoc.Default.Register(true); - } - - private T Resolve(string key = null) - { - return ServiceLocator.Current.GetInstance(key); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs index 45d2e442..37c287bd 100644 --- a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs +++ b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs @@ -8,10 +8,12 @@ namespace DiscordChatExporter.Gui.Converters [ValueConversion(typeof(ExportFormat), typeof(string))] public class ExportFormatToStringConverter : IValueConverter { + public static ExportFormatToStringConverter Instance { get; } = new ExportFormatToStringConverter(); + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var format = (ExportFormat) value; - return format.GetDisplayName(); + var format = (ExportFormat?) value; + return format?.GetDisplayName(); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index 6e867229..3b3f14c1 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -56,26 +56,22 @@ App.xaml + - - - - - - - - - - - - - ExportSetupDialog.xaml + + + + + + + + ExportSetupView.xaml - - MainWindow.xaml + + RootView.xaml - - SettingsDialog.xaml + + SettingsView.xaml @@ -91,7 +87,6 @@ ResXFileCodeGenerator Resources.Designer.cs - @@ -107,38 +102,36 @@ MSBuild:Compile Designer - + Designer MSBuild:Compile - + Designer MSBuild:Compile - + Designer MSBuild:Compile - - 2.0.3 - 1.1.3 - 2.4.0.1044 + 2.5.0.1205 - - 5.4.1 + + 2.6.0 + + + 1.1.22 1.5.1 - - 1.0.5 - + \ No newline at end of file diff --git a/DiscordChatExporter.Gui/FodyWeavers.xml b/DiscordChatExporter.Gui/FodyWeavers.xml new file mode 100644 index 00000000..4e68ed1a --- /dev/null +++ b/DiscordChatExporter.Gui/FodyWeavers.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/DiscordChatExporter.Gui/FodyWeavers.xsd b/DiscordChatExporter.Gui/FodyWeavers.xsd new file mode 100644 index 00000000..a608e3f5 --- /dev/null +++ b/DiscordChatExporter.Gui/FodyWeavers.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + Used to control if the On_PropertyName_Changed feature is enabled. + + + + + Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form. + + + + + Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project. + + + + + Used to control if equality checks should use the Equals method resolved from the base class. + + + + + Used to control if equality checks should use the static Equals method resolved from the base class. + + + + + + + + 'true' to run assembly verification on the target assembly after all weavers have been finished. + + + + + A comma separated list of error codes that can be safely ignored in assembly verification. + + + + + \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Messages/ShowExportSetupMessage.cs b/DiscordChatExporter.Gui/Messages/ShowExportSetupMessage.cs deleted file mode 100644 index 380c8495..00000000 --- a/DiscordChatExporter.Gui/Messages/ShowExportSetupMessage.cs +++ /dev/null @@ -1,17 +0,0 @@ -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Gui.Messages -{ - public class ShowExportSetupMessage - { - public Guild Guild { get; } - - public Channel Channel { get; } - - public ShowExportSetupMessage(Guild guild, Channel channel) - { - Guild = guild; - Channel = channel; - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Messages/ShowNotificationMessage.cs b/DiscordChatExporter.Gui/Messages/ShowNotificationMessage.cs deleted file mode 100644 index 9c2285a5..00000000 --- a/DiscordChatExporter.Gui/Messages/ShowNotificationMessage.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; - -namespace DiscordChatExporter.Gui.Messages -{ - public class ShowNotificationMessage - { - public string Message { get; } - - public string CallbackCaption { get; } - - public Action Callback { get; } - - public ShowNotificationMessage(string message) - { - Message = message; - } - - public ShowNotificationMessage(string message, string callbackCaption, Action callback) - : this(message) - { - CallbackCaption = callbackCaption; - Callback = callback; - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Messages/ShowSettingsMessage.cs b/DiscordChatExporter.Gui/Messages/ShowSettingsMessage.cs deleted file mode 100644 index 61c35d95..00000000 --- a/DiscordChatExporter.Gui/Messages/ShowSettingsMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace DiscordChatExporter.Gui.Messages -{ - public class ShowSettingsMessage - { - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Messages/StartExportMessage.cs b/DiscordChatExporter.Gui/Messages/StartExportMessage.cs deleted file mode 100644 index 2a115be9..00000000 --- a/DiscordChatExporter.Gui/Messages/StartExportMessage.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using DiscordChatExporter.Core.Models; - -namespace DiscordChatExporter.Gui.Messages -{ - public class StartExportMessage - { - public Channel Channel { get; } - - public string FilePath { get; } - - public ExportFormat Format { get; } - - public DateTime? From { get; } - - public DateTime? To { get; } - - public int? PartitionLimit { get; } - - public StartExportMessage(Channel channel, string filePath, ExportFormat format, - DateTime? from, DateTime? to, int? partitionLimit) - { - Channel = channel; - FilePath = filePath; - Format = format; - From = from; - To = to; - PartitionLimit = partitionLimit; - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs new file mode 100644 index 00000000..38328282 --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Gui.ViewModels.Framework; +using Tyrrrz.Extensions; + +namespace DiscordChatExporter.Gui.ViewModels.Dialogs +{ + public class ExportSetupViewModel : DialogScreen + { + private readonly DialogManager _dialogManager; + private readonly SettingsService _settingsService; + + public Guild Guild { get; set; } + + public Channel Channel { get; set; } + + public string FilePath { get; set; } + + public IReadOnlyList AvailableFormats => + Enum.GetValues(typeof(ExportFormat)).Cast().ToArray(); + + public ExportFormat SelectedFormat { get; set; } = ExportFormat.HtmlDark; + + public DateTime? From { get; set; } + + public DateTime? To { get; set; } + + public int? PartitionLimit { get; set; } + + public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService) + { + _dialogManager = dialogManager; + _settingsService = settingsService; + } + + protected override void OnViewLoaded() + { + base.OnViewLoaded(); + + // Persist preferences + SelectedFormat = _settingsService.LastExportFormat; + PartitionLimit = _settingsService.LastPartitionLimit; + } + + public void Confirm() + { + // Persist preferences + _settingsService.LastExportFormat = SelectedFormat; + _settingsService.LastPartitionLimit = PartitionLimit; + + // Clamp 'from' and 'to' values + if (From > To) + From = To; + if (To < From) + To = From; + + // Generate default file name + var ext = SelectedFormat.GetFileExtension(); + var defaultFileName = $"{Guild.Name} - {Channel.Name}.{ext}".Replace(Path.GetInvalidFileNameChars(), '_'); + + // Prompt for output file path + var filter = $"{ext.ToUpperInvariant()} files|*.{ext}"; + FilePath = _dialogManager.PromptSaveFilePath(filter, defaultFileName); + + // If canceled - return + if (FilePath.IsBlank()) + return; + + // Close dialog + Close(true); + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/SettingsViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs similarity index 70% rename from DiscordChatExporter.Gui/ViewModels/SettingsViewModel.cs rename to DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs index 0d9cbe19..47c3a3d1 100644 --- a/DiscordChatExporter.Gui/ViewModels/SettingsViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/SettingsViewModel.cs @@ -1,12 +1,12 @@ using DiscordChatExporter.Core.Services; -using GalaSoft.MvvmLight; +using DiscordChatExporter.Gui.ViewModels.Framework; using Tyrrrz.Extensions; -namespace DiscordChatExporter.Gui.ViewModels +namespace DiscordChatExporter.Gui.ViewModels.Dialogs { - public class SettingsViewModel : ViewModelBase, ISettingsViewModel + public class SettingsViewModel : DialogScreen { - private readonly ISettingsService _settingsService; + private readonly SettingsService _settingsService; public bool IsAutoUpdateEnabled { @@ -26,7 +26,7 @@ namespace DiscordChatExporter.Gui.ViewModels set => _settingsService.MessageGroupLimit = value.ClampMin(0); } - public SettingsViewModel(ISettingsService settingsService) + public SettingsViewModel(SettingsService settingsService) { _settingsService = settingsService; } diff --git a/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs deleted file mode 100644 index df56550b..00000000 --- a/DiscordChatExporter.Gui/ViewModels/ExportSetupViewModel.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Gui.Messages; -using GalaSoft.MvvmLight; -using GalaSoft.MvvmLight.CommandWpf; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Gui.ViewModels -{ - public class ExportSetupViewModel : ViewModelBase, IExportSetupViewModel - { - private readonly ISettingsService _settingsService; - - private string _filePath; - private ExportFormat _format; - private DateTime? _from; - private DateTime? _to; - private int? _partitionLimit; - - public Guild Guild { get; private set; } - - public Channel Channel { get; private set; } - - public string FilePath - { - get => _filePath; - set - { - Set(ref _filePath, value); - ExportCommand.RaiseCanExecuteChanged(); - } - } - - public IReadOnlyList AvailableFormats => - Enum.GetValues(typeof(ExportFormat)).Cast().ToArray(); - - public ExportFormat SelectedFormat - { - get => _format; - set - { - Set(ref _format, value); - - // Replace extension in path - var ext = value.GetFileExtension(); - if (FilePath != null) - FilePath = Path.ChangeExtension(FilePath, ext); - } - } - - public DateTime? From - { - get => _from; - set => Set(ref _from, value); - } - - public DateTime? To - { - get => _to; - set => Set(ref _to, value); - } - - public int? PartitionLimit - { - get => _partitionLimit; - set => Set(ref _partitionLimit, value); - } - - // Commands - public RelayCommand ExportCommand { get; } - - public ExportSetupViewModel(ISettingsService settingsService) - { - _settingsService = settingsService; - - // Commands - ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank()); - - // Messages - MessengerInstance.Register(this, m => - { - Guild = m.Guild; - Channel = m.Channel; - SelectedFormat = _settingsService.LastExportFormat; - FilePath = $"{Guild.Name} - {Channel.Name}.{SelectedFormat.GetFileExtension()}" - .Replace(Path.GetInvalidFileNameChars(), '_'); - From = null; - To = null; - PartitionLimit = _settingsService.LastPartitionLimit; - }); - } - - private void Export() - { - // Persist preferences - _settingsService.LastExportFormat = SelectedFormat; - _settingsService.LastPartitionLimit = PartitionLimit; - - // Clamp 'from' and 'to' values - if (From > To) - From = To; - if (To < From) - To = From; - - // Start export - MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To, PartitionLimit)); - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs b/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs new file mode 100644 index 00000000..a7f60d4d --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Threading.Tasks; +using MaterialDesignThemes.Wpf; +using Microsoft.Win32; +using Stylet; + +namespace DiscordChatExporter.Gui.ViewModels.Framework +{ + public class DialogManager + { + private readonly IViewManager _viewManager; + + public DialogManager(IViewManager viewManager) + { + _viewManager = viewManager; + } + + public async Task ShowDialogAsync(DialogScreen dialogScreen) + { + // Get the view that renders this viewmodel + var view = _viewManager.CreateAndBindViewForModelIfNecessary(dialogScreen); + + // Set up event routing that will close the view when called from viewmodel + DialogOpenedEventHandler onDialogOpened = (sender, e) => + { + // Delegate to close the dialog and unregister event handler + void OnScreenClosed(object o, CloseEventArgs args) + { + e.Session.Close(); + dialogScreen.Closed -= OnScreenClosed; + } + + dialogScreen.Closed += OnScreenClosed; + }; + + // Show view + await DialogHost.Show(view, onDialogOpened); + + // Return the result + return dialogScreen.DialogResult; + } + + public string PromptSaveFilePath(string filter = "All files|*.*", string initialFilePath = "") + { + // Create dialog + var dialog = new SaveFileDialog + { + Filter = filter, + AddExtension = true, + FileName = initialFilePath, + DefaultExt = Path.GetExtension(initialFilePath) ?? "" + }; + + // Show dialog and return result + return dialog.ShowDialog() == true ? dialog.FileName : null; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/DialogScreen.cs b/DiscordChatExporter.Gui/ViewModels/Framework/DialogScreen.cs new file mode 100644 index 00000000..8df6226b --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/Framework/DialogScreen.cs @@ -0,0 +1,26 @@ +using Stylet; + +namespace DiscordChatExporter.Gui.ViewModels.Framework +{ + public abstract class DialogScreen : Screen + { + public T DialogResult { get; private set; } + + public void Close(T dialogResult = default(T)) + { + // Set the result + DialogResult = dialogResult; + + // If there is a parent - ask them to close this dialog + if (Parent != null) + RequestClose(Equals(dialogResult, default(T))); + // Otherwise close ourselves + else + ((IScreenState) this).Close(); + } + } + + public abstract class DialogScreen : DialogScreen + { + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs b/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs new file mode 100644 index 00000000..e741b6d3 --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/Framework/IViewModelFactory.cs @@ -0,0 +1,12 @@ +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 + { + ExportSetupViewModel CreateExportSetupViewModel(); + + SettingsViewModel CreateSettingsViewModel(); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/IExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/IExportSetupViewModel.cs deleted file mode 100644 index 7179da40..00000000 --- a/DiscordChatExporter.Gui/ViewModels/IExportSetupViewModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using DiscordChatExporter.Core.Models; -using GalaSoft.MvvmLight.CommandWpf; - -namespace DiscordChatExporter.Gui.ViewModels -{ - public interface IExportSetupViewModel - { - Guild Guild { get; } - Channel Channel { get; } - string FilePath { get; set; } - IReadOnlyList AvailableFormats { get; } - ExportFormat SelectedFormat { get; set; } - DateTime? From { get; set; } - DateTime? To { get; set; } - int? PartitionLimit { get; set; } - - RelayCommand ExportCommand { get; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs deleted file mode 100644 index 37b48abc..00000000 --- a/DiscordChatExporter.Gui/ViewModels/IMainViewModel.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using DiscordChatExporter.Core.Models; -using GalaSoft.MvvmLight.CommandWpf; - -namespace DiscordChatExporter.Gui.ViewModels -{ - public interface IMainViewModel - { - bool IsBusy { get; } - bool IsDataAvailable { get; } - - bool IsProgressIndeterminate { get; } - double Progress { get; } - - bool IsBotToken { get; set; } - string TokenValue { get; set; } - - IReadOnlyList AvailableGuilds { get; } - Guild SelectedGuild { get; set; } - IReadOnlyList AvailableChannels { get; } - - RelayCommand ViewLoadedCommand { get; } - RelayCommand ViewClosedCommand { get; } - RelayCommand PullDataCommand { get; } - RelayCommand ShowSettingsCommand { get; } - RelayCommand ShowAboutCommand { get; } - RelayCommand ShowExportSetupCommand { get; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/ISettingsViewModel.cs b/DiscordChatExporter.Gui/ViewModels/ISettingsViewModel.cs deleted file mode 100644 index aef5ba3d..00000000 --- a/DiscordChatExporter.Gui/ViewModels/ISettingsViewModel.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace DiscordChatExporter.Gui.ViewModels -{ - public interface ISettingsViewModel - { - bool IsAutoUpdateEnabled { get; set; } - - string DateFormat { get; set; } - int MessageGroupLimit { get; set; } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs b/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs deleted file mode 100644 index c138ecde..00000000 --- a/DiscordChatExporter.Gui/ViewModels/MainViewModel.cs +++ /dev/null @@ -1,280 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Windows; -using DiscordChatExporter.Core.Exceptions; -using DiscordChatExporter.Core.Models; -using DiscordChatExporter.Core.Services; -using DiscordChatExporter.Gui.Messages; -using GalaSoft.MvvmLight; -using GalaSoft.MvvmLight.CommandWpf; -using Tyrrrz.Extensions; - -namespace DiscordChatExporter.Gui.ViewModels -{ - public class MainViewModel : ViewModelBase, IMainViewModel - { - private readonly ISettingsService _settingsService; - private readonly IUpdateService _updateService; - private readonly IDataService _dataService; - private readonly IExportService _exportService; - - private readonly Dictionary> _guildChannelsMap; - - private bool _isBusy; - private double _progress; - private bool _isBotToken; - private string _tokenValue; - private IReadOnlyList _availableGuilds; - private Guild _selectedGuild; - private IReadOnlyList _availableChannels; - - public bool IsBusy - { - get => _isBusy; - private set - { - Set(ref _isBusy, value); - PullDataCommand.RaiseCanExecuteChanged(); - ShowExportSetupCommand.RaiseCanExecuteChanged(); - } - } - - public bool IsDataAvailable => AvailableGuilds.NotNullAndAny(); - - public bool IsProgressIndeterminate => Progress <= 0; - - public double Progress - { - get => _progress; - private set - { - Set(ref _progress, value); - RaisePropertyChanged(() => IsProgressIndeterminate); - } - } - - public bool IsBotToken - { - get => _isBotToken; - set => Set(ref _isBotToken, value); - } - - public string TokenValue - { - get => _tokenValue; - set - { - // Remove invalid chars - value = value?.Trim('"'); - - Set(ref _tokenValue, value); - PullDataCommand.RaiseCanExecuteChanged(); - } - } - - public IReadOnlyList AvailableGuilds - { - get => _availableGuilds; - private set - { - Set(ref _availableGuilds, value); - RaisePropertyChanged(() => IsDataAvailable); - } - } - - public Guild SelectedGuild - { - get => _selectedGuild; - set - { - Set(ref _selectedGuild, value); - AvailableChannels = value != null ? _guildChannelsMap[value] : Array.Empty(); - ShowExportSetupCommand.RaiseCanExecuteChanged(); - } - } - - public IReadOnlyList AvailableChannels - { - get => _availableChannels; - private set => Set(ref _availableChannels, value); - } - - public RelayCommand ViewLoadedCommand { get; } - public RelayCommand ViewClosedCommand { get; } - public RelayCommand PullDataCommand { get; } - public RelayCommand ShowSettingsCommand { get; } - public RelayCommand ShowAboutCommand { get; } - public RelayCommand ShowExportSetupCommand { get; } - - public MainViewModel(ISettingsService settingsService, IUpdateService updateService, IDataService dataService, - IExportService exportService) - { - _settingsService = settingsService; - _updateService = updateService; - _dataService = dataService; - _exportService = exportService; - - _guildChannelsMap = new Dictionary>(); - - // Commands - ViewLoadedCommand = new RelayCommand(ViewLoaded); - ViewClosedCommand = new RelayCommand(ViewClosed); - PullDataCommand = new RelayCommand(PullData, () => TokenValue.IsNotBlank() && !IsBusy); - ShowSettingsCommand = new RelayCommand(ShowSettings); - ShowAboutCommand = new RelayCommand(ShowAbout); - ShowExportSetupCommand = new RelayCommand(ShowExportSetup, _ => !IsBusy); - - // Messages - MessengerInstance.Register(this, - m => Export(m.Channel, m.FilePath, m.Format, m.From, m.To, m.PartitionLimit)); - } - - private async void ViewLoaded() - { - // Load settings - _settingsService.Load(); - - // Get last token - if (_settingsService.LastToken != null) - { - IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot; - TokenValue = _settingsService.LastToken.Value; - } - - // Check and prepare update - try - { - var updateVersion = await _updateService.CheckPrepareUpdateAsync(); - if (updateVersion != null) - { - MessengerInstance.Send(new ShowNotificationMessage( - $"Update to DiscordChatExporter v{updateVersion} will be installed when you exit", - "INSTALL NOW", () => - { - _updateService.NeedRestart = true; - Application.Current.Shutdown(); - })); - } - } - catch - { - MessengerInstance.Send(new ShowNotificationMessage("Failed to perform application auto-update")); - } - } - - private void ViewClosed() - { - // Save settings - _settingsService.Save(); - - // Finalize updates if available - _updateService.FinalizeUpdate(); - } - - private async void PullData() - { - IsBusy = true; - - // Create token - var token = new AuthToken( - IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, - TokenValue); - - // Save token - _settingsService.LastToken = token; - - // Clear existing - _guildChannelsMap.Clear(); - - try - { - // Get DM channels - { - var channels = await _dataService.GetDirectMessageChannelsAsync(token); - var guild = Guild.DirectMessages; - _guildChannelsMap[guild] = channels.OrderBy(c => c.Name).ToArray(); - } - - // Get guild channels - { - var guilds = await _dataService.GetUserGuildsAsync(token); - foreach (var guild in guilds) - { - var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id); - _guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat) - .OrderBy(c => c.Name) - .ToArray(); - } - } - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) - { - MessengerInstance.Send(new ShowNotificationMessage("Unauthorized – make sure the token is valid")); - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - MessengerInstance.Send(new ShowNotificationMessage("Forbidden – account may be locked by 2FA")); - } - - AvailableGuilds = _guildChannelsMap.Keys.ToArray(); - SelectedGuild = AvailableGuilds.FirstOrDefault(); - IsBusy = false; - } - - private void ShowSettings() - { - MessengerInstance.Send(new ShowSettingsMessage()); - } - - private void ShowAbout() - { - Process.Start("https://github.com/Tyrrrz/DiscordChatExporter"); - } - - private void ShowExportSetup(Channel channel) - { - MessengerInstance.Send(new ShowExportSetupMessage(SelectedGuild, channel)); - } - - private async void Export(Channel channel, string filePath, ExportFormat format, - DateTime? from, DateTime? to, int? partitionLimit) - { - IsBusy = true; - - // Get last used token - var token = _settingsService.LastToken; - - // Get guild - var guild = SelectedGuild; - - // Create progress handler - var progressHandler = new Progress(p => Progress = p); - - try - { - // Get chat log - var chatLog = await _dataService.GetChatLogAsync(token, guild, channel, from, to, progressHandler); - - // Export - _exportService.ExportChatLog(chatLog, filePath, format, partitionLimit); - - // Notify completion - MessengerInstance.Send(new ShowNotificationMessage("Export complete")); - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) - { - MessengerInstance.Send(new ShowNotificationMessage("You don't have access to this channel")); - } - catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) - { - MessengerInstance.Send(new ShowNotificationMessage("This channel doesn't exist")); - } - - Progress = 0; - IsBusy = false; - } - } -} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs new file mode 100644 index 00000000..a66688e1 --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -0,0 +1,227 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Reflection; +using DiscordChatExporter.Core.Exceptions; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Core.Services; +using DiscordChatExporter.Gui.ViewModels.Framework; +using MaterialDesignThemes.Wpf; +using Stylet; +using Tyrrrz.Extensions; + +namespace DiscordChatExporter.Gui.ViewModels +{ + public class RootViewModel : Screen + { + private readonly IViewModelFactory _viewModelFactory; + private readonly DialogManager _dialogManager; + private readonly SettingsService _settingsService; + private readonly UpdateService _updateService; + private readonly DataService _dataService; + private readonly ExportService _exportService; + + private readonly Dictionary> _guildChannelsMap = + new Dictionary>(); + + public SnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); + + public bool IsEnabled { get; private set; } = true; + + public bool IsProgressIndeterminate => Progress < 0; + + public double Progress { get; private set; } + + public bool IsBotToken { get; set; } + + public string TokenValue { get; set; } + + public IReadOnlyList AvailableGuilds { get; private set; } + + public Guild SelectedGuild { get; set; } + + public IReadOnlyList AvailableChannels => + SelectedGuild != null ? _guildChannelsMap[SelectedGuild] : Array.Empty(); + + public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager, + SettingsService settingsService, UpdateService updateService, DataService dataService, + ExportService exportService) + { + _viewModelFactory = viewModelFactory; + _dialogManager = dialogManager; + _settingsService = settingsService; + _updateService = updateService; + _dataService = dataService; + _exportService = exportService; + + // Set title + var version = Assembly.GetExecutingAssembly().GetName().Version; + DisplayName = $"DiscordChatExporter v{version}"; + } + + protected override async void OnViewLoaded() + { + // Load settings + _settingsService.Load(); + + // Get last token + if (_settingsService.LastToken != null) + { + IsBotToken = _settingsService.LastToken.Type == AuthTokenType.Bot; + TokenValue = _settingsService.LastToken.Value; + } + + // Check and prepare update + try + { + var updateVersion = await _updateService.CheckPrepareUpdateAsync(); + if (updateVersion != null) + { + Notifications.Enqueue( + $"Update to DiscordChatExporter v{updateVersion} will be installed when you exit", + "INSTALL NOW", () => + { + _updateService.FinalizeUpdate(true); + RequestClose(); + }); + } + } + catch + { + Notifications.Enqueue("Failed to perform application auto-update"); + } + } + + protected override void OnClose() + { + // Save settings + _settingsService.Save(); + + // Finalize updates if necessary + _updateService.FinalizeUpdate(false); + } + + public async void ShowSettings() + { + // Create dialog + var dialog = _viewModelFactory.CreateSettingsViewModel(); + + // Show dialog + await _dialogManager.ShowDialogAsync(dialog); + } + + public void ShowAbout() + { + Process.Start("https://github.com/Tyrrrz/DiscordChatExporter"); + } + + public bool CanPopulateGuildsAndChannels => IsEnabled && TokenValue.IsNotBlank(); + + public async void PopulateGuildsAndChannels() + { + IsEnabled = false; + Progress = -1; + + // Sanitize token + TokenValue = TokenValue.Trim('"'); + + // Create token + var token = new AuthToken( + IsBotToken ? AuthTokenType.Bot : AuthTokenType.User, + TokenValue); + + // Save token + _settingsService.LastToken = token; + + // Clear existing + _guildChannelsMap.Clear(); + + try + { + // Get DM channels + { + var channels = await _dataService.GetDirectMessageChannelsAsync(token); + var guild = Guild.DirectMessages; + _guildChannelsMap[guild] = channels.OrderBy(c => c.Name).ToArray(); + } + + // Get guild channels + { + var guilds = await _dataService.GetUserGuildsAsync(token); + foreach (var guild in guilds) + { + var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id); + _guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat) + .OrderBy(c => c.Name) + .ToArray(); + } + } + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) + { + Notifications.Enqueue("Unauthorized – make sure the token is valid"); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + Notifications.Enqueue("Forbidden – account may be locked by 2FA"); + } + + AvailableGuilds = _guildChannelsMap.Keys.ToArray(); + SelectedGuild = AvailableGuilds.FirstOrDefault(); + + Progress = 0; + IsEnabled = true; + } + + public bool CanExportChannel => IsEnabled; + + public async void ExportChannel(Channel channel) + { + IsEnabled = false; + Progress = -1; + + // Get last used token + var token = _settingsService.LastToken; + + // Create dialog + var dialog = _viewModelFactory.CreateExportSetupViewModel(); + dialog.Guild = SelectedGuild; + dialog.Channel = channel; + + // Show dialog + if (await _dialogManager.ShowDialogAsync(dialog) == true) + { + // Export + try + { + // Create progress handler + var progressHandler = new Progress(p => Progress = p); + + // Get chat log + var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, dialog.Channel, + dialog.From, dialog.To, progressHandler); + + // Export + _exportService.ExportChatLog(chatLog, dialog.FilePath, dialog.SelectedFormat, + dialog.PartitionLimit); + + // Notify completion + Notifications.Enqueue("Export complete"); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + Notifications.Enqueue("You don't have access to this channel"); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + Notifications.Enqueue("This channel doesn't exist"); + } + } + + Progress = 0; + IsEnabled = true; + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Views/ExportSetupDialog.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml similarity index 64% rename from DiscordChatExporter.Gui/Views/ExportSetupDialog.xaml rename to DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 5094c989..3348b87b 100644 --- a/DiscordChatExporter.Gui/Views/ExportSetupDialog.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -1,22 +1,26 @@  + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:s="https://github.com/canton7/Stylet" + MinWidth="325" + d:DataContext="{d:DesignInstance Type=dialogs:ExportSetupViewModel}" + SnapsToDevicePixels="True" + TextElement.FontSize="13" + TextElement.FontWeight="Regular" + TextElement.Foreground="{DynamicResource SecondaryTextBrush}" + TextOptions.TextFormattingMode="Ideal" + TextOptions.TextRenderingMode="Auto" + mc:Ignorable="d"> - - - - + @@ -36,22 +40,20 @@ @@ -65,22 +67,14 @@