This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
using LehrerApp.TemplateDesigner;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.TemplateDesigner.Tests;
|
||||||
|
|
||||||
|
public sealed class ProjectLifecycleTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Reset_ErzeugtEinVollstaendigLeeresUnabhaengigesProjekt()
|
||||||
|
{
|
||||||
|
var viewModel = new DesignerViewModel();
|
||||||
|
viewModel.ImportAsset("logo.png", OnePixelPng);
|
||||||
|
viewModel.Placeholders.Add(new("Test", LehrerApp.Templating.PlaceholderType.Text, false, "Wert"));
|
||||||
|
viewModel.NewElementType = "IMG";
|
||||||
|
viewModel.NewContent = "logo.png";
|
||||||
|
|
||||||
|
viewModel.Reset();
|
||||||
|
|
||||||
|
Assert.Equal("neue-vorlage", viewModel.TemplateId);
|
||||||
|
Assert.Equal("PAGE 210 297 mm\n", viewModel.LayoutSource);
|
||||||
|
Assert.Empty(viewModel.Placeholders);
|
||||||
|
Assert.Empty(viewModel.Assets);
|
||||||
|
Assert.Empty(viewModel.AssetItems);
|
||||||
|
Assert.Equal("de-DE", viewModel.MetadataItems.Single(x => x.Name == "language").Value);
|
||||||
|
Assert.Equal("letter", viewModel.MetadataItems.Single(x => x.Name == "report-type").Value);
|
||||||
|
Assert.Null(viewModel.SelectedAsset);
|
||||||
|
Assert.Null(viewModel.PreviewImage);
|
||||||
|
Assert.Equal("TEXT", viewModel.NewElementType);
|
||||||
|
Assert.Equal("$Brieftext", viewModel.NewContent);
|
||||||
|
Assert.False(viewModel.CanExport);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FreieMetadaten_WerdenInDasManifestUebernommen()
|
||||||
|
{
|
||||||
|
var viewModel = new DesignerViewModel();
|
||||||
|
viewModel.MetadataItems.Add(new("school-year", "2026/27"));
|
||||||
|
|
||||||
|
var manifest = viewModel.BuildManifest();
|
||||||
|
|
||||||
|
Assert.Equal("de-DE", manifest.Metadata["language"]);
|
||||||
|
Assert.Equal("2026/27", manifest.Metadata["school-year"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly byte[] OnePixelPng = Convert.FromBase64String(
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=");
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ public partial class DesignerViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _statusColor = "#475569";
|
[ObservableProperty] private string _statusColor = "#475569";
|
||||||
[ObservableProperty] private bool _canExport;
|
[ObservableProperty] private bool _canExport;
|
||||||
[ObservableProperty] private DesignerPlaceholder? _selectedPlaceholder;
|
[ObservableProperty] private DesignerPlaceholder? _selectedPlaceholder;
|
||||||
|
[ObservableProperty] private DesignerMetadata? _selectedMetadata;
|
||||||
[ObservableProperty] private DesignerAsset? _selectedAsset;
|
[ObservableProperty] private DesignerAsset? _selectedAsset;
|
||||||
[ObservableProperty] private StarterTemplateItem? _selectedStarterTemplate;
|
[ObservableProperty] private StarterTemplateItem? _selectedStarterTemplate;
|
||||||
[ObservableProperty] private OverlayEditorMode _overlayMode = OverlayEditorMode.Measure;
|
[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("Brieftext", PlaceholderType.Multiline, true, "hiermit informieren wir Sie über einen wichtigen Termin.\n\nMit freundlichen Grüßen"),
|
||||||
new("LehrerName", PlaceholderType.Text, true, "M. Mustermann"),
|
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 Dictionary<string, byte[]> Assets { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public ObservableCollection<DesignerAsset> AssetItems { get; } = [];
|
public ObservableCollection<DesignerAsset> AssetItems { get; } = [];
|
||||||
public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = [];
|
public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = [];
|
||||||
@@ -58,8 +64,16 @@ public partial class DesignerViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = "";
|
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = "";
|
||||||
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = "PAGE 210 297 mm\n";
|
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = "PAGE 210 297 mm\n";
|
||||||
Placeholders.Clear(); Assets.Clear(); AssetItems.Clear(); SelectedAsset = null;
|
Placeholders.Clear(); SelectedPlaceholder = null;
|
||||||
PreviewImage = null; CanExport = false;
|
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);
|
SetStatus("Neues Projekt angelegt.", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +82,7 @@ public partial class DesignerViewModel : ObservableObject
|
|||||||
SchemaVersion = TemplateLoader.CurrentSchemaVersion,
|
SchemaVersion = TemplateLoader.CurrentSchemaVersion,
|
||||||
Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(),
|
Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(),
|
||||||
PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl",
|
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(),
|
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;
|
TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description;
|
||||||
PageWidth = (decimal)template.Manifest.PageSize.Width; PageHeight = (decimal)template.Manifest.PageSize.Height;
|
PageWidth = (decimal)template.Manifest.PageSize.Width; PageHeight = (decimal)template.Manifest.PageSize.Height;
|
||||||
Unit = template.Manifest.PageSize.Unit; LayoutSource = layoutSource;
|
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();
|
Placeholders.Clear();
|
||||||
foreach (var placeholder in template.Manifest.Placeholders)
|
foreach (var placeholder in template.Manifest.Placeholders)
|
||||||
Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type)));
|
Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, DesignerPlaceholder.SampleFor(placeholder.Type)));
|
||||||
@@ -131,6 +150,19 @@ public partial class DesignerViewModel : ObservableObject
|
|||||||
?? StarterTemplates.FirstOrDefault();
|
?? 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) =>
|
private static ITemplateDataProvider BuildSampleDataProvider(TemplateManifest manifest) =>
|
||||||
new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name,
|
new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name,
|
||||||
x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(),
|
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
|
public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject
|
||||||
{
|
{
|
||||||
[ObservableProperty] private int _usageCount;
|
[ObservableProperty] private int _usageCount;
|
||||||
|
|||||||
@@ -2,17 +2,46 @@
|
|||||||
xmlns:local="clr-namespace:LehrerApp.TemplateDesigner"
|
xmlns:local="clr-namespace:LehrerApp.TemplateDesigner"
|
||||||
x:Class="LehrerApp.TemplateDesigner.MainWindow" x:DataType="local:DesignerViewModel" Title="LehrerApp Vorlagen-Designer"
|
x:Class="LehrerApp.TemplateDesigner.MainWindow" x:DataType="local:DesignerViewModel" Title="LehrerApp Vorlagen-Designer"
|
||||||
Width="1320" Height="860" MinWidth="1050" MinHeight="700" WindowStartupLocation="CenterScreen">
|
Width="1320" Height="860" MinWidth="1050" MinHeight="700" WindowStartupLocation="CenterScreen">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,Auto,*">
|
||||||
<Border Padding="18,12" Background="#172554">
|
<Menu>
|
||||||
<Grid ColumnDefinitions="*,Auto,8,Auto,8,Auto">
|
<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"/>
|
<StackPanel><TextBlock Text="Vorlagen-Designer" Foreground="White" FontWeight="Bold" FontSize="20"/>
|
||||||
<TextBlock Text="Portable PDF-Briefpakete · Schema 1" Foreground="#BFDBFE" FontSize="12"/></StackPanel>
|
<TextBlock Text="Portable PDF-Briefpakete · Schema 1" Foreground="#BFDBFE" FontSize="12"/></StackPanel>
|
||||||
<Button Grid.Column="1" Content="Neues Projekt" Click="OnNew"/>
|
<StackPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||||
<Button Grid.Column="3" Content="Paket öffnen …" Click="OnOpenPackage"/>
|
<TextBlock x:Name="DocumentNameText" Foreground="White" FontWeight="SemiBold" HorizontalAlignment="Right"/>
|
||||||
<Button Grid.Column="5" Content="Exportieren …" Click="OnExport" IsEnabled="{Binding CanExport}"/>
|
<TextBlock x:Name="DocumentPathText" Foreground="#BFDBFE" FontSize="11" HorizontalAlignment="Right"/>
|
||||||
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="460,*,390">
|
<Grid Grid.Row="2" ColumnDefinitions="460,*,390">
|
||||||
<ScrollViewer Grid.Column="0" Padding="18">
|
<ScrollViewer Grid.Column="0" Padding="18">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Meine Ausgangsvorlagen" Classes="section"/>
|
<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>
|
<StackPanel Grid.Column="4"><TextBlock Text="Einheit" Classes="label"/><ComboBox ItemsSource="{Binding Units}" SelectedItem="{Binding Unit}"/></StackPanel>
|
||||||
</Grid>
|
</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 & Beispieldaten" Classes="section"/>
|
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Platzhalter & Beispieldaten" Classes="section"/>
|
||||||
<Button Grid.Column="1" Content="+" Click="OnAddPlaceholder"/></Grid>
|
<Button Grid.Column="1" Content="+" Click="OnAddPlaceholder"/></Grid>
|
||||||
<DataGrid x:Name="PlaceholderGrid" ItemsSource="{Binding Placeholders}" SelectedItem="{Binding SelectedPlaceholder}"
|
<DataGrid x:Name="PlaceholderGrid" ItemsSource="{Binding Placeholders}" SelectedItem="{Binding SelectedPlaceholder}"
|
||||||
|
|||||||
@@ -13,15 +13,32 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
private readonly DesignerViewModel _viewModel = new();
|
private readonly DesignerViewModel _viewModel = new();
|
||||||
private readonly StarterTemplateLibrary _starterTemplates = new();
|
private readonly StarterTemplateLibrary _starterTemplates = new();
|
||||||
|
private string? _currentPackagePath;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent(); DataContext = _viewModel;
|
InitializeComponent(); DataContext = _viewModel;
|
||||||
RefreshStarterTemplates();
|
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) =>
|
private void OnAddPlaceholder(object? sender, RoutedEventArgs e) =>
|
||||||
_viewModel.Placeholders.Add(new($"Feld{_viewModel.Placeholders.Count + 1}", PlaceholderType.Text, false, "Beispiel"));
|
_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)
|
private void OnRemovePlaceholder(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (this.FindControl<DataGrid>("PlaceholderGrid")?.SelectedItem is DesignerPlaceholder selected)
|
if (this.FindControl<DataGrid>("PlaceholderGrid")?.SelectedItem is DesignerPlaceholder selected)
|
||||||
@@ -104,7 +121,7 @@ public partial class MainWindow : Window
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (template, layout) = _starterTemplates.Load(selected);
|
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); }
|
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 (template, layout) = _starterTemplates.Load(selected);
|
||||||
var projectId = _starterTemplates.CreateUniqueId(template.Manifest.Id + "-neu");
|
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); }
|
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);
|
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());
|
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(); }
|
try { _viewModel.BuildLoaded(); }
|
||||||
catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); return; }
|
catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); return; }
|
||||||
var file = await StorageProvider.SaveFilePickerAsync(new()
|
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()] });
|
DefaultExtension = TemplatePackage.Extension[1..], FileTypeChoices = [PackageType()] });
|
||||||
if (file is null) return;
|
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
|
try
|
||||||
{
|
{
|
||||||
TemplatePackage.Create(file.Path.LocalPath, _viewModel.BuildManifest(), _viewModel.LayoutSource, _viewModel.Assets);
|
_viewModel.BuildLoaded();
|
||||||
_viewModel.SetStatus($"Paket exportiert: {file.Name}", false);
|
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)
|
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 ImageType() => new("PNG/JPEG-Bilder") { Patterns = ["*.png", "*.jpg", "*.jpeg"] };
|
||||||
private static FilePickerFileType PackageType() => new("LehrerApp-Vorlagen") { Patterns = ["*.lavorlage"] };
|
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) =>
|
private void RefreshStarterTemplates(string? selectedId = null) =>
|
||||||
_viewModel.SetStarterTemplates(_starterTemplates.GetAll(), selectedId);
|
_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,
|
Description = source.Description,
|
||||||
PageSize = new(source.PageSize.Width, source.PageSize.Height, source.PageSize.Unit),
|
PageSize = new(source.PageSize.Width, source.PageSize.Height, source.PageSize.Unit),
|
||||||
LayoutFile = source.LayoutFile,
|
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(),
|
Placeholders = source.Placeholders.Select(x => new PlaceholderDefinition(x.Name, x.Type, x.Required)).ToList(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using LehrerApp.Templating;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace LehrerApp.Templating.Tests;
|
||||||
|
|
||||||
|
public sealed class TemplateMetadataTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _directory = Path.Combine(Path.GetTempPath(), $"template-metadata-{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
public TemplateMetadataTests() => Directory.CreateDirectory(_directory);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Paket_SchreibtLesbareMetadatendateiUndLaedtWerteInsManifest()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(_directory, "metadata.lavorlage");
|
||||||
|
var manifest = new TemplateManifest
|
||||||
|
{
|
||||||
|
Id = "metadata", Name = "Metadaten",
|
||||||
|
Metadata = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["language"] = "de-DE",
|
||||||
|
["report-type"] = "parent-letter",
|
||||||
|
["filter"] = "class=7a",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
TemplatePackage.Create(path, manifest, "PAGE 210 297 mm", new Dictionary<string, byte[]>());
|
||||||
|
|
||||||
|
using (var archive = ZipFile.OpenRead(path))
|
||||||
|
using (var reader = new StreamReader(archive.GetEntry("metadata.txt")!.Open()))
|
||||||
|
{
|
||||||
|
var text = reader.ReadToEnd();
|
||||||
|
Assert.Contains("language=de-DE", text);
|
||||||
|
Assert.Contains("report-type=parent-letter", text);
|
||||||
|
}
|
||||||
|
var loaded = new TemplateLoader().LoadFromPackage(path);
|
||||||
|
Assert.Equal("de-DE", loaded.Manifest.Metadata["language"]);
|
||||||
|
Assert.Equal("parent-letter", loaded.Manifest.Metadata["REPORT-TYPE"]);
|
||||||
|
Assert.Equal("class=7a", loaded.Manifest.Metadata["filter"]);
|
||||||
|
Assert.DoesNotContain("metadata.txt", loaded.Assets.Keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Textformat_IgnoriertKommentareUndLehntDoppelteSchluesselAb()
|
||||||
|
{
|
||||||
|
var parsed = TemplateMetadataText.Parse("# Kommentar\nlanguage = de-DE\nreport-type=letter\n");
|
||||||
|
Assert.Equal("de-DE", parsed["language"]);
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidDataException>(() =>
|
||||||
|
TemplateMetadataText.Parse("language=de-DE\nLANGUAGE=en-US"));
|
||||||
|
Assert.Contains("mehrfach", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Directory.Exists(_directory)) Directory.Delete(_directory, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace LehrerApp.Templating;
|
namespace LehrerApp.Templating;
|
||||||
|
|
||||||
public enum PlaceholderType { Text, Multiline, Date, Number, Image, Table, Chart }
|
public enum PlaceholderType { Text, Multiline, Date, Number, Image, Table, Chart }
|
||||||
@@ -31,6 +33,9 @@ public sealed class TemplateManifest
|
|||||||
public string Description { get; set; } = "";
|
public string Description { get; set; } = "";
|
||||||
public PageSizeDefinition PageSize { get; set; } = new(210, 297);
|
public PageSizeDefinition PageSize { get; set; } = new(210, 297);
|
||||||
public string LayoutFile { get; set; } = "layout.tpl";
|
public string LayoutFile { get; set; } = "layout.tpl";
|
||||||
|
public string MetadataFile { get; set; } = TemplateMetadataText.FileName;
|
||||||
|
[JsonIgnore]
|
||||||
|
public Dictionary<string, string> Metadata { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
public List<PlaceholderDefinition> Placeholders { get; set; } = [];
|
public List<PlaceholderDefinition> Placeholders { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
# LehrerApp Templating
|
# LehrerApp Templating
|
||||||
|
|
||||||
|
## Freie Paketmetadaten
|
||||||
|
|
||||||
|
Jedes neu gespeicherte `.lavorlage`-Paket enthält eine lesbare `metadata.txt`. Pro Zeile steht ein
|
||||||
|
frei wählbares Schlüssel-Wert-Paar; leere Zeilen und mit `#` beginnende Kommentare werden beim
|
||||||
|
Einlesen ignoriert:
|
||||||
|
|
||||||
|
```text
|
||||||
|
language=de-DE
|
||||||
|
report-type=parent-letter
|
||||||
|
school-year=2026/27
|
||||||
|
```
|
||||||
|
|
||||||
|
Werte dürfen ein weiteres `=` enthalten. Schlüssel sind ohne Beachtung der Groß-/Kleinschreibung
|
||||||
|
eindeutig und Werte bleiben einzeilig. Der Loader stellt sie der Anwendung direkt über
|
||||||
|
`loadedTemplate.Manifest.Metadata` zur Verfügung. Bestehende Pakete ohne `metadata.txt` werden
|
||||||
|
weiterhin mit einer leeren Metadatensammlung geladen. Die empfohlenen Standardschlüssel stehen
|
||||||
|
zusätzlich als `TemplateMetadataKeys.Language` und `TemplateMetadataKeys.ReportType` bereit.
|
||||||
|
|
||||||
## Bildskalierung
|
## Bildskalierung
|
||||||
|
|
||||||
`IMG` unterstützt neben dem festen Begrenzungsrahmen eine optionale prozentuale Skalierung:
|
`IMG` unterstützt neben dem festen Begrenzungsrahmen eine optionale prozentuale Skalierung:
|
||||||
|
|||||||
@@ -57,11 +57,26 @@ public sealed class TemplateLoader(TemplateLimits? limits = null,
|
|||||||
if (manifest.SchemaVersion != CurrentSchemaVersion)
|
if (manifest.SchemaVersion != CurrentSchemaVersion)
|
||||||
throw Error($"schemaVersion {manifest.SchemaVersion} wird nicht unterstützt.");
|
throw Error($"schemaVersion {manifest.SchemaVersion} wird nicht unterstützt.");
|
||||||
if (!IsSafeRelativePath(manifest.LayoutFile)) throw Error("layoutFile enthält einen unsicheren Pfad.");
|
if (!IsSafeRelativePath(manifest.LayoutFile)) throw Error("layoutFile enthält einen unsicheren Pfad.");
|
||||||
|
if (!IsSafeRelativePath(manifest.MetadataFile)) throw Error("metadataFile enthält einen unsicheren Pfad.");
|
||||||
|
if (Normalize(manifest.MetadataFile).Equals(Normalize(manifest.LayoutFile), StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw Error("metadataFile und layoutFile dürfen nicht identisch sein.");
|
||||||
if (!entries.TryGetValue(Normalize(manifest.LayoutFile), out var layoutEntry))
|
if (!entries.TryGetValue(Normalize(manifest.LayoutFile), out var layoutEntry))
|
||||||
throw Error($"Layoutdatei „{manifest.LayoutFile}“ fehlt.");
|
throw Error($"Layoutdatei „{manifest.LayoutFile}“ fehlt.");
|
||||||
|
|
||||||
string layoutSource;
|
string layoutSource;
|
||||||
using (var reader = new StreamReader(layoutEntry.Open())) layoutSource = reader.ReadToEnd();
|
using (var reader = new StreamReader(layoutEntry.Open())) layoutSource = reader.ReadToEnd();
|
||||||
|
var metadataPath = Normalize(manifest.MetadataFile);
|
||||||
|
manifest.Metadata = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (entries.TryGetValue(metadataPath, out var metadataEntry))
|
||||||
|
{
|
||||||
|
if (metadataEntry.Length > 1024 * 1024) throw Error("metadata.txt überschreitet das Größenlimit.");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(metadataEntry.Open());
|
||||||
|
manifest.Metadata = TemplateMetadataText.Parse(reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
catch (InvalidDataException ex) { throw Error($"{manifest.MetadataFile} ist ungültig: {ex.Message}"); }
|
||||||
|
}
|
||||||
var layout = new LayoutParser().Parse(layoutSource);
|
var layout = new LayoutParser().Parse(layoutSource);
|
||||||
var referencedAssets = layout.Elements.Select(AssetPath).Where(x => x is not null).Cast<string>()
|
var referencedAssets = layout.Elements.Select(AssetPath).Where(x => x is not null).Cast<string>()
|
||||||
.Select(Normalize).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
.Select(Normalize).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
@@ -76,7 +91,8 @@ public sealed class TemplateLoader(TemplateLimits? limits = null,
|
|||||||
var layoutPath = Normalize(manifest.LayoutFile);
|
var layoutPath = Normalize(manifest.LayoutFile);
|
||||||
foreach (var (path, asset) in entries.Where(x =>
|
foreach (var (path, asset) in entries.Where(x =>
|
||||||
!x.Key.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)
|
!x.Key.Equals("manifest.json", StringComparison.OrdinalIgnoreCase)
|
||||||
&& !x.Key.Equals(layoutPath, StringComparison.OrdinalIgnoreCase)))
|
&& !x.Key.Equals(layoutPath, StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& !x.Key.Equals(metadataPath, StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
if (asset.Length > _limits.MaxAssetBytes)
|
if (asset.Length > _limits.MaxAssetBytes)
|
||||||
{ issues.Add(new(ValidationSeverity.Error, $"Bild „{path}“ überschreitet das Größenlimit.")); continue; }
|
{ issues.Add(new(ValidationSeverity.Error, $"Bild „{path}“ überschreitet das Größenlimit.")); continue; }
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
namespace LehrerApp.Templating;
|
||||||
|
|
||||||
|
public static class TemplateMetadataKeys
|
||||||
|
{
|
||||||
|
public const string Language = "language";
|
||||||
|
public const string ReportType = "report-type";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TemplateMetadataText
|
||||||
|
{
|
||||||
|
public const string FileName = "metadata.txt";
|
||||||
|
|
||||||
|
public static Dictionary<string, string> Parse(string source)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var lines = source.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n').Split('\n');
|
||||||
|
for (var index = 0; index < lines.Length; index++)
|
||||||
|
{
|
||||||
|
var line = lines[index].Trim();
|
||||||
|
if (line.Length == 0 || line.StartsWith('#')) continue;
|
||||||
|
var separator = line.IndexOf('=');
|
||||||
|
if (separator < 1)
|
||||||
|
throw new InvalidDataException($"Metadatenzeile {index + 1} muss Schlüssel=Wert enthalten.");
|
||||||
|
var key = line[..separator].Trim();
|
||||||
|
var value = line[(separator + 1)..].Trim();
|
||||||
|
Validate(key, value);
|
||||||
|
if (!result.TryAdd(key, value))
|
||||||
|
throw new InvalidDataException($"Metadatenschlüssel „{key}“ ist mehrfach vorhanden.");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Serialize(IReadOnlyDictionary<string, string> metadata)
|
||||||
|
{
|
||||||
|
if (metadata.Count == 0) return "";
|
||||||
|
var lines = new List<string>(metadata.Count);
|
||||||
|
foreach (var (key, value) in metadata.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
Validate(key, value);
|
||||||
|
lines.Add($"{key.Trim()}={value.Trim()}");
|
||||||
|
}
|
||||||
|
return string.Join('\n', lines) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Validate(string key, string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key)) throw new InvalidDataException("Ein Metadatenschlüssel darf nicht leer sein.");
|
||||||
|
if (key.Contains('=') || key.Contains('\n') || key.Contains('\r'))
|
||||||
|
throw new InvalidDataException($"Metadatenschlüssel „{key}“ enthält ein unzulässiges Zeichen.");
|
||||||
|
if (value.Contains('\n') || value.Contains('\r'))
|
||||||
|
throw new InvalidDataException($"Metadatenwert für „{key}“ darf nur eine Zeile umfassen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,13 @@ public static class TemplatePackage
|
|||||||
public static void Create(string outputPath, TemplateManifest manifest, string layoutSource,
|
public static void Create(string outputPath, TemplateManifest manifest, string layoutSource,
|
||||||
IReadOnlyDictionary<string, byte[]> assets)
|
IReadOnlyDictionary<string, byte[]> assets)
|
||||||
{
|
{
|
||||||
|
if (!TemplateLoader.IsSafeRelativePath(manifest.MetadataFile))
|
||||||
|
throw new InvalidDataException("metadataFile enthält einen unsicheren Pfad.");
|
||||||
|
if (TemplateLoader.Normalize(manifest.MetadataFile).Equals(
|
||||||
|
TemplateLoader.Normalize(manifest.LayoutFile), StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidDataException("metadataFile und layoutFile dürfen nicht identisch sein.");
|
||||||
|
var reservedPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{ "manifest.json", TemplateLoader.Normalize(manifest.LayoutFile), TemplateLoader.Normalize(manifest.MetadataFile) };
|
||||||
if (!string.Equals(Path.GetExtension(outputPath), Extension, StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(Path.GetExtension(outputPath), Extension, StringComparison.OrdinalIgnoreCase))
|
||||||
outputPath += Extension;
|
outputPath += Extension;
|
||||||
var directory = Path.GetDirectoryName(outputPath);
|
var directory = Path.GetDirectoryName(outputPath);
|
||||||
@@ -21,10 +28,13 @@ public static class TemplatePackage
|
|||||||
{
|
{
|
||||||
WriteText(archive, "manifest.json", JsonSerializer.Serialize(manifest, TemplateLoader.JsonOptions));
|
WriteText(archive, "manifest.json", JsonSerializer.Serialize(manifest, TemplateLoader.JsonOptions));
|
||||||
WriteText(archive, manifest.LayoutFile, layoutSource);
|
WriteText(archive, manifest.LayoutFile, layoutSource);
|
||||||
|
WriteText(archive, manifest.MetadataFile, TemplateMetadataText.Serialize(manifest.Metadata));
|
||||||
foreach (var asset in assets)
|
foreach (var asset in assets)
|
||||||
{
|
{
|
||||||
if (!TemplateLoader.IsSafeRelativePath(asset.Key))
|
if (!TemplateLoader.IsSafeRelativePath(asset.Key))
|
||||||
throw new InvalidDataException($"Unsicherer Assetpfad „{asset.Key}“.");
|
throw new InvalidDataException($"Unsicherer Assetpfad „{asset.Key}“.");
|
||||||
|
if (reservedPaths.Contains(TemplateLoader.Normalize(asset.Key)))
|
||||||
|
throw new InvalidDataException($"Assetpfad „{asset.Key}“ ist für Paketdaten reserviert.");
|
||||||
var entry = archive.CreateEntry(TemplateLoader.Normalize(asset.Key), CompressionLevel.Optimal);
|
var entry = archive.CreateEntry(TemplateLoader.Normalize(asset.Key), CompressionLevel.Optimal);
|
||||||
using var stream = entry.Open(); stream.Write(asset.Value);
|
using var stream = entry.Open(); stream.Write(asset.Value);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user