mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-07-09 23:49:34 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9639b6c550 | |||
| 950658c737 | |||
| 560a069c35 | |||
| 1131f8659d | |||
| 38be44debb | |||
| 4b243f2252 | |||
| a31175b2e8 | |||
| 913255f04f | |||
| 873d055191 | |||
| dabed24c16 |
@@ -1,3 +1,13 @@
|
|||||||
|
### v2.36.4 (29-Oct-2022)
|
||||||
|
|
||||||
|
- Changed all mentions of "media" in the context of "download media" or "reuse media" to "assets". CLI options will retain their existing names for backwards compatibility.
|
||||||
|
- [HTML] Fixed an issue which prevented emoji used inside message content from being downloaded when "download assets" option is enabled. (Thanks [@Roberto Blázquez](https://github.com/xBaank))
|
||||||
|
|
||||||
|
### v2.36.3 (21-Oct-2022)
|
||||||
|
|
||||||
|
- [GUI] Fixed an issue where opening a dialog did not prevent user interactions with background UI elements using keyboard. This sometimes caused the application to crash in weird ways.
|
||||||
|
- [Docker] Fixed an issue where DiscordChatExporter was running as root inside the container, making the exported files inaccessible to the host. (Thanks [@Benjamin Just](https://github.com/BamButz))
|
||||||
|
|
||||||
### v2.36.2 (08-Oct-2022)
|
### v2.36.2 (08-Oct-2022)
|
||||||
|
|
||||||
- Removed the message content intent check because the heuristics turned out to be not reliable enough.
|
- Removed the message content intent check because the heuristics turned out to be not reliable enough.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>2.36.2</Version>
|
<Version>2.36.4</Version>
|
||||||
<Company>Tyrrrz</Company>
|
<Company>Tyrrrz</Company>
|
||||||
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
<Copyright>Copyright (c) Oleksii Holub</Copyright>
|
||||||
<LangVersion>preview</LangVersion>
|
<LangVersion>preview</LangVersion>
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
||||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
<PackageReference Include="FluentAssertions" Version="6.8.0" />
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1" PrivateAssets="all" />
|
||||||
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
<PackageReference Include="JsonExtensions" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
<PackageReference Include="xunit" Version="2.4.2" />
|
<PackageReference Include="xunit" Version="2.4.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" PrivateAssets="all" />
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public class SelfContainedSpecs : IClassFixture<TempOutputFixture>
|
|||||||
ChannelIds = new[] { ChannelIds.SelfContainedTestCases },
|
ChannelIds = new[] { ChannelIds.SelfContainedTestCases },
|
||||||
ExportFormat = ExportFormat.HtmlDark,
|
ExportFormat = ExportFormat.HtmlDark,
|
||||||
OutputPath = filePath,
|
OutputPath = filePath,
|
||||||
ShouldDownloadMedia = true
|
ShouldDownloadAssets = true
|
||||||
}.ExecuteAsync(new FakeConsole());
|
}.ExecuteAsync(new FakeConsole());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ COPY Directory.Build.props ./
|
|||||||
COPY DiscordChatExporter.Core ./DiscordChatExporter.Core
|
COPY DiscordChatExporter.Core ./DiscordChatExporter.Core
|
||||||
COPY DiscordChatExporter.Cli ./DiscordChatExporter.Cli
|
COPY DiscordChatExporter.Cli ./DiscordChatExporter.Cli
|
||||||
|
|
||||||
RUN dotnet publish DiscordChatExporter.Cli -c Release -o ./publish
|
RUN dotnet publish DiscordChatExporter.Cli --configuration Release --output ./publish
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS run
|
FROM mcr.microsoft.com/dotnet/runtime:6.0 AS run
|
||||||
|
|
||||||
|
RUN useradd dce
|
||||||
|
USER dce
|
||||||
|
|
||||||
COPY --from=build ./publish ./
|
COPY --from=build ./publish ./
|
||||||
|
|
||||||
WORKDIR ./out
|
WORKDIR ./out
|
||||||
ENTRYPOINT ["dotnet", "/DiscordChatExporter.Cli.dll"]
|
|
||||||
|
ENTRYPOINT ["dotnet", "../DiscordChatExporter.Cli.dll"]
|
||||||
|
|||||||
@@ -76,15 +76,15 @@ public abstract class ExportCommandBase : TokenCommandBase
|
|||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"media",
|
"media",
|
||||||
Description = "Download referenced media content."
|
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
||||||
)]
|
)]
|
||||||
public bool ShouldDownloadMedia { get; init; }
|
public bool ShouldDownloadAssets { get; init; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"reuse-media",
|
"reuse-media",
|
||||||
Description = "Reuse already existing media content to skip redundant downloads."
|
Description = "Reuse previously downloaded assets to avoid redundant requests."
|
||||||
)]
|
)]
|
||||||
public bool ShouldReuseMedia { get; init; }
|
public bool ShouldReuseAssets { get; init; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"dateformat",
|
"dateformat",
|
||||||
@@ -97,9 +97,9 @@ public abstract class ExportCommandBase : TokenCommandBase
|
|||||||
|
|
||||||
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
protected async ValueTask ExecuteAsync(IConsole console, IReadOnlyList<Channel> channels)
|
||||||
{
|
{
|
||||||
// Reuse media option should only be used when the media option is set.
|
// Reuse assets option should only be used when the download assets option is set.
|
||||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
// https://github.com/Tyrrrz/DiscordChatExporter/issues/425
|
||||||
if (ShouldReuseMedia && !ShouldDownloadMedia)
|
if (ShouldReuseAssets && !ShouldDownloadAssets)
|
||||||
{
|
{
|
||||||
throw new CommandException(
|
throw new CommandException(
|
||||||
"Option --reuse-media cannot be used without --media."
|
"Option --reuse-media cannot be used without --media."
|
||||||
@@ -158,8 +158,8 @@ public abstract class ExportCommandBase : TokenCommandBase
|
|||||||
Before,
|
Before,
|
||||||
PartitionLimit,
|
PartitionLimit,
|
||||||
MessageFilter,
|
MessageFilter,
|
||||||
ShouldDownloadMedia,
|
ShouldDownloadAssets,
|
||||||
ShouldReuseMedia,
|
ShouldReuseAssets,
|
||||||
DateFormat
|
DateFormat
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CliFx" Version="2.3.0" />
|
<PackageReference Include="CliFx" Version="2.3.0" />
|
||||||
<PackageReference Include="Spectre.Console" Version="0.44.0" />
|
<PackageReference Include="Spectre.Console" Version="0.45.0" />
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+7
-7
@@ -12,18 +12,18 @@ using DiscordChatExporter.Core.Utils.Extensions;
|
|||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exporting;
|
namespace DiscordChatExporter.Core.Exporting;
|
||||||
|
|
||||||
internal partial class MediaDownloader
|
internal partial class ExportAssetDownloader
|
||||||
{
|
{
|
||||||
private readonly string _workingDirPath;
|
private readonly string _workingDirPath;
|
||||||
private readonly bool _reuseMedia;
|
private readonly bool _reuse;
|
||||||
|
|
||||||
// File paths of already downloaded media
|
// File paths of the previously downloaded assets
|
||||||
private readonly Dictionary<string, string> _pathCache = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, string> _pathCache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
public MediaDownloader(string workingDirPath, bool reuseMedia)
|
public ExportAssetDownloader(string workingDirPath, bool reuse)
|
||||||
{
|
{
|
||||||
_workingDirPath = workingDirPath;
|
_workingDirPath = workingDirPath;
|
||||||
_reuseMedia = reuseMedia;
|
_reuse = reuse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<string> DownloadAsync(string url, CancellationToken cancellationToken = default)
|
public async ValueTask<string> DownloadAsync(string url, CancellationToken cancellationToken = default)
|
||||||
@@ -35,7 +35,7 @@ internal partial class MediaDownloader
|
|||||||
var filePath = Path.Combine(_workingDirPath, fileName);
|
var filePath = Path.Combine(_workingDirPath, fileName);
|
||||||
|
|
||||||
// Reuse existing files if we're allowed to
|
// Reuse existing files if we're allowed to
|
||||||
if (!_reuseMedia || !File.Exists(filePath))
|
if (!_reuse || !File.Exists(filePath))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_workingDirPath);
|
Directory.CreateDirectory(_workingDirPath);
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ internal partial class MediaDownloader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal partial class MediaDownloader
|
internal partial class ExportAssetDownloader
|
||||||
{
|
{
|
||||||
private static string GetUrlHash(string url)
|
private static string GetUrlHash(string url)
|
||||||
{
|
{
|
||||||
@@ -14,7 +14,7 @@ namespace DiscordChatExporter.Core.Exporting;
|
|||||||
|
|
||||||
internal class ExportContext
|
internal class ExportContext
|
||||||
{
|
{
|
||||||
private readonly MediaDownloader _mediaDownloader;
|
private readonly ExportAssetDownloader _assetDownloader;
|
||||||
|
|
||||||
public ExportRequest Request { get; }
|
public ExportRequest Request { get; }
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ internal class ExportContext
|
|||||||
Channels = channels;
|
Channels = channels;
|
||||||
Roles = roles;
|
Roles = roles;
|
||||||
|
|
||||||
_mediaDownloader = new MediaDownloader(request.OutputMediaDirPath, request.ShouldReuseMedia);
|
_assetDownloader = new ExportAssetDownloader(request.OutputAssetsDirPath, request.ShouldReuseAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FormatDate(DateTimeOffset date) => Request.DateFormat switch
|
public string FormatDate(DateTimeOffset date) => Request.DateFormat switch
|
||||||
@@ -63,14 +63,14 @@ internal class ExportContext
|
|||||||
.FirstOrDefault();
|
.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<string> ResolveMediaUrlAsync(string url, CancellationToken cancellationToken = default)
|
public async ValueTask<string> ResolveAssetUrlAsync(string url, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!Request.ShouldDownloadMedia)
|
if (!Request.ShouldDownloadAssets)
|
||||||
return url;
|
return url;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var filePath = await _mediaDownloader.DownloadAsync(url, cancellationToken);
|
var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
|
||||||
|
|
||||||
// We want relative path so that the output files can be copied around without breaking.
|
// We want relative path so that the output files can be copied around without breaking.
|
||||||
// Base directory path may be null if the file is stored at the root or relative to working directory.
|
// Base directory path may be null if the file is stored at the root or relative to working directory.
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public partial record ExportRequest(
|
|||||||
Snowflake? Before,
|
Snowflake? Before,
|
||||||
PartitionLimit PartitionLimit,
|
PartitionLimit PartitionLimit,
|
||||||
MessageFilter MessageFilter,
|
MessageFilter MessageFilter,
|
||||||
bool ShouldDownloadMedia,
|
bool ShouldDownloadAssets,
|
||||||
bool ShouldReuseMedia,
|
bool ShouldReuseAssets,
|
||||||
string DateFormat)
|
string DateFormat)
|
||||||
{
|
{
|
||||||
private string? _outputBaseFilePath;
|
private string? _outputBaseFilePath;
|
||||||
@@ -35,7 +35,7 @@ public partial record ExportRequest(
|
|||||||
|
|
||||||
public string OutputBaseDirPath => Path.GetDirectoryName(OutputBaseFilePath) ?? OutputPath;
|
public string OutputBaseDirPath => Path.GetDirectoryName(OutputBaseFilePath) ?? OutputPath;
|
||||||
|
|
||||||
public string OutputMediaDirPath => $"{OutputBaseFilePath}_Files{Path.DirectorySeparatorChar}";
|
public string OutputAssetsDirPath => $"{OutputBaseFilePath}_Files{Path.DirectorySeparatorChar}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public partial record ExportRequest
|
public partial record ExportRequest
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ internal partial class CsvMessageWriter : MessageWriter
|
|||||||
_writer = new StreamWriter(stream);
|
_writer = new StreamWriter(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatMarkdown(string? markdown) =>
|
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||||
PlainTextMarkdownVisitor.Format(Context, markdown ?? "");
|
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||||
|
|
||||||
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
public override async ValueTask WritePreambleAsync(CancellationToken cancellationToken = default) =>
|
||||||
await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions");
|
await _writer.WriteLineAsync("AuthorID,Author,Date,Content,Attachments,Reactions");
|
||||||
@@ -37,7 +37,7 @@ internal partial class CsvMessageWriter : MessageWriter
|
|||||||
|
|
||||||
buffer
|
buffer
|
||||||
.AppendIfNotEmpty(',')
|
.AppendIfNotEmpty(',')
|
||||||
.Append(await Context.ResolveMediaUrlAsync(attachment.Url, cancellationToken));
|
.Append(await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _writer.WriteAsync(CsvEncode(buffer.ToString()));
|
await _writer.WriteAsync(CsvEncode(buffer.ToString()));
|
||||||
@@ -84,7 +84,7 @@ internal partial class CsvMessageWriter : MessageWriter
|
|||||||
await _writer.WriteAsync(',');
|
await _writer.WriteAsync(',');
|
||||||
|
|
||||||
// Message content
|
// Message content
|
||||||
await _writer.WriteAsync(CsvEncode(FormatMarkdown(message.Content)));
|
await _writer.WriteAsync(CsvEncode(await FormatMarkdownAsync(message.Content)));
|
||||||
await _writer.WriteAsync(',');
|
await _writer.WriteAsync(',');
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
|
|||||||
@@ -10,13 +10,17 @@
|
|||||||
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
|
@inherits MiniRazor.TemplateBase<MessageGroupTemplateContext>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url);
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
|
Model.ExportContext.ResolveAssetUrlAsync(url);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) =>
|
||||||
|
Model.ExportContext.FormatDate(date);
|
||||||
|
|
||||||
string FormatMarkdown(string markdown) => Model.FormatMarkdown(markdown);
|
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
|
Model.FormatMarkdownAsync(markdown);
|
||||||
|
|
||||||
string FormatEmbedMarkdown(string markdown) => Model.FormatMarkdown(markdown, false);
|
ValueTask<string> FormatEmbedMarkdownAsync(string markdown) =>
|
||||||
|
Model.FormatMarkdownAsync(markdown, false);
|
||||||
|
|
||||||
var firstMessage = Model.Messages.First();
|
var firstMessage = Model.Messages.First();
|
||||||
|
|
||||||
@@ -102,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
<img class="chatlog__avatar" src="@await ResolveUrlAsync(message.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
<img class="chatlog__avatar" src="@await ResolveAssetUrlAsync(message.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -119,13 +123,13 @@
|
|||||||
<div class="chatlog__reference">
|
<div class="chatlog__reference">
|
||||||
@if (message.ReferencedMessage is not null)
|
@if (message.ReferencedMessage is not null)
|
||||||
{
|
{
|
||||||
<img class="chatlog__reference-avatar" src="@await ResolveUrlAsync(message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
<img class="chatlog__reference-avatar" src="@await ResolveAssetUrlAsync(message.ReferencedMessage.Author.AvatarUrl)" alt="Avatar" loading="lazy">
|
||||||
<div class="chatlog__reference-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserNick</div>
|
<div class="chatlog__reference-author" style="@(referencedUserColor is not null ? $"color: rgb({referencedUserColor.Value.R}, {referencedUserColor.Value.G}, {referencedUserColor.Value.B})" : null)" title="@message.ReferencedMessage.Author.FullName">@referencedUserNick</div>
|
||||||
<div class="chatlog__reference-content">
|
<div class="chatlog__reference-content">
|
||||||
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
|
<span class="chatlog__reference-link" onclick="scrollToMessage(event, '@message.ReferencedMessage.Id')">
|
||||||
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden())
|
@if (!string.IsNullOrWhiteSpace(message.ReferencedMessage.Content) && !message.ReferencedMessage.IsContentHidden())
|
||||||
{
|
{
|
||||||
<!--wmm:ignore-->@Raw(FormatEmbedMarkdown(message.ReferencedMessage.Content))<!--/wmm:ignore-->
|
<!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(message.ReferencedMessage.Content))<!--/wmm:ignore-->
|
||||||
}
|
}
|
||||||
else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())
|
else if (message.ReferencedMessage.Attachments.Any() || message.ReferencedMessage.Embeds.Any())
|
||||||
{
|
{
|
||||||
@@ -176,7 +180,7 @@
|
|||||||
@{/* Text */}
|
@{/* Text */}
|
||||||
@if (!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden())
|
@if (!string.IsNullOrWhiteSpace(message.Content) && !message.IsContentHidden())
|
||||||
{
|
{
|
||||||
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatMarkdown(message.Content))<!--/wmm:ignore--></span>
|
<span class="chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatMarkdownAsync(message.Content))<!--/wmm:ignore--></span>
|
||||||
}
|
}
|
||||||
|
|
||||||
@{/* Edited timestamp */}
|
@{/* Edited timestamp */}
|
||||||
@@ -200,20 +204,20 @@
|
|||||||
@{/* Attachment preview */}
|
@{/* Attachment preview */}
|
||||||
@if (attachment.IsImage)
|
@if (attachment.IsImage)
|
||||||
{
|
{
|
||||||
<a href="@await ResolveUrlAsync(attachment.Url)">
|
<a href="@await ResolveAssetUrlAsync(attachment.Url)">
|
||||||
<img class="chatlog__attachment-media" src="@await ResolveUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Image attachment")" title="Image: @attachment.FileName (@attachment.FileSize)" loading="lazy">
|
<img class="chatlog__attachment-media" src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Image attachment")" title="Image: @attachment.FileName (@attachment.FileSize)" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
else if (attachment.IsVideo)
|
else if (attachment.IsVideo)
|
||||||
{
|
{
|
||||||
<video class="chatlog__attachment-media" controls>
|
<video class="chatlog__attachment-media" controls>
|
||||||
<source src="@await ResolveUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)">
|
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Video attachment")" title="Video: @attachment.FileName (@attachment.FileSize)">
|
||||||
</video>
|
</video>
|
||||||
}
|
}
|
||||||
else if (attachment.IsAudio)
|
else if (attachment.IsAudio)
|
||||||
{
|
{
|
||||||
<audio class="chatlog__attachment-media" controls>
|
<audio class="chatlog__attachment-media" controls>
|
||||||
<source src="@await ResolveUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)">
|
<source src="@await ResolveAssetUrlAsync(attachment.Url)" alt="@(attachment.Description ?? "Audio attachment")" title="Audio: @attachment.FileName (@attachment.FileSize)">
|
||||||
</audio>
|
</audio>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -223,7 +227,7 @@
|
|||||||
<use href="#attachment-icon"/>
|
<use href="#attachment-icon"/>
|
||||||
</svg>
|
</svg>
|
||||||
<div class="chatlog__attachment-generic-name">
|
<div class="chatlog__attachment-generic-name">
|
||||||
<a href="@await ResolveUrlAsync(attachment.Url)">
|
<a href="@await ResolveAssetUrlAsync(attachment.Url)">
|
||||||
@attachment.FileName
|
@attachment.FileName
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,7 +274,7 @@
|
|||||||
<div class="chatlog__embed-author-container">
|
<div class="chatlog__embed-author-container">
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
||||||
{
|
{
|
||||||
<img class="chatlog__embed-author-icon" src="@await ResolveUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
|
<img class="chatlog__embed-author-icon" src="@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
||||||
@@ -296,12 +300,12 @@
|
|||||||
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
||||||
{
|
{
|
||||||
<a class="chatlog__embed-title-link" href="@embed.Url">
|
<a class="chatlog__embed-title-link" href="@embed.Url">
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(embed.Title))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(embed.Title))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -319,8 +323,8 @@
|
|||||||
else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url))
|
else if (embed.Kind == EmbedKind.Image && !string.IsNullOrWhiteSpace(embed.Url))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed">
|
<div class="chatlog__embed">
|
||||||
<a href="@await ResolveUrlAsync(embed.Url)">
|
<a href="@await ResolveAssetUrlAsync(embed.Url)">
|
||||||
<img class="chatlog__embed-generic-image" src="@await ResolveUrlAsync(embed.Url)" alt="Embedded image" loading="lazy">
|
<img class="chatlog__embed-generic-image" src="@await ResolveAssetUrlAsync(embed.Url)" alt="Embedded image" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -329,7 +333,7 @@
|
|||||||
{
|
{
|
||||||
<div class="chatlog__embed">
|
<div class="chatlog__embed">
|
||||||
<video class="chatlog__embed-generic-gifv" loop width="@embed.Video.Width" height="@embed.Video.Height" onmouseover="this.play()" onmouseout="this.pause()">
|
<video class="chatlog__embed-generic-gifv" loop width="@embed.Video.Width" height="@embed.Video.Height" onmouseover="this.play()" onmouseout="this.pause()">
|
||||||
<source src="@await ResolveUrlAsync(embed.Video.ProxyUrl ?? embed.Video.Url)" alt="Embedded video">
|
<source src="@await ResolveAssetUrlAsync(embed.Video.ProxyUrl ?? embed.Video.Url)" alt="Embedded video">
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -356,7 +360,7 @@
|
|||||||
<div class="chatlog__embed-author-container">
|
<div class="chatlog__embed-author-container">
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.IconUrl))
|
||||||
{
|
{
|
||||||
<img class="chatlog__embed-author-icon" src="@await ResolveUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
|
<img class="chatlog__embed-author-icon" src="@await ResolveAssetUrlAsync(embed.Author.IconProxyUrl ?? embed.Author.IconUrl)" alt="Author icon" loading="lazy" onerror="this.style.visibility='hidden'">
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
@if (!string.IsNullOrWhiteSpace(embed.Author.Name))
|
||||||
@@ -382,12 +386,12 @@
|
|||||||
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
@if (!string.IsNullOrWhiteSpace(embed.Url))
|
||||||
{
|
{
|
||||||
<a class="chatlog__embed-title-link" href="@embed.Url">
|
<a class="chatlog__embed-title-link" href="@embed.Url">
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(embed.Title))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(embed.Title))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Title))<!--/wmm:ignore--></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -396,7 +400,7 @@
|
|||||||
@if (!string.IsNullOrWhiteSpace(embed.Description))
|
@if (!string.IsNullOrWhiteSpace(embed.Description))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-description">
|
<div class="chatlog__embed-description">
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(embed.Description))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(embed.Description))<!--/wmm:ignore--></div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,14 +414,14 @@
|
|||||||
@if (!string.IsNullOrWhiteSpace(field.Name))
|
@if (!string.IsNullOrWhiteSpace(field.Name))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-field-name">
|
<div class="chatlog__embed-field-name">
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(field.Name))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(field.Name))<!--/wmm:ignore--></div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(field.Value))
|
@if (!string.IsNullOrWhiteSpace(field.Value))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-field-value">
|
<div class="chatlog__embed-field-value">
|
||||||
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(FormatEmbedMarkdown(field.Value))<!--/wmm:ignore--></div>
|
<div class="chatlog__markdown chatlog__markdown-preserve"><!--wmm:ignore-->@Raw(await FormatEmbedMarkdownAsync(field.Value))<!--/wmm:ignore--></div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -430,8 +434,8 @@
|
|||||||
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url))
|
@if (embed.Thumbnail is not null && !string.IsNullOrWhiteSpace(embed.Thumbnail.Url))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-thumbnail-container">
|
<div class="chatlog__embed-thumbnail-container">
|
||||||
<a class="chatlog__embed-thumbnail-link" href="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)">
|
<a class="chatlog__embed-thumbnail-link" href="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)">
|
||||||
<img class="chatlog__embed-thumbnail" src="@await ResolveUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail" loading="lazy">
|
<img class="chatlog__embed-thumbnail" src="@await ResolveAssetUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url)" alt="Thumbnail" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -446,8 +450,8 @@
|
|||||||
if (!string.IsNullOrWhiteSpace(image.Url))
|
if (!string.IsNullOrWhiteSpace(image.Url))
|
||||||
{
|
{
|
||||||
<div class="chatlog__embed-image-container">
|
<div class="chatlog__embed-image-container">
|
||||||
<a class="chatlog__embed-image-link" href="@await ResolveUrlAsync(image.ProxyUrl ?? image.Url)">
|
<a class="chatlog__embed-image-link" href="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)">
|
||||||
<img class="chatlog__embed-image" src="@await ResolveUrlAsync(image.ProxyUrl ?? image.Url)" alt="Image" loading="lazy">
|
<img class="chatlog__embed-image" src="@await ResolveAssetUrlAsync(image.ProxyUrl ?? image.Url)" alt="Image" loading="lazy">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -462,7 +466,7 @@
|
|||||||
@{/* Footer icon */}
|
@{/* Footer icon */}
|
||||||
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
@if (!string.IsNullOrWhiteSpace(embed.Footer?.IconUrl))
|
||||||
{
|
{
|
||||||
<img class="chatlog__embed-footer-icon" src="@await ResolveUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)" alt="Footer icon" loading="lazy">
|
<img class="chatlog__embed-footer-icon" src="@await ResolveAssetUrlAsync(embed.Footer.IconProxyUrl ?? embed.Footer.IconUrl)" alt="Footer icon" loading="lazy">
|
||||||
}
|
}
|
||||||
|
|
||||||
<span class="chatlog__embed-footer-text">
|
<span class="chatlog__embed-footer-text">
|
||||||
@@ -496,11 +500,11 @@
|
|||||||
<div class="chatlog__sticker" title="@sticker.Name">
|
<div class="chatlog__sticker" title="@sticker.Name">
|
||||||
@if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated)
|
@if (sticker.Format is StickerFormat.Png or StickerFormat.PngAnimated)
|
||||||
{
|
{
|
||||||
<img class="chatlog__sticker--media" src="@await ResolveUrlAsync(sticker.SourceUrl)" alt="Sticker">
|
<img class="chatlog__sticker--media" src="@await ResolveAssetUrlAsync(sticker.SourceUrl)" alt="Sticker">
|
||||||
}
|
}
|
||||||
else if (sticker.Format == StickerFormat.Lottie)
|
else if (sticker.Format == StickerFormat.Lottie)
|
||||||
{
|
{
|
||||||
<div class="chatlog__sticker--media" data-source="@await ResolveUrlAsync(sticker.SourceUrl)"></div>
|
<div class="chatlog__sticker--media" data-source="@await ResolveAssetUrlAsync(sticker.SourceUrl)"></div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -512,7 +516,7 @@
|
|||||||
@foreach (var reaction in message.Reactions)
|
@foreach (var reaction in message.Reactions)
|
||||||
{
|
{
|
||||||
<div class="chatlog__reaction" title="@reaction.Emoji.Code">
|
<div class="chatlog__reaction" title="@reaction.Emoji.Code">
|
||||||
<img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
|
<img class="chatlog__emoji chatlog__emoji--small" alt="@reaction.Emoji.Name" src="@await ResolveAssetUrlAsync(reaction.Emoji.ImageUrl)" loading="lazy">
|
||||||
<span class="chatlog__reaction-count">@reaction.Count</span>
|
<span class="chatlog__reaction-count">@reaction.Count</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
|
|
||||||
@@ -16,6 +17,6 @@ internal class MessageGroupTemplateContext
|
|||||||
Messages = messages;
|
Messages = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FormatMarkdown(string? markdown, bool isJumboAllowed = true) =>
|
public ValueTask<string> FormatMarkdownAsync(string? markdown, bool isJumboAllowed = true) =>
|
||||||
HtmlMarkdownVisitor.Format(ExportContext, markdown ?? "", isJumboAllowed);
|
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown ?? "", isJumboAllowed);
|
||||||
}
|
}
|
||||||
@@ -14,11 +14,14 @@
|
|||||||
string GetFontUrl(int weight) =>
|
string GetFontUrl(int weight) =>
|
||||||
$"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-{weight}.woff";
|
$"https://cdn.jsdelivr.net/gh/Tyrrrz/DiscordFonts@master/whitney-{weight}.woff";
|
||||||
|
|
||||||
ValueTask<string> ResolveUrlAsync(string url) => Model.ExportContext.ResolveMediaUrlAsync(url, CancellationToken);
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
|
Model.ExportContext.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset date) => Model.ExportContext.FormatDate(date);
|
string FormatDate(DateTimeOffset date) =>
|
||||||
|
Model.ExportContext.FormatDate(date);
|
||||||
|
|
||||||
string FormatMarkdown(string markdown) => Model.FormatMarkdown(markdown);
|
ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
|
Model.FormatMarkdownAsync(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -32,31 +35,31 @@
|
|||||||
@{/* Styling */}
|
@{/* Styling */}
|
||||||
<style>
|
<style>
|
||||||
@@font-face {
|
@@font-face {
|
||||||
src: url(@await ResolveUrlAsync(GetFontUrl(300)));
|
src: url(@await ResolveAssetUrlAsync(GetFontUrl(300)));
|
||||||
font-family: Whitney;
|
font-family: Whitney;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@font-face {
|
@@font-face {
|
||||||
src: url(@await ResolveUrlAsync(GetFontUrl(400)));
|
src: url(@await ResolveAssetUrlAsync(GetFontUrl(400)));
|
||||||
font-family: Whitney;
|
font-family: Whitney;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@font-face {
|
@@font-face {
|
||||||
src: url(@await ResolveUrlAsync(GetFontUrl(500)));
|
src: url(@await ResolveAssetUrlAsync(GetFontUrl(500)));
|
||||||
font-family: Whitney;
|
font-family: Whitney;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@font-face {
|
@@font-face {
|
||||||
src: url(@await ResolveUrlAsync(GetFontUrl(600)));
|
src: url(@await ResolveAssetUrlAsync(GetFontUrl(600)));
|
||||||
font-family: Whitney;
|
font-family: Whitney;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@font-face {
|
@@font-face {
|
||||||
src: url(@await ResolveUrlAsync(GetFontUrl(700)));
|
src: url(@await ResolveAssetUrlAsync(GetFontUrl(700)));
|
||||||
font-family: Whitney;
|
font-family: Whitney;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -752,8 +755,8 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
@{/* Syntax highlighting */}
|
@{/* Syntax highlighting */}
|
||||||
<link rel="stylesheet" href="@await ResolveUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{Model.ThemeName.ToLowerInvariant()}.min.css")">
|
<link rel="stylesheet" href="@await ResolveAssetUrlAsync($"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/styles/solarized-{Model.ThemeName.ToLowerInvariant()}.min.css")">
|
||||||
<script src="@await ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js")"></script>
|
<script src="@await ResolveAssetUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js")"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.chatlog__markdown-pre--multiline').forEach(e => hljs.highlightBlock(e));
|
document.querySelectorAll('.chatlog__markdown-pre--multiline').forEach(e => hljs.highlightBlock(e));
|
||||||
@@ -761,7 +764,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
@{/* Lottie animation support */}
|
@{/* Lottie animation support */}
|
||||||
<script src="@await ResolveUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js")"></script>
|
<script src="@await ResolveAssetUrlAsync("https://cdnjs.cloudflare.com/ajax/libs/lottie-web/5.8.1/lottie.min.js")"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
document.querySelectorAll('.chatlog__sticker--media[data-source]').forEach(e => {
|
||||||
@@ -843,7 +846,7 @@
|
|||||||
|
|
||||||
<div class="preamble">
|
<div class="preamble">
|
||||||
<div class="preamble__guild-icon-container">
|
<div class="preamble__guild-icon-container">
|
||||||
<img class="preamble__guild-icon" src="@await ResolveUrlAsync(Model.ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
<img class="preamble__guild-icon" src="@await ResolveAssetUrlAsync(Model.ExportContext.Request.Guild.IconUrl)" alt="Guild icon" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<div class="preamble__entries-container">
|
<div class="preamble__entries-container">
|
||||||
<div class="preamble__entry">@Model.ExportContext.Request.Guild.Name</div>
|
<div class="preamble__entry">@Model.ExportContext.Request.Guild.Name</div>
|
||||||
@@ -851,7 +854,7 @@
|
|||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.ExportContext.Request.Channel.Topic))
|
@if (!string.IsNullOrWhiteSpace(Model.ExportContext.Request.Channel.Topic))
|
||||||
{
|
{
|
||||||
<div class="preamble__entry preamble__entry--small">@Raw(FormatMarkdown(Model.ExportContext.Request.Channel.Topic))</div>
|
<div class="preamble__entry preamble__entry--small">@Raw(await FormatMarkdownAsync(Model.ExportContext.Request.Channel.Topic))</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null)
|
@if (Model.ExportContext.Request.After is not null || Model.ExportContext.Request.Before is not null)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
using System.Threading.Tasks;
|
||||||
|
using DiscordChatExporter.Core.Exporting.Writers.MarkdownVisitors;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
|
namespace DiscordChatExporter.Core.Exporting.Writers.Html;
|
||||||
|
|
||||||
@@ -14,6 +15,6 @@ internal class PreambleTemplateContext
|
|||||||
ThemeName = themeName;
|
ThemeName = themeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string FormatMarkdown(string? markdown, bool isJumboAllowed = true) =>
|
public ValueTask<string> FormatMarkdownAsync(string? markdown, bool isJumboAllowed = true) =>
|
||||||
HtmlMarkdownVisitor.Format(ExportContext, markdown ?? "", isJumboAllowed);
|
HtmlMarkdownVisitor.FormatAsync(ExportContext, markdown ?? "", isJumboAllowed);
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatMarkdown(string? markdown) =>
|
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||||
PlainTextMarkdownVisitor.Format(Context, markdown ?? "");
|
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||||
|
|
||||||
private async ValueTask WriteAttachmentAsync(
|
private async ValueTask WriteAttachmentAsync(
|
||||||
Attachment attachment,
|
Attachment attachment,
|
||||||
@@ -39,7 +39,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("id", attachment.Id.ToString());
|
_writer.WriteString("id", attachment.Id.ToString());
|
||||||
_writer.WriteString("url", await Context.ResolveMediaUrlAsync(attachment.Url, cancellationToken));
|
_writer.WriteString("url", await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||||
_writer.WriteString("fileName", attachment.FileName);
|
_writer.WriteString("fileName", attachment.FileName);
|
||||||
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
||||||
|
|
||||||
@@ -57,7 +57,12 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteString("url", embedAuthor.Url);
|
_writer.WriteString("url", embedAuthor.Url);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embedAuthor.IconUrl))
|
if (!string.IsNullOrWhiteSpace(embedAuthor.IconUrl))
|
||||||
_writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(embedAuthor.IconProxyUrl ?? embedAuthor.IconUrl, cancellationToken));
|
{
|
||||||
|
_writer.WriteString(
|
||||||
|
"iconUrl",
|
||||||
|
await Context.ResolveAssetUrlAsync(embedAuthor.IconProxyUrl ?? embedAuthor.IconUrl, cancellationToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
@@ -70,7 +75,12 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embedImage.Url))
|
if (!string.IsNullOrWhiteSpace(embedImage.Url))
|
||||||
_writer.WriteString("url", await Context.ResolveMediaUrlAsync(embedImage.ProxyUrl ?? embedImage.Url, cancellationToken));
|
{
|
||||||
|
_writer.WriteString(
|
||||||
|
"url",
|
||||||
|
await Context.ResolveAssetUrlAsync(embedImage.ProxyUrl ?? embedImage.Url, cancellationToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_writer.WriteNumber("width", embedImage.Width);
|
_writer.WriteNumber("width", embedImage.Width);
|
||||||
_writer.WriteNumber("height", embedImage.Height);
|
_writer.WriteNumber("height", embedImage.Height);
|
||||||
@@ -88,7 +98,12 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteString("text", embedFooter.Text);
|
_writer.WriteString("text", embedFooter.Text);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embedFooter.IconUrl))
|
if (!string.IsNullOrWhiteSpace(embedFooter.IconUrl))
|
||||||
_writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(embedFooter.IconProxyUrl ?? embedFooter.IconUrl, cancellationToken));
|
{
|
||||||
|
_writer.WriteString(
|
||||||
|
"iconUrl",
|
||||||
|
await Context.ResolveAssetUrlAsync(embedFooter.IconProxyUrl ?? embedFooter.IconUrl, cancellationToken)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
@@ -100,8 +115,8 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("name", FormatMarkdown(embedField.Name));
|
_writer.WriteString("name", await FormatMarkdownAsync(embedField.Name));
|
||||||
_writer.WriteString("value", FormatMarkdown(embedField.Value));
|
_writer.WriteString("value", await FormatMarkdownAsync(embedField.Value));
|
||||||
_writer.WriteBoolean("isInline", embedField.IsInline);
|
_writer.WriteBoolean("isInline", embedField.IsInline);
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
@@ -114,10 +129,10 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
{
|
{
|
||||||
_writer.WriteStartObject();
|
_writer.WriteStartObject();
|
||||||
|
|
||||||
_writer.WriteString("title", FormatMarkdown(embed.Title));
|
_writer.WriteString("title", await FormatMarkdownAsync(embed.Title));
|
||||||
_writer.WriteString("url", embed.Url);
|
_writer.WriteString("url", embed.Url);
|
||||||
_writer.WriteString("timestamp", embed.Timestamp);
|
_writer.WriteString("timestamp", embed.Timestamp);
|
||||||
_writer.WriteString("description", FormatMarkdown(embed.Description));
|
_writer.WriteString("description", await FormatMarkdownAsync(embed.Description));
|
||||||
|
|
||||||
if (embed.Color is not null)
|
if (embed.Color is not null)
|
||||||
_writer.WriteString("color", embed.Color.Value.ToHex());
|
_writer.WriteString("color", embed.Color.Value.ToHex());
|
||||||
@@ -176,7 +191,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteString("id", sticker.Id.ToString());
|
_writer.WriteString("id", sticker.Id.ToString());
|
||||||
_writer.WriteString("name", sticker.Name);
|
_writer.WriteString("name", sticker.Name);
|
||||||
_writer.WriteString("format", sticker.Format.ToString());
|
_writer.WriteString("format", sticker.Format.ToString());
|
||||||
_writer.WriteString("sourceUrl", await Context.ResolveMediaUrlAsync(sticker.SourceUrl, cancellationToken));
|
_writer.WriteString("sourceUrl", await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken));
|
||||||
|
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
await _writer.FlushAsync(cancellationToken);
|
await _writer.FlushAsync(cancellationToken);
|
||||||
@@ -193,7 +208,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteString("id", reaction.Emoji.Id.ToString());
|
_writer.WriteString("id", reaction.Emoji.Id.ToString());
|
||||||
_writer.WriteString("name", reaction.Emoji.Name);
|
_writer.WriteString("name", reaction.Emoji.Name);
|
||||||
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
|
_writer.WriteBoolean("isAnimated", reaction.Emoji.IsAnimated);
|
||||||
_writer.WriteString("imageUrl", await Context.ResolveMediaUrlAsync(reaction.Emoji.ImageUrl, cancellationToken));
|
_writer.WriteString("imageUrl", await Context.ResolveAssetUrlAsync(reaction.Emoji.ImageUrl, cancellationToken));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
_writer.WriteNumber("count", reaction.Count);
|
_writer.WriteNumber("count", reaction.Count);
|
||||||
@@ -227,7 +242,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteStartObject("guild");
|
_writer.WriteStartObject("guild");
|
||||||
_writer.WriteString("id", Context.Request.Guild.Id.ToString());
|
_writer.WriteString("id", Context.Request.Guild.Id.ToString());
|
||||||
_writer.WriteString("name", Context.Request.Guild.Name);
|
_writer.WriteString("name", Context.Request.Guild.Name);
|
||||||
_writer.WriteString("iconUrl", await Context.ResolveMediaUrlAsync(Context.Request.Guild.IconUrl, cancellationToken));
|
_writer.WriteString("iconUrl", await Context.ResolveAssetUrlAsync(Context.Request.Guild.IconUrl, cancellationToken));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
// Channel
|
// Channel
|
||||||
@@ -268,7 +283,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteBoolean("isPinned", message.IsPinned);
|
_writer.WriteBoolean("isPinned", message.IsPinned);
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
_writer.WriteString("content", FormatMarkdown(message.Content));
|
_writer.WriteString("content", await FormatMarkdownAsync(message.Content));
|
||||||
|
|
||||||
// Author
|
// Author
|
||||||
_writer.WriteStartObject("author");
|
_writer.WriteStartObject("author");
|
||||||
@@ -278,7 +293,7 @@ internal class JsonMessageWriter : MessageWriter
|
|||||||
_writer.WriteString("nickname", Context.TryGetMember(message.Author.Id)?.Nick ?? message.Author.Name);
|
_writer.WriteString("nickname", Context.TryGetMember(message.Author.Id)?.Nick ?? message.Author.Name);
|
||||||
_writer.WriteString("color", Context.TryGetUserColor(message.Author.Id)?.ToHex());
|
_writer.WriteString("color", Context.TryGetUserColor(message.Author.Id)?.ToHex());
|
||||||
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
_writer.WriteBoolean("isBot", message.Author.IsBot);
|
||||||
_writer.WriteString("avatarUrl", await Context.ResolveMediaUrlAsync(message.Author.AvatarUrl, cancellationToken));
|
_writer.WriteString("avatarUrl", await Context.ResolveAssetUrlAsync(message.Author.AvatarUrl, cancellationToken));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
|
|||||||
+34
-23
@@ -3,6 +3,7 @@ using System.Linq;
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Discord.Data;
|
using DiscordChatExporter.Core.Discord.Data;
|
||||||
using DiscordChatExporter.Core.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Core.Markdown.Parsing;
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
@@ -23,15 +24,15 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
_isJumbo = isJumbo;
|
_isJumbo = isJumbo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitText(TextNode text)
|
protected override async ValueTask<MarkdownNode> VisitTextAsync(TextNode text)
|
||||||
{
|
{
|
||||||
_buffer.Append(HtmlEncode(text.Text));
|
_buffer.Append(HtmlEncode(text.Text));
|
||||||
return base.VisitText(text);
|
return await base.VisitTextAsync(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitFormatting(FormattingNode formatting)
|
protected override async ValueTask<MarkdownNode> VisitFormattingAsync(FormattingNode formatting)
|
||||||
{
|
{
|
||||||
var (tagOpen, tagClose) = formatting.Kind switch
|
var (openingTag, closingTag) = formatting.Kind switch
|
||||||
{
|
{
|
||||||
FormattingKind.Bold => (
|
FormattingKind.Bold => (
|
||||||
"<strong>",
|
"<strong>",
|
||||||
@@ -66,24 +67,24 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
_ => throw new InvalidOperationException($"Unknown formatting kind '{formatting.Kind}'.")
|
||||||
};
|
};
|
||||||
|
|
||||||
_buffer.Append(tagOpen);
|
_buffer.Append(openingTag);
|
||||||
var result = base.VisitFormatting(formatting);
|
var result = await base.VisitFormattingAsync(formatting);
|
||||||
_buffer.Append(tagClose);
|
_buffer.Append(closingTag);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock)
|
protected override async ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock)
|
||||||
{
|
{
|
||||||
_buffer
|
_buffer
|
||||||
.Append("<code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
|
.Append("<code class=\"chatlog__markdown-pre chatlog__markdown-pre--inline\">")
|
||||||
.Append(HtmlEncode(inlineCodeBlock.Code))
|
.Append(HtmlEncode(inlineCodeBlock.Code))
|
||||||
.Append("</code>");
|
.Append("</code>");
|
||||||
|
|
||||||
return base.VisitInlineCodeBlock(inlineCodeBlock);
|
return await base.VisitInlineCodeBlockAsync(inlineCodeBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock)
|
protected override async ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock)
|
||||||
{
|
{
|
||||||
var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
var highlightCssClass = !string.IsNullOrWhiteSpace(multiLineCodeBlock.Language)
|
||||||
? $"language-{multiLineCodeBlock.Language}"
|
? $"language-{multiLineCodeBlock.Language}"
|
||||||
@@ -94,10 +95,10 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
.Append(HtmlEncode(multiLineCodeBlock.Code))
|
.Append(HtmlEncode(multiLineCodeBlock.Code))
|
||||||
.Append("</code>");
|
.Append("</code>");
|
||||||
|
|
||||||
return base.VisitMultiLineCodeBlock(multiLineCodeBlock);
|
return await base.VisitMultiLineCodeBlockAsync(multiLineCodeBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitLink(LinkNode link)
|
protected override async ValueTask<MarkdownNode> VisitLinkAsync(LinkNode link)
|
||||||
{
|
{
|
||||||
// Try to extract message ID if the link refers to a Discord message
|
// Try to extract message ID if the link refers to a Discord message
|
||||||
var linkedMessageId = Regex.Match(
|
var linkedMessageId = Regex.Match(
|
||||||
@@ -111,24 +112,31 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
: $"<a href=\"{HtmlEncode(link.Url)}\">"
|
: $"<a href=\"{HtmlEncode(link.Url)}\">"
|
||||||
);
|
);
|
||||||
|
|
||||||
var result = base.VisitLink(link);
|
var result = await base.VisitLinkAsync(link);
|
||||||
_buffer.Append("</a>");
|
_buffer.Append("</a>");
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji)
|
||||||
{
|
{
|
||||||
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
var emojiImageUrl = Emoji.GetImageUrl(emoji.Id, emoji.Name, emoji.IsAnimated);
|
||||||
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
var jumboClass = _isJumbo ? "chatlog__emoji--large" : "";
|
||||||
|
|
||||||
_buffer
|
_buffer.Append(
|
||||||
.Append($"<img loading=\"lazy\" class=\"chatlog__emoji {jumboClass}\" alt=\"{emoji.Name}\" title=\"{emoji.Code}\" src=\"{emojiImageUrl}\">");
|
$"<img " +
|
||||||
|
$"loading=\"lazy\" " +
|
||||||
|
$"class=\"chatlog__emoji {jumboClass}\" " +
|
||||||
|
$"alt=\"{emoji.Name}\" " +
|
||||||
|
$"title=\"{emoji.Code}\" " +
|
||||||
|
$"src=\"{await _context.ResolveAssetUrlAsync(emojiImageUrl)}\"" +
|
||||||
|
$">"
|
||||||
|
);
|
||||||
|
|
||||||
return base.VisitEmoji(emoji);
|
return await base.VisitEmojiAsync(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitMention(MentionNode mention)
|
protected override async ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention)
|
||||||
{
|
{
|
||||||
if (mention.Kind == MentionKind.Everyone)
|
if (mention.Kind == MentionKind.Everyone)
|
||||||
{
|
{
|
||||||
@@ -183,10 +191,10 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
.Append("</span>");
|
.Append("</span>");
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.VisitMention(mention);
|
return await base.VisitMentionAsync(mention);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp)
|
||||||
{
|
{
|
||||||
var dateString = timestamp.Date is not null
|
var dateString = timestamp.Date is not null
|
||||||
? _context.FormatDate(timestamp.Date.Value)
|
? _context.FormatDate(timestamp.Date.Value)
|
||||||
@@ -202,7 +210,7 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
|||||||
.Append(HtmlEncode(dateString))
|
.Append(HtmlEncode(dateString))
|
||||||
.Append("</span>");
|
.Append("</span>");
|
||||||
|
|
||||||
return base.VisitUnixTimestamp(timestamp);
|
return await base.VisitUnixTimestampAsync(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +218,10 @@ internal partial class HtmlMarkdownVisitor
|
|||||||
{
|
{
|
||||||
private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text);
|
private static string HtmlEncode(string text) => WebUtility.HtmlEncode(text);
|
||||||
|
|
||||||
public static string Format(ExportContext context, string markdown, bool isJumboAllowed = true)
|
public static async ValueTask<string> FormatAsync(
|
||||||
|
ExportContext context,
|
||||||
|
string markdown,
|
||||||
|
bool isJumboAllowed = true)
|
||||||
{
|
{
|
||||||
var nodes = MarkdownParser.Parse(markdown);
|
var nodes = MarkdownParser.Parse(markdown);
|
||||||
|
|
||||||
@@ -220,7 +231,7 @@ internal partial class HtmlMarkdownVisitor
|
|||||||
|
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
new HtmlMarkdownVisitor(context, buffer, isJumbo).Visit(nodes);
|
await new HtmlMarkdownVisitor(context, buffer, isJumbo).VisitAsync(nodes);
|
||||||
|
|
||||||
return buffer.ToString();
|
return buffer.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-10
@@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using DiscordChatExporter.Core.Markdown;
|
using DiscordChatExporter.Core.Markdown;
|
||||||
using DiscordChatExporter.Core.Markdown.Parsing;
|
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
@@ -16,13 +17,13 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
|||||||
_buffer = buffer;
|
_buffer = buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitText(TextNode text)
|
protected override async ValueTask<MarkdownNode> VisitTextAsync(TextNode text)
|
||||||
{
|
{
|
||||||
_buffer.Append(text.Text);
|
_buffer.Append(text.Text);
|
||||||
return base.VisitText(text);
|
return await base.VisitTextAsync(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitEmoji(EmojiNode emoji)
|
protected override async ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji)
|
||||||
{
|
{
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
emoji.IsCustomEmoji
|
emoji.IsCustomEmoji
|
||||||
@@ -30,10 +31,10 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
|||||||
: emoji.Name
|
: emoji.Name
|
||||||
);
|
);
|
||||||
|
|
||||||
return base.VisitEmoji(emoji);
|
return await base.VisitEmojiAsync(emoji);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitMention(MentionNode mention)
|
protected override async ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention)
|
||||||
{
|
{
|
||||||
if (mention.Kind == MentionKind.Everyone)
|
if (mention.Kind == MentionKind.Everyone)
|
||||||
{
|
{
|
||||||
@@ -69,10 +70,10 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
|||||||
_buffer.Append($"@{name}");
|
_buffer.Append($"@{name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.VisitMention(mention);
|
return await base.VisitMentionAsync(mention);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp)
|
protected override async ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp)
|
||||||
{
|
{
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
timestamp.Date is not null
|
timestamp.Date is not null
|
||||||
@@ -80,18 +81,18 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
|||||||
: "Invalid date"
|
: "Invalid date"
|
||||||
);
|
);
|
||||||
|
|
||||||
return base.VisitUnixTimestamp(timestamp);
|
return await base.VisitUnixTimestampAsync(timestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal partial class PlainTextMarkdownVisitor
|
internal partial class PlainTextMarkdownVisitor
|
||||||
{
|
{
|
||||||
public static string Format(ExportContext context, string markdown)
|
public static async ValueTask<string> FormatAsync(ExportContext context, string markdown)
|
||||||
{
|
{
|
||||||
var nodes = MarkdownParser.ParseMinimal(markdown);
|
var nodes = MarkdownParser.ParseMinimal(markdown);
|
||||||
var buffer = new StringBuilder();
|
var buffer = new StringBuilder();
|
||||||
|
|
||||||
new PlainTextMarkdownVisitor(context, buffer).Visit(nodes);
|
await new PlainTextMarkdownVisitor(context, buffer).VisitAsync(nodes);
|
||||||
|
|
||||||
return buffer.ToString();
|
return buffer.ToString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||||||
_writer = new StreamWriter(stream);
|
_writer = new StreamWriter(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatMarkdown(string? markdown) =>
|
private ValueTask<string> FormatMarkdownAsync(string? markdown) =>
|
||||||
PlainTextMarkdownVisitor.Format(Context, markdown ?? "");
|
PlainTextMarkdownVisitor.FormatAsync(Context, markdown ?? "");
|
||||||
|
|
||||||
private async ValueTask WriteMessageHeaderAsync(Message message)
|
private async ValueTask WriteMessageHeaderAsync(Message message)
|
||||||
{
|
{
|
||||||
@@ -48,7 +48,7 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await _writer.WriteLineAsync(await Context.ResolveMediaUrlAsync(attachment.Url, cancellationToken));
|
await _writer.WriteLineAsync(await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
@@ -71,27 +71,41 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||||||
await _writer.WriteLineAsync(embed.Url);
|
await _writer.WriteLineAsync(embed.Url);
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embed.Title))
|
if (!string.IsNullOrWhiteSpace(embed.Title))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(embed.Title));
|
await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Title));
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embed.Description))
|
if (!string.IsNullOrWhiteSpace(embed.Description))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(embed.Description));
|
await _writer.WriteLineAsync(await FormatMarkdownAsync(embed.Description));
|
||||||
|
|
||||||
foreach (var field in embed.Fields)
|
foreach (var field in embed.Fields)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(field.Name))
|
if (!string.IsNullOrWhiteSpace(field.Name))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(field.Name));
|
await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Name));
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(field.Value))
|
if (!string.IsNullOrWhiteSpace(field.Value))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(field.Value));
|
await _writer.WriteLineAsync(await FormatMarkdownAsync(field.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))
|
if (!string.IsNullOrWhiteSpace(embed.Thumbnail?.Url))
|
||||||
await _writer.WriteLineAsync(await Context.ResolveMediaUrlAsync(embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url, cancellationToken));
|
{
|
||||||
|
await _writer.WriteLineAsync(
|
||||||
|
await Context.ResolveAssetUrlAsync(
|
||||||
|
embed.Thumbnail.ProxyUrl ?? embed.Thumbnail.Url,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var image in embed.Images)
|
foreach (var image in embed.Images)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(image.Url))
|
if (!string.IsNullOrWhiteSpace(image.Url))
|
||||||
await _writer.WriteLineAsync(await Context.ResolveMediaUrlAsync(image.ProxyUrl ?? image.Url, cancellationToken));
|
{
|
||||||
|
await _writer.WriteLineAsync(
|
||||||
|
await Context.ResolveAssetUrlAsync(
|
||||||
|
image.ProxyUrl ?? image.Url,
|
||||||
|
cancellationToken
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
if (!string.IsNullOrWhiteSpace(embed.Footer?.Text))
|
||||||
@@ -114,7 +128,9 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
await _writer.WriteLineAsync(await Context.ResolveMediaUrlAsync(sticker.SourceUrl, cancellationToken));
|
await _writer.WriteLineAsync(
|
||||||
|
await Context.ResolveAssetUrlAsync(sticker.SourceUrl, cancellationToken)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
@@ -174,7 +190,7 @@ internal class PlainTextMessageWriter : MessageWriter
|
|||||||
|
|
||||||
// Content
|
// Content
|
||||||
if (!string.IsNullOrWhiteSpace(message.Content))
|
if (!string.IsNullOrWhiteSpace(message.Content))
|
||||||
await _writer.WriteLineAsync(FormatMarkdown(message.Content));
|
await _writer.WriteLineAsync(await FormatMarkdownAsync(message.Content));
|
||||||
|
|
||||||
await _writer.WriteLineAsync();
|
await _writer.WriteLineAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,57 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Markdown.Parsing;
|
namespace DiscordChatExporter.Core.Markdown.Parsing;
|
||||||
|
|
||||||
internal abstract class MarkdownVisitor
|
internal abstract class MarkdownVisitor
|
||||||
{
|
{
|
||||||
protected virtual MarkdownNode VisitText(TextNode text) =>
|
protected virtual ValueTask<MarkdownNode> VisitTextAsync(TextNode text) =>
|
||||||
text;
|
new(text);
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitFormatting(FormattingNode formatting)
|
protected virtual async ValueTask<MarkdownNode> VisitFormattingAsync(FormattingNode formatting)
|
||||||
{
|
{
|
||||||
Visit(formatting.Children);
|
await VisitAsync(formatting.Children);
|
||||||
return formatting;
|
return formatting;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitInlineCodeBlock(InlineCodeBlockNode inlineCodeBlock) =>
|
protected virtual ValueTask<MarkdownNode> VisitInlineCodeBlockAsync(InlineCodeBlockNode inlineCodeBlock) =>
|
||||||
inlineCodeBlock;
|
new(inlineCodeBlock);
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitMultiLineCodeBlock(MultiLineCodeBlockNode multiLineCodeBlock) =>
|
protected virtual ValueTask<MarkdownNode> VisitMultiLineCodeBlockAsync(MultiLineCodeBlockNode multiLineCodeBlock) =>
|
||||||
multiLineCodeBlock;
|
new(multiLineCodeBlock);
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitLink(LinkNode link)
|
protected virtual async ValueTask<MarkdownNode> VisitLinkAsync(LinkNode link)
|
||||||
{
|
{
|
||||||
Visit(link.Children);
|
await VisitAsync(link.Children);
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitEmoji(EmojiNode emoji) =>
|
protected virtual ValueTask<MarkdownNode> VisitEmojiAsync(EmojiNode emoji) =>
|
||||||
emoji;
|
new(emoji);
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitMention(MentionNode mention) =>
|
protected virtual ValueTask<MarkdownNode> VisitMentionAsync(MentionNode mention) =>
|
||||||
mention;
|
new(mention);
|
||||||
|
|
||||||
protected virtual MarkdownNode VisitUnixTimestamp(UnixTimestampNode timestamp) =>
|
protected virtual ValueTask<MarkdownNode> VisitUnixTimestampAsync(UnixTimestampNode timestamp) =>
|
||||||
timestamp;
|
new(timestamp);
|
||||||
|
|
||||||
public MarkdownNode Visit(MarkdownNode node) => node switch
|
public async ValueTask<MarkdownNode> VisitAsync(MarkdownNode node) => node switch
|
||||||
{
|
{
|
||||||
TextNode text => VisitText(text),
|
TextNode text => await VisitTextAsync(text),
|
||||||
FormattingNode formatting => VisitFormatting(formatting),
|
FormattingNode formatting => await VisitFormattingAsync(formatting),
|
||||||
InlineCodeBlockNode inlineCodeBlock => VisitInlineCodeBlock(inlineCodeBlock),
|
InlineCodeBlockNode inlineCodeBlock => await VisitInlineCodeBlockAsync(inlineCodeBlock),
|
||||||
MultiLineCodeBlockNode multiLineCodeBlock => VisitMultiLineCodeBlock(multiLineCodeBlock),
|
MultiLineCodeBlockNode multiLineCodeBlock => await VisitMultiLineCodeBlockAsync(multiLineCodeBlock),
|
||||||
LinkNode link => VisitLink(link),
|
LinkNode link => await VisitLinkAsync(link),
|
||||||
EmojiNode emoji => VisitEmoji(emoji),
|
EmojiNode emoji => await VisitEmojiAsync(emoji),
|
||||||
MentionNode mention => VisitMention(mention),
|
MentionNode mention => await VisitMentionAsync(mention),
|
||||||
UnixTimestampNode timestamp => VisitUnixTimestamp(timestamp),
|
UnixTimestampNode timestamp => await VisitUnixTimestampAsync(timestamp),
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
_ => throw new ArgumentOutOfRangeException(nameof(node))
|
||||||
};
|
};
|
||||||
|
|
||||||
public void Visit(IEnumerable<MarkdownNode> nodes)
|
public async ValueTask VisitAsync(IEnumerable<MarkdownNode> nodes)
|
||||||
{
|
{
|
||||||
foreach (var node in nodes)
|
foreach (var node in nodes)
|
||||||
Visit(node);
|
await VisitAsync(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Gress" Version="2.0.1" />
|
<PackageReference Include="Gress" Version="2.0.1" />
|
||||||
<PackageReference Include="MaterialDesignColors" Version="2.0.6" />
|
<PackageReference Include="MaterialDesignColors" Version="2.0.9" />
|
||||||
<PackageReference Include="MaterialDesignThemes" Version="4.5.0" />
|
<PackageReference Include="MaterialDesignThemes" Version="4.6.1" />
|
||||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.39" />
|
||||||
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
<PackageReference Include="Ookii.Dialogs.Wpf" Version="5.0.1" />
|
||||||
<PackageReference Include="Onova" Version="2.6.2" />
|
<PackageReference Include="Onova" Version="2.6.2" />
|
||||||
<PackageReference Include="Stylet" Version="1.3.6" />
|
<PackageReference Include="Stylet" Version="1.3.6" />
|
||||||
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
<PackageReference Include="Tyrrrz.Settings" Version="1.3.4" />
|
||||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.1" PrivateAssets="all" />
|
<PackageReference Include="PropertyChanged.Fody" Version="4.0.5" PrivateAssets="all" />
|
||||||
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
<PackageReference Include="DotnetRuntimeBootstrapper" Version="2.3.1" PrivateAssets="all" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public partial class SettingsService : SettingsManager
|
|||||||
|
|
||||||
public int ParallelLimit { get; set; } = 1;
|
public int ParallelLimit { get; set; } = 1;
|
||||||
|
|
||||||
public bool ShouldReuseMedia { get; set; }
|
public bool ShouldReuseAssets { get; set; }
|
||||||
|
|
||||||
public string? LastToken { get; set; }
|
public string? LastToken { get; set; }
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public partial class SettingsService : SettingsManager
|
|||||||
|
|
||||||
public string? LastMessageFilterValue { get; set; }
|
public string? LastMessageFilterValue { get; set; }
|
||||||
|
|
||||||
public bool LastShouldDownloadMedia { get; set; }
|
public bool LastShouldDownloadAssets { get; set; }
|
||||||
|
|
||||||
public SettingsService()
|
public SettingsService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -186,8 +186,8 @@ public class DashboardViewModel : PropertyChangedBase
|
|||||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||||
dialog.PartitionLimit,
|
dialog.PartitionLimit,
|
||||||
dialog.MessageFilter,
|
dialog.MessageFilter,
|
||||||
dialog.ShouldDownloadMedia,
|
dialog.ShouldDownloadAssets,
|
||||||
_settingsService.ShouldReuseMedia,
|
_settingsService.ShouldReuseAssets,
|
||||||
_settingsService.DateFormat
|
_settingsService.DateFormat
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class ExportSetupViewModel : DialogScreen
|
|||||||
? MessageFilter.Parse(MessageFilterValue)
|
? MessageFilter.Parse(MessageFilterValue)
|
||||||
: MessageFilter.Null;
|
: MessageFilter.Null;
|
||||||
|
|
||||||
public bool ShouldDownloadMedia { get; set; }
|
public bool ShouldDownloadAssets { get; set; }
|
||||||
|
|
||||||
public bool IsAdvancedSectionDisplayed { get; set; }
|
public bool IsAdvancedSectionDisplayed { get; set; }
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ public class ExportSetupViewModel : DialogScreen
|
|||||||
SelectedFormat = _settingsService.LastExportFormat;
|
SelectedFormat = _settingsService.LastExportFormat;
|
||||||
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
PartitionLimitValue = _settingsService.LastPartitionLimitValue;
|
||||||
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
MessageFilterValue = _settingsService.LastMessageFilterValue;
|
||||||
ShouldDownloadMedia = _settingsService.LastShouldDownloadMedia;
|
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
|
||||||
|
|
||||||
// Show the "advanced options" section by default if any
|
// Show the "advanced options" section by default if any
|
||||||
// of the advanced options are set to non-default values.
|
// of the advanced options are set to non-default values.
|
||||||
@@ -81,7 +81,7 @@ public class ExportSetupViewModel : DialogScreen
|
|||||||
Before != default ||
|
Before != default ||
|
||||||
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
|
||||||
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
|
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
|
||||||
ShouldDownloadMedia != default;
|
ShouldDownloadAssets != default;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleAdvancedSection() => IsAdvancedSectionDisplayed = !IsAdvancedSectionDisplayed;
|
public void ToggleAdvancedSection() => IsAdvancedSectionDisplayed = !IsAdvancedSectionDisplayed;
|
||||||
@@ -92,7 +92,7 @@ public class ExportSetupViewModel : DialogScreen
|
|||||||
_settingsService.LastExportFormat = SelectedFormat;
|
_settingsService.LastExportFormat = SelectedFormat;
|
||||||
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
_settingsService.LastPartitionLimitValue = PartitionLimitValue;
|
||||||
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
_settingsService.LastMessageFilterValue = MessageFilterValue;
|
||||||
_settingsService.LastShouldDownloadMedia = ShouldDownloadMedia;
|
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
||||||
|
|
||||||
// If single channel - prompt file path
|
// If single channel - prompt file path
|
||||||
if (IsSingleChannel)
|
if (IsSingleChannel)
|
||||||
|
|||||||
@@ -38,10 +38,10 @@ public class SettingsViewModel : DialogScreen
|
|||||||
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
set => _settingsService.ParallelLimit = Math.Clamp(value, 1, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldReuseMedia
|
public bool ShouldReuseAssets
|
||||||
{
|
{
|
||||||
get => _settingsService.ShouldReuseMedia;
|
get => _settingsService.ShouldReuseAssets;
|
||||||
set => _settingsService.ShouldReuseMedia = value;
|
set => _settingsService.ShouldReuseAssets = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SettingsViewModel(SettingsService settingsService) =>
|
public SettingsViewModel(SettingsService settingsService) =>
|
||||||
|
|||||||
@@ -167,8 +167,8 @@
|
|||||||
Text="{Binding MessageFilterValue}"
|
Text="{Binding MessageFilterValue}"
|
||||||
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')." />
|
ToolTip="Only include messages that satisfy this filter (e.g. 'from:foo#1234' or 'has:image')." />
|
||||||
|
|
||||||
<!-- Download media -->
|
<!-- Download assets -->
|
||||||
<Grid Margin="16,16" ToolTip="Download referenced media content (user avatars, attached files, embedded images, etc)">
|
<Grid Margin="16,16" ToolTip="Download assets referenced by the export (user avatars, attached files, embedded images, etc.)">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
@@ -177,12 +177,12 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Text="Download media" />
|
Text="Download assets" />
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
IsChecked="{Binding ShouldDownloadMedia}" />
|
IsChecked="{Binding ShouldDownloadAssets}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -82,20 +82,20 @@
|
|||||||
IsChecked="{Binding IsTokenPersisted}" />
|
IsChecked="{Binding IsTokenPersisted}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Reuse media -->
|
<!-- Reuse assets -->
|
||||||
<DockPanel
|
<DockPanel
|
||||||
Margin="16,8"
|
Margin="16,8"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
LastChildFill="False"
|
LastChildFill="False"
|
||||||
ToolTip="Reuse already existing media content to skip redundant downloads">
|
ToolTip="Reuse previously downloaded assets to avoid redundant requests">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DockPanel.Dock="Left"
|
DockPanel.Dock="Left"
|
||||||
Text="Reuse downloaded media" />
|
Text="Reuse downloaded assets" />
|
||||||
<ToggleButton
|
<ToggleButton
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
IsChecked="{Binding ShouldReuseMedia}" />
|
IsChecked="{Binding ShouldReuseAssets}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Date format -->
|
<!-- Date format -->
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:s="https://github.com/canton7/Stylet"
|
xmlns:s="https://github.com/canton7/Stylet"
|
||||||
xmlns:viewModels="clr-namespace:DiscordChatExporter.Gui.ViewModels"
|
xmlns:viewModels="clr-namespace:DiscordChatExporter.Gui.ViewModels"
|
||||||
|
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||||
Width="600"
|
Width="600"
|
||||||
Height="550"
|
Height="550"
|
||||||
MinWidth="325"
|
MinWidth="325"
|
||||||
@@ -21,10 +22,11 @@
|
|||||||
</Window.TaskbarItemInfo>
|
</Window.TaskbarItemInfo>
|
||||||
|
|
||||||
<materialDesign:DialogHost
|
<materialDesign:DialogHost
|
||||||
|
x:Name="DialogHost"
|
||||||
Loaded="{s:Action OnViewFullyLoaded}"
|
Loaded="{s:Action OnViewFullyLoaded}"
|
||||||
SnackbarMessageQueue="{Binding Notifications}"
|
SnackbarMessageQueue="{Binding Notifications}"
|
||||||
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
Style="{DynamicResource MaterialDesignEmbeddedDialogHost}">
|
||||||
<Grid>
|
<Grid IsEnabled="{Binding IsOpen, ElementName=DialogHost, Converter={x:Static converters:InverseBoolConverter.Instance}}">
|
||||||
<ContentControl s:View.Model="{Binding Dashboard}" />
|
<ContentControl s:View.Model="{Binding Dashboard}" />
|
||||||
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
<materialDesign:Snackbar MessageQueue="{Binding Notifications}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -108,3 +108,8 @@ The following table lists all available download options:
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
|
## Related projects
|
||||||
|
|
||||||
|
- [**Chat Analytics**](https://github.com/mlomb/chat-analytics) — solution for analyzing chat patterns of Discord users, using exports produced by **DiscordChatExporter**.
|
||||||
|
- [**DiscordChatExporter-frontend**](https://github.com/slatinsky/DiscordChatExporter-frontend) — convenient viewer for exports produced by **DiscordChatExporter**.
|
||||||
Reference in New Issue
Block a user