Add multichannel export to GUI

Closes #12
This commit is contained in:
Alexey Golub
2019-02-09 19:03:34 +02:00
parent 65c5df89f4
commit e4b0d60c40
13 changed files with 366 additions and 136 deletions

View File

@@ -17,9 +17,11 @@ namespace DiscordChatExporter.Gui.ViewModels.Dialogs
public GuildViewModel Guild { get; set; }
public ChannelViewModel Channel { get; set; }
public IReadOnlyList<ChannelViewModel> Channels { get; set; }
public string FilePath { get; set; }
public bool IsSingleChannel => Channels.Count == 1;
public string OutputPath { get; set; }
public IReadOnlyList<ExportFormat> AvailableFormats =>
Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().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);
// If single channel - prompt file path
if (IsSingleChannel)
{
// Get single channel
var channel = Channels.Single();
// Prompt for output file path
var ext = SelectedFormat.GetFileExtension();
var filter = $"{ext.ToUpperInvariant()} files|*.{ext}";
FilePath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
// 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);
}

View File

@@ -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;
}
}
}

View File

@@ -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<ChannelViewModel> channels)
{
var viewModel = factory.CreateGuildViewModel();
viewModel.Model = model;
viewModel.Channels = channels;
return viewModel;
}
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
GuildViewModel guild, IReadOnlyList<ChannelViewModel> 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});
}
}

View File

@@ -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<ChannelViewModel> 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<GuildViewModel>();
// 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 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();
// Create channel view models
var channelViewModels = new List<ChannelViewModel>();
foreach (var channel in textChannels)
{
// Get channels
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
// Get category
var category = categoryChannels.FirstOrDefault(c => c.Id == channel.ParentId)?.Name;
// 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();
// Create channel view models
var channelViewModels = new List<ChannelViewModel>();
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 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
// Get last used token
var token = _settingsService.LastToken;
// Create dialog
var dialog = _viewModelFactory.CreateExportSetupViewModel(SelectedGuild, SelectedChannels);
// 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);
// Export channels
for (var i = 0; i < dialog.Channels.Count; i++)
{
// Set busy state and indeterminate progress
IsEnabled = false;
Progress = -1;
// Get operation and channel
var operation = operations[i];
var channel = dialog.Channels[i];
// Get last used token
var token = _settingsService.LastToken;
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);
// Create dialog
var dialog = _viewModelFactory.CreateExportSetupViewModel();
dialog.Guild = SelectedGuild;
dialog.Channel = channel;
// Combine paths
filePath = Path.Combine(filePath, fileName);
}
// Show dialog, if canceled - return
if (await _dialogManager.ShowDialogAsync(dialog) != true)
return;
// Get chat log
var chatLog = await _dataService.GetChatLogAsync(token, dialog.Guild, channel,
dialog.From, dialog.To, operation);
// Create progress handler
var progressHandler = new Progress<double>(p => Progress = p);
// Export
_exportService.ExportChatLog(chatLog, filePath, dialog.SelectedFormat,
dialog.PartitionLimit);
// 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");
}
finally
{
// Reset busy state and progress
Progress = 0;
IsEnabled = true;
// 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();
}
}
}
}