mirror of
https://github.com/Tyrrrz/DiscordChatExporter.git
synced 2026-08-13 14:03:01 -06:00
Add bulk forum export with configurable asset handling
- add bulk export of active and archived forum threads - add opt-in multi-channel selection mode - separate forum and regular channel export settings - save forum exports as JSON - add configurable asset folder structure and file naming - allow shared, per-thread, or skipped common resources - allow attachments to be grouped by media type or message - make avatar downloads optional and disabled by default - add configurable parallel forum export workers - disable token persistence and auto-updates for the custom build
This commit is contained in:
parent
0358045c94
commit
9ee1599b9d
|
|
@ -17,30 +17,43 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
|||
{
|
||||
private static readonly AsyncKeyedLocker<string> Locker = new();
|
||||
|
||||
// File paths of the previously downloaded assets
|
||||
private readonly Dictionary<string, string> _previousPathsByUrl = new(StringComparer.Ordinal);
|
||||
// File paths of the previously downloaded assets. The same URL can intentionally be stored
|
||||
// in different forum folders, so the destination path is part of the cache key.
|
||||
private readonly Dictionary<string, string> _previousPathsByRequest = new(
|
||||
StringComparer.Ordinal
|
||||
);
|
||||
|
||||
public async ValueTask<string> DownloadAsync(
|
||||
string url,
|
||||
string? relativeDirPath = null,
|
||||
string? preferredFileName = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
var fileName = GetFileNameFromUrl(url);
|
||||
var filePath = Path.Combine(workingDirPath, fileName);
|
||||
var actualWorkingDirPath = !string.IsNullOrWhiteSpace(relativeDirPath)
|
||||
? Path.Combine(workingDirPath, relativeDirPath)
|
||||
: workingDirPath;
|
||||
|
||||
var fileName = !string.IsNullOrWhiteSpace(preferredFileName)
|
||||
? Path.EscapeFileName(preferredFileName)
|
||||
: GetFileNameFromUrl(url);
|
||||
|
||||
var filePath = Path.Combine(actualWorkingDirPath, fileName);
|
||||
var requestKey = url + '\n' + filePath;
|
||||
|
||||
using var _ = await Locker.LockAsync(filePath, cancellationToken);
|
||||
|
||||
if (_previousPathsByUrl.TryGetValue(url, out var cachedFilePath))
|
||||
if (_previousPathsByRequest.TryGetValue(requestKey, out var cachedFilePath))
|
||||
return cachedFilePath;
|
||||
|
||||
// Reuse existing files if we're allowed to
|
||||
if (reuse && File.Exists(filePath))
|
||||
return _previousPathsByUrl[url] = filePath;
|
||||
return _previousPathsByRequest[requestKey] = filePath;
|
||||
|
||||
// Check for a file cached by the legacy naming scheme (5-char hash) and rename it
|
||||
// to the new naming scheme to preserve backwards compatibility with existing exports.
|
||||
// This will catch both the 5-char lowercase hash and the 5-char uppercase hash variants.
|
||||
if (reuse)
|
||||
if (reuse && string.IsNullOrWhiteSpace(preferredFileName))
|
||||
{
|
||||
var legacyFileNames = GetLegacyFileNamesFromUrl(url);
|
||||
foreach (var legacyFileName in legacyFileNames)
|
||||
|
|
@ -53,7 +66,7 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
|||
try
|
||||
{
|
||||
File.Move(legacyFilePath, filePath, true);
|
||||
return _previousPathsByUrl[url] = filePath;
|
||||
return _previousPathsByRequest[requestKey] = filePath;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
|
|
@ -64,7 +77,7 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
|||
}
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(workingDirPath);
|
||||
Directory.CreateDirectory(actualWorkingDirPath);
|
||||
|
||||
await Http.ResiliencePipeline.ExecuteAsync(
|
||||
async innerCancellationToken =>
|
||||
|
|
@ -84,7 +97,7 @@ internal partial class ExportAssetDownloader(string workingDirPath, bool reuse)
|
|||
cancellationToken
|
||||
);
|
||||
|
||||
return _previousPathsByUrl[url] = filePath;
|
||||
return _previousPathsByRequest[requestKey] = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,92 @@ internal class ExportContext(DiscordClient discord, ExportRequest request)
|
|||
public Color? TryGetUserColor(Snowflake id) =>
|
||||
GetUserRoles(id).Where(r => r.Color is not null).Select(r => r.Color).FirstOrDefault();
|
||||
|
||||
private string GetForumThreadDirPath()
|
||||
{
|
||||
var channelName = Path.EscapeFileName(Request.Channel.Name).Truncate(60);
|
||||
return Path.Combine("threads", $"{Request.Channel.Id}-{channelName}");
|
||||
}
|
||||
|
||||
private string? GetForumCommonAssetDirPath() =>
|
||||
Request.ForumCommonAssetMode switch
|
||||
{
|
||||
ForumCommonAssetMode.SharedFolder => "common",
|
||||
ForumCommonAssetMode.PerThreadFolder => Path.Combine(GetForumThreadDirPath(), "common"),
|
||||
ForumCommonAssetMode.Skip => null,
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
|
||||
private string GetForumAttachmentDirPath(Attachment attachment, Snowflake messageId)
|
||||
{
|
||||
var baseDirPath = Path.Combine(GetForumThreadDirPath(), "attachments");
|
||||
|
||||
return Request.ForumAttachmentFolderMode switch
|
||||
{
|
||||
ForumAttachmentFolderMode.PerThread => baseDirPath,
|
||||
ForumAttachmentFolderMode.ByMediaType => Path.Combine(
|
||||
baseDirPath,
|
||||
attachment.IsImage ? "images"
|
||||
: attachment.IsVideo ? "videos"
|
||||
: attachment.IsAudio ? "audio"
|
||||
: "files"
|
||||
),
|
||||
ForumAttachmentFolderMode.ByMessage => Path.Combine(
|
||||
baseDirPath,
|
||||
"messages",
|
||||
messageId.ToString()
|
||||
),
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
}
|
||||
|
||||
private string? GetForumAttachmentFileName(Attachment attachment, Snowflake messageId) =>
|
||||
Request.ForumAttachmentNamingMode switch
|
||||
{
|
||||
ForumAttachmentNamingMode.OriginalWithHash => null,
|
||||
ForumAttachmentNamingMode.AttachmentIdAndOriginal =>
|
||||
$"{attachment.Id}_{attachment.FileName}",
|
||||
ForumAttachmentNamingMode.MessageAndAttachmentIdsAndOriginal =>
|
||||
$"{messageId}_{attachment.Id}_{attachment.FileName}",
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
|
||||
private async ValueTask<string> ResolveDownloadedAssetUrlAsync(
|
||||
string url,
|
||||
string? relativeDirPath,
|
||||
string? preferredFileName,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var filePath = await _assetDownloader.DownloadAsync(
|
||||
url,
|
||||
relativeDirPath,
|
||||
preferredFileName,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);
|
||||
|
||||
// Prefer the relative path so that the export package can be copied around without breaking references.
|
||||
// However, if the assets directory lies outside the export directory, use the absolute path instead.
|
||||
var shouldUseAbsoluteFilePath =
|
||||
relativeFilePath.StartsWith(
|
||||
".." + Path.DirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
|| relativeFilePath.StartsWith(
|
||||
".." + Path.AltDirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
);
|
||||
|
||||
var optimalFilePath = shouldUseAbsoluteFilePath ? filePath : relativeFilePath;
|
||||
|
||||
// For HTML, the path needs to be properly formatted
|
||||
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
|
||||
return Url.EncodeFilePath(optimalFilePath);
|
||||
|
||||
return optimalFilePath;
|
||||
}
|
||||
|
||||
public async ValueTask<string> ResolveAssetUrlAsync(
|
||||
string url,
|
||||
CancellationToken cancellationToken = default
|
||||
|
|
@ -128,30 +214,18 @@ internal class ExportContext(DiscordClient discord, ExportRequest request)
|
|||
if (!Request.ShouldDownloadAssets)
|
||||
return url;
|
||||
|
||||
var relativeDirPath = Request.IsForumExport ? GetForumCommonAssetDirPath() : null;
|
||||
if (Request.IsForumExport && relativeDirPath is null)
|
||||
return url;
|
||||
|
||||
try
|
||||
{
|
||||
var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
|
||||
var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);
|
||||
|
||||
// Prefer the relative path so that the export package can be copied around without breaking references.
|
||||
// However, if the assets directory lies outside the export directory, use the absolute path instead.
|
||||
var shouldUseAbsoluteFilePath =
|
||||
relativeFilePath.StartsWith(
|
||||
".." + Path.DirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
)
|
||||
|| relativeFilePath.StartsWith(
|
||||
".." + Path.AltDirectorySeparatorChar,
|
||||
StringComparison.Ordinal
|
||||
);
|
||||
|
||||
var optimalFilePath = shouldUseAbsoluteFilePath ? filePath : relativeFilePath;
|
||||
|
||||
// For HTML, the path needs to be properly formatted
|
||||
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
|
||||
return Url.EncodeFilePath(optimalFilePath);
|
||||
|
||||
return optimalFilePath;
|
||||
return await ResolveDownloadedAssetUrlAsync(
|
||||
url,
|
||||
relativeDirPath,
|
||||
null,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
// Try to catch only exceptions related to failed HTTP requests
|
||||
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332
|
||||
|
|
@ -163,4 +237,36 @@ internal class ExportContext(DiscordClient discord, ExportRequest request)
|
|||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask<string> ResolveAvatarUrlAsync(
|
||||
string url,
|
||||
CancellationToken cancellationToken = default
|
||||
) =>
|
||||
Request.IsForumExport && !Request.ShouldDownloadForumAvatars
|
||||
? ValueTask.FromResult(url)
|
||||
: ResolveAssetUrlAsync(url, cancellationToken);
|
||||
|
||||
public async ValueTask<string> ResolveAttachmentUrlAsync(
|
||||
Attachment attachment,
|
||||
Snowflake messageId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (!Request.ShouldDownloadAssets || !Request.IsForumExport)
|
||||
return await ResolveAssetUrlAsync(attachment.Url, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
return await ResolveDownloadedAssetUrlAsync(
|
||||
attachment.Url,
|
||||
GetForumAttachmentDirPath(attachment, messageId),
|
||||
GetForumAttachmentFileName(attachment, messageId),
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
catch (Exception ex) when (ex is HttpRequestException or OperationCanceledException)
|
||||
{
|
||||
return attachment.Url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,16 @@ public partial class ExportRequest
|
|||
|
||||
public bool ShouldReuseAssets { get; }
|
||||
|
||||
public bool IsForumExport { get; }
|
||||
|
||||
public bool ShouldDownloadForumAvatars { get; }
|
||||
|
||||
public ForumCommonAssetMode ForumCommonAssetMode { get; }
|
||||
|
||||
public ForumAttachmentFolderMode ForumAttachmentFolderMode { get; }
|
||||
|
||||
public ForumAttachmentNamingMode ForumAttachmentNamingMode { get; }
|
||||
|
||||
public string? Locale { get; }
|
||||
|
||||
public CultureInfo? CultureInfo { get; }
|
||||
|
|
@ -62,7 +72,13 @@ public partial class ExportRequest
|
|||
bool shouldDownloadAssets,
|
||||
bool shouldReuseAssets,
|
||||
string? locale,
|
||||
bool isUtcNormalizationEnabled
|
||||
bool isUtcNormalizationEnabled,
|
||||
bool isForumExport = false,
|
||||
bool shouldDownloadForumAvatars = false,
|
||||
ForumCommonAssetMode forumCommonAssetMode = ForumCommonAssetMode.SharedFolder,
|
||||
ForumAttachmentFolderMode forumAttachmentFolderMode = ForumAttachmentFolderMode.PerThread,
|
||||
ForumAttachmentNamingMode forumAttachmentNamingMode =
|
||||
ForumAttachmentNamingMode.AttachmentIdAndOriginal
|
||||
)
|
||||
{
|
||||
Guild = guild;
|
||||
|
|
@ -76,6 +92,11 @@ public partial class ExportRequest
|
|||
ShouldFormatMarkdown = shouldFormatMarkdown;
|
||||
ShouldDownloadAssets = shouldDownloadAssets;
|
||||
ShouldReuseAssets = shouldReuseAssets;
|
||||
IsForumExport = isForumExport;
|
||||
ShouldDownloadForumAvatars = shouldDownloadForumAvatars;
|
||||
ForumCommonAssetMode = forumCommonAssetMode;
|
||||
ForumAttachmentFolderMode = forumAttachmentFolderMode;
|
||||
ForumAttachmentNamingMode = forumAttachmentNamingMode;
|
||||
Locale = locale;
|
||||
IsUtcNormalizationEnabled = isUtcNormalizationEnabled;
|
||||
|
||||
|
|
@ -83,8 +104,10 @@ public partial class ExportRequest
|
|||
|
||||
OutputDirPath = Path.GetDirectoryName(OutputFilePath)!;
|
||||
|
||||
AssetsDirPath = !string.IsNullOrWhiteSpace(assetsDirPath)
|
||||
? FormatPath(assetsDirPath, Guild, Channel, After, Before)
|
||||
AssetsDirPath =
|
||||
!string.IsNullOrWhiteSpace(assetsDirPath)
|
||||
? FormatPath(assetsDirPath, Guild, Channel, After, Before)
|
||||
: IsForumExport ? Path.Combine(OutputDirPath, "_forum_assets")
|
||||
: $"{OutputFilePath}_Files{Path.DirectorySeparatorChar}";
|
||||
|
||||
CultureInfo = Locale?.Pipe(CultureInfo.GetCultureInfo);
|
||||
|
|
|
|||
22
DiscordChatExporter.Core/Exporting/ForumAssetOptions.cs
Normal file
22
DiscordChatExporter.Core/Exporting/ForumAssetOptions.cs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
namespace DiscordChatExporter.Core.Exporting;
|
||||
|
||||
public enum ForumCommonAssetMode
|
||||
{
|
||||
SharedFolder,
|
||||
PerThreadFolder,
|
||||
Skip,
|
||||
}
|
||||
|
||||
public enum ForumAttachmentFolderMode
|
||||
{
|
||||
PerThread,
|
||||
ByMediaType,
|
||||
ByMessage,
|
||||
}
|
||||
|
||||
public enum ForumAttachmentNamingMode
|
||||
{
|
||||
OriginalWithHash,
|
||||
AttachmentIdAndOriginal,
|
||||
MessageAndAttachmentIdsAndOriginal,
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Text.Encodings.Web;
|
|||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Core.Discord.Data.Embeds;
|
||||
using DiscordChatExporter.Core.Markdown.Parsing;
|
||||
|
|
@ -66,7 +67,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
|
||||
_writer.WriteString(
|
||||
"avatarUrl",
|
||||
await Context.ResolveAssetUrlAsync(
|
||||
await Context.ResolveAvatarUrlAsync(
|
||||
Context.TryGetMember(user.Id)?.AvatarUrl ?? user.AvatarUrl,
|
||||
cancellationToken
|
||||
)
|
||||
|
|
@ -121,6 +122,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
|
||||
private async ValueTask WriteAttachmentAsync(
|
||||
Attachment attachment,
|
||||
Snowflake messageId,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
|
|
@ -129,7 +131,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
_writer.WriteString("id", attachment.Id.ToString());
|
||||
_writer.WriteString(
|
||||
"url",
|
||||
await Context.ResolveAssetUrlAsync(attachment.Url, cancellationToken)
|
||||
await Context.ResolveAttachmentUrlAsync(attachment, messageId, cancellationToken)
|
||||
);
|
||||
_writer.WriteString("fileName", attachment.FileName);
|
||||
_writer.WriteNumber("fileSizeBytes", attachment.FileSize.TotalBytes);
|
||||
|
|
@ -473,7 +475,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
_writer.WriteStartArray("attachments");
|
||||
|
||||
foreach (var attachment in message.Attachments)
|
||||
await WriteAttachmentAsync(attachment, cancellationToken);
|
||||
await WriteAttachmentAsync(attachment, message.Id, cancellationToken);
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
|
|
@ -570,7 +572,7 @@ internal class JsonMessageWriter(Stream stream, ExportContext context)
|
|||
_writer.WriteStartArray("attachments");
|
||||
|
||||
foreach (var attachment in message.ForwardedMessage.Attachments)
|
||||
await WriteAttachmentAsync(attachment, cancellationToken);
|
||||
await WriteAttachmentAsync(attachment, message.Id, cancellationToken);
|
||||
|
||||
_writer.WriteEndArray();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using DiscordChatExporter.Core.Exporting;
|
||||
|
||||
namespace DiscordChatExporter.Gui.Converters;
|
||||
|
||||
public class ForumAssetOptionToStringConverter : IValueConverter
|
||||
{
|
||||
public static ForumAssetOptionToStringConverter Instance { get; } = new();
|
||||
|
||||
public object? Convert(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) =>
|
||||
value switch
|
||||
{
|
||||
ForumCommonAssetMode.SharedFolder => "One shared folder for the whole export",
|
||||
ForumCommonAssetMode.PerThreadFolder => "Separate common folder inside every post",
|
||||
ForumCommonAssetMode.Skip => "Do not download common resources",
|
||||
|
||||
ForumAttachmentFolderMode.PerThread => "One attachments folder per post",
|
||||
ForumAttachmentFolderMode.ByMediaType => "Split into images, videos, audio and files",
|
||||
ForumAttachmentFolderMode.ByMessage => "Separate folder for every message",
|
||||
|
||||
ForumAttachmentNamingMode.OriginalWithHash => "Original name + safety hash",
|
||||
ForumAttachmentNamingMode.AttachmentIdAndOriginal => "Attachment ID + original name",
|
||||
ForumAttachmentNamingMode.MessageAndAttachmentIdsAndOriginal =>
|
||||
"Message ID + attachment ID + original name",
|
||||
_ => value?.ToString(),
|
||||
};
|
||||
|
||||
public object ConvertBack(
|
||||
object? value,
|
||||
Type targetType,
|
||||
object? parameter,
|
||||
CultureInfo culture
|
||||
) => throw new NotSupportedException();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
using DiscordChatExporter.Core.Discord.Data;
|
||||
using DiscordChatExporter.Gui.Localization;
|
||||
using DiscordChatExporter.Gui.ViewModels;
|
||||
|
|
@ -18,13 +19,15 @@ public class ViewModelManager(IServiceProvider services, LocalizationManager loc
|
|||
|
||||
public ExportSetupViewModel GetExportSetupViewModel(
|
||||
Guild guild,
|
||||
IReadOnlyList<Channel> channels
|
||||
IReadOnlyList<Channel> channels,
|
||||
IReadOnlySet<Snowflake> forumChannelIds
|
||||
)
|
||||
{
|
||||
var viewModel = services.GetRequiredService<ExportSetupViewModel>();
|
||||
|
||||
viewModel.Guild = guild;
|
||||
viewModel.Channels = channels;
|
||||
viewModel.ForumChannelIds = forumChannelIds;
|
||||
|
||||
return viewModel;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ public partial class SettingsService()
|
|||
public partial Language Language { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsAutoUpdateEnabled { get; set; } = true;
|
||||
public partial bool IsAutoUpdateEnabled { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsTokenPersisted { get; set; } = true;
|
||||
public partial bool IsTokenPersisted { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial RateLimitPreference RateLimitPreference { get; set; } =
|
||||
|
|
@ -49,7 +49,7 @@ public partial class SettingsService()
|
|||
public partial string? LastToken { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ExportFormat LastExportFormat { get; set; } = ExportFormat.HtmlDark;
|
||||
public partial ExportFormat LastExportFormat { get; set; } = ExportFormat.Json;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? LastPartitionLimitValue { get; set; }
|
||||
|
|
@ -64,14 +64,41 @@ public partial class SettingsService()
|
|||
public partial bool LastShouldFormatMarkdown { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LastShouldDownloadAssets { get; set; }
|
||||
public partial bool LastShouldDownloadAssets { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LastShouldReuseAssets { get; set; }
|
||||
public partial bool LastShouldReuseAssets { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? LastAssetsDirPath { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LastForumShouldDownloadAssets { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LastForumShouldReuseAssets { get; set; } = true;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? LastForumAssetsDirPath { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LastForumShouldDownloadAvatars { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumCommonAssetMode LastForumCommonAssetMode { get; set; } =
|
||||
ForumCommonAssetMode.SharedFolder;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumAttachmentFolderMode LastForumAttachmentFolderMode { get; set; } =
|
||||
ForumAttachmentFolderMode.ByMediaType;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumAttachmentNamingMode LastForumAttachmentNamingMode { get; set; } =
|
||||
ForumAttachmentNamingMode.AttachmentIdAndOriginal;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int LastForumParallelLimit { get; set; } = 4;
|
||||
|
||||
public override void Save()
|
||||
{
|
||||
// Clear the token if it's not supposed to be persisted
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using DiscordChatExporter.Core.Discord;
|
||||
|
|
@ -91,6 +92,25 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
|
||||
public ObservableCollection<ChannelConnection> SelectedChannels { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ChannelSelectionMode))]
|
||||
public partial bool IsMultiSelectionEnabled { get; set; }
|
||||
|
||||
public SelectionMode ChannelSelectionMode =>
|
||||
IsMultiSelectionEnabled
|
||||
? SelectionMode.Multiple | SelectionMode.Toggle
|
||||
: SelectionMode.Single;
|
||||
|
||||
partial void OnIsMultiSelectionEnabledChanged(bool value)
|
||||
{
|
||||
if (value || SelectedChannels.Count <= 1)
|
||||
return;
|
||||
|
||||
var lastSelectedChannel = SelectedChannels[^1];
|
||||
SelectedChannels.Clear();
|
||||
SelectedChannels.Add(lastSelectedChannel);
|
||||
}
|
||||
|
||||
public override Task InitializeAsync()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_settingsService.LastToken))
|
||||
|
|
@ -222,6 +242,47 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
private bool CanExport() =>
|
||||
!IsBusy && _discord is not null && SelectedGuild is not null && SelectedChannels.Any();
|
||||
|
||||
private async Task<IReadOnlyList<Channel>> ExpandForumChannelsAsync(
|
||||
IReadOnlyList<Channel> channels,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (_discord is null)
|
||||
return channels;
|
||||
|
||||
var expandedChannels = new List<Channel>();
|
||||
var seenChannelIds = new HashSet<Snowflake>();
|
||||
|
||||
// Keep regular channels and explicitly selected threads as-is. Forum channels are
|
||||
// containers without their own message history, so they are replaced with all of
|
||||
// their accessible active and archived threads below.
|
||||
foreach (var channel in channels.Where(c => c.Kind != ChannelKind.GuildForum))
|
||||
{
|
||||
if (seenChannelIds.Add(channel.Id))
|
||||
expandedChannels.Add(channel);
|
||||
}
|
||||
|
||||
var forums = channels.Where(c => c.Kind == ChannelKind.GuildForum).ToArray();
|
||||
if (forums.Length <= 0)
|
||||
return expandedChannels;
|
||||
|
||||
await foreach (
|
||||
var thread in _discord.GetChannelThreadsAsync(
|
||||
forums,
|
||||
includeArchived: true,
|
||||
cancellationToken: cancellationToken
|
||||
)
|
||||
)
|
||||
{
|
||||
// A thread may already be selected explicitly or may transition from active to
|
||||
// archived while it is being discovered. Export every thread only once.
|
||||
if (seenChannelIds.Add(thread.Id))
|
||||
expandedChannels.Add(thread);
|
||||
}
|
||||
|
||||
return expandedChannels;
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanExport))]
|
||||
private async Task ExportAsync()
|
||||
{
|
||||
|
|
@ -232,9 +293,31 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
if (_discord is null || SelectedGuild is null || !SelectedChannels.Any())
|
||||
return;
|
||||
|
||||
var selectedChannels = SelectedChannels.Select(c => c.Channel).ToArray();
|
||||
var channelsToExport = await ExpandForumChannelsAsync(selectedChannels);
|
||||
|
||||
if (channelsToExport.Count <= 0)
|
||||
{
|
||||
_snackbarManager.Notify(
|
||||
"No accessible active or archived threads were found in the selected forum."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedForumIds = selectedChannels
|
||||
.Where(c => c.Kind == ChannelKind.GuildForum)
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
var forumChannelIds = channelsToExport
|
||||
.Where(c => c.Parent is not null && selectedForumIds.Contains(c.Parent.Id))
|
||||
.Select(c => c.Id)
|
||||
.ToHashSet();
|
||||
|
||||
var dialog = _viewModelManager.GetExportSetupViewModel(
|
||||
SelectedGuild,
|
||||
SelectedChannels.Select(c => c.Channel).ToArray()
|
||||
channelsToExport,
|
||||
forumChannelIds
|
||||
);
|
||||
|
||||
if (await _dialogManager.ShowDialogAsync(dialog) != true)
|
||||
|
|
@ -252,7 +335,12 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
channelProgressPairs,
|
||||
new ParallelOptions
|
||||
{
|
||||
MaxDegreeOfParallelism = Math.Max(1, _settingsService.ParallelLimit),
|
||||
MaxDegreeOfParallelism = Math.Max(
|
||||
1,
|
||||
dialog.HasForumChannels
|
||||
? dialog.SelectedForumParallelLimit
|
||||
: _settingsService.ParallelLimit
|
||||
),
|
||||
},
|
||||
async (pair, cancellationToken) =>
|
||||
{
|
||||
|
|
@ -261,22 +349,33 @@ public partial class DashboardViewModel : ViewModelBase
|
|||
|
||||
try
|
||||
{
|
||||
var isForumChannel = dialog.IsForumChannel(channel);
|
||||
|
||||
var request = new ExportRequest(
|
||||
dialog.Guild!,
|
||||
channel,
|
||||
dialog.OutputPath!,
|
||||
dialog.AssetsDirPath,
|
||||
dialog.SelectedFormat,
|
||||
isForumChannel ? dialog.ForumAssetsDirPath : dialog.AssetsDirPath,
|
||||
isForumChannel ? ExportFormat.Json : dialog.SelectedFormat,
|
||||
dialog.After?.Pipe(Snowflake.FromDate),
|
||||
dialog.Before?.Pipe(Snowflake.FromDate),
|
||||
dialog.PartitionLimit,
|
||||
dialog.MessageFilter,
|
||||
dialog.IsReverseMessageOrder,
|
||||
dialog.ShouldFormatMarkdown,
|
||||
dialog.ShouldDownloadAssets,
|
||||
dialog.ShouldReuseAssets,
|
||||
isForumChannel
|
||||
? dialog.ForumShouldDownloadAssets
|
||||
: dialog.ShouldDownloadAssets,
|
||||
isForumChannel
|
||||
? dialog.ForumShouldReuseAssets
|
||||
: dialog.ShouldReuseAssets,
|
||||
_settingsService.Locale,
|
||||
_settingsService.IsUtcNormalizationEnabled
|
||||
_settingsService.IsUtcNormalizationEnabled,
|
||||
isForumChannel,
|
||||
dialog.ForumShouldDownloadAvatars,
|
||||
dialog.SelectedForumCommonAssetMode,
|
||||
dialog.SelectedForumAttachmentFolderMode,
|
||||
dialog.SelectedForumAttachmentNamingMode
|
||||
);
|
||||
|
||||
await exporter.ExportChannelAsync(request, progress, cancellationToken);
|
||||
|
|
|
|||
|
|
@ -30,8 +30,20 @@ public partial class ExportSetupViewModel(
|
|||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsSingleChannel))]
|
||||
[NotifyPropertyChangedFor(nameof(HasRegularChannels))]
|
||||
[NotifyPropertyChangedFor(nameof(HasMixedChannelTypes))]
|
||||
[NotifyPropertyChangedFor(nameof(HasOnlyForumChannels))]
|
||||
[NotifyPropertyChangedFor(nameof(HasOnlyRegularChannels))]
|
||||
public partial IReadOnlyList<Channel>? Channels { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(HasForumChannels))]
|
||||
[NotifyPropertyChangedFor(nameof(HasRegularChannels))]
|
||||
[NotifyPropertyChangedFor(nameof(HasMixedChannelTypes))]
|
||||
[NotifyPropertyChangedFor(nameof(HasOnlyForumChannels))]
|
||||
[NotifyPropertyChangedFor(nameof(HasOnlyRegularChannels))]
|
||||
public partial IReadOnlySet<Snowflake>? ForumChannelIds { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? OutputPath { get; set; }
|
||||
|
||||
|
|
@ -77,13 +89,61 @@ public partial class ExportSetupViewModel(
|
|||
[ObservableProperty]
|
||||
public partial string? AssetsDirPath { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ForumShouldDownloadAssets { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ForumShouldReuseAssets { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string? ForumAssetsDirPath { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool ForumShouldDownloadAvatars { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumCommonAssetMode SelectedForumCommonAssetMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumAttachmentFolderMode SelectedForumAttachmentFolderMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial ForumAttachmentNamingMode SelectedForumAttachmentNamingMode { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedForumParallelLimit { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool IsAdvancedSectionDisplayed { get; set; }
|
||||
|
||||
public bool IsSingleChannel => Channels?.Count == 1;
|
||||
|
||||
public bool HasForumChannels => ForumChannelIds?.Count > 0;
|
||||
|
||||
public bool HasRegularChannels =>
|
||||
Channels?.Any(c => ForumChannelIds?.Contains(c.Id) != true) == true;
|
||||
|
||||
public bool HasMixedChannelTypes => HasForumChannels && HasRegularChannels;
|
||||
|
||||
public bool HasOnlyForumChannels => HasForumChannels && !HasRegularChannels;
|
||||
|
||||
public bool HasOnlyRegularChannels => HasRegularChannels && !HasForumChannels;
|
||||
|
||||
public bool IsForumChannel(Channel channel) => ForumChannelIds?.Contains(channel.Id) == true;
|
||||
|
||||
public IReadOnlyList<ExportFormat> AvailableFormats { get; } = Enum.GetValues<ExportFormat>();
|
||||
|
||||
public IReadOnlyList<ForumCommonAssetMode> AvailableForumCommonAssetModes { get; } =
|
||||
Enum.GetValues<ForumCommonAssetMode>();
|
||||
|
||||
public IReadOnlyList<ForumAttachmentFolderMode> AvailableForumAttachmentFolderModes { get; } =
|
||||
Enum.GetValues<ForumAttachmentFolderMode>();
|
||||
|
||||
public IReadOnlyList<ForumAttachmentNamingMode> AvailableForumAttachmentNamingModes { get; } =
|
||||
Enum.GetValues<ForumAttachmentNamingMode>();
|
||||
|
||||
public IReadOnlyList<int> AvailableForumParallelLimits { get; } = [1, 2, 4, 8];
|
||||
|
||||
public bool IsAfterDateSet => AfterDate is not null;
|
||||
|
||||
public DateTimeOffset? After => AfterDate?.Add(AfterTime ?? TimeSpan.Zero);
|
||||
|
|
@ -113,6 +173,14 @@ public partial class ExportSetupViewModel(
|
|||
ShouldDownloadAssets = settingsService.LastShouldDownloadAssets;
|
||||
ShouldReuseAssets = settingsService.LastShouldReuseAssets;
|
||||
AssetsDirPath = settingsService.LastAssetsDirPath;
|
||||
ForumShouldDownloadAssets = settingsService.LastForumShouldDownloadAssets;
|
||||
ForumShouldReuseAssets = settingsService.LastForumShouldReuseAssets;
|
||||
ForumAssetsDirPath = settingsService.LastForumAssetsDirPath;
|
||||
ForumShouldDownloadAvatars = settingsService.LastForumShouldDownloadAvatars;
|
||||
SelectedForumCommonAssetMode = settingsService.LastForumCommonAssetMode;
|
||||
SelectedForumAttachmentFolderMode = settingsService.LastForumAttachmentFolderMode;
|
||||
SelectedForumAttachmentNamingMode = settingsService.LastForumAttachmentNamingMode;
|
||||
SelectedForumParallelLimit = settingsService.LastForumParallelLimit;
|
||||
|
||||
// Show the "advanced options" section by default if any
|
||||
// of the advanced options are set to non-default values.
|
||||
|
|
@ -173,6 +241,14 @@ public partial class ExportSetupViewModel(
|
|||
AssetsDirPath = path;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ShowForumAssetsDirPathPromptAsync()
|
||||
{
|
||||
var path = await dialogManager.PromptDirectoryPathAsync();
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
ForumAssetsDirPath = path;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmAsync()
|
||||
{
|
||||
|
|
@ -195,6 +271,14 @@ public partial class ExportSetupViewModel(
|
|||
settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
|
||||
settingsService.LastShouldReuseAssets = ShouldReuseAssets;
|
||||
settingsService.LastAssetsDirPath = AssetsDirPath;
|
||||
settingsService.LastForumShouldDownloadAssets = ForumShouldDownloadAssets;
|
||||
settingsService.LastForumShouldReuseAssets = ForumShouldReuseAssets;
|
||||
settingsService.LastForumAssetsDirPath = ForumAssetsDirPath;
|
||||
settingsService.LastForumShouldDownloadAvatars = ForumShouldDownloadAvatars;
|
||||
settingsService.LastForumCommonAssetMode = SelectedForumCommonAssetMode;
|
||||
settingsService.LastForumAttachmentFolderMode = SelectedForumAttachmentFolderMode;
|
||||
settingsService.LastForumAttachmentNamingMode = SelectedForumAttachmentNamingMode;
|
||||
settingsService.LastForumParallelLimit = SelectedForumParallelLimit;
|
||||
|
||||
Close(true);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@
|
|||
ItemsSource="{Binding AvailableChannels}"
|
||||
SelectedItems="{Binding SelectedChannels}"
|
||||
SelectionChanged="AvailableChannelsTreeView_OnSelectionChanged"
|
||||
SelectionMode="Multiple"
|
||||
SelectionMode="{Binding ChannelSelectionMode}"
|
||||
TextSearch.Text="Name"
|
||||
>
|
||||
<TreeView.Styles>
|
||||
|
|
@ -302,22 +302,39 @@
|
|||
</ScrollViewer>
|
||||
</Panel>
|
||||
|
||||
<!-- Export button -->
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
<!-- Selection mode and export buttons -->
|
||||
<StackPanel
|
||||
Margin="32,24"
|
||||
Padding="0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Bottom"
|
||||
Background="{DynamicResource MaterialSecondaryMidBrush}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
|
||||
IsVisible="{Binding $self.IsEffectivelyEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}"
|
||||
Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="32" Height="32" Kind="Download" />
|
||||
</Button>
|
||||
<ToggleButton
|
||||
Width="56"
|
||||
Height="56"
|
||||
Padding="0"
|
||||
IsChecked="{Binding IsMultiSelectionEnabled}"
|
||||
Theme="{DynamicResource MaterialIconButton}"
|
||||
ToolTip.Tip="Enable multi-select mode"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="28" Height="28" Kind="CheckboxMultipleMarked" />
|
||||
</ToggleButton>
|
||||
|
||||
<Button
|
||||
Width="56"
|
||||
Height="56"
|
||||
Padding="0"
|
||||
Background="{DynamicResource MaterialSecondaryMidBrush}"
|
||||
Command="{Binding ExportCommand}"
|
||||
Foreground="{DynamicResource MaterialSecondaryMidForegroundBrush}"
|
||||
Theme="{DynamicResource MaterialIconButton}"
|
||||
ToolTip.Tip="Export selected channels"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="32" Height="32" Kind="Download" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -9,9 +9,150 @@
|
|||
xmlns:materialIcons="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:utils="clr-namespace:DiscordChatExporter.Gui.Utils"
|
||||
x:Name="UserControl"
|
||||
Width="380"
|
||||
Width="520"
|
||||
x:DataType="dialogs:ExportSetupViewModel"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<DataTemplate x:Key="ForumAssetSettingsTemplate" x:DataType="dialogs:ExportSetupViewModel">
|
||||
<StackPanel Margin="8" Orientation="Vertical" Spacing="14">
|
||||
<TextBlock FontWeight="SemiBold" Text="Forum export (JSON)" />
|
||||
<TextBlock
|
||||
Opacity="0.75"
|
||||
Text="These options apply only to posts expanded from selected forum channels."
|
||||
TextWrapping="Wrap"
|
||||
/>
|
||||
|
||||
<DockPanel LastChildFill="False">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Download post files and resources" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ForumShouldDownloadAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel IsEnabled="{Binding ForumShouldDownloadAssets}" LastChildFill="False">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Reuse files already downloaded" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ForumShouldReuseAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel IsEnabled="{Binding ForumShouldDownloadAssets}" LastChildFill="False">
|
||||
<TextBlock DockPanel.Dock="Left" Text="Download user avatars" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ForumShouldDownloadAvatars}" />
|
||||
</DockPanel>
|
||||
|
||||
<ComboBox
|
||||
IsEnabled="{Binding ForumShouldDownloadAssets}"
|
||||
ItemsSource="{Binding AvailableForumCommonAssetModes}"
|
||||
materialAssists:ComboBoxAssist.Label="Common resources (emoji, icons, previews)"
|
||||
SelectedItem="{Binding SelectedForumCommonAssetMode}"
|
||||
Theme="{DynamicResource MaterialFilledComboBox}"
|
||||
>
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:ForumAssetOptionToStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ComboBox
|
||||
IsEnabled="{Binding ForumShouldDownloadAssets}"
|
||||
ItemsSource="{Binding AvailableForumAttachmentFolderModes}"
|
||||
materialAssists:ComboBoxAssist.Label="Post attachment folders"
|
||||
SelectedItem="{Binding SelectedForumAttachmentFolderMode}"
|
||||
Theme="{DynamicResource MaterialFilledComboBox}"
|
||||
>
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:ForumAssetOptionToStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ComboBox
|
||||
IsEnabled="{Binding ForumShouldDownloadAssets}"
|
||||
ItemsSource="{Binding AvailableForumAttachmentNamingModes}"
|
||||
materialAssists:ComboBoxAssist.Label="Post attachment names"
|
||||
SelectedItem="{Binding SelectedForumAttachmentNamingMode}"
|
||||
Theme="{DynamicResource MaterialFilledComboBox}"
|
||||
>
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={x:Static converters:ForumAssetOptionToStringConverter.Instance}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ComboBox
|
||||
ItemsSource="{Binding AvailableForumParallelLimits}"
|
||||
materialAssists:ComboBoxAssist.Label="Parallel forum export workers"
|
||||
SelectedItem="{Binding SelectedForumParallelLimit}"
|
||||
Theme="{DynamicResource MaterialFilledComboBox}"
|
||||
ToolTip.Tip="4 is the recommended balance between speed and Discord rate limits."
|
||||
/>
|
||||
|
||||
<TextBox
|
||||
IsEnabled="{Binding ForumShouldDownloadAssets}"
|
||||
materialAssists:TextFieldAssist.Label="Forum assets root (optional)"
|
||||
Text="{Binding ForumAssetsDirPath}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
>
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Margin="8,8,8,6"
|
||||
Padding="8"
|
||||
Command="{Binding ShowForumAssetsDirPathPromptCommand}"
|
||||
Theme="{DynamicResource MaterialFlatButton}"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="20" Height="20" Kind="FolderOpen" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<DataTemplate x:Key="RegularAssetSettingsTemplate" x:DataType="dialogs:ExportSetupViewModel">
|
||||
<StackPanel Margin="8" Orientation="Vertical" Spacing="14">
|
||||
<TextBlock FontWeight="SemiBold" Text="Regular channel assets" />
|
||||
|
||||
<DockPanel
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DownloadAssetsTooltip}"
|
||||
>
|
||||
<TextBlock
|
||||
DockPanel.Dock="Left"
|
||||
Text="{Binding LocalizationManager.DownloadAssetsLabel}"
|
||||
/>
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldDownloadAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<DockPanel
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="{Binding LocalizationManager.ReuseAssetsTooltip}"
|
||||
>
|
||||
<TextBlock DockPanel.Dock="Left" Text="{Binding LocalizationManager.ReuseAssetsLabel}" />
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldReuseAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<TextBox
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.AssetsDirPathLabel}"
|
||||
Text="{Binding AssetsDirPath}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.AssetsDirPathTooltip}"
|
||||
>
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Margin="8,8,8,6"
|
||||
Padding="8"
|
||||
Command="{Binding ShowAssetsDirPathPromptCommand}"
|
||||
Theme="{DynamicResource MaterialFlatButton}"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="20" Height="20" Kind="FolderOpen" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<!-- Guild/channel info -->
|
||||
<Grid Grid.Row="0" Margin="16" ColumnDefinitions="Auto,*">
|
||||
|
|
@ -89,6 +230,7 @@
|
|||
|
||||
<!-- Format -->
|
||||
<ComboBox
|
||||
IsVisible="{Binding HasRegularChannels}"
|
||||
Margin="16,8"
|
||||
materialAssists:ComboBoxAssist.Label="{Binding LocalizationManager.FormatLabel}"
|
||||
ItemsSource="{Binding AvailableFormats}"
|
||||
|
|
@ -103,6 +245,28 @@
|
|||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<ContentControl
|
||||
Margin="16,8"
|
||||
Content="{Binding}"
|
||||
ContentTemplate="{StaticResource ForumAssetSettingsTemplate}"
|
||||
IsVisible="{Binding HasOnlyForumChannels}"
|
||||
/>
|
||||
|
||||
<TabControl Margin="16,8" IsVisible="{Binding HasMixedChannelTypes}">
|
||||
<TabItem Header="FORUM POSTS">
|
||||
<ContentControl
|
||||
Content="{Binding}"
|
||||
ContentTemplate="{StaticResource ForumAssetSettingsTemplate}"
|
||||
/>
|
||||
</TabItem>
|
||||
<TabItem Header="REGULAR CHANNELS">
|
||||
<ContentControl
|
||||
Content="{Binding}"
|
||||
ContentTemplate="{StaticResource RegularAssetSettingsTemplate}"
|
||||
/>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
<!-- Advanced section -->
|
||||
<StackPanel
|
||||
Margin="16,8"
|
||||
|
|
@ -220,51 +384,11 @@
|
|||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldFormatMarkdown}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Download assets -->
|
||||
<DockPanel
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="{Binding LocalizationManager.DownloadAssetsTooltip}"
|
||||
>
|
||||
<TextBlock
|
||||
DockPanel.Dock="Left"
|
||||
Text="{Binding LocalizationManager.DownloadAssetsLabel}"
|
||||
/>
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldDownloadAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Reuse assets -->
|
||||
<DockPanel
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
LastChildFill="False"
|
||||
ToolTip.Tip="{Binding LocalizationManager.ReuseAssetsTooltip}"
|
||||
>
|
||||
<TextBlock
|
||||
DockPanel.Dock="Left"
|
||||
Text="{Binding LocalizationManager.ReuseAssetsLabel}"
|
||||
/>
|
||||
<ToggleSwitch DockPanel.Dock="Right" IsChecked="{Binding ShouldReuseAssets}" />
|
||||
</DockPanel>
|
||||
|
||||
<!-- Assets path -->
|
||||
<TextBox
|
||||
materialAssists:TextFieldAssist.Label="{Binding LocalizationManager.AssetsDirPathLabel}"
|
||||
IsEnabled="{Binding ShouldDownloadAssets}"
|
||||
Text="{Binding AssetsDirPath}"
|
||||
Theme="{DynamicResource FilledTextBox}"
|
||||
ToolTip.Tip="{Binding LocalizationManager.AssetsDirPathTooltip}"
|
||||
>
|
||||
<TextBox.InnerRightContent>
|
||||
<Button
|
||||
Margin="8,8,8,6"
|
||||
Padding="8"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ShowAssetsDirPathPromptCommand}"
|
||||
Theme="{DynamicResource MaterialFlatButton}"
|
||||
>
|
||||
<materialIcons:MaterialIcon Width="20" Height="20" Kind="FolderOpen" />
|
||||
</Button>
|
||||
</TextBox.InnerRightContent>
|
||||
</TextBox>
|
||||
<ContentControl
|
||||
Content="{Binding}"
|
||||
ContentTemplate="{StaticResource RegularAssetSettingsTemplate}"
|
||||
IsVisible="{Binding HasOnlyRegularChannels}"
|
||||
/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
|
|
|||
Loading…
Reference in a new issue