Add shared export infrastructure
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
using System.Text;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Erzeugt semikolongetrennte CSV-Daten für die deutsche Excel-Umgebung und maskiert Felder
|
||||
/// nach RFC-4180-Regeln. Das Trennzeichen bleibt bewusst konfigurierbar für weitere Exportformate.
|
||||
/// </summary>
|
||||
public sealed class CsvBuilder(char separator = ';')
|
||||
{
|
||||
private readonly StringBuilder _content = new();
|
||||
|
||||
public CsvBuilder AddRow(params object?[] values)
|
||||
{
|
||||
for (var index = 0; index < values.Length; index++)
|
||||
{
|
||||
if (index > 0) _content.Append(separator);
|
||||
AppendEscaped(values[index]?.ToString() ?? "");
|
||||
}
|
||||
_content.AppendLine();
|
||||
return this;
|
||||
}
|
||||
|
||||
public CsvBuilder AddBlankRow()
|
||||
{
|
||||
_content.AppendLine();
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString() => _content.ToString();
|
||||
|
||||
private void AppendEscaped(string value)
|
||||
{
|
||||
if (!value.Contains(separator) && !value.Contains('"') &&
|
||||
!value.Contains('\r') && !value.Contains('\n'))
|
||||
{
|
||||
_content.Append(value);
|
||||
return;
|
||||
}
|
||||
|
||||
_content.Append('"');
|
||||
_content.Append(value.Replace("\"", "\"\"", StringComparison.Ordinal));
|
||||
_content.Append('"');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user