308 lines
15 KiB
C#
308 lines
15 KiB
C#
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.Placeholders.Add(new($"Feld{_viewModel.Placeholders.Count + 1}", PlaceholderType.Text, false, "Beispiel"));
|
||
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)
|
||
{
|
||
if (this.FindControl<DataGrid>("PlaceholderGrid")?.SelectedItem is DesignerPlaceholder selected)
|
||
_viewModel.Placeholders.Remove(selected);
|
||
}
|
||
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";
|
||
}
|
||
}
|