From e4b0d60c409c1aa839a6bfb3d21f971fd2ce4919 Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Sat, 9 Feb 2019 19:03:34 +0200 Subject: [PATCH] Add multichannel export to GUI Closes #12 --- .../Helpers/ExportHelper.cs | 7 + .../Services/DataService.cs | 3 - ...lViewModelMultiSelectionListBoxBehavior.cs | 8 + .../MultiSelectionListBoxBehavior.cs | 92 ++++++++ .../ExportFormatToStringConverter.cs | 2 +- .../DiscordChatExporter.Gui.csproj | 12 + .../Dialogs/ExportSetupViewModel.cs | 39 +++- .../ViewModels/Framework/DialogManager.cs | 13 ++ .../ViewModels/Framework/Extensions.cs | 44 ++++ .../ViewModels/RootViewModel.cs | 208 ++++++++++-------- .../Views/Dialogs/ExportSetupView.xaml | 23 +- DiscordChatExporter.Gui/Views/RootView.xaml | 47 ++-- Readme.md | 2 + 13 files changed, 365 insertions(+), 135 deletions(-) create mode 100644 DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs create mode 100644 DiscordChatExporter.Gui/Behaviors/MultiSelectionListBoxBehavior.cs create mode 100644 DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs diff --git a/DiscordChatExporter.Core/Helpers/ExportHelper.cs b/DiscordChatExporter.Core/Helpers/ExportHelper.cs index fa90c78..9147d58 100644 --- a/DiscordChatExporter.Core/Helpers/ExportHelper.cs +++ b/DiscordChatExporter.Core/Helpers/ExportHelper.cs @@ -1,12 +1,19 @@ using System; using System.IO; +using System.Linq; using System.Text; using DiscordChatExporter.Core.Models; +using Tyrrrz.Extensions; namespace DiscordChatExporter.Core.Helpers { public static class ExportHelper { + public static bool IsDirectoryPath(string path) + => path.Last() == Path.DirectorySeparatorChar || + path.Last() == Path.AltDirectorySeparatorChar || + Path.GetExtension(path).IsBlank(); + public static string GetDefaultExportFileName(ExportFormat format, Guild guild, Channel channel, DateTime? from = null, DateTime? to = null) { diff --git a/DiscordChatExporter.Core/Services/DataService.cs b/DiscordChatExporter.Core/Services/DataService.cs index dc5ba1d..1a50e1a 100644 --- a/DiscordChatExporter.Core/Services/DataService.cs +++ b/DiscordChatExporter.Core/Services/DataService.cs @@ -121,9 +121,6 @@ namespace DiscordChatExporter.Core.Services { var result = new List(); - // Report indeterminate progress - progress?.Report(-1); - // Get the snowflakes for the selected range var firstId = from != null ? from.Value.ToSnowflake() : "0"; var lastId = to != null ? to.Value.ToSnowflake() : DateTime.MaxValue.ToSnowflake(); diff --git a/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs b/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs new file mode 100644 index 0000000..37a0836 --- /dev/null +++ b/DiscordChatExporter.Gui/Behaviors/ChannelViewModelMultiSelectionListBoxBehavior.cs @@ -0,0 +1,8 @@ +using DiscordChatExporter.Gui.ViewModels.Components; + +namespace DiscordChatExporter.Gui.Behaviors +{ + public class ChannelViewModelMultiSelectionListBoxBehavior : MultiSelectionListBoxBehavior + { + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Behaviors/MultiSelectionListBoxBehavior.cs b/DiscordChatExporter.Gui/Behaviors/MultiSelectionListBoxBehavior.cs new file mode 100644 index 0000000..5f0144c --- /dev/null +++ b/DiscordChatExporter.Gui/Behaviors/MultiSelectionListBoxBehavior.cs @@ -0,0 +1,92 @@ +using System.Collections; +using System.Collections.Specialized; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interactivity; + +namespace DiscordChatExporter.Gui.Behaviors +{ + public class MultiSelectionListBoxBehavior : Behavior + { + public static readonly DependencyProperty SelectedItemsProperty = + DependencyProperty.Register(nameof(SelectedItems), typeof(IList), + typeof(MultiSelectionListBoxBehavior), + new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + OnSelectedItemsChanged)); + + private static void OnSelectedItemsChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) + { + var behavior = (MultiSelectionListBoxBehavior) sender; + if (behavior._modelHandled) return; + + if (behavior.AssociatedObject == null) + return; + + behavior._modelHandled = true; + behavior.SelectItems(); + behavior._modelHandled = false; + } + + private bool _viewHandled; + private bool _modelHandled; + + public IList SelectedItems + { + get => (IList) GetValue(SelectedItemsProperty); + set => SetValue(SelectedItemsProperty, value); + } + + // Propagate selected items from model to view + private void SelectItems() + { + _viewHandled = true; + + AssociatedObject.SelectedItems.Clear(); + if (SelectedItems != null) + { + foreach (var item in SelectedItems) + AssociatedObject.SelectedItems.Add(item); + } + + _viewHandled = false; + } + + // Propagate selected items from view to model + private void OnListBoxSelectionChanged(object sender, SelectionChangedEventArgs args) + { + if (_viewHandled) return; + if (AssociatedObject.Items.SourceCollection == null) return; + + SelectedItems = AssociatedObject.SelectedItems.Cast().ToArray(); + } + + // Re-select items when the set of items changes + private void OnListBoxItemsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (_viewHandled) return; + if (AssociatedObject.Items.SourceCollection == null) return; + SelectItems(); + } + + protected override void OnAttached() + { + base.OnAttached(); + + AssociatedObject.SelectionChanged += OnListBoxSelectionChanged; + ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged += OnListBoxItemsChanged; + } + + /// + protected override void OnDetaching() + { + base.OnDetaching(); + + if (AssociatedObject != null) + { + AssociatedObject.SelectionChanged -= OnListBoxSelectionChanged; + ((INotifyCollectionChanged) AssociatedObject.Items).CollectionChanged -= OnListBoxItemsChanged; + } + } + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs index 37c287b..6a5ddbb 100644 --- a/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs +++ b/DiscordChatExporter.Gui/Converters/ExportFormatToStringConverter.cs @@ -12,7 +12,7 @@ namespace DiscordChatExporter.Gui.Converters public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - var format = (ExportFormat?) value; + var format = value as ExportFormat?; return format?.GetDisplayName(); } diff --git a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj index e42ca76..46df710 100644 --- a/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj +++ b/DiscordChatExporter.Gui/DiscordChatExporter.Gui.csproj @@ -55,6 +55,8 @@ App.xaml + + @@ -62,6 +64,7 @@ + @@ -117,18 +120,27 @@ + + 1.0.2 + 1.1.3 2.5.0.1205 + + 1.0.0 + 2.6.0 1.1.22 + + 2.0.20525 + 1.5.1 diff --git a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs index 9bee0a9..8f40622 100644 --- a/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/Dialogs/ExportSetupViewModel.cs @@ -17,9 +17,11 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs public GuildViewModel Guild { get; set; } - public ChannelViewModel Channel { get; set; } + public IReadOnlyList Channels { get; set; } - public string FilePath { get; set; } + public bool IsSingleChannel => Channels.Count == 1; + + public string OutputPath { get; set; } public IReadOnlyList AvailableFormats => Enum.GetValues(typeof(ExportFormat)).Cast().ToArray(); @@ -59,18 +61,33 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs if (To < From) To = From; - // Generate default file name - var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, Channel, From, To); - - // Prompt for output file path - var ext = SelectedFormat.GetFileExtension(); - var filter = $"{ext.ToUpperInvariant()} files|*.{ext}"; - FilePath = _dialogManager.PromptSaveFilePath(filter, defaultFileName); + // If single channel - prompt file path + if (IsSingleChannel) + { + // Get single channel + var channel = Channels.Single(); + + // Generate default file name + var defaultFileName = ExportHelper.GetDefaultExportFileName(SelectedFormat, Guild, channel, From, To); + + // Generate filter + var ext = SelectedFormat.GetFileExtension(); + var filter = $"{ext.ToUpperInvariant()} files|*.{ext}"; + + // Prompt user + OutputPath = _dialogManager.PromptSaveFilePath(filter, defaultFileName); + } + // If multiple channels - prompt dir path + else + { + // Prompt user + OutputPath = _dialogManager.PromptDirectoryPath(); + } // If canceled - return - if (FilePath.IsBlank()) + if (OutputPath.IsBlank()) return; - + // Close dialog Close(true); } diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs b/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs index a7f60d4..9e5761f 100644 --- a/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs +++ b/DiscordChatExporter.Gui/ViewModels/Framework/DialogManager.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using MaterialDesignThemes.Wpf; using Microsoft.Win32; +using Ookii.Dialogs.Wpf; using Stylet; namespace DiscordChatExporter.Gui.ViewModels.Framework @@ -54,5 +55,17 @@ namespace DiscordChatExporter.Gui.ViewModels.Framework // Show dialog and return result return dialog.ShowDialog() == true ? dialog.FileName : null; } + + public string PromptDirectoryPath(string initialDirPath = "") + { + // Create dialog + var dialog = new VistaFolderBrowserDialog + { + SelectedPath = initialDirPath + }; + + // Show dialog and return result + return dialog.ShowDialog() == true ? dialog.SelectedPath : null; + } } } \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs b/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs new file mode 100644 index 0000000..aa08969 --- /dev/null +++ b/DiscordChatExporter.Gui/ViewModels/Framework/Extensions.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using DiscordChatExporter.Core.Models; +using DiscordChatExporter.Gui.ViewModels.Components; +using DiscordChatExporter.Gui.ViewModels.Dialogs; + +namespace DiscordChatExporter.Gui.ViewModels.Framework +{ + public static class Extensions + { + public static ChannelViewModel CreateChannelViewModel(this IViewModelFactory factory, Channel model, + string category = null) + { + var viewModel = factory.CreateChannelViewModel(); + viewModel.Model = model; + viewModel.Category = category; + + return viewModel; + } + + public static GuildViewModel CreateGuildViewModel(this IViewModelFactory factory, Guild model, + IReadOnlyList channels) + { + var viewModel = factory.CreateGuildViewModel(); + viewModel.Model = model; + viewModel.Channels = channels; + + return viewModel; + } + + public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory, + GuildViewModel guild, IReadOnlyList channels) + { + var viewModel = factory.CreateExportSetupViewModel(); + viewModel.Guild = guild; + viewModel.Channels = channels; + + return viewModel; + } + + public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory, + GuildViewModel guild, ChannelViewModel channel) + => factory.CreateExportSetupViewModel(guild, new[] {channel}); + } +} \ No newline at end of file diff --git a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs index 61b6539..aa9e4ab 100644 --- a/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs +++ b/DiscordChatExporter.Gui/ViewModels/RootViewModel.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Reflection; using DiscordChatExporter.Core.Exceptions; +using DiscordChatExporter.Core.Helpers; using DiscordChatExporter.Core.Models; using DiscordChatExporter.Core.Services; using DiscordChatExporter.Gui.ViewModels.Components; using DiscordChatExporter.Gui.ViewModels.Framework; +using Gress; using MaterialDesignThemes.Wpf; using Stylet; using Tyrrrz.Extensions; @@ -23,13 +26,13 @@ namespace DiscordChatExporter.Gui.ViewModels private readonly DataService _dataService; private readonly ExportService _exportService; - public SnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); + public ISnackbarMessageQueue Notifications { get; } = new SnackbarMessageQueue(TimeSpan.FromSeconds(5)); - public bool IsEnabled { get; private set; } = true; + public IProgressManager ProgressManager { get; } = new ProgressManager(); - public bool IsProgressIndeterminate => Progress < 0; + public bool IsBusy { get; private set; } - public double Progress { get; private set; } + public bool IsProgressIndeterminate { get; private set; } public bool IsBotToken { get; set; } @@ -39,6 +42,8 @@ namespace DiscordChatExporter.Gui.ViewModels public GuildViewModel SelectedGuild { get; set; } + public IReadOnlyList SelectedChannels { get; set; } + public RootViewModel(IViewModelFactory viewModelFactory, DialogManager dialogManager, SettingsService settingsService, UpdateService updateService, DataService dataService, ExportService exportService) @@ -52,7 +57,14 @@ namespace DiscordChatExporter.Gui.ViewModels // Set title var version = Assembly.GetExecutingAssembly().GetName().Version.ToString(3); - DisplayName = $"DiscordChatExporter v{version}"; + DisplayName = $"DiscordChatExporter v{version}"; + + // Update busy state when progress manager changes + ProgressManager.Bind(o => o.IsActive, (sender, args) => IsBusy = ProgressManager.IsActive); + ProgressManager.Bind(o => o.IsActive, + (sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress <= 0); + ProgressManager.Bind(o => o.Progress, + (sender, args) => IsProgressIndeterminate = ProgressManager.IsActive && ProgressManager.Progress <= 0); } protected override async void OnViewLoaded() @@ -110,16 +122,15 @@ namespace DiscordChatExporter.Gui.ViewModels await _dialogManager.ShowDialogAsync(dialog); } - public bool CanPopulateGuildsAndChannels => IsEnabled && TokenValue.IsNotBlank(); + public bool CanPopulateGuildsAndChannels => !IsBusy && TokenValue.IsNotBlank(); public async void PopulateGuildsAndChannels() { + // Create progress operation + var operation = ProgressManager.CreateOperation(); + try { - // Set busy state and indeterminate progress - IsEnabled = false; - Progress = -1; - // Sanitize token TokenValue = TokenValue.Trim('"'); @@ -134,7 +145,7 @@ namespace DiscordChatExporter.Gui.ViewModels // Prepare available guild list var availableGuilds = new List(); - // Direct Messages + // Get direct messages { // Get fake guild var guild = Guild.DirectMessages; @@ -150,66 +161,57 @@ namespace DiscordChatExporter.Gui.ViewModels var category = channel.Type == ChannelType.DirectTextChat ? "Private" : "Group"; // Create channel view model - var channelViewModel = _viewModelFactory.CreateChannelViewModel(); - channelViewModel.Model = channel; - channelViewModel.Category = category; + var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category); // Add to list channelViewModels.Add(channelViewModel); } // Create guild view model - var guildViewModel = _viewModelFactory.CreateGuildViewModel(); - guildViewModel.Model = guild; - guildViewModel.Channels = channelViewModels.OrderBy(c => c.Category) - .ThenBy(c => c.Model.Name) - .ToArray(); + var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild, + channelViewModels.OrderBy(c => c.Category) + .ThenBy(c => c.Model.Name) + .ToArray()); // Add to list availableGuilds.Add(guildViewModel); } - // Guilds + // Get guilds + var guilds = await _dataService.GetUserGuildsAsync(token); + foreach (var guild in guilds) { - // Get guilds - var guilds = await _dataService.GetUserGuildsAsync(token); - foreach (var guild in guilds) - { - // Get channels - var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id); + // Get channels + var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id); - // Get category channels - var categoryChannels = channels.Where(c => c.Type == ChannelType.Category).ToArray(); + // Get category channels + var categoryChannels = channels.Where(c => c.Type == ChannelType.Category).ToArray(); - // Get text channels - var textChannels = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray(); + // Get text channels + var textChannels = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray(); - // Create channel view models - var channelViewModels = new List(); - foreach (var channel in textChannels) - { - // Get category - var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name; - - // Create channel view model - var channelViewModel = _viewModelFactory.CreateChannelViewModel(); - channelViewModel.Model = channel; - channelViewModel.Category = category; - - // Add to list - channelViewModels.Add(channelViewModel); - } - - // Create guild view model - var guildViewModel = _viewModelFactory.CreateGuildViewModel(); - guildViewModel.Model = guild; - guildViewModel.Channels = channelViewModels.OrderBy(c => c.Category) - .ThenBy(c => c.Model.Name) - .ToArray(); + // Create channel view models + var channelViewModels = new List(); + foreach (var channel in textChannels) + { + // Get category + var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name; + + // Create channel view model + var channelViewModel = _viewModelFactory.CreateChannelViewModel(channel, category); // Add to list - availableGuilds.Add(guildViewModel); + channelViewModels.Add(channelViewModel); } + + // Create guild view model + var guildViewModel = _viewModelFactory.CreateGuildViewModel(guild, + channelViewModels.OrderBy(c => c.Category) + .ThenBy(c => c.Model.Name) + .ToArray()); + + // Add to list + availableGuilds.Add(guildViewModel); } // Update available guild list @@ -228,61 +230,73 @@ namespace DiscordChatExporter.Gui.ViewModels } finally { - // Reset busy state and progress - Progress = 0; - IsEnabled = true; - } + // Dispose progress operation + operation.Dispose(); + } } - public bool CanExportChannel => IsEnabled; + public bool CanExportChannels => !IsBusy && SelectedChannels.NotNullAndAny(); - public async void ExportChannel(ChannelViewModel channel) + public async void ExportChannels() { - try - { - // Set busy state and indeterminate progress - IsEnabled = false; - Progress = -1; + // Get last used token + var token = _settingsService.LastToken; - // Get last used token - var token = _settingsService.LastToken; + // Create dialog + var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels); - // Create dialog - var dialog = _viewModelFactory.CreateExportSetupViewModel(); - dialog.Guild = SelectedGuild; - dialog.Channel = channel; + // Show dialog, if canceled - return + if (await _dialogManager.ShowDialogAsync(dialog) != true) + return; - // Show dialog, if canceled - return - if (await _dialogManager.ShowDialogAsync(dialog) != true) - return; + // Create a progress operation for each channel to export + var operations = ProgressManager.CreateOperations(dialog.Channels.Count); - // Create progress handler - var progressHandler = new Progress(p => Progress = p); + // Export channels + for (var i = 0; i < dialog.Channels.Count; i++) + { + // Get operation and channel + var operation = operations[i]; + var channel = dialog.Channels[i]; - // Get chat log - var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, dialog.Channel, dialog.From, - dialog.To, progressHandler); + try + { + // Generate file path if necessary + var filePath = dialog.OutputPath; + if (ExportHelper.IsDirectoryPath(filePath)) + { + // Generate default file name + var fileName = ExportHelper.GetDefaultExportFileName(dialog.SelectedFormat, dialog.Guild, + channel, dialog.From, dialog.To); - // Export - _exportService.ExportChatLog(chatLog, dialog.FilePath, dialog.SelectedFormat, - dialog.PartitionLimit); + // Combine paths + filePath = Path.Combine(filePath, fileName); + } - // 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"); - } - finally - { - // Reset busy state and progress - Progress = 0; - IsEnabled = true; + // Get chat log + var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, channel, + dialog.From, dialog.To, operation); + + // Export + _exportService.ExportChatLog(chatLog, filePath, dialog.SelectedFormat, + dialog.PartitionLimit); + + // Notify completion + Notifications.Enqueue($"Channel [{channel.Model.Name}] successfully exported"); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden) + { + Notifications.Enqueue($"You don't have access to channel [{channel.Model.Name}]"); + } + catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.NotFound) + { + Notifications.Enqueue($"Channel [{channel.Model.Name}] doesn't exist"); + } + finally + { + // Dispose progress operation + operation.Dispose(); + } } } } diff --git a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml index 5274090..abfd0bf 100644 --- a/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml +++ b/DiscordChatExporter.Gui/Views/Dialogs/ExportSetupView.xaml @@ -30,22 +30,33 @@ - + + Text="Multiple channels" + TextTrimming="CharacterEllipsis" + Visibility="{Binding IsSingleChannel, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}" /> + + + + Text="{Binding Channels[0].Category, Mode=OneWay}" + ToolTip="{Binding Channels[0].Category, Mode=OneWay}" /> + Text="{Binding Channels[0].Model.Name, Mode=OneWay}" + ToolTip="{Binding Channels[0].Model.Name, Mode=OneWay}" /> diff --git a/DiscordChatExporter.Gui/Views/RootView.xaml b/DiscordChatExporter.Gui/Views/RootView.xaml index 0a8f9b2..58f7b90 100644 --- a/DiscordChatExporter.Gui/Views/RootView.xaml +++ b/DiscordChatExporter.Gui/Views/RootView.xaml @@ -2,7 +2,9 @@ x:Class="DiscordChatExporter.Gui.Views.RootView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:behaviors="clr-namespace:DiscordChatExporter.Gui.Behaviors" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:s="https://github.com/canton7/Stylet" @@ -30,7 +32,6 @@ @@ -117,7 +118,7 @@ Grid.Row="1" Background="{DynamicResource PrimaryHueMidBrush}" IsIndeterminate="{Binding IsProgressIndeterminate}" - Value="{Binding Progress, Mode=OneWay}" /> + Value="{Binding ProgressManager.Progress, Mode=OneWay}" /> @@ -186,10 +187,7 @@ - + @@ -203,7 +201,8 @@ + SelectedItem="{Binding SelectedGuild}" + SelectionMode="Single"> - + + + + - - - - - - + + + @@ -268,6 +267,20 @@ + + + diff --git a/Readme.md b/Readme.md index be8e758..6a396e4 100644 --- a/Readme.md +++ b/Readme.md @@ -33,7 +33,9 @@ DiscordChatExporter can be used to export message history from a [Discord](https - [Newtonsoft.Json](http://www.newtonsoft.com/json) - [Scriban](https://github.com/lunet-io/scriban) - [CommandLineParser](https://github.com/commandlineparser/commandline) +- [Ookii.Dialogs](https://github.com/caioproiete/ookii-dialogs-wpf) - [Failsafe](https://github.com/Tyrrrz/Failsafe) +- [Gress](https://github.com/Tyrrrz/Gress) - [Onova](https://github.com/Tyrrrz/Onova) - [Tyrrrz.Extensions](https://github.com/Tyrrrz/Extensions) - [Tyrrrz.Settings](https://github.com/Tyrrrz/Settings)