#!/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 import webbrowser from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan from update_checker import check_for_updates, get_current_version # 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 = [] # Erstelle Menüleiste self.create_menu() # 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() # Update-Prüfung im Hintergrund starten update_thread = threading.Thread(target=self.check_for_updates_background, daemon=True) update_thread.start() # 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 create_menu(self): """Erstelle die Menüleiste""" menubar = tk.Menu(self.root) self.root.config(menu=menubar) # Hilfe-Menü help_menu = tk.Menu(menubar, tearoff=0) menubar.add_cascade(label="Hilfe", menu=help_menu) help_menu.add_command(label="Über dieses Programm", command=self.show_about_dialog) help_menu.add_separator() help_menu.add_command(label="Beenden", command=self.on_closing) def show_about_dialog(self): """Zeige About-Dialog mit Programminformationen""" about_window = tk.Toplevel(self.root) about_window.title("Über dieses Programm") about_window.geometry("500x350") about_window.resizable(False, False) # Zentriere das Fenster about_window.transient(self.root) about_window.grab_set() # Header header = tk.Label( about_window, text="PDF zu ICS Konverter", font=("Arial", 16, "bold"), bg="#2c3e50", fg="white", pady=15 ) header.pack(fill=tk.X) # Content Frame content = tk.Frame(about_window, padx=20, pady=20) content.pack(fill=tk.BOTH, expand=True) # Version version = get_current_version() version_label = tk.Label( content, text=f"Version {version}", font=("Arial", 11, "bold"), fg="#2c3e50" ) version_label.pack(anchor=tk.W, pady=(0, 15)) # Info-Texte info_texts = [ ("Firma:", "Webfarben"), ("Programmierer:", "Sebastian Köhler"), ("Kontakt:", "kontakt@webfarben.de"), ] for label, value in info_texts: frame = tk.Frame(content) frame.pack(anchor=tk.W, pady=3, fill=tk.X) label_widget = tk.Label( frame, text=label, font=("Arial", 10, "bold"), width=15, anchor=tk.W ) label_widget.pack(side=tk.LEFT) value_widget = tk.Label( frame, text=value, font=("Arial", 10), fg="#34495e" ) value_widget.pack(side=tk.LEFT, padx=(5, 0)) # Git Repository mit Link repo_frame = tk.Frame(content) repo_frame.pack(anchor=tk.W, pady=(15, 0), fill=tk.X) repo_label = tk.Label( repo_frame, text="Repository:", font=("Arial", 10, "bold"), width=15, anchor=tk.W ) repo_label.pack(side=tk.LEFT) repo_url = "https://git.file-archive.de/webfarben/pdf_to_ics.git" repo_link = tk.Label( repo_frame, text=repo_url, font=("Arial", 10, "underline"), fg="#3498db", cursor="hand2" ) repo_link.pack(side=tk.LEFT, padx=(5, 0)) repo_link.bind("", lambda e: webbrowser.open(repo_url)) # Beschreibung desc_frame = tk.Frame(content) desc_frame.pack(anchor=tk.W, pady=(20, 0), fill=tk.BOTH, expand=True) desc_text = tk.Text( desc_frame, height=4, font=("Arial", 9), fg="#34495e", wrap=tk.WORD, relief=tk.FLAT, bg=about_window.cget("bg") ) desc_text.insert(tk.END, "Ein Programm zur Konvertierung von Dienstplan-PDFs " "zu ICS-Kalenderdateien für einfaches Importieren " "in Kalenderprogramme.") desc_text.config(state=tk.DISABLED) desc_text.pack(fill=tk.BOTH, expand=True) # Close Button close_btn = tk.Button( about_window, text="Schließen", command=about_window.destroy, bg="#3498db", fg="white", font=("Arial", 10, "bold"), padx=50, pady=12, cursor="hand2" ) close_btn.pack(pady=(10, 15), fill=tk.X, padx=20) 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 check_for_updates_background(self): """Prüfe auf Updates im Hintergrund""" try: update_available, new_version, download_url = check_for_updates() if update_available: # Zeige Update-Dialog auf dem Main-Thread self.root.after(0, self.show_update_dialog, new_version, download_url) except Exception as e: # Stille Fehler ignorieren, damit GUI nicht beeinflusst wird pass def show_update_dialog(self, new_version, download_url): """Zeige Update-Dialog""" current_version = get_current_version() response = messagebox.showinfo( "Update verfügbar", 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?" ) if response == 'ok': # Dialog mit OK-Button import webbrowser webbrowser.open(download_url) 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()