Add support for selectable assets directory in GUI

pull/1003/head
Tyrrrz 2 years ago
parent 95115f3e99
commit d1647e8286

@ -98,20 +98,20 @@ public abstract class ExportCommandBase : TokenCommandBase
)]
public bool ShouldReuseAssets { get; init; }
private readonly string? _assetsPath;
private readonly string? _assetsDirPath;
[CommandOption(
"media-dir",
Description = "Download assets to this directory."
Description = "Download assets to this directory. If not specified, the asset directory path will be derived from the output path."
)]
public string? AssetsPath
public string? AssetsDirPath
{
get => _assetsPath;
get => _assetsDirPath;
// Handle ~/ in paths on Unix systems
// https://github.com/Tyrrrz/DiscordChatExporter/pull/903
init => _assetsPath = 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."
@ -139,7 +139,7 @@ public abstract class ExportCommandBase : TokenCommandBase
}
// Assets directory should only be specified when the download assets option is set
if (!string.IsNullOrWhiteSpace(AssetsPath) && !ShouldDownloadAssets)
if (!string.IsNullOrWhiteSpace(AssetsDirPath) && !ShouldDownloadAssets)
{
throw new CommandException(
"Option --media-dir cannot be used without --media."
@ -194,7 +194,7 @@ public abstract class ExportCommandBase : TokenCommandBase
guild,
channel,
OutputPath,
AssetsPath,
AssetsDirPath,
ExportFormat,
After,
Before,

@ -29,7 +29,7 @@ internal class ExportContext
Request = request;
_assetDownloader = new ExportAssetDownloader(
request.OutputAssetsDirPath,
request.AssetsDirPath,
request.ShouldReuseAssets
);
}
@ -92,34 +92,25 @@ internal class ExportContext
try
{
var absoluteFilePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
// We want relative path so that the output files can be copied around without breaking.
// Base directory path may be null if the file is stored at the root or relative to working directory.
var relativeFilePath = !string.IsNullOrWhiteSpace(Request.OutputBaseDirPath)
? Path.GetRelativePath(Request.OutputBaseDirPath, absoluteFilePath)
: absoluteFilePath;
// If the assets path is outside of the export directory, fall back to absolute path
var filePath = relativeFilePath.StartsWith("..")
? absoluteFilePath
: relativeFilePath;
// HACK: for HTML, we need to format the URL properly
var filePath = await _assetDownloader.DownloadAsync(url, cancellationToken);
var relativeFilePath = Path.GetRelativePath(Request.OutputDirPath, filePath);
// Prefer relative paths so that the output files can be copied around without breaking references.
// If the assets path is outside of the export directory, use the absolute path instead.
var optimalFilePath =
relativeFilePath.StartsWith(".." + Path.DirectorySeparatorChar, StringComparison.Ordinal) ||
relativeFilePath.StartsWith(".." + Path.AltDirectorySeparatorChar, StringComparison.Ordinal)
? filePath
: relativeFilePath;
// For HTML, the path needs to be properly formatted
if (Request.Format is ExportFormat.HtmlDark or ExportFormat.HtmlLight)
{
// Need to escape each path segment while keeping the directory separators intact
return string.Join(
Path.AltDirectorySeparatorChar,
filePath
.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
.Select(Uri.EscapeDataString)
.Select(x => x.Replace("%3A", ":"))
);
// Create a 'file:///' URI and then strip the 'file:///' prefix to allow for relative paths
return new Uri(new Uri("file:///"), optimalFilePath).ToString()[8..];
}
return filePath;
return optimalFilePath;
}
// Try to catch only exceptions related to failed HTTP requests
// https://github.com/Tyrrrz/DiscordChatExporter/issues/332

@ -10,48 +10,87 @@ using DiscordChatExporter.Core.Utils;
namespace DiscordChatExporter.Core.Exporting;
public partial record ExportRequest(
Guild Guild,
Channel Channel,
string OutputPath,
string? AssetsPath,
ExportFormat Format,
Snowflake? After,
Snowflake? Before,
PartitionLimit PartitionLimit,
MessageFilter MessageFilter,
bool ShouldFormatMarkdown,
bool ShouldDownloadAssets,
bool ShouldReuseAssets,
string DateFormat)
public partial class ExportRequest
{
private string? _outputBaseFilePath;
public string OutputBaseFilePath => _outputBaseFilePath ??= GetOutputBaseFilePath(
Guild,
Channel,
OutputPath,
Format,
After,
Before
);
public string OutputBaseDirPath => Path.GetDirectoryName(OutputBaseFilePath) ?? OutputPath;
private string? _outputAssetsDirPath;
public string OutputAssetsDirPath => _outputAssetsDirPath ??= (
AssetsPath is not null
? EvaluateTemplateTokens(
AssetsPath,
Guild,
Channel,
After,
Before
)
: $"{OutputBaseFilePath}_Files{Path.DirectorySeparatorChar}"
public Guild Guild { get; }
public Channel Channel { get; }
public string OutputFilePath { get; }
public string OutputDirPath { get; }
public string AssetsDirPath { get; }
public ExportFormat Format { get; }
public Snowflake? After { get; }
public Snowflake? Before { get; }
public PartitionLimit PartitionLimit { get; }
public MessageFilter MessageFilter { get; }
public bool ShouldFormatMarkdown { get; }
public bool ShouldDownloadAssets { get; }
public bool ShouldReuseAssets { get; }
public string DateFormat { get; }
public ExportRequest(
Guild guild,
Channel channel,
string outputPath,
string? assetsDirPath,
ExportFormat format,
Snowflake? after,
Snowflake? before,
PartitionLimit partitionLimit,
MessageFilter messageFilter,
bool shouldFormatMarkdown,
bool shouldDownloadAssets,
bool shouldReuseAssets,
string dateFormat)
{
Guild = guild;
Channel = channel;
Format = format;
After = after;
Before = before;
PartitionLimit = partitionLimit;
MessageFilter = messageFilter;
ShouldFormatMarkdown = shouldFormatMarkdown;
ShouldDownloadAssets = shouldDownloadAssets;
ShouldReuseAssets = shouldReuseAssets;
DateFormat = dateFormat;
OutputFilePath = GetOutputBaseFilePath(
Guild,
Channel,
outputPath,
Format,
After,
Before
);
OutputDirPath = Path.GetDirectoryName(OutputFilePath)!;
AssetsDirPath = !string.IsNullOrWhiteSpace(assetsDirPath)
? FormatPath(
assetsDirPath,
Guild,
Channel,
After,
Before
)
: $"{OutputFilePath}_Files{Path.DirectorySeparatorChar}";
}
}
public partial record ExportRequest
public partial class ExportRequest
{
public static string GetDefaultOutputFileName(
Guild guild,
@ -95,7 +134,7 @@ public partial record ExportRequest
return PathEx.EscapeFileName(buffer.ToString());
}
private static string EvaluateTemplateTokens(
private static string FormatPath(
string path,
Guild guild,
Channel channel,
@ -120,7 +159,8 @@ public partial record ExportRequest
"%d" => DateTimeOffset.Now.ToString("yyyy-MM-dd"),
"%%" => "%",
_ => m.Value
}));
})
);
}
private static string GetOutputBaseFilePath(
@ -131,7 +171,7 @@ public partial record ExportRequest
Snowflake? after = null,
Snowflake? before = null)
{
var actualOutputPath = EvaluateTemplateTokens(outputPath, guild, channel, after, before);
var actualOutputPath = FormatPath(outputPath, guild, channel, after, before);
// Output is a directory
if (Directory.Exists(actualOutputPath) || string.IsNullOrWhiteSpace(Path.GetExtension(actualOutputPath)))

@ -51,8 +51,8 @@ internal partial class MessageExporter : IAsyncDisposable
if (_writer is not null)
return _writer;
Directory.CreateDirectory(_context.Request.OutputBaseDirPath);
var filePath = GetPartitionFilePath(_context.Request.OutputBaseFilePath, _partitionIndex);
Directory.CreateDirectory(_context.Request.OutputDirPath);
var filePath = GetPartitionFilePath(_context.Request.OutputFilePath, _partitionIndex);
var writer = CreateMessageWriter(filePath, _context.Request.Format, _context);
await writer.WritePreambleAsync(cancellationToken);

@ -35,6 +35,8 @@ public partial class SettingsService : SettingsManager
public bool LastShouldReuseAssets { get; set; }
public string? LastAssetsDirPath { get; set; }
public SettingsService()
{
Configuration.StorageSpace = StorageSpace.Instance;

@ -186,7 +186,7 @@ public class DashboardViewModel : PropertyChangedBase
dialog.Guild!,
channel,
dialog.OutputPath!,
null,
dialog.AssetsDirPath,
dialog.SelectedFormat,
dialog.After?.Pipe(Snowflake.FromDate),
dialog.Before?.Pipe(Snowflake.FromDate),

@ -65,6 +65,8 @@ public class ExportSetupViewModel : DialogScreen
public bool ShouldReuseAssets { get; set; }
public string? AssetsDirPath { get; set; }
public bool IsAdvancedSectionDisplayed { get; set; }
public ExportSetupViewModel(DialogManager dialogManager, SettingsService settingsService)
@ -79,15 +81,18 @@ public class ExportSetupViewModel : DialogScreen
ShouldFormatMarkdown = _settingsService.LastShouldFormatMarkdown;
ShouldDownloadAssets = _settingsService.LastShouldDownloadAssets;
ShouldReuseAssets = _settingsService.LastShouldReuseAssets;
AssetsDirPath = _settingsService.LastAssetsDirPath;
// Show the "advanced options" section by default if any
// of the advanced options are set to non-default values.
IsAdvancedSectionDisplayed =
After != default ||
Before != default ||
After is not null ||
Before is not null ||
!string.IsNullOrWhiteSpace(PartitionLimitValue) ||
!string.IsNullOrWhiteSpace(MessageFilterValue) ||
ShouldDownloadAssets != default;
ShouldDownloadAssets ||
ShouldReuseAssets ||
!string.IsNullOrWhiteSpace(AssetsDirPath);
}
public void ToggleAdvancedSection() => IsAdvancedSectionDisplayed = !IsAdvancedSectionDisplayed;
@ -107,18 +112,25 @@ public class ExportSetupViewModel : DialogScreen
var extension = SelectedFormat.GetFileExtension();
var filter = $"{extension.ToUpperInvariant()} files|*.{extension}";
var outputPath = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
if (!string.IsNullOrWhiteSpace(outputPath))
OutputPath = outputPath;
var path = _dialogManager.PromptSaveFilePath(filter, defaultFileName);
if (!string.IsNullOrWhiteSpace(path))
OutputPath = path;
}
else
{
var outputPath = _dialogManager.PromptDirectoryPath();
if (!string.IsNullOrWhiteSpace(outputPath))
OutputPath = outputPath;
var path = _dialogManager.PromptDirectoryPath();
if (!string.IsNullOrWhiteSpace(path))
OutputPath = path;
}
}
public void ShowAssetsDirPathPrompt()
{
var path = _dialogManager.PromptDirectoryPath();
if (!string.IsNullOrWhiteSpace(path))
AssetsDirPath = path;
}
public void Confirm()
{
// Prompt the output path if it's not set yet
@ -138,6 +150,7 @@ public class ExportSetupViewModel : DialogScreen
_settingsService.LastShouldFormatMarkdown = ShouldFormatMarkdown;
_settingsService.LastShouldDownloadAssets = ShouldDownloadAssets;
_settingsService.LastShouldReuseAssets = ShouldReuseAssets;
_settingsService.LastAssetsDirPath = AssetsDirPath;
Close(true);
}
@ -145,8 +158,10 @@ public class ExportSetupViewModel : DialogScreen
public static class ExportSetupViewModelExtensions
{
public static ExportSetupViewModel CreateExportSetupViewModel(this IViewModelFactory factory,
Guild guild, IReadOnlyList<Channel> channels)
public static ExportSetupViewModel CreateExportSetupViewModel(
this IViewModelFactory factory,
Guild guild,
IReadOnlyList<Channel> channels)
{
var viewModel = factory.CreateExportSetupViewModel();

@ -25,8 +25,10 @@ public static class MessageBoxViewModelExtensions
{
public static MessageBoxViewModel CreateMessageBoxViewModel(
this IViewModelFactory factory,
string title, string message,
string? okButtonText, string? cancelButtonText)
string title,
string message,
string? okButtonText,
string? cancelButtonText)
{
var viewModel = factory.CreateMessageBoxViewModel();
viewModel.Title = title;
@ -42,6 +44,7 @@ public static class MessageBoxViewModelExtensions
public static MessageBoxViewModel CreateMessageBoxViewModel(
this IViewModelFactory factory,
string title, string message) =>
string title,
string message) =>
factory.CreateMessageBoxViewModel(title, message, "CLOSE", null);
}

@ -285,6 +285,27 @@
VerticalAlignment="Center"
IsChecked="{Binding ShouldReuseAssets}" />
</Grid>
<!-- Assets path -->
<Grid Margin="16,8">
<TextBox
Padding="16,16,42,16"
materialDesign:HintAssist.Hint="Assets directory path"
materialDesign:HintAssist.IsFloating="True"
Style="{DynamicResource MaterialDesignOutlinedTextBox}"
Text="{Binding AssetsDirPath}"
ToolTip="Download assets to this directory. If not specified, the asset directory path will be derived from the output path." />
<Button
Width="24"
Height="24"
Margin="0,0,12,0"
Padding="0"
HorizontalAlignment="Right"
Command="{s:Action ShowAssetsDirPathPrompt}"
Style="{DynamicResource MaterialDesignToolForegroundButton}">
<materialDesign:PackIcon Kind="FolderOpen" />
</Button>
</Grid>
</StackPanel>
</StackPanel>
</ScrollViewer>

Loading…
Cancel
Save