Files
LehrerApp/LehrerApp.TemplateDesigner/MainWindow.axaml.cs
T
admin 527c186090
CI / build-and-test (push) Canceled after 0s
Add constant rich text placeholders
2026-08-30 23:23:48 +02:00

320 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO.Compression;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using LehrerApp.Templating;
using PDFtoImage;
using System.Runtime.Versioning;
namespace LehrerApp.TemplateDesigner;
public partial class MainWindow : Window
{
private readonly DesignerViewModel _viewModel = new();
private readonly StarterTemplateLibrary _starterTemplates = new();
private string? _currentPackagePath;
public MainWindow()
{
InitializeComponent(); DataContext = _viewModel;
RefreshStarterTemplates();
UpdateDocumentTitle();
}
private void OnNew(object? sender, RoutedEventArgs e)
{
_viewModel.Reset(); _currentPackagePath = null; UpdateDocumentTitle();
}
private void OnExit(object? sender, RoutedEventArgs e) => Close();
private void OnAddPlaceholder(object? sender, RoutedEventArgs e) => _viewModel.AddPlaceholder();
private void OnAddMetadata(object? sender, RoutedEventArgs e)
{
var item = new DesignerMetadata($"custom-{_viewModel.MetadataItems.Count + 1}", "");
_viewModel.MetadataItems.Add(item); _viewModel.SelectedMetadata = item; _viewModel.CanExport = false;
}
private void OnRemoveMetadata(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedMetadata is not { } selected) return;
_viewModel.MetadataItems.Remove(selected); _viewModel.SelectedMetadata = null; _viewModel.CanExport = false;
}
private void OnRemovePlaceholder(object? sender, RoutedEventArgs e) => _viewModel.RemoveSelectedPlaceholder();
private void OnBoldSelection(object? sender, RoutedEventArgs e) => WrapPlaceholderSelection("[b]", "[/b]");
private void OnItalicSelection(object? sender, RoutedEventArgs e) => WrapPlaceholderSelection("[i]", "[/i]");
private void OnUnderlineSelection(object? sender, RoutedEventArgs e) => WrapPlaceholderSelection("[u]", "[/u]");
private void WrapPlaceholderSelection(string opening, string closing)
{
if (_viewModel.SelectedPlaceholder is not { SupportsRichText: true } placeholder
|| this.FindControl<TextBox>("PlaceholderValueTextBox") is not { } editor) return;
var source = editor.Text ?? "";
var start = Math.Min(editor.SelectionStart, editor.SelectionEnd);
var end = Math.Max(editor.SelectionStart, editor.SelectionEnd);
var updated = source[..start] + opening + source[start..end] + closing + source[end..];
placeholder.Sample = updated; editor.Text = updated;
editor.SelectionStart = start + opening.Length;
editor.SelectionEnd = end + opening.Length;
editor.Focus(); _viewModel.CanExport = false;
}
private void OnAddElement(object? sender, RoutedEventArgs e)
{ try { _viewModel.AddElement(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnMeasureOverlayMode(object? sender, RoutedEventArgs e)
{ _viewModel.OverlayMode = OverlayEditorMode.Measure; _viewModel.SelectedOverlayElement = "Messmodus aktiv"; }
private void OnEditOverlayMode(object? sender, RoutedEventArgs e)
{ _viewModel.OverlayMode = OverlayEditorMode.Edit; _viewModel.SelectedOverlayElement = "Elementmodus aktiv"; }
private void OnOverlayMeasurementCompleted(object? sender, OverlayMeasurement measurement) =>
_viewModel.ApplyMeasurement(measurement);
private void OnOverlayCursorCoordinatesChanged(object? sender, string coordinates) =>
_viewModel.OverlayCoordinates = coordinates;
private void OnOverlayElementSelected(object? sender, string element) =>
_viewModel.SelectedOverlayElement = element;
private void OnOverlayElementGeometryChanged(object? sender, OverlayElementGeometry geometry)
{
try { _viewModel.ApplyElementGeometry(geometry); OnPreview(sender, new RoutedEventArgs()); }
catch (Exception ex) { _viewModel.SetStatus($"Elementänderung fehlgeschlagen: {ex.Message}", true); }
}
private void OnInsertSelectedAsset(object? sender, RoutedEventArgs e)
{ try { _viewModel.InsertSelectedAssetAsImage(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnRemoveSelectedAsset(object? sender, RoutedEventArgs e)
{ try { _viewModel.RemoveSelectedAsset(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private async void OnImportAsset(object? sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = "Bilder ins Vorlagenpaket importieren", AllowMultiple = true, FileTypeFilter = [ImageType()] });
if (files.Count == 0) return;
try
{
foreach (var file in files)
_viewModel.ImportAsset(file.Name, await File.ReadAllBytesAsync(file.Path.LocalPath));
_viewModel.CanExport = false;
_viewModel.SetStatus(files.Count == 1
? $"Asset „{_viewModel.SelectedAsset?.Name}“ importiert."
: $"{files.Count} Assets importiert.", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Bildimport fehlgeschlagen: {ex.Message}", true); }
}
private async void OnReplaceSelectedAsset(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedAsset is null) { _viewModel.SetStatus("Bitte zuerst ein Asset auswählen.", true); return; }
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = $"„{_viewModel.SelectedAsset.Name}“ ersetzen", AllowMultiple = false, FileTypeFilter = [ImageType()] });
if (files.Count == 0) return;
try { _viewModel.ReplaceSelectedAsset(await File.ReadAllBytesAsync(files[0].Path.LocalPath)); }
catch (Exception ex) { _viewModel.SetStatus($"Ersetzen fehlgeschlagen: {ex.Message}", true); }
}
private void OnPreview(object? sender, RoutedEventArgs e)
{
try
{
var bytes = new QuestTemplateRenderer().RenderFirstPageToPng(_viewModel.BuildLoaded(), _viewModel.BuildDataProvider());
using var stream = new MemoryStream(bytes);
_viewModel.PreviewImage?.Dispose(); _viewModel.PreviewImage = new Bitmap(stream);
_viewModel.CanExport = true; _viewModel.SetStatus("Validierung erfolgreich. Vorschau ist aktuell.", false);
}
catch (Exception ex)
{ _viewModel.CanExport = false; _viewModel.SetStatus(ex.Message, true); }
}
private void OnPreviewStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
try { var (template, _) = _starterTemplates.Load(selected); _viewModel.PreviewTemplate(template); }
catch (Exception ex) { _viewModel.SetStatus($"Vorschau fehlgeschlagen: {ex.Message}", true); }
}
private void OnEditStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
try
{
var (template, layout) = _starterTemplates.Load(selected);
_viewModel.Load(template, layout); _currentPackagePath = null; UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Öffnen fehlgeschlagen: {ex.Message}", true); }
}
private void OnUseStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
try
{
var (template, layout) = _starterTemplates.Load(selected);
var projectId = _starterTemplates.CreateUniqueId(template.Manifest.Id + "-neu");
_viewModel.LoadAsNewProject(template, layout, projectId);
_currentPackagePath = null; UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Klonen fehlgeschlagen: {ex.Message}", true); }
}
private void OnSaveStarterTemplate(object? sender, RoutedEventArgs e)
{
try
{
_viewModel.BuildLoaded();
var saved = _starterTemplates.Save(_viewModel.BuildManifest(), _viewModel.LayoutSource, _viewModel.Assets);
RefreshStarterTemplates(saved.Id);
_viewModel.SetStatus($"Ausgangsvorlage „{saved.Name}“ lokal gespeichert/aktualisiert.", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Speichern fehlgeschlagen: {ex.Message}", true); }
}
private void OnDuplicateStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
try
{
var duplicate = _starterTemplates.Duplicate(selected);
RefreshStarterTemplates(duplicate.Id);
_viewModel.SetStatus($"Ausgangsvorlage als „{duplicate.Name}“ dupliziert.", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Duplizieren fehlgeschlagen: {ex.Message}", true); }
}
private async void OnImportStarterTemplate(object? sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = "Ausgangsvorlage importieren", AllowMultiple = false, FileTypeFilter = [PackageType()] });
if (files.Count == 0) return;
try
{
var imported = _starterTemplates.Import(files[0].Path.LocalPath);
RefreshStarterTemplates(imported.Id);
_viewModel.SetStatus($"Ausgangsvorlage „{imported.Name}“ importiert.", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Import fehlgeschlagen: {ex.Message}", true); }
}
private async void OnExportStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
var file = await StorageProvider.SaveFilePickerAsync(new()
{ Title = "Ausgangsvorlage exportieren", SuggestedFileName = selected.Id + TemplatePackage.Extension,
DefaultExtension = TemplatePackage.Extension[1..], FileTypeChoices = [PackageType()] });
if (file is null) return;
try
{
_starterTemplates.Export(selected, file.Path.LocalPath);
_viewModel.SetStatus($"Ausgangsvorlage exportiert: {file.Name}", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Export fehlgeschlagen: {ex.Message}", true); }
}
private void OnDeleteStarterTemplate(object? sender, RoutedEventArgs e)
{
if (_viewModel.SelectedStarterTemplate is not { } selected)
{ _viewModel.SetStatus("Bitte zuerst eine Ausgangsvorlage auswählen.", true); return; }
try
{
_starterTemplates.Delete(selected); RefreshStarterTemplates();
_viewModel.SetStatus($"Ausgangsvorlage „{selected.Name}“ gelöscht.", false);
}
catch (Exception ex) { _viewModel.SetStatus($"Löschen fehlgeschlagen: {ex.Message}", true); }
}
private async void OnOpenPackage(object? sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = "Vorlagenpaket öffnen", AllowMultiple = false, FileTypeFilter = [PackageType()] });
if (files.Count == 0) return;
try
{
var path = files[0].Path.LocalPath; var loaded = new TemplateLoader().LoadFromPackage(path);
using var archive = ZipFile.OpenRead(path); using var reader = new StreamReader(archive.GetEntry(loaded.Manifest.LayoutFile)!.Open());
_viewModel.Load(loaded, reader.ReadToEnd()); _currentPackagePath = Path.GetFullPath(path);
UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Öffnen fehlgeschlagen: {ex.Message}", true); }
}
private async void OnSave(object? sender, RoutedEventArgs e)
{
if (_currentPackagePath is null) { await SaveAsAsync(); return; }
SaveTo(_currentPackagePath);
}
private async void OnSaveAs(object? sender, RoutedEventArgs e) => await SaveAsAsync();
private async Task SaveAsAsync()
{
try { _viewModel.BuildLoaded(); }
catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); return; }
var file = await StorageProvider.SaveFilePickerAsync(new()
{ Title = "Vorlagenpaket speichern unter", SuggestedFileName = _viewModel.TemplateId + TemplatePackage.Extension,
DefaultExtension = TemplatePackage.Extension[1..], FileTypeChoices = [PackageType()] });
if (file is null) return;
var destination = EnsurePackageExtension(file.Path.LocalPath);
if (SaveTo(destination))
{
_currentPackagePath = Path.GetFullPath(destination);
UpdateDocumentTitle();
}
}
private bool SaveTo(string path)
{
try
{
_viewModel.BuildLoaded();
TemplatePackage.Create(path, _viewModel.BuildManifest(), _viewModel.LayoutSource, _viewModel.Assets);
_viewModel.CanExport = true;
_viewModel.SetStatus($"Gespeichert: {Path.GetFileName(path)}", false);
return true;
}
catch (Exception ex) { _viewModel.SetStatus($"Speichern fehlgeschlagen: {ex.Message}", true); return false; }
}
private async void OnImportBackground(object? sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = "Hintergrundbild importieren", AllowMultiple = false,
FileTypeFilter = [ImageType()] });
if (files.Count == 0) return;
try { _viewModel.ImportBackground(files[0].Name, await File.ReadAllBytesAsync(files[0].Path.LocalPath)); }
catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); }
}
[SupportedOSPlatform("windows")]
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("macos")]
private async void OnImportPdfBackground(object? sender, RoutedEventArgs e)
{
var files = await StorageProvider.OpenFilePickerAsync(new()
{ Title = "PDF-Briefkopf importieren", AllowMultiple = false,
FileTypeFilter = [new("PDF-Dateien") { Patterns = ["*.pdf"] }] });
if (files.Count == 0) return;
var temporary = Path.Combine(Path.GetTempPath(), $"lehrerapp-bg-{Guid.NewGuid():N}.png");
try
{
await using var pdf = File.OpenRead(files[0].Path.LocalPath);
Conversion.SavePng(temporary, pdf, page: 0, options: new RenderOptions(Dpi: 300));
_viewModel.ImportBackground("background.png", await File.ReadAllBytesAsync(temporary));
}
catch (Exception ex) { _viewModel.SetStatus($"PDF-Import fehlgeschlagen: {ex.Message}", true); }
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
private static FilePickerFileType ImageType() => new("PNG/JPEG-Bilder") { Patterns = ["*.png", "*.jpg", "*.jpeg"] };
private static FilePickerFileType PackageType() => new("LehrerApp-Vorlagen") { Patterns = ["*.lavorlage"] };
private static string EnsurePackageExtension(string path) =>
string.Equals(Path.GetExtension(path), TemplatePackage.Extension, StringComparison.OrdinalIgnoreCase)
? path : path + TemplatePackage.Extension;
private void RefreshStarterTemplates(string? selectedId = null) =>
_viewModel.SetStarterTemplates(_starterTemplates.GetAll(), selectedId);
private void UpdateDocumentTitle()
{
var fileName = _currentPackagePath is null ? "Unbenannt" : Path.GetFileName(_currentPackagePath);
Title = $"{fileName} LehrerApp Vorlagen-Designer";
DocumentNameText.Text = fileName;
DocumentPathText.Text = _currentPackagePath ?? "Noch nicht gespeichert";
}
}