mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-02-14 15:53:30 -07:00
Replace the date format option with a locale option (#1130)
This commit is contained in:
parent
53b11d6c49
commit
59344cedbe
|
|
@ -59,7 +59,9 @@ public static class ExportWrapper
|
||||||
Token = Secrets.DiscordToken,
|
Token = Secrets.DiscordToken,
|
||||||
ChannelIds = new[] { channelId },
|
ChannelIds = new[] { channelId },
|
||||||
ExportFormat = format,
|
ExportFormat = format,
|
||||||
OutputPath = filePath
|
OutputPath = filePath,
|
||||||
|
Locale = "en-US",
|
||||||
|
IsUtcNormalizationEnabled = true
|
||||||
}.ExecuteAsync(console);
|
}.ExecuteAsync(console);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
using System;
|
using System.Threading.Tasks;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using AngleSharp.Dom;
|
using AngleSharp.Dom;
|
||||||
using DiscordChatExporter.Cli.Tests.Infra;
|
using DiscordChatExporter.Cli.Tests.Infra;
|
||||||
using DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
using DiscordChatExporter.Core.Discord;
|
using DiscordChatExporter.Core.Discord;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
@ -14,218 +12,128 @@ public class HtmlMarkdownSpecs
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323136411078787")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Default timestamp: 2/12/2023 1:36 PM");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323136411078787")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Default timestamp: 02/12/2023 3:36 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323205268967596")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Short time timestamp: 1:36 PM");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323205268967596")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Short time timestamp: 3:36 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323235342139483")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Long time timestamp: 1:36:12 PM");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323235342139483")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Long time timestamp: 3:36:12 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_date_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_short_date_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323326727634984")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Short date timestamp: 2/12/2023");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323326727634984")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Short date timestamp: 02/12/2023");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_date_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_long_date_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323350731640863")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Long date timestamp: Sunday, February 12, 2023");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323350731640863")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Long date timestamp: February 12, 2023");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323374379118593")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Full timestamp: Sunday, February 12, 2023 1:36 PM");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323374379118593")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Full timestamp: February 12, 2023 3:36 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_long_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_full_long_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323409095376947")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message
|
||||||
// Act
|
.Text()
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
.Should()
|
||||||
ChannelIds.MarkdownTestCases,
|
.Contain("Full long timestamp: Sunday, February 12, 2023 1:36:12 PM");
|
||||||
Snowflake.Parse("1074323409095376947")
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message
|
|
||||||
.Text()
|
|
||||||
.Should()
|
|
||||||
.Contain("Full long timestamp: Sunday, February 12, 2023 3:36 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_relative_format()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_a_timestamp_marker_in_the_relative_format()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074323436853285004")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Relative timestamp: 2/12/2023 1:36 PM");
|
||||||
// Act
|
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 1:36 PM");
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074323436853285004")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Relative timestamp: 02/12/2023 3:36 PM");
|
|
||||||
message.InnerHtml.Should().Contain("Sunday, February 12, 2023 3:36 PM");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task I_can_export_a_channel_that_contains_a_message_with_an_invalid_timestamp_marker()
|
public async Task I_can_export_a_channel_that_contains_a_message_with_an_invalid_timestamp_marker()
|
||||||
{
|
{
|
||||||
// Date formatting code relies on the local time zone, so we need to set it to a fixed value
|
// Act
|
||||||
TimeZoneInfoEx.SetLocal(TimeSpan.FromHours(+2));
|
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
||||||
|
ChannelIds.MarkdownTestCases,
|
||||||
|
Snowflake.Parse("1074328534409019563")
|
||||||
|
);
|
||||||
|
|
||||||
try
|
// Assert
|
||||||
{
|
message.Text().Should().Contain("Invalid timestamp: Invalid date");
|
||||||
// Act
|
|
||||||
var message = await ExportWrapper.GetMessageAsHtmlAsync(
|
|
||||||
ChannelIds.MarkdownTestCases,
|
|
||||||
Snowflake.Parse("1074328534409019563")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
message.Text().Should().Contain("Invalid timestamp: Invalid date");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
TimeZoneInfo.ClearCachedData();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System;
|
|
||||||
using ReflectionMagic;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Cli.Tests.Utils;
|
|
||||||
|
|
||||||
internal static class TimeZoneInfoEx
|
|
||||||
{
|
|
||||||
// https://stackoverflow.com/a/63700512/2205454
|
|
||||||
public static void SetLocal(TimeZoneInfo timeZone) =>
|
|
||||||
typeof(TimeZoneInfo).AsDynamicType().s_cachedData._localTimeZone = timeZone;
|
|
||||||
|
|
||||||
public static void SetLocal(TimeSpan offset) =>
|
|
||||||
SetLocal(TimeZoneInfo.CreateCustomTimeZone("test-tz", offset, "test-tz", "test-tz"));
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -88,7 +89,7 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
"media",
|
"media",
|
||||||
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
Description = "Download assets referenced by the export (user avatars, attached files, embedded images, etc.)."
|
||||||
)]
|
)]
|
||||||
public bool ShouldDownloadAssets { get; init; } = false;
|
public bool ShouldDownloadAssets { get; init; }
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"reuse-media",
|
"reuse-media",
|
||||||
|
|
@ -111,9 +112,19 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
init => _assetsDirPath = value is not null ? Path.GetFullPath(value) : null;
|
init => _assetsDirPath = value is not null ? Path.GetFullPath(value) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
[CommandOption("dateformat", Description = "Format used when writing dates.")]
|
[Obsolete("This option doesn't do anything. Kept for backwards compatibility.")]
|
||||||
|
[CommandOption(
|
||||||
|
"dateformat",
|
||||||
|
Description = "This option doesn't do anything. Kept for backwards compatibility."
|
||||||
|
)]
|
||||||
public string DateFormat { get; init; } = "MM/dd/yyyy h:mm tt";
|
public string DateFormat { get; init; } = "MM/dd/yyyy h:mm tt";
|
||||||
|
|
||||||
|
[CommandOption("locale", Description = "Locale to use when formatting dates and numbers.")]
|
||||||
|
public string Locale { get; init; } = CultureInfo.CurrentCulture.Name;
|
||||||
|
|
||||||
|
[CommandOption("utc", Description = "Normalize all timestamps to UTC+0.")]
|
||||||
|
public bool IsUtcNormalizationEnabled { get; init; } = false;
|
||||||
|
|
||||||
[CommandOption(
|
[CommandOption(
|
||||||
"fuck-russia",
|
"fuck-russia",
|
||||||
EnvironmentVariable = "FUCK_RUSSIA",
|
EnvironmentVariable = "FUCK_RUSSIA",
|
||||||
|
|
@ -210,7 +221,8 @@ public abstract class ExportCommandBase : DiscordCommandBase
|
||||||
ShouldFormatMarkdown,
|
ShouldFormatMarkdown,
|
||||||
ShouldDownloadAssets,
|
ShouldDownloadAssets,
|
||||||
ShouldReuseAssets,
|
ShouldReuseAssets,
|
||||||
DateFormat
|
Locale,
|
||||||
|
IsUtcNormalizationEnabled
|
||||||
);
|
);
|
||||||
|
|
||||||
await Exporter.ExportChannelAsync(
|
await Exporter.ExportChannelAsync(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using DiscordChatExporter.Core.Utils.Extensions;
|
using DiscordChatExporter.Core.Utils.Extensions;
|
||||||
|
|
||||||
|
|
@ -19,7 +20,10 @@ public static class ImageCdn
|
||||||
? runes
|
? runes
|
||||||
: runes.Where(r => r.Value != 0xfe0f);
|
: runes.Where(r => r.Value != 0xfe0f);
|
||||||
|
|
||||||
var twemojiId = string.Join("-", filteredRunes.Select(r => r.Value.ToString("x")));
|
var twemojiId = string.Join(
|
||||||
|
"-",
|
||||||
|
filteredRunes.Select(r => r.Value.ToString("x", CultureInfo.InvariantCulture))
|
||||||
|
);
|
||||||
|
|
||||||
return $"https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{twemojiId}.svg";
|
return $"https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/svg/{twemojiId}.svg";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ internal class ExportContext
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset NormalizeDate(DateTimeOffset instant) =>
|
||||||
|
Request.IsUtcNormalizationEnabled ? instant.ToUniversalTime() : instant.ToLocalTime();
|
||||||
|
|
||||||
|
public string FormatDate(DateTimeOffset instant, string format = "g") =>
|
||||||
|
NormalizeDate(instant).ToString(format, Request.CultureInfo);
|
||||||
|
|
||||||
public async ValueTask PopulateChannelsAndRolesAsync(
|
public async ValueTask PopulateChannelsAndRolesAsync(
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
)
|
||||||
|
|
@ -41,10 +47,14 @@ internal class ExportContext
|
||||||
await foreach (
|
await foreach (
|
||||||
var channel in Discord.GetGuildChannelsAsync(Request.Guild.Id, cancellationToken)
|
var channel in Discord.GetGuildChannelsAsync(Request.Guild.Id, cancellationToken)
|
||||||
)
|
)
|
||||||
|
{
|
||||||
_channelsById[channel.Id] = channel;
|
_channelsById[channel.Id] = channel;
|
||||||
|
}
|
||||||
|
|
||||||
await foreach (var role in Discord.GetGuildRolesAsync(Request.Guild.Id, cancellationToken))
|
await foreach (var role in Discord.GetGuildRolesAsync(Request.Guild.Id, cancellationToken))
|
||||||
|
{
|
||||||
_rolesById[role.Id] = role;
|
_rolesById[role.Id] = role;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because members cannot be pulled in bulk, we need to populate them on demand
|
// Because members cannot be pulled in bulk, we need to populate them on demand
|
||||||
|
|
@ -84,14 +94,6 @@ internal class ExportContext
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
) => await PopulateMemberAsync(user.Id, user, cancellationToken);
|
) => await PopulateMemberAsync(user.Id, user, cancellationToken);
|
||||||
|
|
||||||
public string FormatDate(DateTimeOffset instant) =>
|
|
||||||
Request.DateFormat switch
|
|
||||||
{
|
|
||||||
"unix" => instant.ToUnixTimeSeconds().ToString(),
|
|
||||||
"unixms" => instant.ToUnixTimeMilliseconds().ToString(),
|
|
||||||
var format => instant.ToLocalString(format)
|
|
||||||
};
|
|
||||||
|
|
||||||
public Member? TryGetMember(Snowflake id) => _membersById.GetValueOrDefault(id);
|
public Member? TryGetMember(Snowflake id) => _membersById.GetValueOrDefault(id);
|
||||||
|
|
||||||
public Channel? TryGetChannel(Snowflake id) => _channelsById.GetValueOrDefault(id);
|
public Channel? TryGetChannel(Snowflake id) => _channelsById.GetValueOrDefault(id);
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,11 @@ public partial class ExportRequest
|
||||||
|
|
||||||
public bool ShouldReuseAssets { get; }
|
public bool ShouldReuseAssets { get; }
|
||||||
|
|
||||||
public string DateFormat { get; }
|
public string Locale { get; }
|
||||||
|
|
||||||
|
public CultureInfo CultureInfo { get; }
|
||||||
|
|
||||||
|
public bool IsUtcNormalizationEnabled { get; }
|
||||||
|
|
||||||
public ExportRequest(
|
public ExportRequest(
|
||||||
Guild guild,
|
Guild guild,
|
||||||
|
|
@ -54,7 +58,8 @@ public partial class ExportRequest
|
||||||
bool shouldFormatMarkdown,
|
bool shouldFormatMarkdown,
|
||||||
bool shouldDownloadAssets,
|
bool shouldDownloadAssets,
|
||||||
bool shouldReuseAssets,
|
bool shouldReuseAssets,
|
||||||
string dateFormat
|
string locale,
|
||||||
|
bool isUtcNormalizationEnabled
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
Guild = guild;
|
Guild = guild;
|
||||||
|
|
@ -67,7 +72,8 @@ public partial class ExportRequest
|
||||||
ShouldFormatMarkdown = shouldFormatMarkdown;
|
ShouldFormatMarkdown = shouldFormatMarkdown;
|
||||||
ShouldDownloadAssets = shouldDownloadAssets;
|
ShouldDownloadAssets = shouldDownloadAssets;
|
||||||
ShouldReuseAssets = shouldReuseAssets;
|
ShouldReuseAssets = shouldReuseAssets;
|
||||||
DateFormat = dateFormat;
|
Locale = locale;
|
||||||
|
IsUtcNormalizationEnabled = isUtcNormalizationEnabled;
|
||||||
|
|
||||||
OutputFilePath = GetOutputBaseFilePath(Guild, Channel, outputPath, Format, After, Before);
|
OutputFilePath = GetOutputBaseFilePath(Guild, Channel, outputPath, Format, After, Before);
|
||||||
|
|
||||||
|
|
@ -76,6 +82,8 @@ public partial class ExportRequest
|
||||||
AssetsDirPath = !string.IsNullOrWhiteSpace(assetsDirPath)
|
AssetsDirPath = !string.IsNullOrWhiteSpace(assetsDirPath)
|
||||||
? FormatPath(assetsDirPath, Guild, Channel, After, Before)
|
? FormatPath(assetsDirPath, Guild, Channel, After, Before)
|
||||||
: $"{OutputFilePath}_Files{Path.DirectorySeparatorChar}";
|
: $"{OutputFilePath}_Files{Path.DirectorySeparatorChar}";
|
||||||
|
|
||||||
|
CultureInfo = CultureInfo.GetCultureInfo(Locale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,12 +317,12 @@ internal partial class HtmlMarkdownVisitor : MarkdownVisitor
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
var formatted = timestamp.Instant is not null
|
var formatted = timestamp.Instant is not null
|
||||||
? !string.IsNullOrWhiteSpace(timestamp.Format)
|
? _context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g")
|
||||||
? timestamp.Instant.Value.ToLocalString(timestamp.Format)
|
|
||||||
: _context.FormatDate(timestamp.Instant.Value)
|
|
||||||
: "Invalid date";
|
: "Invalid date";
|
||||||
|
|
||||||
var formattedLong = timestamp.Instant?.ToLocalString("dddd, MMMM d, yyyy h:mm tt") ?? "";
|
var formattedLong = timestamp.Instant is not null
|
||||||
|
? _context.FormatDate(timestamp.Instant.Value, "f")
|
||||||
|
: "";
|
||||||
|
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
// lang=html
|
// lang=html
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ internal class HtmlMessageWriter : MessageWriter
|
||||||
Minify(
|
Minify(
|
||||||
await new PostambleTemplate
|
await new PostambleTemplate
|
||||||
{
|
{
|
||||||
ExportContext = Context,
|
Context = Context,
|
||||||
MessagesWritten = MessagesWritten
|
MessagesWritten = MessagesWritten
|
||||||
}.RenderAsync(cancellationToken)
|
}.RenderAsync(cancellationToken)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
@ -196,7 +197,7 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
await FormatMarkdownAsync(embed.Title ?? "", cancellationToken)
|
await FormatMarkdownAsync(embed.Title ?? "", cancellationToken)
|
||||||
);
|
);
|
||||||
_writer.WriteString("url", embed.Url);
|
_writer.WriteString("url", embed.Url);
|
||||||
_writer.WriteString("timestamp", embed.Timestamp);
|
_writer.WriteString("timestamp", embed.Timestamp?.Pipe(Context.NormalizeDate));
|
||||||
_writer.WriteString(
|
_writer.WriteString(
|
||||||
"description",
|
"description",
|
||||||
await FormatMarkdownAsync(embed.Description ?? "", cancellationToken)
|
await FormatMarkdownAsync(embed.Description ?? "", cancellationToken)
|
||||||
|
|
@ -292,12 +293,12 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
|
|
||||||
// Date range
|
// Date range
|
||||||
_writer.WriteStartObject("dateRange");
|
_writer.WriteStartObject("dateRange");
|
||||||
_writer.WriteString("after", Context.Request.After?.ToDate());
|
_writer.WriteString("after", Context.Request.After?.ToDate().Pipe(Context.NormalizeDate));
|
||||||
_writer.WriteString("before", Context.Request.Before?.ToDate());
|
_writer.WriteString("before", Context.Request.Before?.ToDate().Pipe(Context.NormalizeDate));
|
||||||
_writer.WriteEndObject();
|
_writer.WriteEndObject();
|
||||||
|
|
||||||
// Timestamp
|
// Timestamp
|
||||||
_writer.WriteString("exportedAt", System.DateTimeOffset.UtcNow);
|
_writer.WriteString("exportedAt", Context.NormalizeDate(DateTimeOffset.UtcNow));
|
||||||
|
|
||||||
// Message array (start)
|
// Message array (start)
|
||||||
_writer.WriteStartArray("messages");
|
_writer.WriteStartArray("messages");
|
||||||
|
|
@ -316,9 +317,15 @@ internal class JsonMessageWriter : MessageWriter
|
||||||
// Metadata
|
// Metadata
|
||||||
_writer.WriteString("id", message.Id.ToString());
|
_writer.WriteString("id", message.Id.ToString());
|
||||||
_writer.WriteString("type", message.Kind.ToString());
|
_writer.WriteString("type", message.Kind.ToString());
|
||||||
_writer.WriteString("timestamp", message.Timestamp);
|
_writer.WriteString("timestamp", Context.NormalizeDate(message.Timestamp));
|
||||||
_writer.WriteString("timestampEdited", message.EditedTimestamp);
|
_writer.WriteString(
|
||||||
_writer.WriteString("callEndedTimestamp", message.CallEndedTimestamp);
|
"timestampEdited",
|
||||||
|
message.EditedTimestamp?.Pipe(Context.NormalizeDate)
|
||||||
|
);
|
||||||
|
_writer.WriteString(
|
||||||
|
"callEndedTimestamp",
|
||||||
|
message.CallEndedTimestamp?.Pipe(Context.NormalizeDate)
|
||||||
|
);
|
||||||
_writer.WriteBoolean("isPinned", message.IsPinned);
|
_writer.WriteBoolean("isPinned", message.IsPinned);
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
@using System
|
@using System
|
||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
@using System.Globalization
|
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@using System.Threading.Tasks
|
@using System.Threading.Tasks
|
||||||
@using DiscordChatExporter.Core.Discord.Data
|
@using DiscordChatExporter.Core.Discord.Data
|
||||||
|
|
@ -20,8 +19,8 @@
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset instant) =>
|
string FormatDate(DateTimeOffset instant, string format = "g") =>
|
||||||
Context.FormatDate(instant);
|
Context.FormatDate(instant, format);
|
||||||
|
|
||||||
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
Context.Request.ShouldFormatMarkdown
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
|
@ -100,7 +99,7 @@
|
||||||
}
|
}
|
||||||
else if (message.Kind == MessageKind.Call)
|
else if (message.Kind == MessageKind.Call)
|
||||||
{
|
{
|
||||||
<span>started a call that lasted @(((message.CallEndedTimestamp ?? message.Timestamp) - message.Timestamp).TotalMinutes.ToString("n0", CultureInfo.InvariantCulture)) minutes</span>
|
<span>started a call that lasted @(((message.CallEndedTimestamp ?? message.Timestamp) - message.Timestamp).TotalMinutes.ToString("n0", Context.Request.CultureInfo)) minutes</span>
|
||||||
}
|
}
|
||||||
else if (message.Kind == MessageKind.ChannelNameChange)
|
else if (message.Kind == MessageKind.ChannelNameChange)
|
||||||
{
|
{
|
||||||
|
|
@ -132,7 +131,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@* Timestamp *@
|
@* Timestamp *@
|
||||||
<span class="chatlog__system-notification-timestamp">
|
<span class="chatlog__system-notification-timestamp" title="@FormatDate(message.Timestamp, "f")">
|
||||||
<a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a>
|
<a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -154,7 +153,7 @@
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="chatlog__short-timestamp" title="@FormatDate(message.Timestamp)">@message.Timestamp.ToLocalString("t")</div>
|
<div class="chatlog__short-timestamp" title="@FormatDate(message.Timestamp, "f")">@FormatDate(message.Timestamp, "t")</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -194,7 +193,7 @@
|
||||||
|
|
||||||
@if (message.ReferencedMessage.EditedTimestamp is not null)
|
@if (message.ReferencedMessage.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<span class="chatlog__reply-edited-timestamp" title="@FormatDate(message.ReferencedMessage.EditedTimestamp.Value)">(edited)</span>
|
<span class="chatlog__reply-edited-timestamp" title="@FormatDate(message.ReferencedMessage.EditedTimestamp.Value, "f")">(edited)</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -241,7 +240,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Timestamp *@
|
@* Timestamp *@
|
||||||
<span class="chatlog__timestamp"><a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a></span>
|
<span class="chatlog__timestamp" title="@FormatDate(message.Timestamp, "f")"><a href="#chatlog__message-container-@message.Id">@FormatDate(message.Timestamp)</a></span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -258,7 +257,7 @@
|
||||||
@* Edited timestamp *@
|
@* Edited timestamp *@
|
||||||
@if (message.EditedTimestamp is not null)
|
@if (message.EditedTimestamp is not null)
|
||||||
{
|
{
|
||||||
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value)">(edited)</span>
|
<span class="chatlog__edited-timestamp" title="@FormatDate(message.EditedTimestamp.Value, "f")">(edited)</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,9 +91,7 @@ internal partial class PlainTextMarkdownVisitor : MarkdownVisitor
|
||||||
{
|
{
|
||||||
_buffer.Append(
|
_buffer.Append(
|
||||||
timestamp.Instant is not null
|
timestamp.Instant is not null
|
||||||
? !string.IsNullOrWhiteSpace(timestamp.Format)
|
? _context.FormatDate(timestamp.Instant.Value, timestamp.Format ?? "g")
|
||||||
? timestamp.Instant.Value.ToLocalString(timestamp.Format)
|
|
||||||
: _context.FormatDate(timestamp.Instant.Value)
|
|
||||||
: "Invalid date"
|
: "Invalid date"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
@inherits RazorBlade.HtmlTemplate
|
@using System
|
||||||
|
|
||||||
|
@inherits RazorBlade.HtmlTemplate
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
public required ExportContext ExportContext { get; init; }
|
public required ExportContext Context { get; init; }
|
||||||
|
|
||||||
public required long MessagesWritten { get; init; }
|
public required long MessagesWritten { get; init; }
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +16,8 @@
|
||||||
<!--/wmm:ignore-->
|
<!--/wmm:ignore-->
|
||||||
|
|
||||||
<div class="postamble">
|
<div class="postamble">
|
||||||
<div class="postamble__entry">Exported @MessagesWritten.ToString("n0") message(s)</div>
|
<div class="postamble__entry">Exported @MessagesWritten.ToString("n0", Context.Request.CultureInfo) message(s)</div>
|
||||||
|
<div class="postamble__entry">Timezone: UTC@((Context.Request.IsUtcNormalizationEnabled ? 0 : TimeZoneInfo.Local.BaseUtcOffset.TotalHours).ToString("+#.#;-#.#;+0", Context.Request.CultureInfo))</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@
|
||||||
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
ValueTask<string> ResolveAssetUrlAsync(string url) =>
|
||||||
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
Context.ResolveAssetUrlAsync(url, CancellationToken);
|
||||||
|
|
||||||
string FormatDate(DateTimeOffset instant) =>
|
string FormatDate(DateTimeOffset instant, string format = "g") =>
|
||||||
Context.FormatDate(instant);
|
Context.FormatDate(instant, format);
|
||||||
|
|
||||||
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
async ValueTask<string> FormatMarkdownAsync(string markdown) =>
|
||||||
Context.Request.ShouldFormatMarkdown
|
Context.Request.ShouldFormatMarkdown
|
||||||
|
|
|
||||||
|
|
@ -340,16 +340,13 @@ internal static partial class MarkdownParser
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
var format = m.Groups[2].Value switch
|
var format = m.Groups[2].Value.NullIfWhiteSpace() switch
|
||||||
{
|
{
|
||||||
"t" => "h:mm tt",
|
// Ignore the 'relative' format because it doesn't make sense in a static export
|
||||||
"T" => "h:mm:ss tt",
|
"r" => null,
|
||||||
"d" => "MM/dd/yyyy",
|
"R" => null,
|
||||||
"D" => "MMMM dd, yyyy",
|
// Discord's date formats are (mostly) compatible with .NET's date formats
|
||||||
"f" => "MMMM dd, yyyy h:mm tt",
|
var f => f
|
||||||
"F" => "dddd, MMMM dd, yyyy h:mm tt",
|
|
||||||
// Relative format is ignored because it doesn't make much sense in a static export
|
|
||||||
_ => null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return new TimestampNode(instant, format);
|
return new TimestampNode(instant, format);
|
||||||
|
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace DiscordChatExporter.Core.Utils.Extensions;
|
|
||||||
|
|
||||||
public static class DateExtensions
|
|
||||||
{
|
|
||||||
public static string ToLocalString(this DateTimeOffset instant, string format) =>
|
|
||||||
instant.ToLocalTime().ToString(format, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace DiscordChatExporter.Gui.Converters;
|
||||||
|
|
||||||
|
[ValueConversion(typeof(string), typeof(string))]
|
||||||
|
public class LocaleToDisplayNameConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public static LocaleToDisplayNameConverter Instance { get; } = new();
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||||
|
value is string locale ? CultureInfo.GetCultureInfo(locale).DisplayName : null;
|
||||||
|
|
||||||
|
public object ConvertBack(
|
||||||
|
object value,
|
||||||
|
Type targetType,
|
||||||
|
object parameter,
|
||||||
|
CultureInfo culture
|
||||||
|
) => throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Cogwheel;
|
using Cogwheel;
|
||||||
using DiscordChatExporter.Core.Exporting;
|
using DiscordChatExporter.Core.Exporting;
|
||||||
|
|
@ -19,7 +20,9 @@ public partial class SettingsService : SettingsBase
|
||||||
|
|
||||||
public ThreadInclusionMode ThreadInclusionMode { get; set; } = ThreadInclusionMode.None;
|
public ThreadInclusionMode ThreadInclusionMode { get; set; } = ThreadInclusionMode.None;
|
||||||
|
|
||||||
public string DateFormat { get; set; } = "MM/dd/yyyy h:mm tt";
|
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
|
||||||
|
|
||||||
|
public bool IsUtcNormalizationEnabled { get; set; }
|
||||||
|
|
||||||
public int ParallelLimit { get; set; } = 1;
|
public int ParallelLimit { get; set; } = 1;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,11 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
// due to the channels being asynchronously loaded.
|
// due to the channels being asynchronously loaded.
|
||||||
AvailableChannels = null;
|
AvailableChannels = null;
|
||||||
SelectedChannels = null;
|
SelectedChannels = null;
|
||||||
|
|
||||||
|
// Pull channels for the selected guild
|
||||||
|
// (ideally this should be called inside `PullGuilds()`,
|
||||||
|
// but Stylet doesn't support async commands)
|
||||||
|
PullChannels();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -88,14 +93,14 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
Token = _settingsService.LastToken;
|
Token = _settingsService.LastToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask ShowSettingsAsync() =>
|
public async void ShowSettings() =>
|
||||||
await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateSettingsViewModel());
|
await _dialogManager.ShowDialogAsync(_viewModelFactory.CreateSettingsViewModel());
|
||||||
|
|
||||||
public void ShowHelp() => ProcessEx.StartShellExecute(App.DocumentationUrl);
|
public void ShowHelp() => ProcessEx.StartShellExecute(App.DocumentationUrl);
|
||||||
|
|
||||||
public bool CanPullGuildsAsync => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
public bool CanPullGuilds => !IsBusy && !string.IsNullOrWhiteSpace(Token);
|
||||||
|
|
||||||
public async ValueTask PullGuildsAsync()
|
public async void PullGuilds()
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
var progress = _progressMuxer.CreateInput();
|
var progress = _progressMuxer.CreateInput();
|
||||||
|
|
@ -118,9 +123,6 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
|
|
||||||
AvailableGuilds = guilds;
|
AvailableGuilds = guilds;
|
||||||
SelectedGuild = guilds.FirstOrDefault();
|
SelectedGuild = guilds.FirstOrDefault();
|
||||||
|
|
||||||
// Pull channels for the selected guild
|
|
||||||
await PullChannelsAsync();
|
|
||||||
}
|
}
|
||||||
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
catch (DiscordChatExporterException ex) when (!ex.IsFatal)
|
||||||
{
|
{
|
||||||
|
|
@ -142,10 +144,9 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanPullChannelsAsync =>
|
public bool CanPullChannels => !IsBusy && _discord is not null && SelectedGuild is not null;
|
||||||
!IsBusy && _discord is not null && SelectedGuild is not null;
|
|
||||||
|
|
||||||
public async ValueTask PullChannelsAsync()
|
public async void PullChannels()
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
var progress = _progressMuxer.CreateInput();
|
var progress = _progressMuxer.CreateInput();
|
||||||
|
|
@ -206,13 +207,13 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool CanExportAsync =>
|
public bool CanExport =>
|
||||||
!IsBusy
|
!IsBusy
|
||||||
&& _discord is not null
|
&& _discord is not null
|
||||||
&& SelectedGuild is not null
|
&& SelectedGuild is not null
|
||||||
&& SelectedChannels?.Any() is true;
|
&& SelectedChannels?.Any() is true;
|
||||||
|
|
||||||
public async ValueTask ExportAsync()
|
public async void Export()
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
|
|
||||||
|
|
@ -267,7 +268,8 @@ public class DashboardViewModel : PropertyChangedBase
|
||||||
dialog.ShouldFormatMarkdown,
|
dialog.ShouldFormatMarkdown,
|
||||||
dialog.ShouldDownloadAssets,
|
dialog.ShouldDownloadAssets,
|
||||||
dialog.ShouldReuseAssets,
|
dialog.ShouldReuseAssets,
|
||||||
_settingsService.DateFormat
|
_settingsService.Locale,
|
||||||
|
_settingsService.IsUtcNormalizationEnabled
|
||||||
);
|
);
|
||||||
|
|
||||||
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using DiscordChatExporter.Gui.Models;
|
using DiscordChatExporter.Gui.Models;
|
||||||
using DiscordChatExporter.Gui.Services;
|
using DiscordChatExporter.Gui.Services;
|
||||||
using DiscordChatExporter.Gui.ViewModels.Framework;
|
using DiscordChatExporter.Gui.ViewModels.Framework;
|
||||||
|
|
@ -37,10 +39,52 @@ public class SettingsViewModel : DialogScreen
|
||||||
set => _settingsService.ThreadInclusionMode = value;
|
set => _settingsService.ThreadInclusionMode = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string DateFormat
|
public IReadOnlyList<string> AvailableLocales { get; } = new[]
|
||||||
|
{
|
||||||
|
// Current locale
|
||||||
|
CultureInfo.CurrentCulture.Name,
|
||||||
|
// Locales supported by the Discord app
|
||||||
|
"da-DK",
|
||||||
|
"de-DE",
|
||||||
|
"en-GB",
|
||||||
|
"en-US",
|
||||||
|
"es-ES",
|
||||||
|
"fr-FR",
|
||||||
|
"hr-HR",
|
||||||
|
"it-IT",
|
||||||
|
"lt-LT",
|
||||||
|
"hu-HU",
|
||||||
|
"nl-NL",
|
||||||
|
"no-NO",
|
||||||
|
"pl-PL",
|
||||||
|
"pt-BR",
|
||||||
|
"ro-RO",
|
||||||
|
"fi-FI",
|
||||||
|
"sv-SE",
|
||||||
|
"vi-VN",
|
||||||
|
"tr-TR",
|
||||||
|
"cs-CZ",
|
||||||
|
"el-GR",
|
||||||
|
"bg-BG",
|
||||||
|
"ru-RU",
|
||||||
|
"uk-UA",
|
||||||
|
"th-TH",
|
||||||
|
"zh-CN",
|
||||||
|
"ja-JP",
|
||||||
|
"zh-TW",
|
||||||
|
"ko-KR"
|
||||||
|
}.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
|
|
||||||
|
public string Locale
|
||||||
{
|
{
|
||||||
get => _settingsService.DateFormat;
|
get => _settingsService.Locale;
|
||||||
set => _settingsService.DateFormat = value;
|
set => _settingsService.Locale = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsUtcNormalizationEnabled
|
||||||
|
{
|
||||||
|
get => _settingsService.IsUtcNormalizationEnabled;
|
||||||
|
set => _settingsService.IsUtcNormalizationEnabled = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ParallelLimit
|
public int ParallelLimit
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
Margin="0,6,6,6"
|
Margin="0,6,6,6"
|
||||||
Padding="4"
|
Padding="4"
|
||||||
Command="{s:Action PullGuildsAsync}"
|
Command="{s:Action PullGuilds}"
|
||||||
IsDefault="True"
|
IsDefault="True"
|
||||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
ToolTip="Pull available guilds and channels (Enter)">
|
ToolTip="Pull available guilds and channels (Enter)">
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="6"
|
Margin="6"
|
||||||
Padding="4"
|
Padding="4"
|
||||||
Command="{s:Action ShowSettingsAsync}"
|
Command="{s:Action ShowSettings}"
|
||||||
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
Foreground="{DynamicResource MaterialDesignDarkForeground}"
|
||||||
Style="{DynamicResource MaterialDesignFlatButton}"
|
Style="{DynamicResource MaterialDesignFlatButton}"
|
||||||
ToolTip="Settings">
|
ToolTip="Settings">
|
||||||
|
|
@ -274,7 +274,7 @@
|
||||||
Margin="-8"
|
Margin="-8"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
Cursor="Hand"
|
Cursor="Hand"
|
||||||
MouseLeftButtonUp="{s:Action PullChannelsAsync}"
|
MouseLeftButtonUp="{s:Action PullChannels}"
|
||||||
ToolTip="{Binding Name}">
|
ToolTip="{Binding Name}">
|
||||||
<!-- Guild icon placeholder -->
|
<!-- Guild icon placeholder -->
|
||||||
<Ellipse
|
<Ellipse
|
||||||
|
|
@ -350,7 +350,7 @@
|
||||||
<DataTemplate DataType="{x:Type data:Channel}">
|
<DataTemplate DataType="{x:Type data:Channel}">
|
||||||
<Grid Margin="-8" Background="Transparent">
|
<Grid Margin="-8" Background="Transparent">
|
||||||
<Grid.InputBindings>
|
<Grid.InputBindings>
|
||||||
<MouseBinding Command="{s:Action ExportAsync}" MouseAction="LeftDoubleClick" />
|
<MouseBinding Command="{s:Action Export}" MouseAction="LeftDoubleClick" />
|
||||||
</Grid.InputBindings>
|
</Grid.InputBindings>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto" />
|
<ColumnDefinition Width="Auto" />
|
||||||
|
|
@ -412,9 +412,9 @@
|
||||||
Margin="32,24"
|
Margin="32,24"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Bottom"
|
||||||
Command="{s:Action ExportAsync}"
|
Command="{s:Action Export}"
|
||||||
Style="{DynamicResource MaterialDesignFloatingActionAccentButton}"
|
Style="{DynamicResource MaterialDesignFloatingActionAccentButton}"
|
||||||
Visibility="{Binding CanExportAsync, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
Visibility="{Binding CanExport, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||||
<materialDesign:PackIcon
|
<materialDesign:PackIcon
|
||||||
Width="32"
|
Width="32"
|
||||||
Height="32"
|
Height="32"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
x:Class="DiscordChatExporter.Gui.Views.Dialogs.SettingsView"
|
x:Class="DiscordChatExporter.Gui.Views.Dialogs.SettingsView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="clr-namespace:DiscordChatExporter.Gui.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
xmlns:dialogs="clr-namespace:DiscordChatExporter.Gui.ViewModels.Dialogs"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
|
@ -93,27 +94,51 @@
|
||||||
DockPanel.Dock="Left"
|
DockPanel.Dock="Left"
|
||||||
Text="Show threads" />
|
Text="Show threads" />
|
||||||
<ComboBox
|
<ComboBox
|
||||||
|
Width="150"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
ItemsSource="{Binding AvailableThreadInclusions}"
|
ItemsSource="{Binding AvailableThreadInclusions}"
|
||||||
SelectedItem="{Binding ThreadInclusionMode}" />
|
SelectedItem="{Binding ThreadInclusionMode}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Date format -->
|
<!-- Locale -->
|
||||||
<DockPanel
|
<DockPanel
|
||||||
Margin="16,8"
|
Margin="16,8"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
LastChildFill="False"
|
LastChildFill="False"
|
||||||
ToolTip="Format used when writing dates (uses .NET date formatting rules)">
|
ToolTip="Locale to use when formatting dates and numbers">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DockPanel.Dock="Left"
|
DockPanel.Dock="Left"
|
||||||
Text="Date format" />
|
Text="Locale" />
|
||||||
<TextBox
|
<ComboBox
|
||||||
Width="150"
|
Width="150"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
DockPanel.Dock="Right"
|
DockPanel.Dock="Right"
|
||||||
Text="{Binding DateFormat}" />
|
ItemsSource="{Binding AvailableLocales}"
|
||||||
|
SelectedItem="{Binding Locale}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Converter={x:Static converters:LocaleToDisplayNameConverter.Instance}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- UTC normalization -->
|
||||||
|
<DockPanel
|
||||||
|
Margin="16,8"
|
||||||
|
Background="Transparent"
|
||||||
|
LastChildFill="False"
|
||||||
|
ToolTip="Normalize all timestamps to UTC+0">
|
||||||
|
<TextBlock
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
DockPanel.Dock="Left"
|
||||||
|
Text="Normalize to UTC" />
|
||||||
|
<ToggleButton
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
DockPanel.Dock="Right"
|
||||||
|
IsChecked="{Binding IsUtcNormalizationEnabled}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<!-- Parallel limit -->
|
<!-- Parallel limit -->
|
||||||
|
|
@ -139,9 +164,13 @@
|
||||||
<Slider
|
<Slider
|
||||||
Width="150"
|
Width="150"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
|
IsSnapToTickEnabled="True"
|
||||||
|
LargeChange="1"
|
||||||
Maximum="10"
|
Maximum="10"
|
||||||
Minimum="1"
|
Minimum="1"
|
||||||
|
SmallChange="1"
|
||||||
Style="{DynamicResource MaterialDesignThinSlider}"
|
Style="{DynamicResource MaterialDesignThinSlider}"
|
||||||
|
TickFrequency="1"
|
||||||
Value="{Binding ParallelLimit}" />
|
Value="{Binding ParallelLimit}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue