mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-09 15:39:34 +02:00
Compare commits
35 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 | |||
| ca452d2421 | |||
| 9e7d4033e0 | |||
| 263a606bff | |||
| 1e853f11b0 | |||
| 77d0956f5d | |||
| d251e8ecf6 | |||
| 01f0e6952d | |||
| 28a67ab60d |
+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"
|
||||
@@ -11,31 +11,33 @@ namespace DiscordChatExporter
|
||||
{
|
||||
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
|
||||
|
||||
// Settings
|
||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||
ServiceLocator.Current.GetInstance<ISettingsService>().Load();
|
||||
|
||||
// Services
|
||||
SimpleIoc.Default.Register<IDataService, DataService>();
|
||||
SimpleIoc.Default.Register<IExportService, ExportService>();
|
||||
SimpleIoc.Default.Register<IMessageGroupService, MessageGroupService>();
|
||||
SimpleIoc.Default.Register<ISettingsService, SettingsService>();
|
||||
|
||||
// View models
|
||||
SimpleIoc.Default.Register<IErrorViewModel, ErrorViewModel>(true);
|
||||
SimpleIoc.Default.Register<IExportDoneViewModel, ExportDoneViewModel>(true);
|
||||
SimpleIoc.Default.Register<IExportSetupViewModel, ExportSetupViewModel>(true);
|
||||
SimpleIoc.Default.Register<IMainViewModel, MainViewModel>(true);
|
||||
SimpleIoc.Default.Register<ISettingsViewModel, SettingsViewModel>(true);
|
||||
SimpleIoc.Default.Register<IExportDoneViewModel, ExportDoneViewModel>(true);
|
||||
|
||||
// Load settings
|
||||
ServiceLocator.Current.GetInstance<ISettingsService>().Load();
|
||||
}
|
||||
|
||||
public static void Cleanup()
|
||||
{
|
||||
// Save settings
|
||||
// Settings
|
||||
ServiceLocator.Current.GetInstance<ISettingsService>().Save();
|
||||
}
|
||||
|
||||
public IErrorViewModel ErrorViewModel => ServiceLocator.Current.GetInstance<IErrorViewModel>();
|
||||
public IExportDoneViewModel ExportDoneViewModel => ServiceLocator.Current.GetInstance<IExportDoneViewModel>();
|
||||
public IExportSetupViewModel ExportSetupViewModel => ServiceLocator.Current.GetInstance<IExportSetupViewModel>();
|
||||
public IMainViewModel MainViewModel => ServiceLocator.Current.GetInstance<IMainViewModel>();
|
||||
public ISettingsViewModel SettingsViewModel => ServiceLocator.Current.GetInstance<ISettingsViewModel>();
|
||||
public IExportDoneViewModel ExportDoneViewModel => ServiceLocator.Current.GetInstance<IExportDoneViewModel>();
|
||||
}
|
||||
}
|
||||
@@ -84,14 +84,22 @@
|
||||
<Compile Include="Exceptions\HttpErrorStatusCodeException.cs" />
|
||||
<Compile Include="Messages\ShowErrorMessage.cs" />
|
||||
<Compile Include="Messages\ShowExportDoneMessage.cs" />
|
||||
<Compile Include="Messages\ShowExportSetupMessage.cs" />
|
||||
<Compile Include="Messages\ShowSettingsMessage.cs" />
|
||||
<Compile Include="Messages\StartExportMessage.cs" />
|
||||
<Compile Include="Models\AttachmentType.cs" />
|
||||
<Compile Include="Models\ChannelChatLog.cs" />
|
||||
<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" />
|
||||
<Compile Include="ViewModels\ExportSetupViewModel.cs" />
|
||||
<Compile Include="ViewModels\IErrorViewModel.cs" />
|
||||
<Compile Include="ViewModels\IExportSetupViewModel.cs" />
|
||||
<Compile Include="ViewModels\ISettingsViewModel.cs" />
|
||||
<Compile Include="ViewModels\IExportDoneViewModel.cs" />
|
||||
<Compile Include="ViewModels\SettingsViewModel.cs" />
|
||||
@@ -102,6 +110,9 @@
|
||||
<Compile Include="Views\ExportDoneDialog.ammy.cs">
|
||||
<DependentUpon>ExportDoneDialog.ammy</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\ExportSetupDialog.ammy.cs">
|
||||
<DependentUpon>ExportSetupDialog.ammy</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Views\SettingsDialog.ammy.cs">
|
||||
<DependentUpon>SettingsDialog.ammy</DependentUpon>
|
||||
</Compile>
|
||||
@@ -120,6 +131,11 @@
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<DependentUpon>ExportDoneDialog.ammy</DependentUpon>
|
||||
</Page>
|
||||
<Page Include="Views\ExportSetupDialog.g.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<DependentUpon>ExportSetupDialog.ammy</DependentUpon>
|
||||
</Page>
|
||||
<Page Include="Views\MainWindow.g.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
@@ -134,7 +150,6 @@
|
||||
<Compile Include="Models\Guild.cs" />
|
||||
<Compile Include="Models\Message.cs" />
|
||||
<Compile Include="Models\MessageGroup.cs" />
|
||||
<Compile Include="Models\Theme.cs" />
|
||||
<Compile Include="Models\User.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Services\DataService.cs" />
|
||||
@@ -174,6 +189,7 @@
|
||||
</None>
|
||||
<None Include="Views\ErrorDialog.ammy" />
|
||||
<None Include="Views\ExportDoneDialog.ammy" />
|
||||
<None Include="Views\ExportSetupDialog.ammy" />
|
||||
<None Include="Views\MainWindow.ammy" />
|
||||
<None Include="Views\SettingsDialog.ammy" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using DiscordChatExporter.Models;
|
||||
|
||||
namespace DiscordChatExporter.Messages
|
||||
{
|
||||
public class ShowExportSetupMessage
|
||||
{
|
||||
public Guild Guild { get; }
|
||||
|
||||
public Channel Channel { get; }
|
||||
|
||||
public ShowExportSetupMessage(Guild guild, Channel channel)
|
||||
{
|
||||
Guild = guild;
|
||||
Channel = channel;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using DiscordChatExporter.Models;
|
||||
|
||||
namespace DiscordChatExporter.Messages
|
||||
{
|
||||
public class StartExportMessage
|
||||
{
|
||||
public Channel Channel { get; }
|
||||
|
||||
public string FilePath { get; }
|
||||
|
||||
public ExportFormat Format { get; }
|
||||
|
||||
public DateTime? From { get; }
|
||||
|
||||
public DateTime? To { get; }
|
||||
|
||||
public StartExportMessage(Channel channel, string filePath, ExportFormat format,
|
||||
DateTime? from, DateTime? to)
|
||||
{
|
||||
Channel = channel;
|
||||
FilePath = filePath;
|
||||
Format = format;
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public enum ExportFormat
|
||||
{
|
||||
PlainText,
|
||||
HtmlDark,
|
||||
HtmlLight
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static string GetFileExtension(this ExportFormat format)
|
||||
{
|
||||
if (format == ExportFormat.PlainText)
|
||||
return "txt";
|
||||
if (format == ExportFormat.HtmlDark)
|
||||
return "html";
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace DiscordChatExporter.Models
|
||||
{
|
||||
public enum Theme
|
||||
{
|
||||
Dark,
|
||||
Light
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using AmmySidekick;
|
||||
|
||||
namespace DiscordChatExporter
|
||||
@@ -15,5 +18,19 @@ namespace DiscordChatExporter
|
||||
|
||||
app.Run();
|
||||
}
|
||||
|
||||
public static string GetResourceString(string resourcePath)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
var stream = assembly.GetManifestResourceStream(resourcePath);
|
||||
if (stream == null)
|
||||
throw new MissingManifestResourceException("Could not find resource");
|
||||
|
||||
using (stream)
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,5 +3,5 @@
|
||||
[assembly: AssemblyTitle("DiscordChatExporter")]
|
||||
[assembly: AssemblyCompany("Tyrrrz")]
|
||||
[assembly: AssemblyCopyright("Copyright (c) 2017 Alexey Golub")]
|
||||
[assembly: AssemblyVersion("2.1")]
|
||||
[assembly: AssemblyFileVersion("2.1")]
|
||||
[assembly: AssemblyVersion("2.3")]
|
||||
[assembly: AssemblyFileVersion("2.3")]
|
||||
@@ -14,8 +14,11 @@ a:hover { text-decoration: underline; }
|
||||
|
||||
div.pre {
|
||||
background-color: #2F3136;
|
||||
color: rgb(131, 148, 150);
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
@@ -23,6 +26,7 @@ span.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
@@ -102,6 +106,7 @@ span.msg-edited {
|
||||
div.msg-content {
|
||||
font-size: .9375rem;
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
@@ -112,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;
|
||||
}
|
||||
@@ -14,8 +14,11 @@ a:hover { text-decoration: underline; }
|
||||
|
||||
div.pre {
|
||||
background-color: #F9F9F9;
|
||||
color: rgb(101, 123, 131);
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
margin-top: 4px;
|
||||
padding: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
span.pre {
|
||||
@@ -23,6 +26,7 @@ span.pre {
|
||||
font-family: Consolas, Courier New, Courier, Monospace;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
div#info {
|
||||
@@ -102,6 +106,7 @@ span.msg-edited {
|
||||
div.msg-content {
|
||||
font-size: .9375rem;
|
||||
padding-top: 5px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
div.msg-attachment {
|
||||
@@ -112,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,16 +94,21 @@ namespace DiscordChatExporter.Services
|
||||
// Parse
|
||||
var channels = JArray.Parse(content).Select(ParseChannel).ToArray();
|
||||
|
||||
// Cache
|
||||
foreach (var channel in channels)
|
||||
_channelCache[channel.Id] = channel;
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId)
|
||||
public async Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||
DateTime? from, DateTime? to)
|
||||
{
|
||||
var result = new List<Message>();
|
||||
|
||||
// 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
|
||||
@@ -89,18 +120,27 @@ 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;
|
||||
}
|
||||
|
||||
// If no messages - break
|
||||
if (currentMessageId == null) break;
|
||||
if (currentMessageId == null)
|
||||
break;
|
||||
|
||||
// Otherwise offset the next request
|
||||
beforeId = currentMessageId;
|
||||
@@ -129,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");
|
||||
@@ -139,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)
|
||||
@@ -169,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"]);
|
||||
@@ -202,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
@@ -19,12 +18,53 @@ namespace DiscordChatExporter.Services
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task ExportAsync(string filePath, ChannelChatLog log, Theme theme)
|
||||
private async Task ExportAsTextAsync(string filePath, ChannelChatLog log)
|
||||
{
|
||||
var themeCss = GetThemeCss(theme);
|
||||
var dateFormat = _settingsService.DateFormat;
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
||||
{
|
||||
// Generation info
|
||||
await writer.WriteLineAsync("https://github.com/Tyrrrz/DiscordChatExporter");
|
||||
await writer.WriteLineAsync();
|
||||
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128*1024))
|
||||
// Guild and channel info
|
||||
await writer.WriteLineAsync('='.Repeat(48));
|
||||
await writer.WriteLineAsync($"Guild: {log.Guild}");
|
||||
await writer.WriteLineAsync($"Channel: {log.Channel}");
|
||||
await writer.WriteLineAsync($"Messages: {log.TotalMessageCount:N0}");
|
||||
await writer.WriteLineAsync('='.Repeat(48));
|
||||
await writer.WriteLineAsync();
|
||||
|
||||
// Chat log
|
||||
foreach (var group in log.MessageGroups)
|
||||
{
|
||||
var timeStampFormatted = group.TimeStamp.ToString(_settingsService.DateFormat);
|
||||
await writer.WriteLineAsync($"{group.Author} [{timeStampFormatted}]");
|
||||
|
||||
// Messages
|
||||
foreach (var message in group.Messages)
|
||||
{
|
||||
// Content
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
var contentFormatted = FormatMessageContentText(message);
|
||||
await writer.WriteLineAsync(contentFormatted);
|
||||
}
|
||||
|
||||
// Attachments
|
||||
foreach (var attachment in message.Attachments)
|
||||
{
|
||||
await writer.WriteLineAsync(attachment.Url);
|
||||
}
|
||||
}
|
||||
|
||||
await writer.WriteLineAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExportAsHtmlAsync(string filePath, ChannelChatLog log, string css)
|
||||
{
|
||||
using (var writer = new StreamWriter(filePath, false, Encoding.UTF8, 128 * 1024))
|
||||
{
|
||||
// Generation info
|
||||
await writer.WriteLineAsync("<!-- https://github.com/Tyrrrz/DiscordChatExporter -->");
|
||||
@@ -35,10 +75,10 @@ namespace DiscordChatExporter.Services
|
||||
|
||||
// HEAD
|
||||
await writer.WriteLineAsync("<head>");
|
||||
await writer.WriteLineAsync($"<title>{log.Guild.Name} - {log.Channel.Name}</title>");
|
||||
await writer.WriteLineAsync($"<title>{log.Guild} - {log.Channel}</title>");
|
||||
await writer.WriteLineAsync("<meta charset=\"utf-8\" />");
|
||||
await writer.WriteLineAsync("<meta name=\"viewport\" content=\"width=device-width\" />");
|
||||
await writer.WriteLineAsync($"<style>{themeCss}</style>");
|
||||
await writer.WriteLineAsync($"<style>{css}</style>");
|
||||
await writer.WriteLineAsync("</head>");
|
||||
|
||||
// Body start
|
||||
@@ -50,8 +90,8 @@ namespace DiscordChatExporter.Services
|
||||
await writer.WriteLineAsync($"<img class=\"guild-icon\" src=\"{log.Guild.IconUrl}\" />");
|
||||
await writer.WriteLineAsync("</div>"); // info-left
|
||||
await writer.WriteLineAsync("<div class=\"info-right\">");
|
||||
await writer.WriteLineAsync($"<div class=\"guild-name\">{log.Guild.Name}</div>");
|
||||
await writer.WriteLineAsync($"<div class=\"channel-name\">{log.Channel.Name}</div>");
|
||||
await writer.WriteLineAsync($"<div class=\"guild-name\">{log.Guild}</div>");
|
||||
await writer.WriteLineAsync($"<div class=\"channel-name\">{log.Channel}</div>");
|
||||
await writer.WriteLineAsync($"<div class=\"misc\">{log.TotalMessageCount:N0} messages</div>");
|
||||
await writer.WriteLineAsync("</div>"); // info-right
|
||||
await writer.WriteLineAsync("</div>"); // info
|
||||
@@ -66,25 +106,27 @@ namespace DiscordChatExporter.Services
|
||||
await writer.WriteLineAsync("</div>");
|
||||
|
||||
await writer.WriteLineAsync("<div class=\"msg-right\">");
|
||||
await writer.WriteLineAsync($"<span class=\"msg-user\">{HtmlEncode(group.Author.Name)}</span>");
|
||||
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(dateFormat));
|
||||
await writer.WriteAsync($"<span class=\"msg-user\" title=\"{HtmlEncode(group.Author)}\">");
|
||||
await writer.WriteAsync(HtmlEncode(group.Author.Name));
|
||||
await writer.WriteLineAsync("</span>");
|
||||
var timeStampFormatted = HtmlEncode(group.TimeStamp.ToString(_settingsService.DateFormat));
|
||||
await writer.WriteLineAsync($"<span class=\"msg-date\">{timeStampFormatted}</span>");
|
||||
|
||||
// Message
|
||||
// Messages
|
||||
foreach (var message in group.Messages)
|
||||
{
|
||||
// Content
|
||||
if (message.Content.IsNotBlank())
|
||||
{
|
||||
await writer.WriteLineAsync("<div class=\"msg-content\">");
|
||||
var contentFormatted = FormatMessageContent(message.Content);
|
||||
var contentFormatted = FormatMessageContentHtml(message);
|
||||
await writer.WriteAsync(contentFormatted);
|
||||
|
||||
// Edited timestamp
|
||||
if (message.EditedTimeStamp != null)
|
||||
{
|
||||
var editedTimeStampFormatted =
|
||||
HtmlEncode(message.EditedTimeStamp.Value.ToString(dateFormat));
|
||||
HtmlEncode(message.EditedTimeStamp.Value.ToString(_settingsService.DateFormat));
|
||||
await writer.WriteAsync(
|
||||
$"<span class=\"msg-edited\" title=\"{editedTimeStampFormatted}\">(edited)</span>");
|
||||
}
|
||||
@@ -99,7 +141,8 @@ namespace DiscordChatExporter.Services
|
||||
{
|
||||
await writer.WriteLineAsync("<div class=\"msg-attachment\">");
|
||||
await writer.WriteLineAsync($"<a href=\"{attachment.Url}\">");
|
||||
await writer.WriteLineAsync($"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
||||
await writer.WriteLineAsync(
|
||||
$"<img class=\"msg-attachment\" src=\"{attachment.Url}\" />");
|
||||
await writer.WriteLineAsync("</a>");
|
||||
await writer.WriteLineAsync("</div>");
|
||||
}
|
||||
@@ -123,31 +166,40 @@ namespace DiscordChatExporter.Services
|
||||
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
|
||||
{
|
||||
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)
|
||||
{
|
||||
return WebUtility.HtmlEncode(str);
|
||||
}
|
||||
|
||||
private static string HtmlEncode(object obj)
|
||||
{
|
||||
return WebUtility.HtmlEncode(obj.ToString());
|
||||
}
|
||||
|
||||
private static string FormatFileSize(long fileSize)
|
||||
{
|
||||
string[] units = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"};
|
||||
@@ -163,38 +215,102 @@ namespace DiscordChatExporter.Services
|
||||
return $"{size:0.#} {units[unit]}";
|
||||
}
|
||||
|
||||
private static string FormatMessageContent(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);
|
||||
|
||||
// Links from URLs
|
||||
content = Regex.Replace(content, "((https?|ftp)://[^\\s/$.?#].[^\\s]*)",
|
||||
"<a href=\"$1\">$1</a>");
|
||||
// Pre multiline (```text```)
|
||||
content = Regex.Replace(content, "```+(?:[^`]*?\\n)?([^`]+)\\n?```+", "<div class=\"pre\">$1</div>");
|
||||
|
||||
// Preformatted multiline
|
||||
content = Regex.Replace(content, "```([^`]*?)```",
|
||||
m => "<div class=\"pre\">" + m.Groups[1].Value.Trim('\n') + "</div>");
|
||||
// Pre inline (`text`)
|
||||
content = Regex.Replace(content, "`([^`]+)`", "<span class=\"pre\">$1</span>");
|
||||
|
||||
// Preformatted inline
|
||||
content = Regex.Replace(content, "`([^`]*?)`",
|
||||
m => "<span class=\"pre\">" + m.Groups[1].Value + "</span>");
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Models;
|
||||
|
||||
@@ -12,6 +13,7 @@ namespace DiscordChatExporter.Services
|
||||
|
||||
Task<IReadOnlyList<Channel>> GetGuildChannelsAsync(string token, string guildId);
|
||||
|
||||
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId);
|
||||
Task<IReadOnlyList<Message>> GetChannelMessagesAsync(string token, string channelId,
|
||||
DateTime? from, DateTime? to);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@ namespace DiscordChatExporter.Services
|
||||
{
|
||||
public interface IExportService
|
||||
{
|
||||
Task ExportAsync(string filePath, ChannelChatLog log, Theme theme);
|
||||
Task ExportAsync(ExportFormat format, string filePath, ChannelChatLog log);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ namespace DiscordChatExporter.Services
|
||||
{
|
||||
public interface ISettingsService
|
||||
{
|
||||
string Token { get; set; }
|
||||
Theme Theme { get; set; }
|
||||
string DateFormat { get; set; }
|
||||
int MessageGroupLimit { get; set; }
|
||||
|
||||
string LastToken { get; set; }
|
||||
ExportFormat LastExportFormat { get; set; }
|
||||
|
||||
void Load();
|
||||
void Save();
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace DiscordChatExporter.Services
|
||||
|
||||
public IReadOnlyList<MessageGroup> GroupMessages(IReadOnlyList<Message> messages)
|
||||
{
|
||||
var groupLimit = _settingsService.MessageGroupLimit;
|
||||
var result = new List<MessageGroup>();
|
||||
|
||||
// Group adjacent messages by timestamp and author
|
||||
@@ -31,7 +30,7 @@ namespace DiscordChatExporter.Services
|
||||
message.Author.Id != groupFirst.Author.Id ||
|
||||
(message.TimeStamp - groupFirst.TimeStamp).TotalHours > 1 ||
|
||||
message.TimeStamp.Hour != groupFirst.TimeStamp.Hour ||
|
||||
groupBuffer.Count >= groupLimit
|
||||
groupBuffer.Count >= _settingsService.MessageGroupLimit
|
||||
);
|
||||
|
||||
// If condition is true - flush buffer
|
||||
|
||||
@@ -5,11 +5,12 @@ namespace DiscordChatExporter.Services
|
||||
{
|
||||
public class SettingsService : SettingsManager, ISettingsService
|
||||
{
|
||||
public string Token { get; set; }
|
||||
public Theme Theme { get; set; }
|
||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm";
|
||||
public string DateFormat { get; set; } = "dd-MMM-yy hh:mm tt";
|
||||
public int MessageGroupLimit { get; set; } = 20;
|
||||
|
||||
public string LastToken { get; set; }
|
||||
public ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
Configuration.StorageSpace = StorageSpace.Instance;
|
||||
|
||||
@@ -9,7 +9,11 @@ namespace DiscordChatExporter.ViewModels
|
||||
|
||||
public ErrorViewModel()
|
||||
{
|
||||
MessengerInstance.Register<ShowErrorMessage>(this, m => Message = m.Message);
|
||||
// Messages
|
||||
MessengerInstance.Register<ShowErrorMessage>(this, m =>
|
||||
{
|
||||
Message = m.Message;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,14 @@ namespace DiscordChatExporter.ViewModels
|
||||
|
||||
public ExportDoneViewModel()
|
||||
{
|
||||
MessengerInstance.Register<ShowExportDoneMessage>(this, m => _filePath = m.FilePath);
|
||||
|
||||
// Commands
|
||||
OpenCommand = new RelayCommand(Open);
|
||||
|
||||
// Messages
|
||||
MessengerInstance.Register<ShowExportDoneMessage>(this, m =>
|
||||
{
|
||||
_filePath = m.FilePath;
|
||||
});
|
||||
}
|
||||
|
||||
private void Open()
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Messages;
|
||||
using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.CommandWpf;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
public class ExportSetupViewModel : ViewModelBase, IExportSetupViewModel
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
private string _filePath;
|
||||
private ExportFormat _format;
|
||||
private DateTime? _from;
|
||||
private DateTime? _to;
|
||||
|
||||
public Guild Guild { get; private set; }
|
||||
|
||||
public Channel Channel { get; private set; }
|
||||
|
||||
public string FilePath
|
||||
{
|
||||
get => _filePath;
|
||||
set
|
||||
{
|
||||
Set(ref _filePath, value);
|
||||
ExportCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<ExportFormat> AvailableFormats { get; }
|
||||
|
||||
public ExportFormat SelectedFormat
|
||||
{
|
||||
get => _format;
|
||||
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
|
||||
{
|
||||
get => _from;
|
||||
set => Set(ref _from, value);
|
||||
}
|
||||
|
||||
public DateTime? To
|
||||
{
|
||||
get => _to;
|
||||
set => Set(ref _to, value);
|
||||
}
|
||||
|
||||
// Commands
|
||||
public RelayCommand ExportCommand { get; }
|
||||
|
||||
public ExportSetupViewModel(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
|
||||
// Defaults
|
||||
AvailableFormats = Enum.GetValues(typeof(ExportFormat)).Cast<ExportFormat>().ToArray();
|
||||
|
||||
// Commands
|
||||
ExportCommand = new RelayCommand(Export, () => FilePath.IsNotBlank());
|
||||
|
||||
// Messages
|
||||
MessengerInstance.Register<ShowExportSetupMessage>(this, m =>
|
||||
{
|
||||
Guild = m.Guild;
|
||||
Channel = m.Channel;
|
||||
SelectedFormat = _settingsService.LastExportFormat;
|
||||
FilePath = $"{Guild} - {Channel}.{SelectedFormat.GetFileExtension()}"
|
||||
.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||
From = null;
|
||||
To = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void Export()
|
||||
{
|
||||
// Save format
|
||||
_settingsService.LastExportFormat = SelectedFormat;
|
||||
|
||||
// Start export
|
||||
MessengerInstance.Send(new StartExportMessage(Channel, FilePath, SelectedFormat, From, To));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Models;
|
||||
using GalaSoft.MvvmLight.CommandWpf;
|
||||
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
public interface IExportSetupViewModel
|
||||
{
|
||||
Guild Guild { get; }
|
||||
Channel Channel { get; }
|
||||
string FilePath { get; set; }
|
||||
IReadOnlyList<ExportFormat> AvailableFormats { get; }
|
||||
ExportFormat SelectedFormat { get; set; }
|
||||
DateTime? From { get; set; }
|
||||
DateTime? To { get; set; }
|
||||
|
||||
RelayCommand ExportCommand { get; }
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,8 @@ namespace DiscordChatExporter.ViewModels
|
||||
IReadOnlyList<Channel> AvailableChannels { get; }
|
||||
|
||||
RelayCommand PullDataCommand { get; }
|
||||
RelayCommand<Channel> ExportChannelCommand { get; }
|
||||
RelayCommand ShowSettingsCommand { get; }
|
||||
RelayCommand ShowAboutCommand { get; }
|
||||
RelayCommand<Channel> ShowExportSetupCommand { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Models;
|
||||
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
public interface ISettingsViewModel
|
||||
{
|
||||
IReadOnlyList<Theme> AvailableThemes { get; }
|
||||
Theme Theme { get; set; }
|
||||
string DateFormat { get; set; }
|
||||
int MessageGroupLimit { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using DiscordChatExporter.Exceptions;
|
||||
@@ -9,7 +9,6 @@ using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using GalaSoft.MvvmLight.CommandWpf;
|
||||
using Microsoft.Win32;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
namespace DiscordChatExporter.ViewModels
|
||||
@@ -24,10 +23,10 @@ namespace DiscordChatExporter.ViewModels
|
||||
private readonly Dictionary<Guild, IReadOnlyList<Channel>> _guildChannelsMap;
|
||||
|
||||
private bool _isBusy;
|
||||
private string _token;
|
||||
private IReadOnlyList<Guild> _availableGuilds;
|
||||
private Guild _selectedGuild;
|
||||
private IReadOnlyList<Channel> _availableChannels;
|
||||
private string _cachedToken;
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
@@ -36,7 +35,7 @@ namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
Set(ref _isBusy, value);
|
||||
PullDataCommand.RaiseCanExecuteChanged();
|
||||
ExportChannelCommand.RaiseCanExecuteChanged();
|
||||
ShowExportSetupCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +43,13 @@ namespace DiscordChatExporter.ViewModels
|
||||
|
||||
public string Token
|
||||
{
|
||||
get => _settingsService.Token;
|
||||
get => _token;
|
||||
set
|
||||
{
|
||||
// Remove invalid chars
|
||||
value = value?.Trim('"');
|
||||
|
||||
_settingsService.Token = value;
|
||||
Set(ref _token, value);
|
||||
PullDataCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
@@ -72,7 +71,7 @@ namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
Set(ref _selectedGuild, value);
|
||||
AvailableChannels = value != null ? _guildChannelsMap[value] : new Channel[0];
|
||||
ExportChannelCommand.RaiseCanExecuteChanged();
|
||||
ShowExportSetupCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +82,9 @@ namespace DiscordChatExporter.ViewModels
|
||||
}
|
||||
|
||||
public RelayCommand PullDataCommand { get; }
|
||||
public RelayCommand<Channel> ExportChannelCommand { get; }
|
||||
public RelayCommand ShowSettingsCommand { get; }
|
||||
public RelayCommand ShowAboutCommand { get; }
|
||||
public RelayCommand<Channel> ShowExportSetupCommand { get; }
|
||||
|
||||
public MainViewModel(ISettingsService settingsService, IDataService dataService,
|
||||
IMessageGroupService messageGroupService, IExportService exportService)
|
||||
@@ -99,15 +98,29 @@ namespace DiscordChatExporter.ViewModels
|
||||
|
||||
// Commands
|
||||
PullDataCommand = new RelayCommand(PullData, () => Token.IsNotBlank() && !IsBusy);
|
||||
ExportChannelCommand = new RelayCommand<Channel>(ExportChannel, _ => !IsBusy);
|
||||
ShowSettingsCommand = new RelayCommand(ShowSettings);
|
||||
ShowAboutCommand = new RelayCommand(ShowAbout);
|
||||
ShowExportSetupCommand = new RelayCommand<Channel>(ShowExportSetup, _ => !IsBusy);
|
||||
|
||||
// Messages
|
||||
MessengerInstance.Register<StartExportMessage>(this, m =>
|
||||
{
|
||||
Export(m.Channel, m.FilePath, m.Format, m.From, m.To);
|
||||
});
|
||||
|
||||
// Defaults
|
||||
_token = _settingsService.LastToken;
|
||||
}
|
||||
|
||||
private async void PullData()
|
||||
{
|
||||
IsBusy = true;
|
||||
_cachedToken = Token;
|
||||
|
||||
// Copy token so it doesn't get mutated
|
||||
var token = Token;
|
||||
|
||||
// Save token
|
||||
_settingsService.LastToken = token;
|
||||
|
||||
// Clear existing
|
||||
_guildChannelsMap.Clear();
|
||||
@@ -116,24 +129,29 @@ namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
// Get DM channels
|
||||
{
|
||||
var channels = await _dataService.GetDirectMessageChannelsAsync(_cachedToken);
|
||||
var channels = await _dataService.GetDirectMessageChannelsAsync(token);
|
||||
var guild = new Guild("@me", "Direct Messages", null);
|
||||
_guildChannelsMap[guild] = channels.ToArray();
|
||||
}
|
||||
|
||||
// Get guild channels
|
||||
{
|
||||
var guilds = await _dataService.GetGuildsAsync(_cachedToken);
|
||||
var guilds = await _dataService.GetGuildsAsync(token);
|
||||
foreach (var guild in guilds)
|
||||
{
|
||||
var channels = await _dataService.GetGuildChannelsAsync(_cachedToken, guild.Id);
|
||||
var channels = await _dataService.GetGuildChannelsAsync(token, guild.Id);
|
||||
_guildChannelsMap[guild] = channels.Where(c => c.Type == ChannelType.GuildTextChat).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
const string message = "Could not authorize using the given token. Make sure it's valid.";
|
||||
const string message = "Unauthorized to perform request. Make sure token is valid.";
|
||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
||||
}
|
||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
const string message = "Forbidden to perform request. The account may be locked by 2FA.";
|
||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
||||
}
|
||||
|
||||
@@ -142,55 +160,6 @@ namespace DiscordChatExporter.ViewModels
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
private async void ExportChannel(Channel channel)
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
// Get safe file names
|
||||
var safeGuildName = SelectedGuild.Name.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||
var safeChannelName = channel.Name.Replace(Path.GetInvalidFileNameChars(), '_');
|
||||
|
||||
// Ask for path
|
||||
var sfd = new SaveFileDialog
|
||||
{
|
||||
FileName = $"{safeGuildName} - {safeChannelName}.html",
|
||||
Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*",
|
||||
DefaultExt = "html",
|
||||
AddExtension = true
|
||||
};
|
||||
if (sfd.ShowDialog() != true)
|
||||
{
|
||||
IsBusy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Export
|
||||
try
|
||||
{
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(_cachedToken, channel.Id);
|
||||
|
||||
// Group them
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Create log
|
||||
var chatLog = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
|
||||
|
||||
// Export
|
||||
await _exportService.ExportAsync(sfd.FileName, chatLog, _settingsService.Theme);
|
||||
|
||||
// Show dialog
|
||||
MessengerInstance.Send(new ShowExportDoneMessage(sfd.FileName));
|
||||
}
|
||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
const string message = "You don't have access to the messages in that channel.";
|
||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
MessengerInstance.Send(new ShowSettingsMessage());
|
||||
@@ -200,5 +169,43 @@ namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
Process.Start("https://github.com/Tyrrrz/DiscordChatExporter");
|
||||
}
|
||||
|
||||
private void ShowExportSetup(Channel channel)
|
||||
{
|
||||
MessengerInstance.Send(new ShowExportSetupMessage(SelectedGuild, channel));
|
||||
}
|
||||
|
||||
private async void Export(Channel channel, string filePath, ExportFormat format, DateTime? from, DateTime? to)
|
||||
{
|
||||
IsBusy = true;
|
||||
|
||||
// Get last used token
|
||||
var token = _settingsService.LastToken;
|
||||
|
||||
try
|
||||
{
|
||||
// Get messages
|
||||
var messages = await _dataService.GetChannelMessagesAsync(token, channel.Id, from, to);
|
||||
|
||||
// Group them
|
||||
var messageGroups = _messageGroupService.GroupMessages(messages);
|
||||
|
||||
// Create log
|
||||
var log = new ChannelChatLog(SelectedGuild, channel, messageGroups, messages.Count);
|
||||
|
||||
// Export
|
||||
await _exportService.ExportAsync(format, filePath, log);
|
||||
|
||||
// Notify completion
|
||||
MessengerInstance.Send(new ShowExportDoneMessage(filePath));
|
||||
}
|
||||
catch (HttpErrorStatusCodeException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
const string message = "Forbidden to view messages in that channel.";
|
||||
MessengerInstance.Send(new ShowErrorMessage(message));
|
||||
}
|
||||
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.Services;
|
||||
using DiscordChatExporter.Services;
|
||||
using GalaSoft.MvvmLight;
|
||||
using Tyrrrz.Extensions;
|
||||
|
||||
@@ -12,14 +8,6 @@ namespace DiscordChatExporter.ViewModels
|
||||
{
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public IReadOnlyList<Theme> AvailableThemes { get; }
|
||||
|
||||
public Theme Theme
|
||||
{
|
||||
get => _settingsService.Theme;
|
||||
set => _settingsService.Theme = value;
|
||||
}
|
||||
|
||||
public string DateFormat
|
||||
{
|
||||
get => _settingsService.DateFormat;
|
||||
@@ -35,9 +23,6 @@ namespace DiscordChatExporter.ViewModels
|
||||
public SettingsViewModel(ISettingsService settingsService)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
|
||||
// Defaults
|
||||
AvailableThemes = Enum.GetValues(typeof(Theme)).Cast<Theme>().ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DiscordChatExporter.Views
|
||||
namespace DiscordChatExporter.Views
|
||||
{
|
||||
public partial class ErrorDialog
|
||||
{
|
||||
@@ -13,4 +7,4 @@ namespace DiscordChatExporter.Views
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,19 @@ UserControl "DiscordChatExporter.Views.ExportDoneDialog" {
|
||||
HorizontalAlignment: Right
|
||||
|
||||
// Open
|
||||
Button {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
|
||||
namespace DiscordChatExporter.Views
|
||||
{
|
||||
@@ -12,5 +9,10 @@ namespace DiscordChatExporter.Views
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void OpenButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
DialogHost.CloseDialogCommand.Execute(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using DiscordChatExporter.Models
|
||||
using MaterialDesignThemes.Wpf
|
||||
|
||||
UserControl "DiscordChatExporter.Views.ExportSetupDialog" {
|
||||
DataContext: bind ExportSetupViewModel from $resource Container
|
||||
Width: 325
|
||||
|
||||
StackPanel {
|
||||
// File path
|
||||
TextBox {
|
||||
Margin: [16, 16, 16, 8]
|
||||
HintAssist.Hint: "Output file"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
Text: bind FilePath
|
||||
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
|
||||
Grid {
|
||||
#TwoColumns("*", "*")
|
||||
|
||||
DatePicker {
|
||||
#Cell(0, 0)
|
||||
Margin: [16, 20, 8, 8]
|
||||
HintAssist.Hint: "From (optional)"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind From
|
||||
}
|
||||
|
||||
DatePicker {
|
||||
#Cell(0, 1)
|
||||
Margin: [8, 20, 16, 8]
|
||||
HintAssist.Hint: "To (optional)"
|
||||
HintAssist.IsFloating: true
|
||||
SelectedDate: bind To
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons
|
||||
@StackPanelHorizontal {
|
||||
HorizontalAlignment: Right
|
||||
|
||||
// Browse
|
||||
Button "BrowseButton" {
|
||||
Margin: 8
|
||||
Click: BrowseButton_Click
|
||||
Content: "BROWSE"
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Export
|
||||
Button "ExportButton" {
|
||||
Margin: 8
|
||||
Click: ExportButton_Click
|
||||
Command: bind ExportCommand
|
||||
Content: "EXPORT"
|
||||
IsDefault: true
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
|
||||
// Cancel
|
||||
Button {
|
||||
Margin: 8
|
||||
Command: DialogHost.CloseDialogCommand
|
||||
Content: "CANCEL"
|
||||
Style: resource dyn "MaterialDesignFlatButton"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Windows;
|
||||
using DiscordChatExporter.Models;
|
||||
using DiscordChatExporter.ViewModels;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DiscordChatExporter.Views
|
||||
{
|
||||
public partial class ExportSetupDialog
|
||||
{
|
||||
private IExportSetupViewModel ViewModel => (IExportSetupViewModel) DataContext;
|
||||
|
||||
public ExportSetupDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void BrowseButton_Click(object sender, RoutedEventArgs args)
|
||||
{
|
||||
// Get file extension of the selected format
|
||||
var ext = ViewModel.SelectedFormat.GetFileExtension();
|
||||
|
||||
// Open dialog
|
||||
var sfd = new SaveFileDialog
|
||||
{
|
||||
FileName = ViewModel.FilePath,
|
||||
Filter = $"{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)
|
||||
{
|
||||
DialogHost.CloseDialogCommand.Execute(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
|
||||
DialogHost {
|
||||
DockPanel {
|
||||
IsEnabled: bind IsBusy
|
||||
convert (bool b) => b ? false : true
|
||||
|
||||
// Toolbar
|
||||
Border {
|
||||
DockPanel.Dock: Top
|
||||
@@ -30,30 +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]
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -67,7 +72,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
|
||||
// Popup menu
|
||||
PopupBox {
|
||||
Grid.Column: 1
|
||||
#Cell(0, 1)
|
||||
Foreground: resource dyn "PrimaryHueMidForegroundBrush"
|
||||
PlacementMode: LeftAndAlignTopEdges
|
||||
|
||||
@@ -98,8 +103,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
Grid {
|
||||
DockPanel {
|
||||
Background: resource dyn "MaterialDesignCardBackground"
|
||||
IsEnabled: bind IsBusy
|
||||
convert (bool b) => b ? false : true
|
||||
Visibility: bind IsDataAvailable
|
||||
convert (bool b) => b ? Visibility.Visible : Visibility.Hidden
|
||||
|
||||
@@ -119,7 +122,7 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
TransitioningContent {
|
||||
OpeningEffect: TransitionEffect {
|
||||
Duration: "0:0:0.3"
|
||||
Kind: SlideInFromRight
|
||||
Kind: SlideInFromLeft
|
||||
}
|
||||
|
||||
Border {
|
||||
@@ -128,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 }
|
||||
@@ -138,8 +143,6 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
GradientStop { Color: "#00000000", Offset: 1 }
|
||||
]
|
||||
}
|
||||
Source: bind IconUrl
|
||||
ToolTip: bind Name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,19 +170,19 @@ Window "DiscordChatExporter.Views.MainWindow" {
|
||||
Cursor: CursorType.Hand
|
||||
InputBindings: [
|
||||
MouseBinding {
|
||||
Command: bind DataContext.ExportChannelCommand from $ancestor<ItemsControl>
|
||||
Command: bind DataContext.ShowExportSetupCommand from $ancestor<ItemsControl>
|
||||
CommandParameter: bind
|
||||
MouseAction: LeftClick
|
||||
}
|
||||
]
|
||||
|
||||
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
|
||||
@@ -193,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
|
||||
|
||||
@@ -203,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,25 +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<ShowSettingsMessage>(this, m => DialogHost.Show(new SettingsDialog()).Forget());
|
||||
Messenger.Default.Register<ShowExportDoneMessage>(this, m => DialogHost.Show(new ExportDoneDialog()).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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,9 @@ UserControl "DiscordChatExporter.Views.SettingsDialog" {
|
||||
Width: 250
|
||||
|
||||
StackPanel {
|
||||
// Theme
|
||||
ComboBox {
|
||||
Margin: "16 16 16 8"
|
||||
HintAssist.Hint: "Theme"
|
||||
HintAssist.IsFloating: true
|
||||
IsReadOnly: true
|
||||
ItemsSource: bind AvailableThemes
|
||||
SelectedItem: bind Theme
|
||||
}
|
||||
|
||||
// Date format
|
||||
TextBox {
|
||||
Margin: "16 8 16 8"
|
||||
Margin: [16, 16, 16, 8]
|
||||
HintAssist.Hint: "Date format"
|
||||
HintAssist.IsFloating: true
|
||||
Text: bind DateFormat
|
||||
@@ -25,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,26 +1,34 @@
|
||||
# DiscordChatExporter
|
||||
|
||||
DiscordChatExporter can be used to export message history from [Discord](https://discordapp.com) to an HTML file. It works for both direct message chats and guild chats, supports markdown, message grouping, and attachments. There are options to configure the output, such as date format, color theme, message grouping limit, etc.
|
||||
[](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
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## 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
|
||||
|
||||
- Exports to a self-contained HTML file
|
||||
- Supports both dark and light theme
|
||||
- Displays user avatars
|
||||
- Intuitive GUI that displays available guilds and channels
|
||||
- Date ranges to limit messages
|
||||
- Groups messages by author and time
|
||||
- Handles Discord markdown characters
|
||||
- Converts URLs to links
|
||||
- Inlines attached images with a link to the full version
|
||||
- Inserts download links for other types of attached files
|
||||
- Marks edited messages with a timestamp
|
||||
- Export to a plain text file
|
||||
- Export to an HTML file
|
||||
- Dark and light themes
|
||||
- User avatars
|
||||
- Inline image attachments
|
||||
- Full markdown support
|
||||
- Automatic links
|
||||
- Styled similarly to the app
|
||||
|
||||
## Libraries used
|
||||
|
||||
|
||||
Reference in New Issue
Block a user