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\Guild.cs" />
<Compile Include="Models\Message.cs" /> <Compile Include="Models\Message.cs" />
<Compile Include="Models\MessageGroup.cs" /> <Compile Include="Models\MessageGroup.cs" />
<Compile Include="Models\Theme.cs" />
<Compile Include="Models\User.cs" /> <Compile Include="Models\User.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Services\DataService.cs" /> <Compile Include="Services\DataService.cs" />

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

@ -1,14 +1,31 @@
namespace DiscordChatExporter.Models using System;
namespace DiscordChatExporter.Models
{ {
public static class Extensions public static class Extensions
{ {
public static string GetFileExtension(this ExportFormat format) public static string GetFileExtension(this ExportFormat format)
{ {
if (format == ExportFormat.Text) if (format == ExportFormat.PlainText)
return "txt"; return "txt";
if (format == ExportFormat.Html) if (format == ExportFormat.HtmlDark)
return "html"; 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;
using System.IO;
using System.Reflection;
using System.Resources;
using AmmySidekick; using AmmySidekick;
namespace DiscordChatExporter namespace DiscordChatExporter
@ -15,5 +18,19 @@ namespace DiscordChatExporter
app.Run(); 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; 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>(); var result = new List<Message>();
@ -100,10 +101,12 @@ namespace DiscordChatExporter.Services
} }
// If no messages - break // If no messages - break
if (currentMessageId == null) break; if (currentMessageId == null)
break;
// If last message is older than from date - 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 // Otherwise offset the next request
beforeId = currentMessageId; beforeId = currentMessageId;

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

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

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

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

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

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

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

@ -1,8 +1,4 @@
using System; using DiscordChatExporter.Services;
using System.Collections.Generic;
using System.Linq;
using DiscordChatExporter.Models;
using DiscordChatExporter.Services;
using GalaSoft.MvvmLight; using GalaSoft.MvvmLight;
using Tyrrrz.Extensions; using Tyrrrz.Extensions;
@ -12,14 +8,6 @@ namespace DiscordChatExporter.ViewModels
{ {
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
public IReadOnlyList<Theme> AvailableThemes { get; }
public Theme Theme
{
get => _settingsService.Theme;
set => _settingsService.Theme = value;
}
public string DateFormat public string DateFormat
{ {
get => _settingsService.DateFormat; get => _settingsService.DateFormat;
@ -35,9 +23,6 @@ namespace DiscordChatExporter.ViewModels
public SettingsViewModel(ISettingsService settingsService) public SettingsViewModel(ISettingsService settingsService)
{ {
_settingsService = 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" { UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
DataContext: bind ExportSetupViewModel from $resource Container DataContext: bind ExportSetupViewModel from $resource Container
Width: 350 Width: 325
StackPanel { StackPanel {
// File path // File path
@ -15,13 +16,29 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
set [ UpdateSourceTrigger: PropertyChanged ] 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 // Date range
Grid { Grid {
#TwoColumns("*", "*") #TwoColumns("*", "*")
DatePicker { DatePicker {
Grid.Column: 0 Grid.Column: 0
Margin: "16 16 8 8" Margin: "16 20 8 8"
HintAssist.Hint: "From" HintAssist.Hint: "From"
HintAssist.IsFloating: true HintAssist.IsFloating: true
SelectedDate: bind From SelectedDate: bind From
@ -29,7 +46,7 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
DatePicker { DatePicker {
Grid.Column: 1 Grid.Column: 1
Margin: "8 16 16 8" Margin: "8 20 16 8"
HintAssist.Hint: "To" HintAssist.Hint: "To"
HintAssist.IsFloating: true HintAssist.IsFloating: true
SelectedDate: bind To SelectedDate: bind To
@ -40,6 +57,14 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
@StackPanelHorizontal { @StackPanelHorizontal {
HorizontalAlignment: Right HorizontalAlignment: Right
// Browse
Button "BrowseButton" {
Click: BrowseButton_Click
Content: "BROWSE"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton"
}
// Export // Export
Button "ExportButton" { Button "ExportButton" {
Click: ExportButton_Click Click: ExportButton_Click
@ -49,14 +74,6 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
// Browse
Button "BrowseButton" {
Click: BrowseButton_Click
Content: "BROWSE"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton"
}
// Cancel // Cancel
Button { Button {
Command: DialogHost.CloseDialogCommand Command: DialogHost.CloseDialogCommand

@ -1,10 +1,8 @@
using System.Collections.Generic; using System.Windows;
using System.Windows;
using DiscordChatExporter.Models; using DiscordChatExporter.Models;
using DiscordChatExporter.ViewModels; using DiscordChatExporter.ViewModels;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Microsoft.Win32; using Microsoft.Win32;
using Tyrrrz.Extensions;
namespace DiscordChatExporter.Views namespace DiscordChatExporter.Views
{ {
@ -17,39 +15,30 @@ namespace DiscordChatExporter.Views
InitializeComponent(); 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) 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 var sfd = new SaveFileDialog
{ {
FileName = ViewModel.FilePath, FileName = ViewModel.FilePath,
Filter = GetFilter(), Filter = $"{ext.ToUpperInvariant()} Files|*.{ext}|All Files|*.*",
FilterIndex = ViewModel.AvailableFormats.IndexOf(ViewModel.SelectedFormat) + 1,
AddExtension = true, AddExtension = true,
Title = "Select output file" Title = "Select output file"
}; };
// Assign new file path if dialog was successful
if (sfd.ShowDialog() == true) if (sfd.ShowDialog() == true)
{ {
ViewModel.FilePath = sfd.FileName; 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 FontSize: 16
Text: bind Token Text: bind Token
set [ UpdateSourceTrigger: PropertyChanged ] set [ UpdateSourceTrigger: PropertyChanged ]
TextFieldAssist.DecorationVisibility: Hidden
TextFieldAssist.TextBoxViewMargin: "0 0 2 0"
} }
// Submit // Submit

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

Loading…
Cancel
Save