using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Threading.Tasks; using System.Timers; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Threading; using PdfSharp.Pdf; using PdfSharp.Drawing; using PdfSharp.Drawing.Layout; using PdfSharp.Fonts; using System.Globalization; // ToDo // - Rand [check] // - 1,5 zeilig [check] // - Namen auf jeder Seite [check] // - PDF Export keine Namen -> Error // - White Mode Access // - Information on last page namespace PLG_Exam { public partial class MainWindow : Window { private int _tabCounter = 0; private bool _isSaved = true; private string? _currentFilePath; public static Exam _currentExam = new(); public MainWindow() { InitializeComponent(); InitializeBackupTimer(); AddNewTab(); this.KeyDown += OnKeyDown; GlobalFontSettings.FontResolver = new CustomFontResolver(); } // Event für "Neuen Tab hinzufügen" private void OnAddTabClick(object? sender, RoutedEventArgs e) { AddNewTab(); } private async void OnKeyDown(object? sender, Avalonia.Input.KeyEventArgs e) { if (e.KeyModifiers.HasFlag(Avalonia.Input.KeyModifiers.Control)) { switch (e.Key) { case Avalonia.Input.Key.S: if (e.KeyModifiers.HasFlag(Avalonia.Input.KeyModifiers.Shift)) { // Strg + Shift + S: Speichern Unter OnSaveAsClick(sender, e); } else { // Strg + S: Speichern OnSaveClick(sender, e); } break; case Avalonia.Input.Key.O: // Strg + O: Öffnen OnOpenClick(sender, e); break; case Avalonia.Input.Key.T: // Strg + T: Neuer Tab OnAddTabClick(sender, e); break; case Avalonia.Input.Key.N: // Strg + N: Neu OnNewClick(sender, e); break; case Avalonia.Input.Key.R: // Strg + R: Abgeben (coming soon) OnSubmitClick(sender, e); break; } } } private async void OnSubmitClick(object sender, RoutedEventArgs e) { ExportToPdf(); } private void AddNewTab() { _tabCounter++; var closeButton = new Button { Content = "×", FontSize = 14, HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center, Width = 20, Height = 20, Background = new SolidColorBrush(Color.FromRgb(123,35,39)), Foreground = new SolidColorBrush(Color.FromRgb(0,0,0)), Padding = new Thickness(0), Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; ToolTip.SetTip(closeButton, "Tab schließen"); var headerStackPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal }; var tabHeader = new TextBlock { Text = $"{_tabCounter} - Neu", VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; headerStackPanel.Children.Add(tabHeader); headerStackPanel.Children.Add(closeButton); var tabItem = new TabItem { Header = headerStackPanel, Content = CreateTabContent() }; // Schließen-Event hinzufügen closeButton.Click += async (sender, e) => { var result = await MessageBox.Show(this, "Diese Aktion schließt die Aufgabe unwiderruflich. Möchten Sie fortfahren?", "Tab schließen", MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) { TabView.Items.Remove(tabItem); _isSaved = false; } }; TabView.Items.Add(tabItem); TabView.SelectedItem = tabItem; _isSaved = false; } // Tab entfernen mit Sicherheitsabfrage private async void OnRemoveTabClick(object? sender, RoutedEventArgs e) { if (TabView.SelectedItem is TabItem selectedTab) { var result = await MessageBox.Show(this, "Dieser Tab wird unwiderruflich gelöscht. Möchten Sie fortfahren?", "Tab löschen", MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) { TabView.Items.Remove(selectedTab); _isSaved = false; } } } // Speichern private async void OnSaveClick(object? sender, RoutedEventArgs e) { if (string.IsNullOrEmpty(_currentFilePath)) { await SaveAs(); } else { SaveToFile(_currentFilePath); } } private async void OnSaveAsClick(object? sender, RoutedEventArgs e) { await SaveAs(); } private async Task SaveAs() { var saveDialog = new SaveFileDialog { DefaultExtension = "exam", Filters = { new FileDialogFilter { Name = "Exam files", Extensions = { "exam" } } } }; var filePath = await saveDialog.ShowAsync(this); if (!string.IsNullOrEmpty(filePath)) { SaveToFile(filePath); return true; } else { return false; } } private void SaveToFile(string filePath) { // Update Exam-Daten _currentExam.Name = NameField.Text; _currentExam.Title = TitleField.Text; _currentExam.Vorname = VornameField.Text; _currentExam.Datum = DatumField.SelectedDate?.UtcDateTime; _currentExam.Tabs = TabView.Items.OfType() .Select(tab => { var grid = tab.Content as Grid; if (grid == null) return null; var aufgabennummer = (grid.Children[0] as Grid)?.Children[0] as TextBox; var ueberschrift = (grid.Children[0] as Grid)?.Children[1] as TextBox; var beschreibung = grid.Children[1] as TextBox; return new ExamTab { Aufgabennummer = aufgabennummer?.Text ?? "", Überschrift = ueberschrift?.Text ?? "", Inhalt = beschreibung?.Text ?? "" }; }) .Where(tab => tab != null) .ToList(); var json = JsonSerializer.Serialize(_currentExam, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(filePath, json); _currentFilePath = filePath; _isSaved = true; } // Öffnen private async void OnOpenClick(object? sender, RoutedEventArgs e) { var openDialog = new OpenFileDialog { AllowMultiple = false, Filters = { new FileDialogFilter { Name = "Exam files", Extensions = { "exam" } } } }; var result = await openDialog.ShowAsync(this); if (result != null && result.Length > 0) { LoadFromFile(result[0]); } } private void LoadFromFile(string filePath) { var fileContent = File.ReadAllText(filePath); var exam = JsonSerializer.Deserialize(fileContent); if (exam == null) return; // Daten wiederherstellen _currentExam = exam; NameField.Text = exam.Name; TitleField.Text = exam.Title; VornameField.Text = exam.Vorname; DatumField.SelectedDate = exam.Datum; TabView.Items.Clear(); _tabCounter = 0; foreach (var tab in exam.Tabs) { _tabCounter++; var tabItem = new TabItem { Content = CreateTabContent(tab.Aufgabennummer, tab.Überschrift, tab.Inhalt) }; tabItem.Header = ReturnTabHeaderContent($"{tab.Aufgabennummer} - {tab.Überschrift}", tabItem); TabView.Items.Add(tabItem); } _currentFilePath = filePath; _isSaved = true; } private StackPanel ReturnTabHeaderContent(string name, TabItem curr_tab){ var closeButton = new Button { Content = "×", FontSize = 14, HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center, VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Center, Width = 20, Height = 20, Background = new SolidColorBrush(Color.FromRgb(123,35,39)), Foreground = new SolidColorBrush(Color.FromRgb(0,0,0)), Padding = new Thickness(0), Margin = new Thickness(5, 0, 0, 0), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; ToolTip.SetTip(closeButton, "Tab schließen"); var headerStackPanel = new StackPanel { Orientation = Avalonia.Layout.Orientation.Horizontal }; var tabHeader = new TextBlock { Text = name, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center }; headerStackPanel.Children.Add(tabHeader); headerStackPanel.Children.Add(closeButton); // Schließen-Event hinzufügen closeButton.Click += async (sender, e) => { var result = await MessageBox.Show(this, "Diese Aktion schließt die Aufgabe unwiderruflich. Möchten Sie fortfahren?", "Tab schließen", MessageBoxButton.YesNo); if (result == MessageBoxResult.Yes) { TabView.Items.Remove(curr_tab); _isSaved = false; } }; return headerStackPanel; } // Neu private async void OnNewClick(object? sender, RoutedEventArgs e) { if (!_isSaved) { var result = await MessageBox.Show(this, "Möchten Sie die aktuellen Änderungen speichern?", "Nicht gespeicherte Änderungen", MessageBoxButton.YesNoCancel); if (result == MessageBoxResult.Cancel) return; if (result == MessageBoxResult.Yes) { if((await SaveAs()) != true){ return; } } } _currentExam = new Exam(); NameField.Text = ""; VornameField.Text = ""; DatumField.SelectedDate = null; TabView.Items.Clear(); _tabCounter = 0; _currentFilePath = null; _isSaved = true; } private Grid CreateTabContent(string? aufgabennummer = null, string? ueberschrift = null, string? beschreibung = null) { var grid = new Grid(); grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(33) }); grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); var headerGrid = new Grid { Margin = new Thickness(0, 0, 0, 10) }; headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(250) }); headerGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); var aufgabenNummerTextBox = new TextBox { Name = "Aufgabennummer", Watermark = "Aufgabennummer", Margin = new Thickness(0,10,0,0), Text = aufgabennummer ?? "" }; var ueberschriftTextBox = new TextBox { Name = "Überschrift", Watermark = "Überschrift (optional)", Margin = new Thickness(10,10,0,0), Text = ueberschrift ?? "" }; var beschreibungTextBox = new TextBox { Name = "Beschreibung", AcceptsReturn = true, VerticalContentAlignment = Avalonia.Layout.VerticalAlignment.Stretch, Watermark = "Beschreibung", Text = beschreibung ?? "", }; // Event für dynamische Tab-Umbenennung aufgabenNummerTextBox.KeyUp += (s, e) => UpdateTabHeader(TabView.SelectedItem as TabItem, aufgabenNummerTextBox, ueberschriftTextBox); ueberschriftTextBox.KeyUp += (s, e) => UpdateTabHeader(TabView.SelectedItem as TabItem, aufgabenNummerTextBox, ueberschriftTextBox); headerGrid.Children.Add(aufgabenNummerTextBox); headerGrid.Children.Add(ueberschriftTextBox); Grid.SetColumn(aufgabenNummerTextBox, 0); Grid.SetColumn(ueberschriftTextBox, 1); grid.Children.Add(headerGrid); grid.Children.Add(beschreibungTextBox); Grid.SetRow(headerGrid, 0); Grid.SetRow(beschreibungTextBox, 1); return grid; } private void UpdateTabHeader(TabItem? tab, TextBox aufgabennummer, TextBox ueberschrift) { if (tab == null) return; var aufgabeText = string.IsNullOrWhiteSpace(aufgabennummer.Text) ? "Neu" : aufgabennummer.Text; var ueberschriftText = string.IsNullOrWhiteSpace(ueberschrift.Text) ? "" : ueberschrift.Text; tab.Header = ReturnTabHeaderContent(string.IsNullOrWhiteSpace(ueberschriftText) ? aufgabeText : $"{aufgabeText} - {ueberschriftText}", tab); } // // Backups // private const int BackupInterval = 20000; // 20 Sekunden in Millisekunden private DispatcherTimer _backupTimer; private void InitializeBackupTimer() { _backupTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(20) }; _backupTimer.Tick += (sender, e) => CreateBackup(); _backupTimer.Start(); } private const int MaxBackupFiles = 5; // Maximal 5 Backups private readonly string BackupFolderPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PLG_Exam_Backup"); // Backup erstellen private void CreateBackup() { try { // Ordner erstellen, falls nicht vorhanden if (!Directory.Exists(BackupFolderPath)) { Directory.CreateDirectory(BackupFolderPath); } // Erstelle Dateinamen mit Zeitstempel var timestamp = DateTime.Now.ToString("yyMMdd_HHmmss"); var backupFileName = Path.Combine(BackupFolderPath, $"{timestamp}_backup.exam"); // Aktuelle Daten in JSON-Format speichern var jsonData = GetCurrentExamDataAsJson(); File.WriteAllText(backupFileName, jsonData); // Alte Backups löschen, wenn mehr als MaxBackupFiles vorhanden sind CleanupOldBackups(); } catch (Exception ex) { Console.WriteLine($"Backup konnte nicht erstellt werden: {ex.Message}"); } } // Löscht alte Backups, wenn die maximale Anzahl überschritten wird private void CleanupOldBackups() { var backupFiles = Directory.GetFiles(BackupFolderPath, "*_backup.exam") .OrderBy(File.GetCreationTime) .ToList(); while (backupFiles.Count > MaxBackupFiles) { var oldestFile = backupFiles.First(); File.Delete(oldestFile); backupFiles.RemoveAt(0); } } // Löscht alle Backups unwiderruflich (mit Überschreiben) private void DeleteAllBackups() { try { if (Directory.Exists(BackupFolderPath)) { foreach (var file in Directory.GetFiles(BackupFolderPath, "*_backup.exam")) { OverwriteAndDeleteFile(file); } Directory.Delete(BackupFolderPath, true); } } catch (Exception ex) { Console.WriteLine($"Backups konnten nicht gelöscht werden: {ex.Message}"); } } // Überschreibt eine Datei mit Zufallsdaten und löscht sie anschließend private void OverwriteAndDeleteFile(string filePath) { try { if (!File.Exists(filePath)) return; var fileLength = new FileInfo(filePath).Length; // Datei mit Zufallsdaten überschreiben using (var stream = new FileStream(filePath, FileMode.Open)) { var randomData = new byte[fileLength]; new Random().NextBytes(randomData); stream.Write(randomData, 0, randomData.Length); } // Datei löschen File.Delete(filePath); } catch (Exception ex) { Console.WriteLine($"Datei konnte nicht überschrieben und gelöscht werden: {ex.Message}"); } } // Holt die aktuelle Exam-Daten als JSON private string GetCurrentExamDataAsJson() { _currentExam.Title = TitleField.Text; _currentExam.Name = NameField.Text; _currentExam.Vorname = VornameField.Text; _currentExam.Datum = DatumField.SelectedDate?.UtcDateTime; _currentExam.Tabs = TabView.Items.OfType() .Select(tab => { var grid = tab.Content as Grid; if (grid == null) return null; var aufgabennummer = (grid.Children[0] as Grid)?.Children[0] as TextBox; var ueberschrift = (grid.Children[0] as Grid)?.Children[1] as TextBox; var beschreibung = grid.Children[1] as TextBox; return new ExamTab { Aufgabennummer = aufgabennummer?.Text ?? "", Überschrift = ueberschrift?.Text ?? "", Inhalt = beschreibung?.Text ?? "" }; }) .Where(tab => tab != null) .ToList(); return System.Text.Json.JsonSerializer.Serialize(_currentExam, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); } private async void ExportToPdf() { try { _ = GetCurrentExamDataAsJson(); var saveDialog = new SaveFileDialog { DefaultExtension = "pdf", Filters = { new FileDialogFilter { Name = "PDF files", Extensions = { "pdf" } } } }; var filePath = await saveDialog.ShowAsync(this); if (string.IsNullOrEmpty(filePath)) return; var font = new XFont("Cantarell", 14, XFontStyleEx.Bold); var fontB = new XFont("Cantarell", 14, XFontStyleEx.Bold); var fontsmall = new XFont("Cantarell", 10, XFontStyleEx.Regular); var descriptionFont = new XFont("Cantarell", 12, XFontStyleEx.Regular); using var document = new PdfDocument(); document.Info.Title = _currentExam.Title; document.Info.Author = _currentExam.Vorname + " " + _currentExam.Name; document.Info.Subject = "PLG Exam Submission"; document.Info.Keywords = "Exam, PLG, Report, PDF"; SetPdfLanguage(document); var firstPage = document.AddPage(); var gfx = XGraphics.FromPdfPage(firstPage); gfx.DrawString(_currentExam.Title, fontB, XBrushes.Black, new XRect(0, 40, firstPage.Width, 50), XStringFormats.TopCenter); gfx.DrawString($"Name: {_currentExam.Name}", fontB, XBrushes.Black, new XRect(50, 100, firstPage.Width, firstPage.Height), XStringFormats.TopLeft); gfx.DrawString($"Vorname: {_currentExam.Vorname}", fontB, XBrushes.Black, new XRect(50, 130, firstPage.Width, firstPage.Height), XStringFormats.TopLeft); gfx.DrawString($"Datum: {_currentExam.Datum?.ToShortDateString() ?? "N/A"}", fontB, XBrushes.Black, new XRect(50, 160, firstPage.Width, firstPage.Height), XStringFormats.TopLeft); foreach (var tab in _currentExam.Tabs) { DrawTaskWithPageBreak(document, tab, descriptionFont, font, fontsmall); } var endpage = document.AddPage(); var endpageGfx = XGraphics.FromPdfPage(endpage); // Beschreibung am unteren Rand platzieren var bottomDescriptionRect = new XRect( 50, // X-Position (horizontaler Abstand) endpage.Height - 100, // Y-Position (Seitenhöhe - Textbereichshöhe - Margin) endpage.Width - 100, // Breite des Bereichs 100 // Höhe des Bereichs ); var endformatter = new XTextFormatter(endpageGfx) { Alignment = XParagraphAlignment.Left }; // Text zeichnen endformatter.DrawString( "Erstellt mit PLG Exam - powered by PLG Development\nhttps://github.com/PLG-Development/PLG-Exam\n(c) 2025 - PLG Development", fontsmall, XBrushes.Black, bottomDescriptionRect ); //endpageGfx.DrawString($"Erstellt mit PLG Exam - powered by PLG Development\nhttps://github.com/PLG-Development/PLG-Exam\n(c) 2024 - PLG Development", fontsmall, XBrushes.Black, new XRect(0,0, firstPage.Width, firstPage.Height), XStringFormats.BottomLeft); document.Save(filePath); await MessageBox.Show(this, "PDF erfolgreich gespeichert!", "Erfolg", MessageBoxButton.Ok); } catch (Exception ex) { Console.WriteLine($"Fehler beim PDF-Export: {ex.Message}"); await MessageBox.Show(this, "Fehler beim PDF-Export.", "Fehler", MessageBoxButton.Ok); } } private void DrawTaskWithPageBreak(PdfDocument document, ExamTab tab, XFont font, XFont headerFont, XFont smallFont) { const double margin = 50; // Seitenränder const double corr_margin = 250; // Seitenrand Korrektur const double lineHeight = 21; // Höhe einer Textzeile const double headerHeight = 30; // Platz für die Kopfzeile pro Seite const double footerHeight = 20; // Platz für die Fußzeile const double usableHeight = 842 - margin * 2 - headerHeight - footerHeight; // Höhe des nutzbaren Bereichs // Text aufteilen var lines = SplitTextIntoLines(tab.Inhalt, document.Pages[0].Width - margin -corr_margin, font); double currentHeight = 0; // Aktuelle Höhe, die der Text benötigt PdfPage page = null; XGraphics gfx = null; foreach (var line in lines) { // Neue Seite erstellen, falls nötig if (page == null || currentHeight + lineHeight > usableHeight) { page = document.AddPage(); gfx = XGraphics.FromPdfPage(page); currentHeight = 0; // Kopfzeile zeichnen DrawName(gfx, tab, smallFont, margin, headerHeight); DrawHeader(gfx, tab, headerFont, margin, headerHeight); } // Zeile zeichnen gfx.DrawString(line, font, XBrushes.Black, new XRect(margin, margin + headerHeight + currentHeight, page.Width - margin - corr_margin, lineHeight), XStringFormats.TopLeft); currentHeight += lineHeight; } } // Methode zum Zeichnen der Kopfzeile private void DrawHeader(XGraphics gfx, ExamTab tab, XFont font, double margin, double headerHeight) { var headerText = $"Aufgabe {tab.Aufgabennummer}: {tab.Überschrift}"; gfx.DrawString(headerText, font, XBrushes.Gray, new XRect(margin, margin, gfx.PageSize.Width - margin * 2, headerHeight), XStringFormats.TopLeft); } private void DrawName(XGraphics gfx, ExamTab tab, XFont font, double margin, double headerHeight) { var headerText = $"{_currentExam.Name}, {_currentExam.Vorname}"; gfx.DrawString(headerText, font, XBrushes.Gray, new XRect(margin, margin-15, gfx.PageSize.Width - margin * 2, headerHeight), XStringFormats.TopLeft); var headerText2 = _currentExam.Datum.Value.ToString("dd.MM.yyyy", CultureInfo.CreateSpecificCulture("de-DE")); gfx.DrawString(headerText2, font, XBrushes.Gray, new XRect(margin, margin-15, gfx.PageSize.Width - margin * 2, headerHeight), XStringFormats.TopRight); } // Methode zum Aufteilen des Textes in Zeilen private List SplitTextIntoLines(string text, double maxWidth, XFont font) { var lines = new List(); // Text anhand von \n aufteilen, einschließlich leerer Zeilen var manualLines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); using (var gfx = XGraphics.CreateMeasureContext(new XSize(maxWidth, 842), XGraphicsUnit.Point, XPageDirection.Downwards)) { foreach (var manualLine in manualLines) { // Falls die Zeile leer ist (z. B. durch mehrere \n), direkt hinzufügen if (string.IsNullOrWhiteSpace(manualLine)) { lines.Add(""); // Leere Zeile hinzufügen continue; } // Falls eine Zeile zu lang ist, weiter aufteilen var words = manualLine.Split(' '); var currentLine = ""; foreach (var word in words) { var testLine = string.IsNullOrEmpty(currentLine) ? word : $"{currentLine} {word}"; var size = gfx.MeasureString(testLine, font); if (size.Width > maxWidth) { lines.Add(currentLine); currentLine = word; } else { currentLine = testLine; } } // Letzte Zeile hinzufügen if (!string.IsNullOrEmpty(currentLine)) { lines.Add(currentLine); } } } return lines; } private void SetPdfLanguage(PdfSharp.Pdf.PdfDocument document, string language = "de") { var catalog = document.Internals.Catalog; if (catalog.Elements.ContainsKey("/Lang")) { catalog.Elements["/Lang"] = new PdfSharp.Pdf.PdfString(language); } else { catalog.Elements.Add("/Lang", new PdfSharp.Pdf.PdfString(language)); } } } }