Füge native wxPython GUI hinzu
- Erstelle gui_wxpython.py mit vollständiger nativer GUI-Integration - Funktioniert auf macOS 13.6 (im Gegensatz zu BeeWare/Toga) - Native Cocoa-Widgets auf macOS, Win32 auf Windows, GTK auf Linux - Alle Features der Tkinter-Version vollständig implementiert - Automatische Dark Mode Unterstützung - Thread-sichere UI-Updates mit wx.CallAfter - Native File-Dialoge und Menüleiste - Füge WXPYTHON_README.md mit vollständiger Dokumentation hinzu - Emojis aus Buttons entfernt für zuverlässige Darstellung - Einheitliches Button-Styling
This commit is contained in:
477
gui_wxpython.py
Normal file
477
gui_wxpython.py
Normal file
@@ -0,0 +1,477 @@
|
||||
#!/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.lib.scrolledpanel as scrolled
|
||||
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 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()
|
||||
}
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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()
|
||||
)
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user