mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-07 22:54:38 +02:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5d6edfd937 | |||
| 60a5a69aa4 | |||
| cc565598d3 | |||
| 6a7000cbc9 | |||
| 6706ff95f1 | |||
| ecc95c9c7f | |||
| d4f2049db6 | |||
| 4d5118de76 | |||
| 171718989a | |||
| 75e8d59cc3 | |||
| 1d7264a71b | |||
| 2c0a63352c | |||
| 42d4d64695 | |||
| 6968f987ce | |||
| 5ecbfd50b8 | |||
| e59a1ea8b4 | |||
| 540ce7f3c3 | |||
| a0d261f966 | |||
| 10446064d5 | |||
| 6709821324 | |||
| 705e60c76d | |||
| 8ac8c16db3 | |||
| 1850caba71 | |||
| 6ccaf3f106 | |||
| d7345e91d3 | |||
| e5e9d4c9ff | |||
| b280f8b736 |
+4
-1
@@ -261,4 +261,7 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
|
||||||
# Ammy auto-generated XAML
|
# 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"
|
||||||
@@ -11,11 +11,14 @@ namespace DiscordChatExporter
|
|||||||
{
|
{
|
||||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||||
|
ServiceLocator.Current.GetInstance<ISettingsService>().Load();
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
|
||||||
|
|
||||||
// View models
|
// View models
|
||||||
SimpleIoc.Default.Register<IErrorViewModel, ErrorViewModel>(true);
|
SimpleIoc.Default.Register<IErrorViewModel, ErrorViewModel>(true);
|
||||||
@@ -23,14 +26,11 @@ namespace DiscordChatExporter
|
|||||||
SimpleIoc.Default.Register<IExportSetupViewModel, ExportSetupViewModel>(true);
|
SimpleIoc.Default.Register<IExportSetupViewModel, ExportSetupViewModel>(true);
|
||||||
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
||||||
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
||||||
|
|
||||||
// Load settings
|
|
||||||
ServiceLocator.Current.GetInstance<ISettingsService>().Load();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Cleanup()
|
public static void Cleanup()
|
||||||
{
|
{
|
||||||
// Save settings
|
// Settings
|
||||||
ServiceLocator.Current.GetInstance<ISettingsService>().Save();
|
ServiceLocator.Current.GetInstance<ISettingsService>().Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
@@ -148,7 +150,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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")]
|
[assembly: AssemblyVersion("2.3")]
|
||||||
[assembly: AssemblyFileVersion("2.2")]
|
[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;
|
||||||
}
|
}
|
||||||
@@ -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,16 +94,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
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>();
|
||||||
|
|
||||||
// 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
|
||||||
@@ -89,21 +120,27 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 (from != null && result.Last().TimeStamp < from) break;
|
|
||||||
|
|
||||||
// Otherwise offset the next request
|
// Otherwise offset the next request
|
||||||
beforeId = currentMessageId;
|
beforeId = currentMessageId;
|
||||||
@@ -112,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,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");
|
||||||
@@ -148,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)
|
||||||
@@ -178,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"]);
|
||||||
@@ -211,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,32 +18,35 @@ 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;
|
|
||||||
|
|
||||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
||||||
{
|
{
|
||||||
|
// Generation info
|
||||||
|
await writer.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
|
||||||
|
await writer.WriteLineAsync();
|
||||||
|
|
||||||
// Guild and channel info
|
// Guild and channel info
|
||||||
await writer.WriteLineAsync("=".Repeat(16));
|
await writer.WriteLineAsync('='.Repeat(48));
|
||||||
await writer.WriteLineAsync($"Guild: {log.Guild}");
|
await writer.WriteLineAsync($"Guild: {log.Guild}");
|
||||||
await writer.WriteLineAsync($"Channel: {log.Channel}");
|
await writer.WriteLineAsync($"Channel: {log.Channel}");
|
||||||
await writer.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
await writer.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
||||||
await writer.WriteLineAsync("=".Repeat(16));
|
await writer.WriteLineAsync('='.Repeat(48));
|
||||||
await writer.WriteLineAsync();
|
await writer.WriteLineAsync();
|
||||||
|
|
||||||
// Chat log
|
// Chat log
|
||||||
foreach (var group in log.MessageGroups)
|
foreach (var group in log.MessageGroups)
|
||||||
{
|
{
|
||||||
var timeStampFormatted = group.TimeStamp.ToString(dateFormat);
|
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
|
||||||
await writer.WriteLineAsync($"{group.Author} [{timeStampFormatted}]");
|
await writer.WriteLineAsync($"{group.Author} [{timeStampFormatted}]");
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
foreach (var message in group.Messages)
|
foreach (var message in group.Messages)
|
||||||
{
|
{
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,11 +62,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))
|
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
||||||
{
|
{
|
||||||
// Generation info
|
// Generation info
|
||||||
@@ -80,7 +78,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
|
||||||
@@ -111,7 +109,7 @@ namespace DiscordChatExporter.Services
|
|||||||
await writer.WriteAsync($"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author)}\">");
|
await writer.WriteAsync($"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author)}\">");
|
||||||
await writer.WriteAsync(HtmlEncode(group.Author.Name));
|
await writer.WriteAsync(HtmlEncode(group.Author.Name));
|
||||||
await writer.WriteLineAsync("</span>");
|
await writer.WriteLineAsync("</span>");
|
||||||
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(dateFormat));
|
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
|
||||||
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
@@ -121,14 +119,14 @@ 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
|
||||||
if (message.EditedTimeStamp != null)
|
if (message.EditedTimeStamp != null)
|
||||||
{
|
{
|
||||||
var editedTimeStampFormatted =
|
var editedTimeStampFormatted =
|
||||||
HtmlEncode(message.EditedTimeStamp.Value.ToString(dateFormat));
|
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
|
||||||
await writer.WriteAsync(
|
await writer.WriteAsync(
|
||||||
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
||||||
}
|
}
|
||||||
@@ -168,26 +166,30 @@ namespace DiscordChatExporter.Services
|
|||||||
await writer.WriteLineAsync("</html>");
|
await writer.WriteLineAsync("</html>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log)
|
||||||
|
{
|
||||||
|
if (format == ExportFormat.PlainText)
|
||||||
|
{
|
||||||
|
return ExportAsTextAsync(filePath, log);
|
||||||
|
}
|
||||||
|
if (format == ExportFormat.HtmlDark)
|
||||||
|
{
|
||||||
|
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
|
public partial class ExportService
|
||||||
{
|
{
|
||||||
private static string GetThemeCss(Theme theme)
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
return reader.ReadToEnd();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string HtmlEncode(string str)
|
private static string HtmlEncode(string str)
|
||||||
{
|
{
|
||||||
return WebUtility.HtmlEncode(str);
|
return WebUtility.HtmlEncode(str);
|
||||||
@@ -213,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, $"<@!?{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;
|
return content;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ namespace DiscordChatExporter.Services
|
|||||||
|
|
||||||
public IReadOnlyList<MessageGroup> GroupMessages(IReadOnlyList<Message> messages)
|
public IReadOnlyList<MessageGroup> GroupMessages(IReadOnlyList<Message> messages)
|
||||||
{
|
{
|
||||||
var groupLimit = _settingsService.MessageGroupLimit;
|
|
||||||
var result = new List<MessageGroup>();
|
var result = new List<MessageGroup>();
|
||||||
|
|
||||||
// Group adjacent messages by timestamp and author
|
// Group adjacent messages by timestamp and author
|
||||||
@@ -31,7 +30,7 @@ namespace DiscordChatExporter.Services
|
|||||||
message.Author.Id != groupFirst.Author.Id ||
|
message.Author.Id != groupFirst.Author.Id ||
|
||||||
(message.TimeStamp - groupFirst.TimeStamp).TotalHours > 1 ||
|
(message.TimeStamp - groupFirst.TimeStamp).TotalHours > 1 ||
|
||||||
message.TimeStamp.Hour != groupFirst.TimeStamp.Hour ||
|
message.TimeStamp.Hour != groupFirst.TimeStamp.Hour ||
|
||||||
groupBuffer.Count >= groupLimit
|
groupBuffer.Count >= _settingsService.MessageGroupLimit
|
||||||
);
|
);
|
||||||
|
|
||||||
// If condition is true - flush buffer
|
// If condition is true - flush buffer
|
||||||
|
|||||||
@@ -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";
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
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
|
||||||
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
|
||||||
@@ -15,22 +16,39 @@ 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
|
||||||
|
SelectedItem: bind SelectedFormat
|
||||||
|
|
||||||
|
ItemTemplate: DataTemplate {
|
||||||
|
TextBlock {
|
||||||
|
Text: bind
|
||||||
|
convert (ExportFormat f) => Extensions.GetDisplayName(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
Grid {
|
Grid {
|
||||||
#TwoColumns("*", "*")
|
#TwoColumns("*", "*")
|
||||||
|
|
||||||
DatePicker {
|
DatePicker {
|
||||||
Grid.Column: 0
|
#Cell(0, 0)
|
||||||
Margin: "16 16 8 8"
|
Margin: [16, 20, 8, 8]
|
||||||
HintAssist.Hint: "From"
|
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 16 16 8"
|
Margin: [8, 20, 16, 8]
|
||||||
HintAssist.Hint: "To"
|
HintAssist.Hint: "To (optional)"
|
||||||
HintAssist.IsFloating: true
|
HintAssist.IsFloating: true
|
||||||
SelectedDate: bind To
|
SelectedDate: bind To
|
||||||
}
|
}
|
||||||
@@ -40,28 +58,29 @@ UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
|||||||
@StackPanelHorizontal {
|
@StackPanelHorizontal {
|
||||||
HorizontalAlignment: Right
|
HorizontalAlignment: Right
|
||||||
|
|
||||||
// Export
|
// Browse
|
||||||
Button "ExportButton" {
|
Button "BrowseButton" {
|
||||||
Click: ExportButton_Click
|
|
||||||
Command: bind ExportCommand
|
|
||||||
Content: "EXPORT"
|
|
||||||
Margin: 8
|
Margin: 8
|
||||||
|
Click: BrowseButton_Click
|
||||||
|
Content: "BROWSE"
|
||||||
Style: resource dyn "MaterialDesignFlatButton"
|
Style: resource dyn "MaterialDesignFlatButton"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse
|
// Export
|
||||||
Button "BrowseButton" {
|
Button "ExportButton" {
|
||||||
Click: BrowseButton_Click
|
|
||||||
Content: "BROWSE"
|
|
||||||
Margin: 8
|
Margin: 8
|
||||||
|
Click: ExportButton_Click
|
||||||
|
Command: bind ExportCommand
|
||||||
|
Content: "EXPORT"
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
public void BrowseButton_Click(object sender, RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
var filters = new List<string>();
|
// Get file extension of the selected format
|
||||||
foreach (var format in ViewModel.AvailableFormats)
|
var ext = ViewModel.SelectedFormat.GetFileExtension();
|
||||||
{
|
|
||||||
var ext = format.GetFileExtension();
|
|
||||||
filters.Add($"{format} (*.{ext})|*.{ext}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return filters.JoinToString("|");
|
// Open dialog
|
||||||
|
var sfd = new SaveFileDialog
|
||||||
|
{
|
||||||
|
FileName = ViewModel.FilePath,
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExportButton_Click(object sender, RoutedEventArgs args)
|
public void ExportButton_Click(object sender, RoutedEventArgs args)
|
||||||
{
|
{
|
||||||
DialogHost.CloseDialogCommand.Execute(null, null);
|
DialogHost.CloseDialogCommand.Execute(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void BrowseButton_Click(object sender, RoutedEventArgs args)
|
|
||||||
{
|
|
||||||
var sfd = new SaveFileDialog
|
|
||||||
{
|
|
||||||
FileName = ViewModel.FilePath,
|
|
||||||
Filter = GetFilter(),
|
|
||||||
FilterIndex = ViewModel.AvailableFormats.IndexOf(ViewModel.SelectedFormat) + 1,
|
|
||||||
AddExtension = true,
|
|
||||||
Title = "Select output file"
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sfd.ShowDialog() == true)
|
|
||||||
{
|
|
||||||
ViewModel.FilePath = sfd.FileName;
|
|
||||||
ViewModel.SelectedFormat = ViewModel.AvailableFormats[sfd.FilterIndex - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,9 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
|||||||
|
|
||||||
DialogHost {
|
DialogHost {
|
||||||
DockPanel {
|
DockPanel {
|
||||||
|
IsEnabled: bind IsBusy
|
||||||
|
convert (bool b) => b ? false : true
|
||||||
|
|
||||||
// Toolbar
|
// Toolbar
|
||||||
Border {
|
Border {
|
||||||
DockPanel.Dock: Top
|
DockPanel.Dock: Top
|
||||||
@@ -30,30 +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.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 {
|
||||||
@@ -67,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
|
||||||
|
|
||||||
@@ -98,8 +103,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
|||||||
Grid {
|
Grid {
|
||||||
DockPanel {
|
DockPanel {
|
||||||
Background: resource dyn "MaterialDesignCardBackground"
|
Background: resource dyn "MaterialDesignCardBackground"
|
||||||
IsEnabled: bind IsBusy
|
|
||||||
convert (bool b) => b ? false : true
|
|
||||||
Visibility: bind IsDataAvailable
|
Visibility: bind IsDataAvailable
|
||||||
convert (bool b) => b ? Visibility.Visible : Visibility.Hidden
|
convert (bool b) => b ? Visibility.Visible : Visibility.Hidden
|
||||||
|
|
||||||
@@ -119,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 {
|
||||||
@@ -128,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 }
|
||||||
@@ -138,8 +143,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
|||||||
GradientStop { Color: "#00000000", Offset: 1 }
|
GradientStop { Color: "#00000000", Offset: 1 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Source: bind IconUrl
|
|
||||||
ToolTip: bind Name
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,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
|
||||||
@@ -193,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
|
||||||
|
|
||||||
@@ -203,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 {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
@@ -25,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
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# DiscordChatExporter
|
# 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.
|
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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user