Files
pdf_to_ics/gui.py
webfarben fa59ef5e8a refactor: Improve about dialog with better layout and clickable repository link
- Convert about dialog from messagebox to custom Toplevel window
- Improve text formatting and readability with proper spacing
- Make Git repository link clickable (opens in browser with webbrowser module)
- Better layout with aligned labels and structured information display
- Add close button for better user experience
2026-02-23 18:41:43 +01:00

637 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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("<Button-1>", 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=30,
pady=8,
cursor="hand2"
)
close_btn.pack(pady=(0, 15))
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('<<Drop>>', 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('<<Drop>>', 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()