Compare commits

...

20 Commits

Author SHA1 Message Date
Alexey Golub 5d6edfd937 Update version 2017-10-27 22:36:25 +03:00
Alexey Golub 60a5a69aa4 Improve message date filtering, convert from snowflake to get the 'to' message ID 2017-10-27 22:35:04 +03:00
Alexey Golub cc565598d3 Add support for channel mentions formatting
Closes #19
2017-10-27 22:08:53 +03:00
Alexey Golub 6a7000cbc9 Re-order URL formatting in HTML formatter
Slightly helps #18
2017-10-27 21:34:45 +03:00
Alexey Golub 6706ff95f1 Fix user mentions not resolving on bots 2017-10-27 21:07:54 +03:00
Alexey Golub ecc95c9c7f Make the mentions resolving safer 2017-10-27 21:04:10 +03:00
Alexey Golub d4f2049db6 Fix wrapping issues in html export 2017-10-26 23:36:10 +03:00
Alexey Golub 4d5118de76 Add mention support (#17)
* Support for user mentions and incomplete support for role mentions

* Improve formatting a bit

* Implement a hack to get roles
2017-10-26 22:21:44 +02:00
Alexey Golub 171718989a Add workarounds for non-default message types
Fixes #16
2017-10-26 22:14:50 +03:00
Alexey Golub 75e8d59cc3 Wrap text in inline pre 2017-10-25 23:37:18 +03:00
Alexey Golub 1d7264a71b Add support for custom emojis
Closes #14
2017-10-25 23:31:32 +03:00
Alexey Golub 2c0a63352c Simplify regexes 2017-10-25 22:19:25 +03:00
Alexey Golub 42d4d64695 Add underscore italics to markdown formatter
Fixes #15
2017-10-25 22:04:47 +03:00
Alexey Golub 6968f987ce Small readability improvement 2017-10-16 20:24:34 +03:00
Alexey Golub 5ecbfd50b8 Update deployment script 2017-10-16 13:00:58 +03:00
Alexey Golub e59a1ea8b4 Add deployment script 2017-10-16 12:47:55 +03:00
Alexey Golub 540ce7f3c3 Update readme 2017-10-16 00:37:34 +03:00
Alexey Golub a0d261f966 Add fancy badges 2017-10-15 23:58:33 +03:00
Alexey Golub 10446064d5 Ammy formatting 2017-10-07 22:03:43 +03:00
Alexey Golub 6709821324 Cleanup views 2017-10-06 23:27:17 +03:00
18 changed files with 345 additions and 110 deletions
+4 -1
View File
@@ -261,4 +261,7 @@ __pycache__/
*.pyc *.pyc
# Ammy auto-generated XAML # Ammy auto-generated XAML
*.g.xaml *.g.xaml
# Deploy output
Deploy/Output/
+22
View File
@@ -0,0 +1,22 @@
$path = "$PSScriptRoot\..\DiscordChatExporter\bin\Release\*"
$include = "*.exe", "*.dll", "*.config"
$outputDir = "$PSScriptRoot\Output"
$outputFile = "DiscordChatExporter.zip"
# Create output directory
if (-Not (Test-Path $outputDir))
{
New-Item $outputDir -ItemType Directory
}
# Delete output if already exists
if (Test-Path("$outputDir\$outputFile"))
{
Remove-Item -Path "$outputDir\$outputFile"
}
# Get files
$files = Get-ChildItem -Path $path -Include $include
# Pack into archive
$files | Compress-Archive -DestinationPath "$outputDir\$outputFile"
@@ -92,6 +92,8 @@
<Compile Include="Models\ChannelType.cs" /> <Compile Include="Models\ChannelType.cs" />
<Compile Include="Models\ExportFormat.cs" /> <Compile Include="Models\ExportFormat.cs" />
<Compile Include="Models\Extensions.cs" /> <Compile Include="Models\Extensions.cs" />
<Compile Include="Models\Role.cs" />
<Compile Include="Models\MessageType.cs" />
<Compile Include="Services\IMessageGroupService.cs" /> <Compile Include="Services\IMessageGroupService.cs" />
<Compile Include="Services\MessageGroupService.cs" /> <Compile Include="Services\MessageGroupService.cs" />
<Compile Include="ViewModels\ErrorViewModel.cs" /> <Compile Include="ViewModels\ErrorViewModel.cs" />
+12 -1
View File
@@ -17,9 +17,17 @@ namespace DiscordChatExporter.Models
public IReadOnlyList<Attachment> Attachments { get; } public IReadOnlyList<Attachment> Attachments { get; }
public IReadOnlyList<User> MentionedUsers { get; }
public IReadOnlyList<Role> MentionedRoles { get; }
public IReadOnlyList<Channel> MentionedChannels { get; }
public Message(string id, User author, public Message(string id, User author,
DateTime timeStamp, DateTime? editedTimeStamp, DateTime timeStamp, DateTime? editedTimeStamp,
string content, IReadOnlyList<Attachment> attachments) string content, IReadOnlyList<Attachment> attachments,
IReadOnlyList<User> mentionedUsers, IReadOnlyList<Role> mentionedRoles,
IReadOnlyList<Channel> mentionedChannels)
{ {
Id = id; Id = id;
Author = author; Author = author;
@@ -27,6 +35,9 @@ namespace DiscordChatExporter.Models
EditedTimeStamp = editedTimeStamp; EditedTimeStamp = editedTimeStamp;
Content = content; Content = content;
Attachments = attachments; Attachments = attachments;
MentionedUsers = mentionedUsers;
MentionedRoles = mentionedRoles;
MentionedChannels = mentionedChannels;
} }
public override string ToString() public override string ToString()
+14
View File
@@ -0,0 +1,14 @@
namespace DiscordChatExporter.Models
{
public enum MessageType
{
Default,
RecipientAdd,
RecipientRemove,
Call,
ChannelNameChange,
ChannelIconChange,
ChannelPinnedMessage,
GuildMemberJoin
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace DiscordChatExporter.Models
{
public class Role
{
public string Id { get; }
public string Name { get; }
public Role(string id, string name)
{
Id = id;
Name = name;
}
public override string ToString()
{
return Name;
}
}
}
@@ -3,5 +3,5 @@
[assembly: AssemblyTitle("DiscordChatExporter")] [assembly: AssemblyTitle("DiscordChatExporter")]
[assembly: AssemblyCompany("Tyrrrz")] [assembly: AssemblyCompany("Tyrrrz")]
[assembly: AssemblyCopyright("Copyright (c) 2017 Alexey Golub")] [assembly: AssemblyCopyright("Copyright (c) 2017 Alexey Golub")]
[assembly: AssemblyVersion("2.2.1")] [assembly: AssemblyVersion("2.3")]
[assembly: AssemblyFileVersion("2.2.1")] [assembly: AssemblyFileVersion("2.3")]
@@ -18,7 +18,7 @@ div.pre {
font-family: Consolas, Courier New, Courier, Monospace; font-family: Consolas, Courier New, Courier, Monospace;
margin-top: 4px; margin-top: 4px;
padding: 8px; padding: 8px;
white-space: pre; white-space: pre-wrap;
} }
span.pre { span.pre {
@@ -26,7 +26,7 @@ span.pre {
font-family: Consolas, Courier New, Courier, Monospace; font-family: Consolas, Courier New, Courier, Monospace;
padding-left: 2px; padding-left: 2px;
padding-right: 2px; padding-right: 2px;
white-space: pre; white-space: pre-wrap;
} }
div#info { div#info {
@@ -106,6 +106,7 @@ span.msg-edited {
div.msg-content { div.msg-content {
font-size: .9375rem; font-size: .9375rem;
padding-top: 5px; padding-top: 5px;
word-break: break-all;
} }
div.msg-attachment { div.msg-attachment {
@@ -116,4 +117,14 @@ div.msg-attachment {
img.msg-attachment { img.msg-attachment {
max-height: 500px; max-height: 500px;
max-width: 50%; max-width: 50%;
}
img.emoji {
height: 24px;
width: 24px;
vertical-align: -.4em;
}
span.mention {
font-weight: 600;
} }
@@ -18,6 +18,7 @@ div.pre {
font-family: Consolas, Courier New, Courier, Monospace; font-family: Consolas, Courier New, Courier, Monospace;
margin-top: 4px; margin-top: 4px;
padding: 8px; padding: 8px;
white-space: pre-wrap;
} }
span.pre { span.pre {
@@ -25,6 +26,7 @@ span.pre {
font-family: Consolas, Courier New, Courier, Monospace; font-family: Consolas, Courier New, Courier, Monospace;
padding-left: 2px; padding-left: 2px;
padding-right: 2px; padding-right: 2px;
white-space: pre-wrap;
} }
div#info { div#info {
@@ -104,6 +106,7 @@ span.msg-edited {
div.msg-content { div.msg-content {
font-size: .9375rem; font-size: .9375rem;
padding-top: 5px; padding-top: 5px;
word-break: break-all;
} }
div.msg-attachment { div.msg-attachment {
@@ -114,4 +117,14 @@ div.msg-attachment {
img.msg-attachment { img.msg-attachment {
max-height: 500px; max-height: 500px;
max-width: 50%; max-width: 50%;
}
img.emoji {
height: 24px;
width: 24px;
vertical-align: -.4em;
}
span.mention {
font-weight: 600;
} }
+96 -20
View File
@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using DiscordChatExporter.Exceptions; using DiscordChatExporter.Exceptions;
using DiscordChatExporter.Models; using DiscordChatExporter.Models;
@@ -13,7 +14,10 @@ namespace DiscordChatExporter.Services
public partial class DataService : IDataService, IDisposable public partial class DataService : IDataService, IDisposable
{ {
private const string ApiRoot = "https://discordapp.com/api/v6"; private const string ApiRoot = "https://discordapp.com/api/v6";
private readonly HttpClient _httpClient = new HttpClient(); private readonly HttpClient _httpClient = new HttpClient();
private readonly Dictionary<string, Role> _roleCache = new Dictionary<string, Role>();
private readonly Dictionary<string, Channel> _channelCache = new Dictionary<string, Channel>();
private async Task<string> GetStringAsync(string url) private async Task<string> GetStringAsync(string url)
{ {
@@ -29,6 +33,20 @@ namespace DiscordChatExporter.Services
} }
} }
private async Task<IReadOnlyList<Role>> GetGuildRolesAsync(string token, string guildId)
{
// Form request url
var url = $"{ApiRoot}/guilds/{guildId}?token={token}";
// Get response
var content = await GetStringAsync(url);
// Parse
var roles = JToken.Parse(content)["roles"].Select(ParseRole).ToArray();
return roles;
}
public async Task<IReadOnlyList<Guild>> GetGuildsAsync(string token) public async Task<IReadOnlyList<Guild>> GetGuildsAsync(string token)
{ {
// Form request url // Form request url
@@ -40,6 +58,14 @@ namespace DiscordChatExporter.Services
// Parse // Parse
var guilds = JArray.Parse(content).Select(ParseGuild).ToArray(); var guilds = JArray.Parse(content).Select(ParseGuild).ToArray();
// HACK: also get roles for all of them
foreach (var guild in guilds)
{
var roles = await GetGuildRolesAsync(token, guild.Id);
foreach (var role in roles)
_roleCache[role.Id] = role;
}
return guilds; return guilds;
} }
@@ -68,6 +94,10 @@ namespace DiscordChatExporter.Services
// Parse // Parse
var channels = JArray.Parse(content).Select(ParseChannel).ToArray(); var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
// Cache
foreach (var channel in channels)
_channelCache[channel.Id] = channel;
return channels; return channels;
} }
@@ -78,7 +108,7 @@ namespace DiscordChatExporter.Services
// We are going backwards from last message to first // We are going backwards from last message to first
// collecting everything between them in batches // collecting everything between them in batches
string beforeId = null; var beforeId = to != null ? DateTimeToSnowflake(to.Value) : null;
while (true) while (true)
{ {
// Form request url // Form request url
@@ -90,12 +120,20 @@ namespace DiscordChatExporter.Services
var content = await GetStringAsync(url); var content = await GetStringAsync(url);
// Parse // Parse
var messages = JArray.Parse(content).Select(ParseMessage); var messages = JArray.Parse(content).Select(j => ParseMessage(j, _roleCache, _channelCache));
// Add messages to list // Add messages to list
var currentMessageId = default(string); string currentMessageId = null;
foreach (var message in messages) foreach (var message in messages)
{ {
// Break when the message is older than from date
if (from != null && message.TimeStamp < from)
{
currentMessageId = null;
break;
}
// Add message
result.Add(message); result.Add(message);
currentMessageId = message.Id; currentMessageId = message.Id;
} }
@@ -104,10 +142,6 @@ namespace DiscordChatExporter.Services
if (currentMessageId == null) if (currentMessageId == null)
break; break;
// If last message is older than from date - break
if (from != null && result.Last().TimeStamp < from)
break;
// Otherwise offset the next request // Otherwise offset the next request
beforeId = currentMessageId; beforeId = currentMessageId;
} }
@@ -115,12 +149,6 @@ namespace DiscordChatExporter.Services
// Messages appear newest first, we need to reverse // Messages appear newest first, we need to reverse
result.Reverse(); result.Reverse();
// Filter
if (from != null)
result.RemoveAll(m => m.TimeStamp < from);
if (to != null)
result.RemoveAll(m => m.TimeStamp > to);
return result; return result;
} }
@@ -141,6 +169,23 @@ namespace DiscordChatExporter.Services
public partial class DataService public partial class DataService
{ {
private static string DateTimeToSnowflake(DateTime dateTime)
{
const long epoch = 62135596800000;
var unixTime = dateTime.ToUniversalTime().Ticks / TimeSpan.TicksPerMillisecond - epoch;
var value = ((ulong) unixTime - 1420070400000UL) << 22;
return value.ToString();
}
private static Guild ParseGuild(JToken token)
{
var id = token.Value<string>("id");
var name = token.Value<string>("name");
var iconHash = token.Value<string>("icon");
return new Guild(id, name, iconHash);
}
private static User ParseUser(JToken token) private static User ParseUser(JToken token)
{ {
var id = token.Value<string>("id"); var id = token.Value<string>("id");
@@ -151,13 +196,12 @@ namespace DiscordChatExporter.Services
return new User(id, discriminator, name, avatarHash); return new User(id, discriminator, name, avatarHash);
} }
private static Guild ParseGuild(JToken token) private static Role ParseRole(JToken token)
{ {
var id = token.Value<string>("id"); var id = token.Value<string>("id");
var name = token.Value<string>("name"); var name = token.Value<string>("name");
var iconHash = token.Value<string>("icon");
return new Guild(id, name, iconHash); return new Role(id, name);
} }
private static Channel ParseChannel(JToken token) private static Channel ParseChannel(JToken token)
@@ -181,17 +225,31 @@ namespace DiscordChatExporter.Services
return new Channel(id, name, type); return new Channel(id, name, type);
} }
private static Message ParseMessage(JToken token) private static Message ParseMessage(JToken token,
IDictionary<string, Role> roles, IDictionary<string, Channel> channels)
{ {
// Get basic data // Get basic data
var id = token.Value<string>("id"); var id = token.Value<string>("id");
var timeStamp = token.Value<DateTime>("timestamp"); var timeStamp = token.Value<DateTime>("timestamp");
var editedTimeStamp = token.Value<DateTime?>("edited_timestamp"); var editedTimeStamp = token.Value<DateTime?>("edited_timestamp");
var content = token.Value<string>("content"); var content = token.Value<string>("content");
var type = (MessageType) token.Value<int>("type");
// Lazy workaround for calls // Workarounds for non-default types
if (token["call"] != null) if (type == MessageType.RecipientAdd)
content = "Added a recipient.";
else if (type == MessageType.RecipientRemove)
content = "Removed a recipient.";
else if (type == MessageType.Call)
content = "Started a call."; content = "Started a call.";
else if (type == MessageType.ChannelNameChange)
content = "Changed the channel name.";
else if (type == MessageType.ChannelIconChange)
content = "Changed the channel icon.";
else if (type == MessageType.ChannelPinnedMessage)
content = "Pinned a message.";
else if (type == MessageType.GuildMemberJoin)
content = "Joined the server.";
// Get author // Get author
var author = ParseUser(token["author"]); var author = ParseUser(token["author"]);
@@ -214,7 +272,25 @@ namespace DiscordChatExporter.Services
attachments.Add(attachment); attachments.Add(attachment);
} }
return new Message(id, author, timeStamp, editedTimeStamp, content, attachments); // Get user mentions
var mentionedUsers = token["mentions"].Select(ParseUser).ToArray();
// Get role mentions
var mentionedRoles = token["mention_roles"]
.Values<string>()
.Select(i => roles.GetOrDefault(i) ?? new Role(i, "deleted-role"))
.ToArray();
// Get channel mentions
var mentionedChanenls = Regex.Matches(content, "<#(\\d+)>")
.Cast<Match>()
.Select(m => m.Groups[1].Value)
.ExceptBlank()
.Select(i => channels.GetOrDefault(i) ?? new Channel(i, "deleted-channel", ChannelType.GuildTextChat))
.ToArray();
return new Message(id, author, timeStamp, editedTimeStamp, content, attachments,
mentionedUsers, mentionedRoles, mentionedChanenls);
} }
} }
} }
+81 -17
View File
@@ -46,7 +46,7 @@ namespace DiscordChatExporter.Services
// Content // Content
if (message.Content.IsNotBlank()) if (message.Content.IsNotBlank())
{ {
var contentFormatted = message.Content.Replace("\n", Environment.NewLine); var contentFormatted = FormatMessageContentText(message);
await writer.WriteLineAsync(contentFormatted); await writer.WriteLineAsync(contentFormatted);
} }
@@ -119,7 +119,7 @@ namespace DiscordChatExporter.Services
if (message.Content.IsNotBlank()) if (message.Content.IsNotBlank())
{ {
await writer.WriteLineAsync("<div class=\"msg-content\">"); await writer.WriteLineAsync("<div class=\"msg-content\">");
var contentFormatted = FormatMessageContentHtml(message.Content); var contentFormatted = FormatMessageContentHtml(message);
await writer.WriteAsync(contentFormatted); await writer.WriteAsync(contentFormatted);
// Edited timestamp // Edited timestamp
@@ -215,38 +215,102 @@ namespace DiscordChatExporter.Services
return $"{size:0.#} {units[unit]}"; return $"{size:0.#} {units[unit]}";
} }
private static string FormatMessageContentHtml(string content) public static string FormatMessageContentText(Message message)
{ {
var content = message.Content;
// New lines
content = content.Replace("\n", Environment.NewLine);
// User mentions (<@id>)
foreach (var mentionedUser in message.MentionedUsers)
content = Regex.Replace(content, $"<@!?{mentionedUser.Id}>", $"@{mentionedUser}");
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
content = content.Replace($"<@&{mentionedRole.Id}>", $"@{mentionedRole.Name}");
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
content = content.Replace($"<#{mentionedChannel.Id}>", $"#{mentionedChannel.Name}");
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "<(:.*?:)\\d*>", "$1");
return content;
}
private static string FormatMessageContentHtml(Message message)
{
var content = message.Content;
// Encode HTML // Encode HTML
content = HtmlEncode(content); content = HtmlEncode(content);
// Preformatted div // Pre multiline (```text```)
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", "<div class=\"pre\">$1</div>");
m => "<div class=\"pre\">" + m.Groups[1].Value + "</div>");
// Preformatted span // Pre inline (`text`)
content = Regex.Replace(content, "`([^`]+)`", content = Regex.Replace(content, "`([^`]+)`", "<span class=\"pre\">$1</span>");
m => "<span class=\"pre\">" + m.Groups[1].Value + "</span>");
// Links from URLs // Bold (**text**)
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)",
"<a href=\"$1\">$1</a>");
// Bold
content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>"); content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>");
// Italic // Italic (*text*)
content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>"); content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>");
// Underline // Underline (__text__)
content = Regex.Replace(content, "__([^_]*?)__", "<u>$1</u>"); content = Regex.Replace(content, "__([^_]*?)__", "<u>$1</u>");
// Strike through // Italic (_text_)
content = Regex.Replace(content, "_([^_]*?)_", "<i>$1</i>");
// Strike through (~~text~~)
content = Regex.Replace(content, "~~([^~]*?)~~", "<s>$1</s>"); content = Regex.Replace(content, "~~([^~]*?)~~", "<s>$1</s>");
// New lines // New lines
content = content.Replace("\n", "<br />"); content = content.Replace("\n", "<br />");
// URL links
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s<>]*)", "<a href=\"$1\">$1</a>");
// Meta mentions (@everyone)
content = content.Replace("@everyone", "<span class=\"mention\">@everyone</span>");
// Meta mentions (@here)
content = content.Replace("@here", "<span class=\"mention\">@here</span>");
// User mentions (<@id>)
foreach (var mentionedUser in message.MentionedUsers)
{
content = Regex.Replace(content, $"&lt;@!?{mentionedUser.Id}&gt;",
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
$"@{HtmlEncode(mentionedUser.Name)}" +
"</span>");
}
// Role mentions (<@&id>)
foreach (var mentionedRole in message.MentionedRoles)
{
content = content.Replace($"&lt;@&amp;{mentionedRole.Id}&gt;",
"<span class=\"mention\">" +
$"@{HtmlEncode(mentionedRole.Name)}" +
"</span>");
}
// Channel mentions (<#id>)
foreach (var mentionedChannel in message.MentionedChannels)
{
content = content.Replace($"&lt;#{mentionedChannel.Id}&gt;",
"<span class=\"mention\">" +
$"#{HtmlEncode(mentionedChannel.Name)}" +
"</span>");
}
// Custom emojis (<:name:id>)
content = Regex.Replace(content, "&lt;(:.*?:)(\\d*)&gt;",
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
return content; return content;
} }
} }
+18 -18
View File
@@ -2,24 +2,24 @@
UserControl "DiscordChatExporter.Views.ErrorDialog" { UserControl "DiscordChatExporter.Views.ErrorDialog" {
DataContext: bind ErrorViewModel from $resource Container DataContext: bind ErrorViewModel from $resource Container
Width: 250 Width: 250
StackPanel { StackPanel {
// Message // Message
TextBlock { TextBlock {
Margin: 16 Margin: 16
FontSize: 16 FontSize: 16
TextWrapping: WrapWithOverflow TextWrapping: WrapWithOverflow
Text: bind Message Text: bind Message
}
// OK
Button {
Margin: 8
Command: DialogHost.CloseDialogCommand
Content: "OK"
HorizontalAlignment: Right
Style: resource dyn "MaterialDesignFlatButton"
}
} }
// OK
Button {
Margin: 8
Command: DialogHost.CloseDialogCommand
Content: "OK"
HorizontalAlignment: Right
Style: resource dyn "MaterialDesignFlatButton"
}
}
} }
@@ -19,18 +19,18 @@ UserControl "DiscordChatExporter.Views.ExportDoneDialog" {
// Open // Open
Button "OpenButton" { Button "OpenButton" {
Margin: 8
Click: OpenButton_Click Click: OpenButton_Click
Command: bind OpenCommand Command: bind OpenCommand
Content: "OPEN IT" Content: "OPEN IT"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
// Dismiss // Dismiss
Button { Button {
Margin: 8
Command: DialogHost.CloseDialogCommand Command: DialogHost.CloseDialogCommand
Content: "DISMISS" Content: "DISMISS"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
} }
@@ -8,7 +8,7 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
StackPanel { StackPanel {
// File path // File path
TextBox { TextBox {
Margin: "16 16 16 8" Margin: [16, 16, 16, 8]
HintAssist.Hint: "Output file" HintAssist.Hint: "Output file"
HintAssist.IsFloating: true HintAssist.IsFloating: true
IsReadOnly: true IsReadOnly: true
@@ -18,18 +18,19 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
// Format // Format
ComboBox { ComboBox {
Margin: "16 8 16 8" Margin: [16, 8, 16, 8]
HintAssist.Hint: "Export format" HintAssist.Hint: "Export format"
HintAssist.IsFloating: true HintAssist.IsFloating: true
IsReadOnly: true IsReadOnly: true
ItemsSource: bind AvailableFormats ItemsSource: bind AvailableFormats
SelectedItem: bind SelectedFormat
ItemTemplate: DataTemplate { ItemTemplate: DataTemplate {
TextBlock { TextBlock {
Text: bind Text: bind
convert (ExportFormat f) => Extensions.GetDisplayName(f) convert (ExportFormat f) => Extensions.GetDisplayName(f)
} }
} }
SelectedItem: bind SelectedFormat
} }
// Date range // Date range
@@ -37,16 +38,16 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
#TwoColumns("*", "*") #TwoColumns("*", "*")
DatePicker { DatePicker {
Grid.Column: 0 #Cell(0, 0)
Margin: "16 20 8 8" Margin: [16, 20, 8, 8]
HintAssist.Hint: "From (optional)" HintAssist.Hint: "From (optional)"
HintAssist.IsFloating: true HintAssist.IsFloating: true
SelectedDate: bind From SelectedDate: bind From
} }
DatePicker { DatePicker {
Grid.Column: 1 #Cell(0, 1)
Margin: "8 20 16 8" Margin: [8, 20, 16, 8]
HintAssist.Hint: "To (optional)" HintAssist.Hint: "To (optional)"
HintAssist.IsFloating: true HintAssist.IsFloating: true
SelectedDate: bind To SelectedDate: bind To
@@ -59,26 +60,27 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
// Browse // Browse
Button "BrowseButton" { Button "BrowseButton" {
Margin: 8
Click: BrowseButton_Click Click: BrowseButton_Click
Content: "BROWSE" Content: "BROWSE"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
// Export // Export
Button "ExportButton" { Button "ExportButton" {
Margin: 8
Click: ExportButton_Click Click: ExportButton_Click
Command: bind ExportCommand Command: bind ExportCommand
Content: "EXPORT" Content: "EXPORT"
Margin: 8 IsDefault: true
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
// Cancel // Cancel
Button { Button {
Margin: 8
Command: DialogHost.CloseDialogCommand Command: DialogHost.CloseDialogCommand
Content: "CANCEL" Content: "CANCEL"
Margin: 8
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
} }
} }
+17 -17
View File
@@ -33,32 +33,32 @@ Window "DiscordChatExporter.Views.MainWindow" {
#TwoColumns("*", "Auto") #TwoColumns("*", "Auto")
Card { Card {
Grid.Column: 0 #Cell(0, 0)
Margin: "6 6 0 6" Margin: [6, 6, 0, 6]
Grid { Grid {
#TwoColumns("*", "Auto") #TwoColumns("*", "Auto")
// Token // Token
TextBox "TokenTextBox" { TextBox "TokenTextBox" {
Grid.Column: 0 #Cell(0, 0)
Margin: 6 Margin: 6
BorderThickness: 0 BorderThickness: 0
HintAssist.Hint: "Token" HintAssist.Hint: "Token"
KeyDown: TokenTextBox_KeyDown
FontSize: 16 FontSize: 16
Text: bind Token Text: bind Token
set [ UpdateSourceTrigger: PropertyChanged ] set [ UpdateSourceTrigger: PropertyChanged ]
TextFieldAssist.DecorationVisibility: Hidden TextFieldAssist.DecorationVisibility: Hidden
TextFieldAssist.TextBoxViewMargin: "0 0 2 0" TextFieldAssist.TextBoxViewMargin: [0, 0, 2, 0]
} }
// Submit // Submit
Button { Button {
Grid.Column: 1 #Cell(0, 1)
Margin: "0 6 6 6" Margin: [0, 6, 6, 6]
Padding: 4 Padding: 4
Command: bind PullDataCommand Command: bind PullDataCommand
IsDefault: true
Style: resource dyn "MaterialDesignFlatButton" Style: resource dyn "MaterialDesignFlatButton"
PackIcon { PackIcon {
@@ -72,7 +72,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
// Popup menu // Popup menu
PopupBox { PopupBox {
Grid.Column: 1 #Cell(0, 1)
Foreground: resource dyn "PrimaryHueMidForegroundBrush" Foreground: resource dyn "PrimaryHueMidForegroundBrush"
PlacementMode: LeftAndAlignTopEdges PlacementMode: LeftAndAlignTopEdges
@@ -122,7 +122,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
TransitioningContent { TransitioningContent {
OpeningEffect: TransitionEffect { OpeningEffect: TransitionEffect {
Duration: "0:0:0.3" Duration: "0:0:0.3"
Kind: SlideInFromRight Kind: SlideInFromLeft
} }
Border { Border {
@@ -131,9 +131,11 @@ Window "DiscordChatExporter.Views.MainWindow" {
Cursor: CursorType.Hand Cursor: CursorType.Hand
Image { Image {
Margin: "12 4 12 4" Margin: [12, 4, 12, 4]
Width: 48 Width: 48
Height: 48 Height: 48
Source: bind IconUrl
ToolTip: bind Name
OpacityMask: RadialGradientBrush { OpacityMask: RadialGradientBrush {
GradientStops: [ GradientStops: [
GradientStop { Color: "#FF000000", Offset: 0 } GradientStop { Color: "#FF000000", Offset: 0 }
@@ -141,8 +143,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
GradientStop { Color: "#00000000", Offset: 1 } GradientStop { Color: "#00000000", Offset: 1 }
] ]
} }
Source: bind IconUrl
ToolTip: bind Name
} }
} }
} }
@@ -177,12 +177,12 @@ Window "DiscordChatExporter.Views.MainWindow" {
] ]
PackIcon { PackIcon {
Margin: "16 7 0 6" Margin: [16, 7, 0, 6]
Kind: PackIconKind.Pound Kind: PackIconKind.Pound
VerticalAlignment: Center VerticalAlignment: Center
} }
TextBlock { TextBlock {
Margin: "3 8 8 8" Margin: [3, 8, 8, 8]
FontSize: 14 FontSize: 14
Text: bind Name Text: bind Name
VerticalAlignment: Center VerticalAlignment: Center
@@ -196,7 +196,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
// Content placeholder // Content placeholder
StackPanel { StackPanel {
Margin: "32 32 8 8" Margin: [32, 32, 8, 8]
Visibility: bind IsDataAvailable Visibility: bind IsDataAvailable
convert (bool b) => b ? Visibility.Hidden : Visibility.Visible convert (bool b) => b ? Visibility.Hidden : Visibility.Visible
@@ -206,13 +206,13 @@ Window "DiscordChatExporter.Views.MainWindow" {
} }
TextBlock { TextBlock {
Margin: "0 8 0 0" Margin: [0, 8, 0, 0]
FontSize: 16 FontSize: 16
Text: "To obtain it, follow these steps:" Text: "To obtain it, follow these steps:"
} }
TextBlock { TextBlock {
Margin: "8 0 0 0" Margin: [8, 0, 0, 0]
FontSize: 14 FontSize: 14
Run { Run {
+9 -17
View File
@@ -1,7 +1,5 @@
using System.Reflection; using System.Reflection;
using System.Windows.Input;
using DiscordChatExporter.Messages; using DiscordChatExporter.Messages;
using DiscordChatExporter.ViewModels;
using GalaSoft.MvvmLight.Messaging; using GalaSoft.MvvmLight.Messaging;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Tyrrrz.Extensions; using Tyrrrz.Extensions;
@@ -10,26 +8,20 @@ namespace DiscordChatExporter.Views
{ {
public partial class MainWindow public partial class MainWindow
{ {
private IMainViewModel ViewModel => (IMainViewModel) DataContext;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
Title += $" v{Assembly.GetExecutingAssembly().GetName().Version}"; Title += $" v{Assembly.GetExecutingAssembly().GetName().Version}";
Messenger.Default.Register<ShowErrorMessage>(this, m => DialogHost.Show(new ErrorDialog()).Forget()); // Dialogs
Messenger.Default.Register<ShowExportDoneMessage>(this, m => DialogHost.Show(new ExportDoneDialog()).Forget()); Messenger.Default.Register<ShowErrorMessage>(this,
Messenger.Default.Register<ShowExportSetupMessage>(this, m => DialogHost.Show(new ExportSetupDialog()).Forget()); m => DialogHost.Show(new ErrorDialog()).Forget());
Messenger.Default.Register<ShowSettingsMessage>(this, m => DialogHost.Show(new SettingsDialog()).Forget()); Messenger.Default.Register<ShowExportDoneMessage>(this,
} m => DialogHost.Show(new ExportDoneDialog()).Forget());
Messenger.Default.Register<ShowExportSetupMessage>(this,
public void TokenTextBox_KeyDown(object sender, KeyEventArgs e) m => DialogHost.Show(new ExportSetupDialog()).Forget());
{ Messenger.Default.Register<ShowSettingsMessage>(this,
if (e.Key == Key.Enter) m => DialogHost.Show(new SettingsDialog()).Forget());
{
// Execute command
ViewModel.PullDataCommand.Execute(null);
}
} }
} }
} }
@@ -7,7 +7,7 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
StackPanel { StackPanel {
// Date format // Date format
TextBox { TextBox {
Margin: "16 16 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
@@ -15,7 +15,7 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
// Group limit // Group limit
TextBox { TextBox {
Margin: "16 8 16 8" Margin: [16, 8, 16, 8]
HintAssist.Hint: "Message group limit" HintAssist.Hint: "Message group limit"
HintAssist.IsFloating: true HintAssist.IsFloating: true
Text: bind MessageGroupLimit Text: bind MessageGroupLimit
+6 -1
View File
@@ -1,5 +1,9 @@
# DiscordChatExporter # DiscordChatExporter
[![Build](https://img.shields.io/appveyor/ci/Tyrrrz/DiscordChatExporter/master.svg)](https://ci.appveyor.com/project/Tyrrrz/DiscordChatExporter)
[![Release](https://img.shields.io/github/release/Tyrrrz/DiscordChatExporter.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)
[![Downloads](https://img.shields.io/github/downloads/Tyrrrz/DiscordChatExporter/total.svg)](https://github.com/Tyrrrz/DiscordChatExporter/releases)
DiscordChatExporter can be used to export message history from a [Discord](https://discordapp.com) channel to a file. It works for both direct message chats and guild chats, supports markdown, message grouping, and attachments. The tool also lets you select from/to dates to limit the exported messages. There are options to configure the output, such as date format, color theme, message grouping limit, etc. DiscordChatExporter can be used to export message history from a [Discord](https://discordapp.com) channel to a file. It works for both direct message chats and guild chats, supports markdown, message grouping, and attachments. The tool also lets you select from/to dates to limit the exported messages. There are options to configure the output, such as date format, color theme, message grouping limit, etc.
## Screenshots ## Screenshots
@@ -9,7 +13,8 @@ DiscordChatExporter can be used to export message history from a [Discord](https
## Download ## Download
- [See releases](https://github.com/Tyrrrz/DiscordChatExporter/releases) - [Stable releases](https://github.com/Tyrrrz/DiscordChatExporter/releases)
- [Continuous integration](https://ci.appveyor.com/project/Tyrrrz/DiscordChatExporter)
## Features ## Features