Add designer file menu and package metadata
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-30 20:49:13 +02:00
parent 23abe7af63
commit 6468596bf3
11 changed files with 363 additions and 20 deletions
@@ -28,6 +28,7 @@ public partial class DesignerViewModel : ObservableObject
[ObservableProperty] private string _statusColor = "#475569";
[ObservableProperty] private bool _canExport;
[ObservableProperty] private DesignerPlaceholder? _selectedPlaceholder;
[ObservableProperty] private DesignerMetadata? _selectedMetadata;
[ObservableProperty] private DesignerAsset? _selectedAsset;
[ObservableProperty] private StarterTemplateItem? _selectedStarterTemplate;
[ObservableProperty] private OverlayEditorMode _overlayMode = OverlayEditorMode.Measure;
@@ -50,6 +51,11 @@ public partial class DesignerViewModel : ObservableObject
new("Brieftext", PlaceholderType.Multiline, true, "hiermit informieren wir Sie über einen wichtigen Termin.\n\nMit freundlichen Grüßen"),
new("LehrerName", PlaceholderType.Text, true, "M. Mustermann"),
];
public ObservableCollection<DesignerMetadata> MetadataItems { get; } =
[
new(TemplateMetadataKeys.Language, "de-DE"),
new(TemplateMetadataKeys.ReportType, "letter"),
];
public Dictionary<string, byte[]> Assets { get; } = new(StringComparer.OrdinalIgnoreCase);
public ObservableCollection<DesignerAsset> AssetItems { get; } = [];
public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = [];
@@ -58,8 +64,16 @@ public partial class DesignerViewModel : ObservableObject
{
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = "";
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = "PAGE 210 297 mm\n";
Placeholders.Clear(); Assets.Clear(); AssetItems.Clear(); SelectedAsset = null;
PreviewImage = null; CanExport = false;
Placeholders.Clear(); SelectedPlaceholder = null;
MetadataItems.Clear(); MetadataItems.Add(new(TemplateMetadataKeys.Language, "de-DE"));
MetadataItems.Add(new(TemplateMetadataKeys.ReportType, "letter"));
SelectedMetadata = null;
Assets.Clear(); AssetItems.Clear(); SelectedAsset = null;
NewElementType = "TEXT"; NewX = "20"; NewY = "50"; NewWidth = "170"; NewHeight = "30";
NewImageScale = "100"; NewContent = "$Brieftext"; NewAttributes = "size=11";
PreviewImage?.Dispose(); PreviewImage = null; CanExport = false;
OverlayMode = OverlayEditorMode.Measure; OverlayCoordinates = "x= · y=";
SelectedOverlayElement = "Kein Element ausgewählt";
SetStatus("Neues Projekt angelegt.", false);
}
@@ -68,6 +82,7 @@ public partial class DesignerViewModel : ObservableObject
SchemaVersion = TemplateLoader.CurrentSchemaVersion,
Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(),
PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl",
Metadata = BuildMetadata(),
Placeholders = Placeholders.Select(x => new PlaceholderDefinition(x.Name.Trim(), x.Type, x.Required)).ToList(),
};
@@ -96,6 +111,10 @@ public partial class DesignerViewModel : ObservableObject
TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description;
PageWidth = (decimal)template.Manifest.PageSize.Width; PageHeight = (decimal)template.Manifest.PageSize.Height;
Unit = template.Manifest.PageSize.Unit; LayoutSource = layoutSource;
MetadataItems.Clear();
foreach (var item in template.Manifest.Metadata.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
MetadataItems.Add(new(item.Key, item.Value));
SelectedMetadata = null;
Placeholders.Clear();
foreach (var placeholder in template.Manifest.Placeholders)
Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type)));
@@ -131,6 +150,19 @@ public partial class DesignerViewModel : ObservableObject
?? StarterTemplates.FirstOrDefault();
}
private Dictionary<string, string> BuildMetadata()
{
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in MetadataItems)
{
var key = item.Name.Trim(); var value = item.Value.Trim();
if (!result.TryAdd(key, value))
throw new InvalidDataException($"Metadatenschlüssel „{key}“ ist mehrfach vorhanden.");
}
TemplateMetadataText.Serialize(result);
return result;
}
private static ITemplateDataProvider BuildSampleDataProvider(TemplateManifest manifest) =>
new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name,
x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(),
@@ -337,6 +369,12 @@ public partial class DesignerViewModel : ObservableObject
""";
}
public partial class DesignerMetadata(string name, string value) : ObservableObject
{
[ObservableProperty] private string _name = name;
[ObservableProperty] private string _value = value;
}
public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject
{
[ObservableProperty] private int _usageCount;
+51 -7
View File
@@ -2,17 +2,46 @@
xmlns:local="clr-namespace:LehrerApp.TemplateDesigner"
x:Class="LehrerApp.TemplateDesigner.MainWindow" x:DataType="local:DesignerViewModel" Title="LehrerApp Vorlagen-Designer"
Width="1320" Height="860" MinWidth="1050" MinHeight="700" WindowStartupLocation="CenterScreen">
<Grid RowDefinitions="Auto,*">
<Border Padding="18,12" Background="#172554">
<Grid ColumnDefinitions="*,Auto,8,Auto,8,Auto">
<Grid RowDefinitions="Auto,Auto,*">
<Menu>
<MenuItem Header="_Datei">
<MenuItem Header="Neu (leeres Projekt)" InputGesture="Ctrl+N" Click="OnNew"/>
<MenuItem Header="Öffnen …" InputGesture="Ctrl+O" Click="OnOpenPackage"/>
<Separator/>
<MenuItem Header="Speichern" InputGesture="Ctrl+S" Click="OnSave"/>
<MenuItem Header="Speichern unter …" InputGesture="Ctrl+Shift+S" Click="OnSaveAs"/>
<Separator/>
<MenuItem Header="Ausgangsvorlage importieren …" Click="OnImportStarterTemplate"/>
<Separator/>
<MenuItem Header="Beenden" Click="OnExit"/>
</MenuItem>
<MenuItem Header="_Vorlage">
<MenuItem Header="Prüfen und Vorschau" InputGesture="F5" Click="OnPreview"/>
<MenuItem Header="Aktuelles Projekt als Ausgangsvorlage speichern" Click="OnSaveStarterTemplate"/>
<Separator/>
<MenuItem Header="Ausgewählte Ausgangsvorlage bearbeiten" Click="OnEditStarterTemplate"/>
<MenuItem Header="Ausgewählte Ausgangsvorlage als neues Projekt" Click="OnUseStarterTemplate"/>
<MenuItem Header="Ausgewählte Ausgangsvorlage duplizieren" Click="OnDuplicateStarterTemplate"/>
<MenuItem Header="Ausgewählte Ausgangsvorlage exportieren …" Click="OnExportStarterTemplate"/>
</MenuItem>
<MenuItem Header="_Einfügen">
<MenuItem Header="Bild-Asset …" Click="OnImportAsset"/>
<Separator/>
<MenuItem Header="PNG/JPEG als Hintergrund …" Click="OnImportBackground"/>
<MenuItem Header="PDF als Hintergrund …" Click="OnImportPdfBackground"/>
</MenuItem>
</Menu>
<Border Grid.Row="1" Padding="18,12" Background="#172554">
<Grid ColumnDefinitions="*,Auto">
<StackPanel><TextBlock Text="Vorlagen-Designer" Foreground="White" FontWeight="Bold" FontSize="20"/>
<TextBlock Text="Portable PDF-Briefpakete · Schema 1" Foreground="#BFDBFE" FontSize="12"/></StackPanel>
<Button Grid.Column="1" Content="Neues Projekt" Click="OnNew"/>
<Button Grid.Column="3" Content="Paket öffnen …" Click="OnOpenPackage"/>
<Button Grid.Column="5" Content="Exportieren …" Click="OnExport" IsEnabled="{Binding CanExport}"/>
<StackPanel Grid.Column="1" HorizontalAlignment="Right">
<TextBlock x:Name="DocumentNameText" Foreground="White" FontWeight="SemiBold" HorizontalAlignment="Right"/>
<TextBlock x:Name="DocumentPathText" Foreground="#BFDBFE" FontSize="11" HorizontalAlignment="Right"/>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1" ColumnDefinitions="460,*,390">
<Grid Grid.Row="2" ColumnDefinitions="460,*,390">
<ScrollViewer Grid.Column="0" Padding="18">
<StackPanel Spacing="14">
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Meine Ausgangsvorlagen" Classes="section"/>
@@ -47,6 +76,21 @@
<StackPanel Grid.Column="4"><TextBlock Text="Einheit" Classes="label"/><ComboBox ItemsSource="{Binding Units}" SelectedItem="{Binding Unit}"/></StackPanel>
</Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Freie Metadaten" Classes="section"/>
<Button Grid.Column="1" Content="" Click="OnAddMetadata"/></Grid>
<DataGrid ItemsSource="{Binding MetadataItems}" SelectedItem="{Binding SelectedMetadata}"
AutoGenerateColumns="False" Height="125" CanUserResizeColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="*"/>
<DataGridTextColumn Header="Wert" Binding="{Binding Value}" Width="1.4*"/>
</DataGrid.Columns>
</DataGrid>
<Grid ColumnDefinitions="Auto,*">
<Button Content="Markierten Eintrag entfernen" Click="OnRemoveMetadata"/>
<TextBlock Grid.Column="1" Margin="10,0,0,0" Text="z. B. language=de-DE · report-type=letter"
FontSize="11" Opacity="0.6" VerticalAlignment="Center" TextWrapping="Wrap"/>
</Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Platzhalter &amp; Beispieldaten" Classes="section"/>
<Button Grid.Column="1" Content="" Click="OnAddPlaceholder"/></Grid>
<DataGrid x:Name="PlaceholderGrid" ItemsSource="{Binding Placeholders}" SelectedItem="{Binding SelectedPlaceholder}"
+61 -10
View File
@@ -13,15 +13,32 @@ 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();
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)
@@ -104,7 +121,7 @@ public partial class MainWindow : Window
try
{
var (template, layout) = _starterTemplates.Load(selected);
_viewModel.Load(template, layout); OnPreview(sender, e);
_viewModel.Load(template, layout); _currentPackagePath = null; UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Öffnen fehlgeschlagen: {ex.Message}", true); }
}
@@ -117,7 +134,8 @@ public partial class MainWindow : Window
{
var (template, layout) = _starterTemplates.Load(selected);
var projectId = _starterTemplates.CreateUniqueId(template.Manifest.Id + "-neu");
_viewModel.LoadAsNewProject(template, layout, projectId); OnPreview(sender, e);
_viewModel.LoadAsNewProject(template, layout, projectId);
_currentPackagePath = null; UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Klonen fehlgeschlagen: {ex.Message}", true); }
}
@@ -198,25 +216,47 @@ public partial class MainWindow : Window
{
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()); OnPreview(sender, e);
_viewModel.Load(loaded, reader.ReadToEnd()); _currentPackagePath = Path.GetFullPath(path);
UpdateDocumentTitle(); OnPreview(sender, e);
}
catch (Exception ex) { _viewModel.SetStatus($"Import fehlgeschlagen: {ex.Message}", true); }
catch (Exception ex) { _viewModel.SetStatus($"Öffnen fehlgeschlagen: {ex.Message}", true); }
}
private async void OnExport(object? sender, RoutedEventArgs e)
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 exportieren", SuggestedFileName = _viewModel.TemplateId + TemplatePackage.Extension,
{ 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
{
TemplatePackage.Create(file.Path.LocalPath, _viewModel.BuildManifest(), _viewModel.LayoutSource, _viewModel.Assets);
_viewModel.SetStatus($"Paket exportiert: {file.Name}", false);
_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($"Export fehlgeschlagen: {ex.Message}", true); }
catch (Exception ex) { _viewModel.SetStatus($"Speichern fehlgeschlagen: {ex.Message}", true); return false; }
}
private async void OnImportBackground(object? sender, RoutedEventArgs e)
@@ -251,6 +291,17 @@ public partial class MainWindow : Window
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";
}
}
@@ -108,6 +108,8 @@ public sealed class StarterTemplateLibrary
Description = source.Description,
PageSize = new(source.PageSize.Width, source.PageSize.Height, source.PageSize.Unit),
LayoutFile = source.LayoutFile,
MetadataFile = source.MetadataFile,
Metadata = new(source.Metadata, StringComparer.OrdinalIgnoreCase),
Placeholders = source.Placeholders.Select(x => new PlaceholderDefinition(x.Name, x.Type, x.Required)).ToList(),
};