Improve UX of ExportSetupDialog

Closes #11
pull/17/head
Alexey Golub 7 years ago
parent e5e9d4c9ff
commit d7345e91d3

@ -148,7 +148,6 @@
<Compile Include="Models\Guild.cs" />
<Compile Include="Models\Message.cs" />
<Compile Include="Models\MessageGroup.cs" />
<Compile Include="Models\Theme.cs" />
<Compile Include="Models\User.cs" />
<Compile Include="Program.cs" />
<Compile Include="Services\DataService.cs" />

@ -2,7 +2,8 @@
{
public enum ExportFormat
{
Text,
Html
PlainText,
HtmlDark,
HtmlLight
}
}

@ -1,14 +1,31 @@
namespace DiscordChatExporter.Models
using System;
namespace DiscordChatExporter.Models
{
public static class Extensions
{
public static string GetFileExtension(this ExportFormat format)
{
if (format == ExportFormat.Text)
if (format == ExportFormat.PlainText)
return "txt";
if (format == ExportFormat.Html)
if (format == ExportFormat.HtmlDark)
return "html";
return null;
if (format == ExportFormat.HtmlLight)
return "html";
throw new NotImplementedException();
}
public static string GetDisplayName(this ExportFormat format)
{
if (format == ExportFormat.PlainText)
return "Plain Text";
if (format == ExportFormat.HtmlDark)
return "HTML (Dark)";
if (format == ExportFormat.HtmlLight)
return "HTML (Light)";
throw new NotImplementedException();
}
}
}

@ -1,8 +0,0 @@
namespace DiscordChatExporter.Models
{
public enum Theme
{
Dark,
Light
}
}

@ -1,4 +1,7 @@
using System;
using System.IO;
using System.Reflection;
using System.Resources;
using AmmySidekick;
namespace DiscordChatExporter
@ -15,5 +18,19 @@ namespace DiscordChatExporter
app.Run();
}
public static string GetResourceString(string resourcePath)
{
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(resourcePath);
if (stream == null)
throw new MissingManifestResourceException("Could not find resource");
using (stream)
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
}

@ -71,7 +71,8 @@ namespace DiscordChatExporter.Services
return channels;
}
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId, DateTime? from, DateTime? to)
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
DateTime? from, DateTime? to)
{
var result = new List<Message>();
@ -100,10 +101,12 @@ namespace DiscordChatExporter.Services
}
// If no messages - break
if (currentMessageId == null) break;
if (currentMessageId == null)
break;
// If last message is older than from date - break
if (from != null && result.Last().TimeStamp < from) break;
if (from != null && result.Last().TimeStamp < from)
break;
// Otherwise offset the next request
beforeId = currentMessageId;

@ -1,8 +1,6 @@
using System;
using System.IO;
using System.Net;
using System.Reflection;
using System.Resources;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -20,7 +18,7 @@ namespace DiscordChatExporter.Services
_settingsService = settingsService;
}
public async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
private async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
{
var dateFormat = _settingsService.DateFormat;
@ -66,9 +64,8 @@ namespace DiscordChatExporter.Services
}
}
public async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, Theme theme)
private async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, string css)
{
var themeCss = GetThemeCss(theme);
var dateFormat = _settingsService.DateFormat;
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
@ -85,7 +82,7 @@ namespace DiscordChatExporter.Services
await writer.WriteLineAsync($"<title>{log.Guild} - {log.Channel}</title>");
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
await writer.WriteLineAsync($"<style>{themeCss}</style>");
await writer.WriteLineAsync($"<style>{css}</style>");
await writer.WriteLineAsync("</head>");
// Body start
@ -173,26 +170,30 @@ namespace DiscordChatExporter.Services
await writer.WriteLineAsync("</html>");
}
}
}
public partial class ExportService
{
private static string GetThemeCss(Theme theme)
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
{
var resourcePath = $"DiscordChatExporter.Resources.ExportService.{theme}Theme.css";
var assembly = Assembly.GetExecutingAssembly();
var stream = assembly.GetManifestResourceStream(resourcePath);
if (stream == null)
throw new MissingManifestResourceException("Could not find style resource");
using (stream)
using (var reader = new StreamReader(stream))
if (format == ExportFormat.PlainText)
{
return ExportAsTextAsync(filePath, log);
}
if (format == ExportFormat.HtmlDark)
{
return reader.ReadToEnd();
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.DarkTheme.css");
return ExportAsHtmlAsync(filePath, log, css);
}
if (format == ExportFormat.HtmlLight)
{
var css = Program.GetResourceString("DiscordChatExporter.Resources.ExportService.LightTheme.css");
return ExportAsHtmlAsync(filePath, log, css);
}
throw new NotImplementedException();
}
}
public partial class ExportService
{
private static string HtmlEncode(string str)
{
return WebUtility.HtmlEncode(str);

@ -5,7 +5,6 @@ namespace DiscordChatExporter.Services
{
public interface IExportService
{
Task ExportAsTextAsync(string filePath, ChannelChatLog log);
Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, Theme theme);
Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log);
}
}

