Add command line interface and change solution structure (#26)

This commit is contained in:
Alexey Golub
2018-01-12 20:28:36 +01:00
committed by GitHub
parent 7da82f9ef4
commit 8515efe11b
73 changed files with 489 additions and 199 deletions

View File

@@ -0,0 +1,19 @@
using DiscordChatExporter.Gui.Messages;
using GalaSoft.MvvmLight;
namespace DiscordChatExporter.Gui.ViewModels
{
public class ErrorViewModel : ViewModelBase, IErrorViewModel
{
public string Message { get; private set; }
public ErrorViewModel()
{
// Messages
MessengerInstance.Register<ShowErrorMessage>(this, m =>
{
Message = m.Message;
});
}
}
}

View File

@@ -0,0 +1,32 @@
using System.Diagnostics;
using DiscordChatExporter.Gui.Messages;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;
namespace DiscordChatExporter.Gui.ViewModels
{
public class ExportDoneViewModel : ViewModelBase, IExportDoneViewModel
{
private string _filePath;
// Commands
public RelayCommand OpenCommand { get; }
public ExportDoneViewModel()
{
// Commands
OpenCommand = new RelayCommand(Open);
// Messages
MessengerInstance.Register<ShowExportDoneMessage>(this, m =>
{
_filePath = m.FilePath;
});
}
private void Open()
{
Process.Start(_filePath);
}
}
}

View File

@@ -0,0 +1,100 @@
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;
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<ExportFormat> AvailableFormats { get; }
public ExportFormat SelectedFormat
{
get => _format;
set
{
Set(ref _format, value);
// Replace extension in path
var newExt = value.GetFileExtension();
if (FilePath != null && !FilePath.EndsWith(newExt))
FilePath = FilePath.SubstringUntilLast(".") + "." + newExt;
}
}
public DateTime? From
{
get => _from;
set => Set(ref _from, value);
}
public DateTime? To
{
get => _to;
set => Set(ref _to, value);
}
// Commands
public RelayCommand ExportCommand { get; }
public ExportSetupViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
// Defaults
AvailableFormats = Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
// Commands
ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank());
// Messages
MessengerInstance.Register<ShowExportSetupMessage>(this, m =>
{
Guild = m.Guild;
Channel = m.Channel;
SelectedFormat = _settingsService.LastExportFormat;
FilePath = $"{Guild} - {Channel}.{SelectedFormat.GetFileExtension()}"
.Replace(Path.GetInvalidFileNameChars(), '_');
From = null;
To = null;
});
}
private void Export()
{
// Save format
_settingsService.LastExportFormat = SelectedFormat;
// Start export
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DiscordChatExporter.Gui.ViewModels
{
public interface IErrorViewModel
{
string Message { get; }
}
}

View File

@@ -0,0 +1,9 @@
using GalaSoft.MvvmLight.CommandWpf;
namespace DiscordChatExporter.Gui.ViewModels
{
public interface IExportDoneViewModel
{
RelayCommand OpenCommand { get; }
}
}

View File

@@ -0,0 +1,20 @@
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<ExportFormat> AvailableFormats { get; }
ExportFormat SelectedFormat { get; set; }
DateTime? From { get; set; }
DateTime? To { get; set; }
RelayCommand ExportCommand { get; }
}
}

View File

@@ -0,0 +1,23 @@
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; }
string Token { get; set; }
IReadOnlyList<Guild> AvailableGuilds { get; }
Guild SelectedGuild { get; set; }
IReadOnlyList<Channel> AvailableChannels { get; }
RelayCommand PullDataCommand { get; }
RelayCommand ShowSettingsCommand { get; }
RelayCommand ShowAboutCommand { get; }
RelayCommand<Channel> ShowExportSetupCommand { get; }
}
}

View File

@@ -0,0 +1,8 @@
namespace DiscordChatExporter.Gui.ViewModels
{
public interface ISettingsViewModel
{
string DateFormat { get; set; }
int MessageGroupLimit { get; set; }
}
}

View File

@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
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 IDataService _dataService;
private readonly IMessageGroupService _messageGroupService;
private readonly IExportService _exportService;
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
private bool _isBusy;
private string _token;
private IReadOnlyList<Guild> _availableGuilds;
private Guild _selectedGuild;
private IReadOnlyList<Channel> _availableChannels;
public bool IsBusy
{
get => _isBusy;
private set
{
Set(ref _isBusy, value);
PullDataCommand.RaiseCanExecuteChanged();
ShowExportSetupCommand.RaiseCanExecuteChanged();
}
}
public bool IsDataAvailable => AvailableGuilds.NotNullAndAny();
public string Token
{
get => _token;
set
{
// Remove invalid chars
value = value?.Trim('"');
Set(ref _token, value);
PullDataCommand.RaiseCanExecuteChanged();
}
}
public IReadOnlyList<Guild> 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] : new Channel[0];
ShowExportSetupCommand.RaiseCanExecuteChanged();
}
}
public IReadOnlyList<Channel> AvailableChannels
{
get => _availableChannels;
private set => Set(ref _availableChannels, value);
}
public RelayCommand PullDataCommand { get; }
public RelayCommand ShowSettingsCommand { get; }
public RelayCommand ShowAboutCommand { get; }
public RelayCommand<Channel> ShowExportSetupCommand { get; }
public MainViewModel(ISettingsService settingsService, IDataService dataService,
IMessageGroupService messageGroupService, IExportService exportService)
{
_settingsService = settingsService;
_dataService = dataService;
_messageGroupService = messageGroupService;
_exportService = exportService;
_guildChannelsMap = new Dictionary<Guild, IReadOnlyList<Channel>>();
// Commands
PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy);
ShowSettingsCommand = new RelayCommand(ShowSettings);
ShowAboutCommand = new RelayCommand(ShowAbout);
ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy);
// Messages
MessengerInstance.Register<StartExportMessage>(this, m =>
{
Export(m.Channel, m.FilePath, m.Format, m.From, m.To);
});
// Defaults
_token = _settingsService.LastToken;
}
private async void PullData()
{
IsBusy = true;
// Copy token so it doesn't get mutated
var token = Token;
// 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.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).ToArray();
}
}
}
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
{
const string message = "Unauthorized to perform request. Make sure token is valid.";
MessengerInstance.Send(new ShowErrorMessage(message));
}
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
const string message = "Forbidden to perform request. The account may be locked by 2FA.";
MessengerInstance.Send(new ShowErrorMessage(message));
}
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)
{
IsBusy = true;
// Get last used token
var token = _settingsService.LastToken;
try
{
// Get messages
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
// Group them
var messageGroups = _messageGroupService.GroupMessages(messages);
// Create log
var log = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
// Export
await _exportService.ExportAsync(format, filePath, log);
// Notify completion
MessengerInstance.Send(new ShowExportDoneMessage(filePath));
}
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
const string message = "Forbidden to view messages in that channel.";
MessengerInstance.Send(new ShowErrorMessage(message));
}
IsBusy = false;
}
}
}

View File

@@ -0,0 +1,28 @@
using DiscordChatExporter.Core.Services;
using GalaSoft.MvvmLight;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Gui.ViewModels
{
public class SettingsViewModel : ViewModelBase, ISettingsViewModel
{
private readonly ISettingsService _settingsService;
public string DateFormat
{
get => _settingsService.DateFormat;
set => _settingsService.DateFormat = value;
}
public int MessageGroupLimit
{
get => _settingsService.MessageGroupLimit;
set => _settingsService.MessageGroupLimit = value.ClampMin(0);
}
public SettingsViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
}
}
}