#!/usr/bin/env python3 """ GUI für PDF zu ICS Konverter Grafische Benutzeroberfläche mit Tkinter """ import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext from pathlib import Path import threading import re import json from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan # Versuche tkinterdnd2 zu importieren (optional für besseres Drag & Drop) try: from tkinterdnd2 import DND_FILES, TkinterDnD HAS_TKINTERDND = True except ImportError: HAS_TKINTERDND = False # Konfigurationsdatei CONFIG_FILE = Path.home() / '.pdf_to_ics_config.json' class PDFtoICSGUI: def __init__(self, root): self.root = root self.root.title("PDF zu ICS Konverter - Dienstplan Importer") self.root.geometry("800x600") self.root.minsize(700, 500) # 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 = tk.StringVar(value=str(default_dir)) # Letztes PDF-Verzeichnis merken self.last_pdf_dir = self.config.get('last_pdf_dir', str(Path.home())) # Exportoptionen self.exclude_rest = tk.BooleanVar(value=self.config.get('exclude_rest', False)) # UI erstellen self.create_widgets() # Drag & Drop einrichten self.setup_drag_and_drop() # Speichere Konfiguration beim Schließen self.root.protocol("WM_DELETE_WINDOW", self.on_closing) def create_widgets(self): """Erstelle die UI-Komponenten""" # Header header_frame = tk.Frame(self.root, bg="#2c3e50", height=80) header_frame.pack(fill=tk.X) header_frame.pack_propagate(False) title_label = tk.Label( header_frame, text="📅 PDF zu ICS Konverter", font=("Arial", 20, "bold"), bg="#2c3e50", fg="white" ) title_label.pack(pady=20) # Main Content Frame content_frame = tk.Frame(self.root, padx=20, pady=20) content_frame.pack(fill=tk.BOTH, expand=True) # PDF-Dateien Bereich pdf_label = tk.Label( content_frame, text="PDF-Dateien:", font=("Arial", 12, "bold") ) pdf_label.pack(anchor=tk.W, pady=(0, 5)) # Listbox mit Scrollbar für PDFs list_frame = tk.Frame(content_frame) list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10)) scrollbar = tk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.pdf_listbox = tk.Listbox( list_frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set, font=("Arial", 10) ) self.pdf_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.pdf_listbox.yview) # Buttons für PDF-Verwaltung button_frame = tk.Frame(content_frame) button_frame.pack(fill=tk.X, pady=(0, 10)) add_btn = tk.Button( button_frame, text="➕ PDF hinzufügen", command=self.add_pdf_files, bg="#3498db", fg="white", font=("Arial", 10, "bold"), padx=20, pady=8, cursor="hand2" ) add_btn.pack(side=tk.LEFT, padx=(0, 5)) remove_btn = tk.Button( button_frame, text="➖ Entfernen", command=self.remove_selected_pdfs, bg="#e74c3c", fg="white", font=("Arial", 10, "bold"), padx=20, pady=8, cursor="hand2" ) remove_btn.pack(side=tk.LEFT, padx=(0, 5)) clear_btn = tk.Button( button_frame, text="🗑️ Alle entfernen", command=self.clear_all_pdfs, bg="#95a5a6", fg="white", font=("Arial", 10, "bold"), padx=20, pady=8, cursor="hand2" ) clear_btn.pack(side=tk.LEFT) # Konvertieren Button (rechts in der Zeile) self.convert_btn = tk.Button( button_frame, text="📄 ICS Datei erstellen", command=self.convert_pdfs, bg="#27ae60", fg="white", font=("Arial", 10, "bold"), padx=20, pady=8, cursor="hand2" ) self.convert_btn.pack(side=tk.RIGHT) # Ausgabe-Verzeichnis output_frame = tk.Frame(content_frame) output_frame.pack(fill=tk.X, pady=(10, 10)) output_label = tk.Label( output_frame, text="Ausgabe-Verzeichnis:", font=("Arial", 10) ) output_label.pack(side=tk.LEFT, padx=(0, 5)) output_entry = tk.Entry( output_frame, textvariable=self.output_dir, font=("Arial", 10), state="readonly" ) output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) browse_btn = tk.Button( output_frame, text="📁 Durchsuchen", command=self.browse_output_dir, bg="#16a085", fg="white", font=("Arial", 10), padx=15, pady=5, cursor="hand2" ) browse_btn.pack(side=tk.LEFT) # Exportoptionen options_frame = tk.Frame(content_frame) options_frame.pack(fill=tk.X, pady=(10, 5)) exclude_rest_check = tk.Checkbutton( options_frame, text="🧘 Ruhetage ausschließen - (Ruhe, R56, R36, vRWF48, RWE, vR48)", variable=self.exclude_rest, font=("Arial", 10) ) exclude_rest_check.pack(anchor=tk.W) # Log-Bereich log_label = tk.Label( content_frame, text="Status:", font=("Arial", 10, "bold") ) log_label.pack(anchor=tk.W, pady=(10, 5)) self.log_text = scrolledtext.ScrolledText( content_frame, height=8, font=("Consolas", 9), bg="#f8f9fa", state=tk.DISABLED ) self.log_text.pack(fill=tk.BOTH, expand=True) # Fortschrittsbalken self.progress = ttk.Progressbar( content_frame, mode='determinate', length=300 ) drag_info = " (Drag & Drop unterstützt)" if HAS_TKINTERDND else " (Tipp: Installiere tkinterdnd2 für Drag & Drop)" self.log(f"Bereit. Fügen Sie PDF-Dateien hinzu um zu starten.{drag_info}") 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.get(), 'last_pdf_dir': self.last_pdf_dir, 'exclude_rest': self.exclude_rest.get() } 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 on_closing(self): """Handle für Fenster schließen""" self.save_config() self.root.destroy() def setup_drag_and_drop(self): """Richte Drag & Drop ein""" if HAS_TKINTERDND: # Verwende tkinterdnd2 wenn verfügbar self.pdf_listbox.drop_target_register(DND_FILES) self.pdf_listbox.dnd_bind('<>', self.drop_files) else: # Fallback: Native Tkinter Drag & Drop (funktioniert auf Unix) try: self.pdf_listbox.drop_target_register('DND_Files') self.pdf_listbox.dnd_bind('<>', self.drop_files) except: # Wenn auch das nicht funktioniert, zeige Hilfstext pass def drop_files(self, event): """Handle für Drag & Drop Events""" files = [] if HAS_TKINTERDND: # Parse tkinterdnd2 format files_str = event.data # Entferne geschweifte Klammern und splitte files_str = files_str.strip('{}') files = re.findall(r'[^\s{}]+(?:\s+[^\s{}]+)*', files_str) else: # Fallback parsing if hasattr(event, 'data'): files_str = event.data.strip('{}') files = [f.strip() for f in files_str.split()] # Füge nur PDF-Dateien hinzu pdf_count = 0 for file_path in files: file_path = file_path.strip() if file_path.lower().endswith('.pdf'): if file_path not in self.pdf_files: self.pdf_files.append(file_path) self.pdf_listbox.insert(tk.END, Path(file_path).name) pdf_count += 1 if pdf_count > 0: self.log(f"✓ {pdf_count} PDF-Datei(en) per Drag & Drop hinzugefügt") elif files: self.log("⚠ Nur PDF-Dateien können hinzugefügt werden") return 'break' def log(self, message): """Füge eine Nachricht zum Log hinzu""" self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, message + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) def add_pdf_files(self): """Öffne Datei-Dialog zum Hinzufügen von PDFs""" # Starte im letzten PDF-Verzeichnis initial_dir = self.last_pdf_dir if initial_dir.startswith('.') or not Path(initial_dir).exists(): initial_dir = str(Path.home()) files = filedialog.askopenfilenames( title="PDF-Dateien auswählen", filetypes=[("PDF Dateien", "*.pdf"), ("Alle Dateien", "*.*")], initialdir=initial_dir ) for file in files: if file not in self.pdf_files: self.pdf_files.append(file) self.pdf_listbox.insert(tk.END, Path(file).name) if files: # Merke Verzeichnis der ersten ausgewählten Datei self.last_pdf_dir = str(Path(files[0]).parent) self.log(f"✓ {len(files)} PDF-Datei(en) hinzugefügt") def remove_selected_pdfs(self): """Entferne ausgewählte PDFs aus der Liste""" selected = self.pdf_listbox.curselection() # Rückwärts durchlaufen, um Indexprobleme zu vermeiden for index in reversed(selected): self.pdf_listbox.delete(index) del self.pdf_files[index] if selected: self.log(f"✓ {len(selected)} PDF-Datei(en) entfernt") def clear_all_pdfs(self): """Entferne alle PDFs aus der Liste""" count = len(self.pdf_files) self.pdf_listbox.delete(0, tk.END) self.pdf_files.clear() if count > 0: self.log(f"✓ Alle {count} PDF-Datei(en) entfernt") def browse_output_dir(self): """Öffne Dialog zur Auswahl des Ausgabe-Verzeichnisses""" # Verhindere Start in versteckten Verzeichnissen initial_dir = self.output_dir.get() if initial_dir.startswith('.') or '/.venv' in initial_dir or '/__pycache__' in initial_dir: initial_dir = str(Path.home()) directory = filedialog.askdirectory( title="Ausgabe-Verzeichnis auswählen", initialdir=initial_dir, mustexist=True ) if directory: self.output_dir.set(directory) self.save_config() # Sofort speichern self.log(f"✓ Ausgabe-Verzeichnis: {directory}") def convert_pdfs(self): """Konvertiere alle PDFs zu ICS""" if not self.pdf_files: messagebox.showwarning( "Keine PDFs", "Bitte fügen Sie mindestens eine PDF-Datei hinzu." ) 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""" self.progress['maximum'] = len(self.pdf_files) self.progress['value'] = 0 output_dir = Path(self.output_dir.get()) 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.get()) self.log(f" ✓ ICS erstellt: {ics_filename}") success_count += 1 except Exception as e: self.log(f" ✗ Fehler: {str(e)}") finally: self.progress['value'] = i # Zusammenfassung self.log("\n" + "="*50) self.log(f"✅ Fertig! {success_count}/{len(self.pdf_files)} ICS-Dateien erstellt") self.log("="*50 + "\n") # Erfolgsmeldung if success_count > 0: self.root.after(0, lambda: messagebox.showinfo( "Konvertierung abgeschlossen", f"Es wurden {success_count} ICS-Datei(en) erfolgreich erstellt!\n\n" f"Speicherort: {output_dir}" )) def main(): """Hauptfunktion""" if HAS_TKINTERDND: # Verwende TkinterDnD root für besseres Drag & Drop root = TkinterDnD.Tk() else: # Standard Tkinter root = tk.Tk() app = PDFtoICSGUI(root) root.mainloop() if __name__ == '__main__': main()