Add shared export infrastructure

This commit is contained in:
2026-08-18 21:21:56 +02:00
parent 41bf2c0756
commit 26461bd2f2
13 changed files with 336 additions and 63 deletions
@@ -0,0 +1,75 @@
using Avalonia.Platform.Storage;
using System.Text;
namespace LehrerApp.Desktop.Services;
public sealed record ExportFormat(string Label, string Extension, string MimeType)
{
public static ExportFormat Csv { get; } = new("CSV-Dateien", ".csv", "text/csv");
public static ExportFormat Json { get; } = new("JSON-Dateien", ".json", "application/json");
}
public sealed record ExportFile(
string DialogTitle,
string SuggestedFileName,
ExportFormat Format,
ReadOnlyMemory<byte> Content)
{
public static ExportFile Csv(string dialogTitle, string suggestedFileName, string content) =>
FromText(dialogTitle, suggestedFileName, ExportFormat.Csv, content, includeUtf8Bom: true);
public static ExportFile Json(string dialogTitle, string suggestedFileName, string content) =>
FromText(dialogTitle, suggestedFileName, ExportFormat.Json, content);
private static ExportFile FromText(string dialogTitle, string suggestedFileName,
ExportFormat format, string content, bool includeUtf8Bom = false)
{
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: includeUtf8Bom);
var preamble = encoding.GetPreamble();
var text = encoding.GetBytes(content);
var bytes = new byte[preamble.Length + text.Length];
preamble.CopyTo(bytes, 0);
text.CopyTo(bytes, preamble.Length);
return new ExportFile(dialogTitle,
EnsureExtension(SanitizeFileName(suggestedFileName), format.Extension), format, bytes);
}
private static string EnsureExtension(string fileName, string extension) =>
fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ? fileName : fileName + extension;
private static string SanitizeFileName(string fileName)
{
var invalidCharacters = Path.GetInvalidFileNameChars();
return string.Concat(fileName.Select(character =>
invalidCharacters.Contains(character) ? '_' : character));
}
}
/// <summary>Zentrale Dateiauswahl und Stream-Ausgabe für alle Exportformate.</summary>
public sealed class ExportService
{
public async Task<bool> SaveAsync(IStorageProvider storageProvider, ExportFile export)
{
var file = await storageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = export.DialogTitle,
SuggestedFileName = export.SuggestedFileName,
DefaultExtension = export.Format.Extension.TrimStart('.'),
FileTypeChoices =
[
new FilePickerFileType(export.Format.Label)
{
Patterns = [$"*{export.Format.Extension}"],
MimeTypes = [export.Format.MimeType],
},
],
});
if (file is null) return false;
await using var stream = await file.OpenWriteAsync();
if (stream.CanSeek) stream.SetLength(0);
await stream.WriteAsync(export.Content);
return true;
}
}