Files
pdf_to_ics/gui_wxpython.py

517 lines
19 KiB
Python

#!/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.adv
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 FileDropTarget(wx.FileDropTarget):
"""Custom FileDropTarget für Drag & Drop von PDF-Dateien"""
def __init__(self, window):
wx.FileDropTarget.__init__(self)
self.window = window
def OnDropFiles(self, x, y, filenames):
"""Handle für Drag & Drop Events"""
pdf_count = 0
for filepath in filenames:
# Nur PDF-Dateien akzeptieren
if filepath.lower().endswith('.pdf'):
if filepath not in self.window.pdf_files:
self.window.pdf_files.append(filepath)
self.window.pdf_listbox.Append(Path(filepath).name)
pdf_count += 1
# Merke Verzeichnis
self.window.last_pdf_dir = str(Path(filepath).parent)
if pdf_count > 0:
self.window.log(f"{pdf_count} PDF-Datei(en) per Drag & Drop hinzugefügt")
elif filenames:
self.window.log("⚠ Nur PDF-Dateien können hinzugefügt werden")
return True
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(),
'exclude_vacation': self.exclude_vacation_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)
# Richte Drag & Drop ein
self.pdf_listbox.SetDropTarget(FileDropTarget(self))
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)
self.exclude_vacation_checkbox = wx.CheckBox(
panel,
label='🏖️ Urlaub ausschließen (060, 0060)'
)
self.exclude_vacation_checkbox.SetValue(self.config.get('exclude_vacation', False))
content_sizer.Add(self.exclude_vacation_checkbox, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 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(),
exclude_vacation=self.exclude_vacation_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()