QuestPDF als PDF-Bibliothek eingeführt (11.3), bewusst nur in LehrerApp.Desktop referenziert - zieht SkiaSharp-Native-Binaries mit, die im Server-Docker-Image nichts verloren haben. Neuer PdfExportService mit gemeinsamem Kopf-/Fußzeilen-Layout (Titel, Stand-Datum, Seitenzahlen) und schlanken Druck-DTOs statt ViewModels, damit die Erzeugung ohne Avalonia-Bezug testbar bleibt. Speicherdialog läuft über den bestehenden ExportService (neues PDF-Format). Fünf Druckvorlagen (11.4) als "Als PDF"-Button direkt in der jeweiligen Ansicht: - Sitzplan: Raster mit Tafel-Banner, Tischabständen als echte Lücken, ausgeblendeten Plätzen als leere Rasterposition. - Notenliste: druckt exakt die aktuell angezeigte Matrix (Zeitraum/Darstellung/Sortierung). - Klausur-Notenspiegel: Kennzahlen, Notenverteilung und Aufgabenanalyse mit Balkendiagrammen, auffällig schwache Aufgaben rot markiert. - Kompetenzbericht: Analyse-Tabelle für den ausgewählten Schüler (erledigt zugleich 8.3.3). - Schülerdokumentation: alle Einträge chronologisch, vertrauliche/Entwurfs-Einträge markiert. Layout an gerenderten Muster-PDFs visuell geprüft, nicht nur per Unit-Test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
81 lines
3.3 KiB
C#
81 lines
3.3 KiB
C#
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 static ExportFormat Pdf { get; } = new("PDF-Dateien", ".pdf", "application/pdf");
|
|
}
|
|
|
|
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);
|
|
|
|
public static ExportFile Pdf(string dialogTitle, string suggestedFileName, byte[] content) =>
|
|
new(dialogTitle, EnsureExtension(SanitizeFileName(suggestedFileName), ExportFormat.Pdf.Extension),
|
|
ExportFormat.Pdf, 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;
|
|
}
|
|
}
|