#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ GUI für PDF zu ICS Konverter mit wxPython Native Benutzeroberfläche für macOS, Windows, Linux """ import wx import wx.lib.scrolledpanel as scrolled from pathlib import Path import threading import json import webbrowser from datetime import datetime from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan from update_checker import check_for_updates, get_current_version # Konfigurationsdatei CONFIG_FILE = Path.home() / '.pdf_to_ics_config.json' class PDFtoICSFrame(wx.Frame): def __init__(self): super().__init__(parent=None, title='PDF zu ICS Konverter - Dienstplan Importer', size=(800, 700)) # Lade gespeicherte Einstellungen self.config = self.load_config() # Variablen self.pdf_files = [] # Nutze letztes Ausgabeverzeichnis oder Standard default_dir = self.config.get('last_output_dir', None) if not default_dir or not Path(default_dir).exists(): default_dir = Path.cwd() if str(default_dir).split('/')[-1].startswith('.'): default_dir = Path.home() self.output_dir = str(default_dir) # Letztes PDF-Verzeichnis merken self.last_pdf_dir = self.config.get('last_pdf_dir', str(Path.home())) # Erstelle UI self.create_widgets() # Erstelle Menüleiste self.create_menu() # Center window self.Centre() # Update-Prüfung im Hintergrund starten update_thread = threading.Thread(target=self.check_for_updates_background, daemon=True) update_thread.start() # Handle window close self.Bind(wx.EVT_CLOSE, self.on_closing) def load_config(self): """Lade gespeicherte Konfiguration""" try: if CONFIG_FILE.exists(): with open(CONFIG_FILE, 'r') as f: return json.load(f) except Exception as e: print(f"Warnung: Konfiguration konnte nicht geladen werden: {e}") return {} def save_config(self): """Speichere Konfiguration""" try: config = { 'last_output_dir': self.output_dir, 'last_pdf_dir': self.last_pdf_dir, 'exclude_rest': self.exclude_rest_checkbox.GetValue() } with open(CONFIG_FILE, 'w') as f: json.dump(config, f, indent=2) except Exception as e: print(f"Warnung: Konfiguration konnte nicht gespeichert werden: {e}") def create_menu(self): """Erstelle die Menüleiste""" menubar = wx.MenuBar() # Hilfe-Menü help_menu = wx.Menu() android_item = help_menu.Append(wx.ID_ANY, 'PDF-Export auf Android (iPD)\tCtrl+H') help_menu.AppendSeparator() about_item = help_menu.Append(wx.ID_ABOUT, 'Über dieses Programm\tCtrl+I') help_menu.AppendSeparator() quit_item = help_menu.Append(wx.ID_EXIT, 'Beenden\tCtrl+Q') menubar.Append(help_menu, '&Hilfe') self.SetMenuBar(menubar) # Bind events self.Bind(wx.EVT_MENU, self.show_android_export_guide, android_item) self.Bind(wx.EVT_MENU, self.show_about_dialog, about_item) self.Bind(wx.EVT_MENU, self.on_closing, quit_item) def create_widgets(self): """Erstelle die UI-Komponenten""" panel = wx.Panel(self) main_sizer = wx.BoxSizer(wx.VERTICAL) # ========== HEADER ========== header_panel = wx.Panel(panel) header_panel.SetBackgroundColour('#2c3e50') header_sizer = wx.BoxSizer(wx.VERTICAL) title_label = wx.StaticText(header_panel, label='PDF zu ICS Konverter') title_font = wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) title_label.SetFont(title_font) title_label.SetForegroundColour(wx.WHITE) header_sizer.Add(title_label, 0, wx.ALL | wx.ALIGN_CENTER, 20) header_panel.SetSizer(header_sizer) main_sizer.Add(header_panel, 0, wx.EXPAND) # ========== CONTENT AREA ========== content_sizer = wx.BoxSizer(wx.VERTICAL) # PDF-Dateien Bereich pdf_label = wx.StaticText(panel, label='PDF-Dateien:') pdf_font = wx.Font(12, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) pdf_label.SetFont(pdf_font) content_sizer.Add(pdf_label, 0, wx.ALL, 10) # ListBox für PDFs self.pdf_listbox = wx.ListBox(panel, style=wx.LB_EXTENDED) content_sizer.Add(self.pdf_listbox, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) # Buttons für PDF-Verwaltung button_sizer = wx.BoxSizer(wx.HORIZONTAL) add_btn = wx.Button(panel, label='PDF hinzufügen') add_btn.Bind(wx.EVT_BUTTON, self.add_pdf_files) button_sizer.Add(add_btn, 1, wx.ALL, 5) remove_btn = wx.Button(panel, label='Entfernen') remove_btn.Bind(wx.EVT_BUTTON, self.remove_selected_pdfs) button_sizer.Add(remove_btn, 1, wx.ALL, 5) clear_btn = wx.Button(panel, label='Alle entfernen') clear_btn.Bind(wx.EVT_BUTTON, self.clear_all_pdfs) button_sizer.Add(clear_btn, 1, wx.ALL, 5) content_sizer.Add(button_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) # Ausgabe-Verzeichnis output_label = wx.StaticText(panel, label='Ausgabe-Verzeichnis:') content_sizer.Add(output_label, 0, wx.ALL, 10) output_sizer = wx.BoxSizer(wx.HORIZONTAL) self.output_text = wx.TextCtrl(panel, value=self.output_dir, style=wx.TE_READONLY) output_sizer.Add(self.output_text, 1, wx.ALIGN_CENTER_VERTICAL, 5) browse_btn = wx.Button(panel, label='Durchsuchen') browse_btn.Bind(wx.EVT_BUTTON, self.browse_output_dir) output_sizer.Add(browse_btn, 0, wx.LEFT, 5) content_sizer.Add(output_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) # Exportoptionen self.exclude_rest_checkbox = wx.CheckBox( panel, label='🧘 Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48)' ) self.exclude_rest_checkbox.SetValue(self.config.get('exclude_rest', False)) content_sizer.Add(self.exclude_rest_checkbox, 0, wx.ALL, 10) # Log-Bereich log_label = wx.StaticText(panel, label='Status:') log_label.SetFont(pdf_font) content_sizer.Add(log_label, 0, wx.ALL, 10) self.log_text = wx.TextCtrl( panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP ) self.log_text.SetBackgroundColour('#f8f9fa') content_sizer.Add(self.log_text, 2, wx.EXPAND | wx.LEFT | wx.RIGHT, 10) # Konvertieren Button self.convert_btn = wx.Button(panel, label='ICS Datei erstellen') self.convert_btn.Bind(wx.EVT_BUTTON, self.convert_pdfs) content_sizer.Add(self.convert_btn, 0, wx.EXPAND | wx.ALL, 10) main_sizer.Add(content_sizer, 1, wx.EXPAND) panel.SetSizer(main_sizer) # Initial log message self.log("Bereit. Fügen Sie PDF-Dateien hinzu um zu starten.") self.log("✓ Native wxPython GUI - perfekte Integration auf macOS, Windows & Linux!") def log(self, message): """Füge eine Nachricht zum Log hinzu""" timestamp = datetime.now().strftime("%H:%M:%S") wx.CallAfter(self._append_log, f"[{timestamp}] {message}\n") def _append_log(self, message): """Thread-sichere Log-Ausgabe""" self.log_text.AppendText(message) def add_pdf_files(self, event=None): """Öffne Datei-Dialog zum Hinzufügen von PDFs""" with wx.FileDialog( self, "PDF-Dateien auswählen", defaultDir=self.last_pdf_dir, wildcard="PDF Dateien (*.pdf)|*.pdf|Alle Dateien (*.*)|*.*", style=wx.FD_OPEN | wx.FD_FILE_MUST_EXIST | wx.FD_MULTIPLE ) as fileDialog: if fileDialog.ShowModal() == wx.ID_CANCEL: return paths = fileDialog.GetPaths() for path in paths: if path not in self.pdf_files: self.pdf_files.append(path) self.pdf_listbox.Append(Path(path).name) if paths: # Merke Verzeichnis der ersten ausgewählten Datei self.last_pdf_dir = str(Path(paths[0]).parent) self.log(f"✓ {len(paths)} PDF-Datei(en) hinzugefügt") def remove_selected_pdfs(self, event=None): """Entferne ausgewählte PDFs aus der Liste""" selections = self.pdf_listbox.GetSelections() if not selections: return # Rückwärts durchlaufen, um Indexprobleme zu vermeiden for index in reversed(selections): self.pdf_listbox.Delete(index) del self.pdf_files[index] self.log(f"✓ {len(selections)} PDF-Datei(en) entfernt") def clear_all_pdfs(self, event=None): """Entferne alle PDFs aus der Liste""" count = len(self.pdf_files) self.pdf_listbox.Clear() self.pdf_files.clear() if count > 0: self.log(f"✓ Alle {count} PDF-Datei(en) entfernt") def browse_output_dir(self, event=None): """Öffne Dialog zur Auswahl des Ausgabe-Verzeichnisses""" with wx.DirDialog( self, "Ausgabe-Verzeichnis auswählen", defaultPath=self.output_dir, style=wx.DD_DEFAULT_STYLE | wx.DD_DIR_MUST_EXIST ) as dirDialog: if dirDialog.ShowModal() == wx.ID_CANCEL: return self.output_dir = dirDialog.GetPath() self.output_text.SetValue(self.output_dir) self.save_config() self.log(f"✓ Ausgabe-Verzeichnis: {self.output_dir}") def convert_pdfs(self, event=None): """Konvertiere alle PDFs zu ICS""" if not self.pdf_files: wx.MessageBox( 'Bitte fügen Sie mindestens eine PDF-Datei hinzu.', 'Keine PDFs', wx.OK | wx.ICON_WARNING ) return # Starte Konvertierung in separatem Thread thread = threading.Thread(target=self._convert_worker, daemon=True) thread.start() def _convert_worker(self): """Worker-Thread für Konvertierung""" # Deaktiviere Button wx.CallAfter(self.convert_btn.Enable, False) output_dir = Path(self.output_dir) success_count = 0 self.log("\n" + "="*50) self.log("🔄 Starte Konvertierung...") self.log("="*50) for i, pdf_path in enumerate(self.pdf_files, 1): try: self.log(f"\n[{i}/{len(self.pdf_files)}] Verarbeite: {Path(pdf_path).name}") # Extrahiere Daten dienstplan = extract_dienstplan_data(pdf_path) # Zeige Informationen self.log(f" ├─ Name: {dienstplan['vorname']} {dienstplan['name']}") self.log(f" ├─ Personalnummer: {dienstplan['personalnummer']}") self.log(f" ├─ Betriebshof: {dienstplan['betriebshof']}") self.log(f" └─ Events gefunden: {len(dienstplan['events'])}") if not dienstplan['events']: self.log(" ⚠️ Warnung: Keine Events gefunden!") continue # Erstelle ICS-Datei ics_filename = Path(pdf_path).stem + '.ics' ics_path = output_dir / ics_filename create_ics_from_dienstplan( dienstplan, str(ics_path), exclude_rest=self.exclude_rest_checkbox.GetValue() ) self.log(f" ✓ ICS erstellt: {ics_filename}") success_count += 1 except Exception as e: self.log(f" ✗ Fehler: {str(e)}") # Zusammenfassung self.log("\n" + "="*50) self.log(f"✅ Fertig! {success_count}/{len(self.pdf_files)} ICS-Dateien erstellt") self.log("="*50 + "\n") # Speichere Config self.save_config() # Reaktiviere Button wx.CallAfter(self.convert_btn.Enable, True) # Erfolgsmeldung if success_count > 0: wx.CallAfter( wx.MessageBox, f"Es wurden {success_count} ICS-Datei(en) erfolgreich erstellt!\n\n" f"Speicherort: {output_dir}", "Konvertierung abgeschlossen", wx.OK | wx.ICON_INFORMATION ) def show_android_export_guide(self, event=None): """Zeige Anleitung für PDF-Export aus Android App (iPD)""" guide_window = wx.Dialog(self, title="PDF-Export auf Android (iPD)", size=(600, 600)) panel = wx.Panel(guide_window) sizer = wx.BoxSizer(wx.VERTICAL) # Header header = wx.StaticText(panel, label="PDF-Export aus iPD") header_font = wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD) header.SetFont(header_font) sizer.Add(header, 0, wx.ALL | wx.ALIGN_CENTER, 15) # Anleitung-Text guide_text = wx.TextCtrl( panel, value="""1. Öffne die iPD App auf deinem Android-Gerät 2. Öffne einen Dienstplan 3. Wähle den gewünschten Monat aus 4. Tippe auf das PDF-Symbol (rechts oben, links neben dem 3-Punkte-Menü) 5. Tippe auf "Datei herunterladen" (rechts oben, neben Drucker-Button) 6. Wähle "Im Arbeitsprofil speichern" 7. Sende die PDF-Datei als E-Mail-Anhang an deine private E-Mailadresse 8. Transferiere die PDF-Datei auf deinen Computer 9. Öffne diese Anwendung und füge die PDF ein 10. Klicke "ICS Datei erstellen" 11. Importiere die ICS-Datei in deinen Kalender ✓ Fertig!""", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP ) guide_text.SetBackgroundColour('#f8f9fa') sizer.Add(guide_text, 1, wx.EXPAND | wx.ALL, 10) # Buttons btn_sizer = wx.BoxSizer(wx.HORIZONTAL) online_btn = wx.Button(panel, label='Detaillierte Anleitung online') online_btn.Bind(wx.EVT_BUTTON, lambda e: webbrowser.open("https://git.file-archive.de/webfarben/pdf_to_ics")) btn_sizer.Add(online_btn, 0, wx.ALL, 5) close_btn = wx.Button(panel, wx.ID_CLOSE, 'Schließen') close_btn.Bind(wx.EVT_BUTTON, lambda e: guide_window.Close()) btn_sizer.Add(close_btn, 0, wx.ALL, 5) sizer.Add(btn_sizer, 0, wx.ALIGN_CENTER | wx.ALL, 10) panel.SetSizer(sizer) guide_window.ShowModal() guide_window.Destroy() def show_about_dialog(self, event=None): """Zeige About-Dialog mit Programminformationen""" version = get_current_version() info = wx.adv.AboutDialogInfo() info.SetName("PDF zu ICS Konverter") info.SetVersion(f"Version {version}") info.SetDescription( "Ein Programm zur Konvertierung von Dienstplan-PDFs " "zu ICS-Kalenderdateien für einfaches Importieren " "in Kalenderprogramme." ) info.SetWebSite("https://git.file-archive.de/webfarben/pdf_to_ics") info.AddDeveloper("Sebastian Köhler - Webfarben") info.SetLicence("Proprietär") wx.adv.AboutBox(info) def check_for_updates_background(self): """Prüfe auf Updates im Hintergrund""" try: update_available, new_version, download_url = check_for_updates() if update_available: wx.CallAfter(self.show_update_dialog, new_version, download_url) except Exception: pass def show_update_dialog(self, new_version, download_url): """Zeige Update-Dialog""" current_version = get_current_version() result = wx.MessageBox( f"Eine neue Version ist verfügbar!\n\n" f"Aktuelle Version: v{current_version}\n" f"Neue Version: v{new_version}\n\n" f"Möchten Sie die neue Version herunterladen?", "Update verfügbar", wx.YES_NO | wx.ICON_INFORMATION ) if result == wx.YES: webbrowser.open(download_url) def on_closing(self, event=None): """Handle für Fenster schließen""" self.save_config() self.Destroy() def main(): """Hauptfunktion""" app = wx.App() frame = PDFtoICSFrame() frame.Show() app.MainLoop() if __name__ == '__main__': main()