@ -4,7 +4,6 @@ namespace DiscordChatExporter.Services
{
public interface ISettingsService
{
Theme Theme { get; set; }
string DateFormat { get; set; }
int MessageGroupLimit { get; set; }

@ -5,12 +5,11 @@ namespace DiscordChatExporter.Services
{
public class SettingsService : SettingsManager, ISettingsService
{
public Theme Theme { get; set; }
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
public int MessageGroupLimit { get; set; } = 20;
public string LastToken { get; set; }
public ExportFormat LastExportFormat { get; set; } = ExportFormat.Html;
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
public SettingsService()
{

@ -39,7 +39,15 @@ namespace DiscordChatExporter.ViewModels
public ExportFormat SelectedFormat
{
get => _format;
set => Set(ref _format, value);
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
@ -82,7 +90,10 @@ namespace DiscordChatExporter.ViewModels
private void Export()
{
// Save format
_settingsService.LastExportFormat = SelectedFormat;
// Start export
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
}
}

@ -1,12 +1,7 @@
using System.Collections.Generic;
using DiscordChatExporter.Models;
namespace DiscordChatExporter.ViewModels
namespace DiscordChatExporter.ViewModels
{
public interface ISettingsViewModel
{
IReadOnlyList<Theme> AvailableThemes { get; }
Theme Theme { get; set; }
string DateFormat { get; set; }
int MessageGroupLimit { get; set; }
}

@ -23,10 +23,10 @@ namespace DiscordChatExporter.ViewModels
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
private bool _isBusy;
private string _token;
private IReadOnlyList<Guild> _availableGuilds;
private Guild _selectedGuild;
private IReadOnlyList<Channel> _availableChannels;
private string _cachedToken;
public bool IsBusy
{
@ -43,13 +43,13 @@ namespace DiscordChatExporter.ViewModels
public string Token
{
get => _settingsService.LastToken;
get => _token;
set
{
// Remove invalid chars
value = value?.Trim('"');
_settingsService.LastToken = value;
Set(ref _token, value);
PullDataCommand.RaiseCanExecuteChanged();
}
}
@ -107,12 +107,20 @@ namespace DiscordChatExporter.ViewModels
{
Export(m.Channel, m.FilePath, m.Format, m.From, m.To);
});
// Defaults
_token = _settingsService.LastToken;
}
private async void PullData()
{
IsBusy = true;
_cachedToken = Token;
// Copy token so it doesn't get mutated
var token = Token;
// Save token
_settingsService.LastToken = token;
// Clear existing
_guildChannelsMap.Clear();
@ -121,17 +129,17 @@ namespace DiscordChatExporter.ViewModels
{
// Get DM channels
{
var channels = await _dataService.GetDirectMessageChannelsAsync(_cachedToken);
var channels = await _dataService.GetDirectMessageChannelsAsync(token);
var guild = new Guild("@me", "Direct Messages", null);
_guildChannelsMap[guild] = channels.ToArray();
}
// Get guild channels
{
var guilds = await _dataService.GetGuildsAsync(_cachedToken);
var guilds = await _dataService.GetGuildsAsync(token);
foreach (var guild in guilds)
{
var channels = await _dataService.GetGuildChannelsAsync(_cachedToken, guild.Id);
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
_guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
}
}
@ -171,22 +179,22 @@ namespace DiscordChatExporter.ViewModels
{
IsBusy = true;
// Get last used token
var token = _settingsService.LastToken;
try
{
// Get messages
var messages = await _dataService.GetChannelMessagesAsync(_cachedToken, channel.Id, from, to);
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
// Group them
var messageGroups = _messageGroupService.GroupMessages(messages);
// Create log
var chatLog = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
var log = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
// Export
if (format == ExportFormat.Text)
await _exportService.ExportAsTextAsync(filePath, chatLog);
else if (format == ExportFormat.Html)
await _exportService.ExportAsHtmlAsync(filePath, chatLog, _settingsService.Theme);
await _exportService.ExportAsync(format, filePath, log);
// Notify completion
MessengerInstance.Send(new ShowExportDoneMessage(filePath));

@ -1,8 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Models;
using DiscordChatExporter.Services;
using DiscordChatExporter.Services;
using GalaSoft.MvvmLight;
using Tyrrrz.Extensions;
@ -12,14 +8,6 @@ namespace DiscordChatExporter.ViewModels
{
private readonly ISettingsService _settingsService;
public IReadOnlyList<Theme> AvailableThemes { get; }
public Theme Theme
{
get => _settingsService.Theme;
set => _settingsService.Theme = value;
}
public string DateFormat
{
get => _settingsService.DateFormat;
@ -35,9 +23,6 @@ namespace DiscordChatExporter.ViewModels
public SettingsViewModel(ISettingsService settingsService)
{
_settingsService = settingsService;
// Defaults
AvailableThemes = Enum.GetValues(typeof(Theme)).Cast<Theme>().ToArray();
}
}
}

@ -1,8 +1,9 @@
using MaterialDesignThemes.Wpf
using DiscordChatExporter.Models
using MaterialDesignThemes.Wpf
UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
DataContext: bind ExportSetupViewModel from $resource Container
Width: 350
Width: 325
StackPanel {
// File path
@ -15,13 +16,29 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
set [ UpdateSourceTrigger: PropertyChanged ]
}
// Format
ComboBox {
Margin: "16 8 16 8"
HintAssist.Hint: "Export format"
HintAssist.IsFloating: true
IsReadOnly: true
ItemsSource: bind AvailableFormats
ItemTemplate: DataTemplate {
TextBlock {
Text: bind
convert (ExportFormat f) => Extensions.GetDisplayName(f)
}
}
SelectedItem: bind SelectedFormat
}
// Date range
Grid {
#TwoColumns("*", "*")
DatePicker {
Grid.Column: 0
Margin: "16 16 8 8"
Margin: "16 20 8 8"
HintAssist.Hint: "From"
HintAssist.IsFloating: true
SelectedDate: bind From
@ -29,7 +46,7 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
DatePicker {
Grid.Column: 1
Margin: "8 16 16 8"
Margin: "8 20 16 8"
HintAssist.Hint: "To"
HintAssist.IsFloating: true
SelectedDate: bind To
@ -40,6 +57,14 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
@StackPanelHorizontal {
HorizontalAlignment: Right
// Browse
Button "BrowseButton" {
Click: BrowseButton_Click
Content: "BROWSE"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton"
}
// Export
Button "ExportButton" {
Click: ExportButton_Click
@ -49,14 +74,6 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
Style: resource dyn "MaterialDesignFlatButton"
}
// Browse
Button "BrowseButton" {
Click: BrowseButton_Click
Content: "BROWSE"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton"
}
// Cancel
Button {
Command: DialogHost.CloseDialogCommand

@ -1,10 +1,8 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows;
using DiscordChatExporter.Models;
using DiscordChatExporter.ViewModels;
using MaterialDesignThemes.Wpf;
using Microsoft.Win32;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Views
{
@ -17,39 +15,30 @@ namespace DiscordChatExporter.Views
InitializeComponent();
}
private string GetFilter()
{
var filters = new List<string>();
foreach (var format in ViewModel.AvailableFormats)
{
var ext = format.GetFileExtension();
filters.Add($"{format} (*.{ext})|*.{ext}");
}
return filters.JoinToString("|");
}
public void ExportButton_Click(object sender, RoutedEventArgs args)
{
DialogHost.CloseDialogCommand.Execute(null, null);
}
public void BrowseButton_Click(object sender, RoutedEventArgs args)
{
// Get file extension of the selected format
var ext = ViewModel.SelectedFormat.GetFileExtension();
// Open dialog
var sfd = new SaveFileDialog
{
FileName = ViewModel.FilePath,
Filter = GetFilter(),
FilterIndex = ViewModel.AvailableFormats.IndexOf(ViewModel.SelectedFormat) + 1,
Filter = $"{ext.ToUpperInvariant()} Files|*.{ext}|All Files|*.*",
AddExtension = true,
Title = "Select output file"
};
// Assign new file path if dialog was successful
if (sfd.ShowDialog() == true)
{
ViewModel.FilePath = sfd.FileName;
ViewModel.SelectedFormat = ViewModel.AvailableFormats[sfd.FilterIndex - 1];
}
}
public void ExportButton_Click(object sender, RoutedEventArgs args)
{
DialogHost.CloseDialogCommand.Execute(null, null);
}
}
}

@ -46,6 +46,8 @@ Window "DiscordChatExporter.Views.MainWindow" {
FontSize: 16
Text: bind Token
set [ UpdateSourceTrigger: PropertyChanged ]
TextFieldAssist.DecorationVisibility: Hidden
TextFieldAssist.TextBoxViewMargin: "0 0 2 0"
}
// Submit

@ -5,19 +5,9 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
Width: 250
StackPanel {
// Theme
ComboBox {
Margin: "16 16 16 8"
HintAssist.Hint: "Theme"
HintAssist.IsFloating: true
IsReadOnly: true
ItemsSource: bind AvailableThemes
SelectedItem: bind Theme
}
// Date format
TextBox {
Margin: "16 8 16 8"
Margin: "16 16 16 8"
HintAssist.Hint: "Date format"
HintAssist.IsFloating: true
Text: bind DateFormat

Loading…
Cancel
Save