mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 22:54:38 +02:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d6edfd937 | |||
| 60a5a69aa4 | |||
| cc565598d3 | |||
| 6a7000cbc9 | |||
| 6706ff95f1 | |||
| ecc95c9c7f | |||
| d4f2049db6 | |||
| 4d5118de76 | |||
| 171718989a | |||
| 75e8d59cc3 | |||
| 1d7264a71b | |||
| 2c0a63352c | |||
| 42d4d64695 | |||
| 6968f987ce | |||
| 5ecbfd50b8 | |||
| e59a1ea8b4 | |||
| 540ce7f3c3 | |||
| a0d261f966 | |||
| 10446064d5 | |||
| 6709821324 |
+4
-1
@@ -261,4 +261,7 @@ __pycache__/
|
||||
*.pyc
|
||||
|
||||
# Ammy auto-generated XAML
|
||||
*.g.xaml
|
||||
*.g.xaml
|
||||
|
||||
# Deploy output
|
||||
Deploy/Output/
|
||||
@@ -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\ExportFormat.cs" />
|
||||
<Compile Include="Models\Extensions.cs" />
|
||||
<Compile Include="Models\Role.cs" />
|
||||
<Compile Include="Models\MessageType.cs" />
|
||||
<Compile Include="Services\IMessageGroupService.cs" />
|
||||
<Compile Include="Services\MessageGroupService.cs" />
|
||||
<Compile Include="ViewModels\ErrorViewModel.cs" />
|
||||
|
||||
@@ -17,9 +17,17 @@ namespace DiscordChatExporter.Models
|
||||
|
||||
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,
|
||||
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;
|
||||
Author = author;
|
||||
@@ -27,6 +35,9 @@ namespace DiscordChatExporter.Models
|
||||
EditedTimeStamp = editedTimeStamp;
|
||||
Content = content;
|
||||
Attachments = attachments;
|
||||
MentionedUsers = mentionedUsers;
|
||||
MentionedRoles = mentionedRoles;
|
||||
MentionedChannels = mentionedChannels;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public enum MessageType
|
||||
{
|
||||
Default,
|
||||
RecipientAdd,
|
||||
RecipientRemove,
|
||||
Call,
|
||||
ChannelNameChange,
|
||||
ChannelIconChange,
|
||||
ChannelPinnedMessage,
|
||||
GuildMemberJoin
|
||||
}
|
||||
}
|
||||
@@ -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: AssemblyCompany("Tyrrrz")]
|
||||
[assembly: AssemblyCopyright("Copyright (c) 2017 Alexey Golub")]
|
||||
[assembly: AssemblyVersion("2.2.1")]
|
||||
[assembly: AssemblyFileVersion("2.2.1")]
|
||||
[assembly: AssemblyVersion("2.3")]
|
||||
[assembly: AssemblyFileVersion("2.3")]
|
||||
@@ -18,7 +18,7 @@ div.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
@@ -26,7 +26,7 @@ span.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
@@ -106,6 +106,7 @@ span.msg-edited {
|
||||
div.msg-content {
|
||||
font-size: .9375rem;
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
@@ -116,4 +117,14 @@ div.msg-attachment {
|
||||
img.msg-attachment {
|
||||
max-height: 500px;
|
||||
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;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
@@ -25,6 +26,7 @@ span.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
@@ -104,6 +106,7 @@ span.msg-edited {
|
||||
div.msg-content {
|
||||
font-size: .9375rem;
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
@@ -114,4 +117,14 @@ div.msg-attachment {
|
||||
img.msg-attachment {
|
||||
max-height: 500px;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
img.emoji {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
vertical-align: -.4em;
|
||||
}
|
||||
|
||||
span.mention {
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Exceptions;
|
||||
using DiscordChatExporter.Models;
|
||||
@@ -13,7 +14,10 @@ namespace DiscordChatExporter.Services
|
||||
public partial class DataService : IDataService, IDisposable
|
||||
{
|
||||
private const string ApiRoot = "https://discordapp.com/api/v6";
|
||||
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
// Form request url
|
||||
@@ -40,6 +58,14 @@ namespace DiscordChatExporter.Services
|
||||
// Parse
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -68,6 +94,10 @@ namespace DiscordChatExporter.Services
|
||||
// Parse
|
||||
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
||||
|
||||
// Cache
|
||||
foreach (var channel in channels)
|
||||
_channelCache[channel.Id] = channel;
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
@@ -78,7 +108,7 @@ namespace DiscordChatExporter.Services
|
||||
|
||||
// We are going backwards from last message to first
|
||||
// collecting everything between them in batches
|
||||
string beforeId = null;
|
||||
var beforeId = to != null ? DateTimeToSnowflake(to.Value) : null;
|
||||
while (true)
|
||||
{
|
||||
// Form request url
|
||||
@@ -90,12 +120,20 @@ namespace DiscordChatExporter.Services
|
||||
var content = await GetStringAsync(url);
|
||||
|
||||
// Parse
|
||||
var messages = JArray.Parse(content).Select(ParseMessage);
|
||||
var messages = JArray.Parse(content).Select(j => ParseMessage(j, _roleCache, _channelCache));
|
||||
|
||||
// Add messages to list
|
||||
var currentMessageId = default(string);
|
||||
string currentMessageId = null;
|
||||
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);
|
||||
currentMessageId = message.Id;
|
||||
}
|
||||
@@ -104,10 +142,6 @@ namespace DiscordChatExporter.Services
|
||||
if (currentMessageId == null)
|
||||
break;
|
||||
|
||||
// If last message is older than from date - break
|
||||
if (from != null && result.Last().TimeStamp < from)
|
||||
break;
|
||||
|
||||
// Otherwise offset the next request
|
||||
beforeId = currentMessageId;
|
||||
}
|
||||
@@ -115,12 +149,6 @@ namespace DiscordChatExporter.Services
|
||||
// Messages appear newest first, we need to reverse
|
||||
result.Reverse();
|
||||
|
||||
// Filter
|
||||
if (from != null)
|
||||
result.RemoveAll(m => m.TimeStamp < from);
|
||||
if (to != null)
|
||||
result.RemoveAll(m => m.TimeStamp > to);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -141,6 +169,23 @@ namespace DiscordChatExporter.Services
|
||||
|
||||
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)
|
||||
{
|
||||
var id = token.Value<string>("id");
|
||||
@@ -151,13 +196,12 @@ namespace DiscordChatExporter.Services
|
||||
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 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)
|
||||
@@ -181,17 +225,31 @@ namespace DiscordChatExporter.Services
|
||||
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
|
||||
var id = token.Value<string>("id");
|
||||
var timeStamp = token.Value<DateTime>("timestamp");
|
||||
var editedTimeStamp = token.Value<DateTime?>("edited_timestamp");
|
||||
var content = token.Value<string>("content");
|
||||
var type = (MessageType) token.Value<int>("type");
|
||||
|
||||
// Lazy workaround for calls
|
||||
if (token["call"] != null)
|
||||
// Workarounds for non-default types
|
||||
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.";
|
||||
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
|
||||
var author = ParseUser(token["author"]);
|
||||
@@ -214,7 +272,25 @@ namespace DiscordChatExporter.Services
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ namespace DiscordChatExporter.Services
|
||||
// Content
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
var contentFormatted = message.Content.Replace("\n", Environment.NewLine);
|
||||
var contentFormatted = FormatMessageContentText(message);
|
||||
await writer.WriteLineAsync(contentFormatted);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace DiscordChatExporter.Services
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
||||
var contentFormatted = FormatMessageContentHtml(message.Content);
|
||||
var contentFormatted = FormatMessageContentHtml(message);
|
||||
await writer.WriteAsync(contentFormatted);
|
||||
|
||||
// Edited timestamp
|
||||
@@ -215,38 +215,102 @@ namespace DiscordChatExporter.Services
|
||||
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
|
||||
content = HtmlEncode(content);
|
||||
|
||||
// Preformatted div
|
||||
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+",
|
||||
m => "<div class=\"pre\">" + m.Groups[1].Value + "</div>");
|
||||
// Pre multiline (```text```)
|
||||
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", "<div class=\"pre\">$1</div>");
|
||||
|
||||
// Preformatted span
|
||||
content = Regex.Replace(content, "`([^`]+)`",
|
||||
m => "<span class=\"pre\">" + m.Groups[1].Value + "</span>");
|
||||
// Pre inline (`text`)
|
||||
content = Regex.Replace(content, "`([^`]+)`", "<span class=\"pre\">$1</span>");
|
||||
|
||||
// Links from URLs
|
||||
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)",
|
||||
"<a href=\"$1\">$1</a>");
|
||||
|
||||
// Bold
|
||||
// Bold (**text**)
|
||||
content = Regex.Replace(content, "\\*\\*([^\\*]*?)\\*\\*", "<b>$1</b>");
|
||||
|
||||
// Italic
|
||||
// Italic (*text*)
|
||||
content = Regex.Replace(content, "\\*([^\\*]*?)\\*", "<i>$1</i>");
|
||||
|
||||
// Underline
|
||||
// Underline (__text__)
|
||||
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>");
|
||||
|
||||
// New lines
|
||||
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, $"<@!?{mentionedUser.Id}>",
|
||||
$"<span class=\"mention\" title=\"{HtmlEncode(mentionedUser)}\">" +
|
||||
$"@{HtmlEncode(mentionedUser.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Role mentions (<@&id>)
|
||||
foreach (var mentionedRole in message.MentionedRoles)
|
||||
{
|
||||
content = content.Replace($"<@&{mentionedRole.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"@{HtmlEncode(mentionedRole.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Channel mentions (<#id>)
|
||||
foreach (var mentionedChannel in message.MentionedChannels)
|
||||
{
|
||||
content = content.Replace($"<#{mentionedChannel.Id}>",
|
||||
"<span class=\"mention\">" +
|
||||
$"#{HtmlEncode(mentionedChannel.Name)}" +
|
||||
"</span>");
|
||||
}
|
||||
|
||||
// Custom emojis (<:name:id>)
|
||||
content = Regex.Replace(content, "<(:.*?:)(\\d*)>",
|
||||
"<img class=\"emoji\" title=\"$1\" src=\"https://cdn.discordapp.com/emojis/$2.png\" />");
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
|
||||
UserControl "DiscordChatExporter.Views.ErrorDialog" {
|
||||
DataContext: bind ErrorViewModel from $resource Container
|
||||
Width: 250
|
||||
Width: 250
|
||||
|
||||
StackPanel {
|
||||
// Message
|
||||
TextBlock {
|
||||
Margin: 16
|
||||
FontSize: 16
|
||||
TextWrapping: WrapWithOverflow
|
||||
Text: bind Message
|
||||
}
|
||||
|
||||
// OK
|
||||
Button {
|
||||
Margin: 8
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
Content: "OK"
|
||||
HorizontalAlignment: Right
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
StackPanel {
|
||||
// Message
|
||||
TextBlock {
|
||||
Margin: 16
|
||||
FontSize: 16
|
||||
TextWrapping: WrapWithOverflow
|
||||
Text: bind Message
|
||||
}
|
||||
|
||||
// OK
|
||||
Button {
|
||||
Margin: 8
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
Content: "OK"
|
||||
HorizontalAlignment: Right
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,18 +19,18 @@ UserControl "DiscordChatExporter.Views.ExportDoneDialog" {
|
||||
|
||||
// Open
|
||||
Button "OpenButton" {
|
||||
Margin: 8
|
||||
Click: OpenButton_Click
|
||||
Command: bind OpenCommand
|
||||
Content: "OPEN IT"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Dismiss
|
||||
Button {
|
||||
Margin: 8
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
Content: "DISMISS"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
StackPanel {
|
||||
// File path
|
||||
TextBox {
|
||||
Margin: "16 16 16 8"
|
||||
Margin: [16, 16, 16, 8]
|
||||
HintAssist.Hint: "Output file"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
@@ -18,18 +18,19 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
|
||||
// Format
|
||||
ComboBox {
|
||||
Margin: "16 8 16 8"
|
||||
Margin: [16, 8, 16, 8]
|
||||
HintAssist.Hint: "Export format"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
ItemsSource: bind AvailableFormats
|
||||
SelectedItem: bind SelectedFormat
|
||||
|
||||
ItemTemplate: DataTemplate {
|
||||
TextBlock {
|
||||
Text: bind
|
||||
convert (ExportFormat f) => Extensions.GetDisplayName(f)
|
||||
}
|
||||
}
|
||||
SelectedItem: bind SelectedFormat
|
||||
}
|
||||
|
||||
// Date range
|
||||
@@ -37,16 +38,16 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
#TwoColumns("*", "*")
|
||||
|
||||
DatePicker {
|
||||
Grid.Column: 0
|
||||
Margin: "16 20 8 8"
|
||||
#Cell(0, 0)
|
||||
Margin: [16, 20, 8, 8]
|
||||
HintAssist.Hint: "From (optional)"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind From
|
||||
}
|
||||
|
||||
DatePicker {
|
||||
Grid.Column: 1
|
||||
Margin: "8 20 16 8"
|
||||
#Cell(0, 1)
|
||||
Margin: [8, 20, 16, 8]
|
||||
HintAssist.Hint: "To (optional)"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind To
|
||||
@@ -59,26 +60,27 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
|
||||
// Browse
|
||||
Button "BrowseButton" {
|
||||
Margin: 8
|
||||
Click: BrowseButton_Click
|
||||
Content: "BROWSE"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Export
|
||||
Button "ExportButton" {
|
||||
Margin: 8
|
||||
Click: ExportButton_Click
|
||||
Command: bind ExportCommand
|
||||
Content: "EXPORT"
|
||||
Margin: 8
|
||||
IsDefault: true
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Cancel
|
||||
Button {
|
||||
Margin: 8
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
Content: "CANCEL"
|
||||
Margin: 8
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,32 +33,32 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
#TwoColumns("*", "Auto")
|
||||
|
||||
Card {
|
||||
Grid.Column: 0
|
||||
Margin: "6 6 0 6"
|
||||
#Cell(0, 0)
|
||||
Margin: [6, 6, 0, 6]
|
||||
|
||||
Grid {
|
||||
#TwoColumns("*", "Auto")
|
||||
|
||||
// Token
|
||||
TextBox "TokenTextBox" {
|
||||
Grid.Column: 0
|
||||
#Cell(0, 0)
|
||||
Margin: 6
|
||||
BorderThickness: 0
|
||||
HintAssist.Hint: "Token"
|
||||
KeyDown: TokenTextBox_KeyDown
|
||||
FontSize: 16
|
||||
Text: bind Token
|
||||
set [ UpdateSourceTrigger: PropertyChanged ]
|
||||
TextFieldAssist.DecorationVisibility: Hidden
|
||||
TextFieldAssist.TextBoxViewMargin: "0 0 2 0"
|
||||
TextFieldAssist.TextBoxViewMargin: [0, 0, 2, 0]
|
||||
}
|
||||
|
||||
// Submit
|
||||
Button {
|
||||
Grid.Column: 1
|
||||
Margin: "0 6 6 6"
|
||||
#Cell(0, 1)
|
||||
Margin: [0, 6, 6, 6]
|
||||
Padding: 4
|
||||
Command: bind PullDataCommand
|
||||
IsDefault: true
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
|
||||
PackIcon {
|
||||
@@ -72,7 +72,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
|
||||
// Popup menu
|
||||
PopupBox {
|
||||
Grid.Column: 1
|
||||
#Cell(0, 1)
|
||||
Foreground: resource dyn "PrimaryHueMidForegroundBrush"
|
||||
PlacementMode: LeftAndAlignTopEdges
|
||||
|
||||
@@ -122,7 +122,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
TransitioningContent {
|
||||
OpeningEffect: TransitionEffect {
|
||||
Duration: "0:0:0.3"
|
||||
Kind: SlideInFromRight
|
||||
Kind: SlideInFromLeft
|
||||
}
|
||||
|
||||
Border {
|
||||
@@ -131,9 +131,11 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
Cursor: CursorType.Hand
|
||||
|
||||
Image {
|
||||
Margin: "12 4 12 4"
|
||||
Margin: [12, 4, 12, 4]
|
||||
Width: 48
|
||||
Height: 48
|
||||
Source: bind IconUrl
|
||||
ToolTip: bind Name
|
||||
OpacityMask: RadialGradientBrush {
|
||||
GradientStops: [
|
||||
GradientStop { Color: "#FF000000", Offset: 0 }
|
||||
@@ -141,8 +143,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
GradientStop { Color: "#00000000", Offset: 1 }
|
||||
]
|
||||
}
|
||||
Source: bind IconUrl
|
||||
ToolTip: bind Name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,12 +177,12 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
]
|
||||
|
||||
PackIcon {
|
||||
Margin: "16 7 0 6"
|
||||
Margin: [16, 7, 0, 6]
|
||||
Kind: PackIconKind.Pound
|
||||
VerticalAlignment: Center
|
||||
}
|
||||
TextBlock {
|
||||
Margin: "3 8 8 8"
|
||||
Margin: [3, 8, 8, 8]
|
||||
FontSize: 14
|
||||
Text: bind Name
|
||||
VerticalAlignment: Center
|
||||
@@ -196,7 +196,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
|
||||
// Content placeholder
|
||||
StackPanel {
|
||||
Margin: "32 32 8 8"
|
||||
Margin: [32, 32, 8, 8]
|
||||
Visibility: bind IsDataAvailable
|
||||
convert (bool b) => b ? Visibility.Hidden : Visibility.Visible
|
||||
|
||||
@@ -206,13 +206,13 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
}
|
||||
|
||||
TextBlock {
|
||||
Margin: "0 8 0 0"
|
||||
Margin: [0, 8, 0, 0]
|
||||
FontSize: 16
|
||||
Text: "To obtain it, follow these steps:"
|
||||
}
|
||||
|
||||
TextBlock {
|
||||
Margin: "8 0 0 0"
|
||||
Margin: [8, 0, 0, 0]
|
||||
FontSize: 14
|
||||
|
||||
Run {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using DiscordChatExporter.Messages;
|
||||
using DiscordChatExporter.ViewModels;
|
||||
using GalaSoft.MvvmLight.Messaging;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Tyrrrz.Extensions;
|
||||
@@ -10,26 +8,20 @@ namespace DiscordChatExporter.Views
|
||||
{
|
||||
public partial class MainWindow
|
||||
{
|
||||
private IMainViewModel ViewModel => (IMainViewModel) DataContext;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Title += $" v{Assembly.GetExecutingAssembly().GetName().Version}";
|
||||
|
||||
Messenger.Default.Register<ShowErrorMessage>(this, m => DialogHost.Show(new ErrorDialog()).Forget());
|
||||
Messenger.Default.Register<ShowExportDoneMessage>(this, m => DialogHost.Show(new ExportDoneDialog()).Forget());
|
||||
Messenger.Default.Register<ShowExportSetupMessage>(this, m => DialogHost.Show(new ExportSetupDialog()).Forget());
|
||||
Messenger.Default.Register<ShowSettingsMessage>(this, m => DialogHost.Show(new SettingsDialog()).Forget());
|
||||
}
|
||||
|
||||
public void TokenTextBox_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
{
|
||||
// Execute command
|
||||
ViewModel.PullDataCommand.Execute(null);
|
||||
}
|
||||
// Dialogs
|
||||
Messenger.Default.Register<ShowErrorMessage>(this,
|
||||
m => DialogHost.Show(new ErrorDialog()).Forget());
|
||||
Messenger.Default.Register<ShowExportDoneMessage>(this,
|
||||
m => DialogHost.Show(new ExportDoneDialog()).Forget());
|
||||
Messenger.Default.Register<ShowExportSetupMessage>(this,
|
||||
m => DialogHost.Show(new ExportSetupDialog()).Forget());
|
||||
Messenger.Default.Register<ShowSettingsMessage>(this,
|
||||
m => DialogHost.Show(new SettingsDialog()).Forget());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
|
||||
StackPanel {
|
||||
// Date format
|
||||
TextBox {
|
||||
Margin: "16 16 16 8"
|
||||
Margin: [16, 16, 16, 8]
|
||||
HintAssist.Hint: "Date format"
|
||||
HintAssist.IsFloating: true
|
||||
Text: bind DateFormat
|
||||
@@ -15,7 +15,7 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
|
||||
|
||||
// Group limit
|
||||
TextBox {
|
||||
Margin: "16 8 16 8"
|
||||
Margin: [16, 8, 16, 8]
|
||||
HintAssist.Hint: "Message group limit"
|
||||
HintAssist.IsFloating: true
|
||||
Text: bind MessageGroupLimit
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# DiscordChatExporter
|
||||
|
||||
[](https://ci.appveyor.com/project/Tyrrrz/DiscordChatExporter)
|
||||
[](https://github.com/Tyrrrz/DiscordChatExporter/releases)
|
||||
[](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.
|
||||
|
||||
## Screenshots
|
||||
@@ -9,7 +13,8 @@ DiscordChatExporter can be used to export message history from a [Discord](https
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user