517 lines
19 KiB
Python
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()
|