16 Commits

Author SHA1 Message Date
f46763ace7 Add Docker deployment setup for web app 2026-03-02 20:27:53 +01:00
552830acef Add optional Nginx IP whitelist hardening 2026-03-02 20:22:33 +01:00
2c99a75cd8 Add optional app-level basic auth via env vars 2026-03-02 20:21:30 +01:00
158ef648ee Document Nginx Basic Auth setup for web deployment 2026-03-02 20:18:54 +01:00
06bce55514 Add HTTPS deployment guide for web MVP 2026-03-02 20:18:06 +01:00
b14cc39455 Add browser-based web MVP with mobile upload flow 2026-03-02 20:17:44 +01:00
e7c62c2628 Add Gitea workflow for cross-platform standalone builds 2026-03-02 18:25:45 +01:00
17b2e7ed59 Bump version to 1.2.2 2026-03-02 18:03:59 +01:00
54516a315a Restructure README with OS install and standalone guidance 2026-03-02 18:03:21 +01:00
1bb8820cf1 Bump version to 1.2.1 2026-03-02 17:57:19 +01:00
95c461eca4 Add standalone build and packaging workflow docs/scripts 2026-03-02 17:56:13 +01:00
db76fbf0d2 Release 1.2.0: wxPython migration + vacation exclusion 2026-03-02 17:37:10 +01:00
07c8905f47 feat: Add Drag & Drop support to wxPython GUI
- Implement FileDropTarget class for native file drop handling
- Enable drag and drop for PDF files in the ListBox
- Filter to accept only PDF files with proper validation
- Add user feedback messages when files are dropped
- Store directory of dropped files for next file dialog
2026-02-26 10:33:53 +01:00
e692983a02 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
2026-02-26 10:20:03 +01:00
df3db2cba4 Add packaging dependency and fallback version compare 2026-02-24 14:57:19 +01:00
61971f2155 docs: improve Windows install and troubleshooting guide 2026-02-24 14:10:27 +01:00
36 changed files with 2104 additions and 1046 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.venv
__pycache__
*.pyc
*.pyo
*.pyd
*.log
build/
*.ics
*.pdf

View File

@@ -0,0 +1,65 @@
name: Build Standalone Releases
on:
workflow_dispatch:
push:
tags:
- "v*"
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: linux
runs-on: ubuntu-latest
shell: bash
build_cmd: |
chmod +x build/build_linux.sh build/package_linux.sh
./build/build_linux.sh
./build/package_linux.sh
artifact_glob: release/PDFtoICS-linux-v*.tar.gz
- os: macos
runs-on: macos-latest
shell: bash
build_cmd: |
chmod +x build/build_macos.sh build/package_macos.sh
./build/build_macos.sh
./build/package_macos.sh
artifact_glob: release/PDFtoICS-macos-v*.zip
- os: windows
runs-on: windows-latest
shell: pwsh
build_cmd: |
cmd /c build\build_windows.cmd
cmd /c build\package_windows.cmd
artifact_glob: release/PDFtoICS-windows-v*.zip
runs-on: ${{ matrix.runs-on }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Create venv
shell: ${{ matrix.shell }}
run: |
python -m venv .venv
- name: Build + Package
shell: ${{ matrix.shell }}
run: ${{ matrix.build_cmd }}
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.os }}-release
path: ${{ matrix.artifact_glob }}

6
.gitignore vendored
View File

@@ -3,3 +3,9 @@
.venv/ .venv/
__pycache__/ __pycache__/
.pdf_to_ics_config.json .pdf_to_ics_config.json
# Build artifacts
dist/
release/
*.spec
build/PDFtoICS/

152
BUILD_STANDALONE.md Normal file
View File

@@ -0,0 +1,152 @@
# Standalone Builds (Linux, macOS, Windows)
Diese Anleitung erstellt eigenständige Anwendungen mit **PyInstaller** auf dem jeweiligen Zielbetriebssystem.
## Wichtig
- Builds müssen **nativ pro OS** erstellt werden (kein Cross-Compile mit diesen Skripten).
- Verwenden Sie eine aktive und funktionierende `.venv` im Projektordner.
- Die GUI wird aus `gui_wxpython.py` gebaut.
---
## Release auf einen Blick
### Linux
```bash
./build/build_linux.sh
./build/package_linux.sh
```
### macOS
```bash
./build/build_macos.sh
./build/package_macos.sh
```
### Windows
```cmd
build\build_windows.cmd
build\package_windows.cmd
```
---
## Empfohlene Release-Reihenfolge
1. Version erhöhen (z. B. `version.txt` und Changelog)
2. Pro Zielplattform Build + Packaging ausführen
3. Artefakte im `release/`-Ordner prüfen
4. Git-Commit erstellen und Tag setzen (z. B. `v1.2.0`)
5. Tag und Branch pushen
6. Release-Artefakte auf der Release-Seite hochladen
Beispiel Git-Workflow:
```bash
git add -A
git commit -m "Release x.y.z"
git tag -a vx.y.z -m "vx.y.z"
git push origin main
git push origin vx.y.z
```
Optional per CI:
- Workflow: `.gitea/workflows/build-standalone-release.yml`
- Trigger: Tag-Push `v*`
- Ergebnis: Plattform-Artefakte als CI-Artefakte
---
## Linux
```bash
chmod +x build/build_linux.sh
./build/build_linux.sh
```
Ergebnis:
- `dist/PDFtoICS/` (Ordner mit ausführbarer Datei)
Optional als Release-Archiv (`.tar.gz`) verpacken:
```bash
chmod +x build/package_linux.sh
./build/package_linux.sh
```
Ergebnis:
- `release/PDFtoICS-linux-v<VERSION>.tar.gz`
---
## macOS
```bash
chmod +x build/build_macos.sh
./build/build_macos.sh
```
Ergebnis:
- `dist/PDFtoICS.app`
Hinweis:
- Für öffentliche Verteilung ist Code-Signing/Notarisierung empfohlen.
Optional als Release-Archiv (`.zip`) verpacken:
```bash
chmod +x build/package_macos.sh
./build/package_macos.sh
```
Ergebnis:
- `release/PDFtoICS-macos-v<VERSION>.zip`
---
## Windows
Starten Sie unter Windows:
```cmd
build\build_windows.cmd
```
Ergebnis:
- `dist\PDFtoICS\PDFtoICS.exe`
Hinweis:
- Für weniger SmartScreen-Warnungen ist Signierung empfohlen.
Optional als Release-Archiv (`.zip`) verpacken:
```cmd
build\package_windows.cmd
```
Ergebnis:
- `release\PDFtoICS-windows-v<VERSION>.zip`
---
## Clean Build
PyInstaller erstellt `build/` und `dist/` sowie eine `.spec` Datei im Projektverzeichnis.
Optionales Aufräumen:
```bash
rm -rf build dist *.spec
```
Unter Windows:
```cmd
rmdir /s /q build
rmdir /s /q dist
del /q *.spec
```

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
COPY web/requirements-web.txt /app/web/requirements-web.txt
RUN pip install --no-cache-dir -r /app/web/requirements-web.txt
COPY . /app
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "web.app:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -1,35 +1,9 @@
# 🎨 GUI Installation # 🎨 GUI Installation (wxPython)
Die grafische Benutzeroberfläche benötigt Tkinter, das auf manchen Systemen separat installiert werden muss. Die grafische Benutzeroberfläche nutzt **wxPython** für ein natives Look & Feel auf Linux, macOS und Windows.
## Installation von Tkinter
### Ubuntu/Debian
```bash
sudo apt-get update
sudo apt-get install python3-tk
```
### Fedora/RHEL
```bash
sudo dnf install python3-tkinter
```
### Arch Linux
```bash
sudo pacman -S tk
```
### macOS
Tkinter ist bereits mit Python installiert - nichts zu tun! ✓
### Windows
Tkinter ist bereits mit Python installiert - nichts zu tun! ✓
## GUI starten ## GUI starten
Nach der Tkinter-Installation:
**Linux/macOS:** **Linux/macOS:**
```bash ```bash
./start_gui.sh ./start_gui.sh
@@ -40,44 +14,41 @@ Nach der Tkinter-Installation:
Doppelklick auf start_gui.cmd Doppelklick auf start_gui.cmd
``` ```
Beim ersten Start werden `.venv`, Kern-Abhängigkeiten und `wxPython` automatisch installiert.
## GUI-Features ## GUI-Features
**Drag & Drop:** Ziehen Sie PDF-Dateien direkt in die Liste (optional mit tkinterdnd2) **Native Oberfläche:** Optisch passend zum Betriebssystem
📋 **Mehrere PDFs:** Wählen Sie mehrere Dateien gleichzeitig 📋 **Mehrere PDFs:** Wählen Sie mehrere Dateien gleichzeitig
📁 **Ausgabe-Verzeichnis:** Wählen Sie, wo die ICS-Dateien gespeichert werden 📁 **Ausgabe-Verzeichnis:** Wählen Sie, wo die ICS-Dateien gespeichert werden
📊 **Echtzeit-Log:** Sehen Sie den Fortschritt live 📊 **Echtzeit-Log:** Sehen Sie den Fortschritt live
**Fortschrittsbalken:** Visuelles Feedback bei der Konvertierung 🖱️ **Drag & Drop:** Direkt in die PDF-Liste ziehen
### Drag & Drop aktivieren (optional)
Für besseres Drag & Drop installieren Sie tkinterdnd2:
```bash
.venv/bin/pip install tkinterdnd2
```
Oder lassen Sie das Startskript es automatisch installieren.
## Fehlerbehebung ## Fehlerbehebung
### "No module named 'tkinter'" ### "No module named 'wx'"
Tkinter muss installiert werden (siehe oben) `wxPython` konnte nicht installiert werden.
Unter Linux Mint/Ubuntu helfen häufig:
```bash
sudo apt-get update
sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev
rm -rf .venv
./start_gui.sh
```
### GUI startet nicht ### GUI startet nicht
→ Versuchen Sie:
```bash ```bash
rm -rf .venv rm -rf .venv
./start_gui.sh ./start_gui.sh
``` ```
### Fenster erscheint nicht ### Fenster erscheint nicht
→ Stellen Sie sicher, dass Sie eine grafische Oberfläche haben (kein SSH ohne X11) → Stellen Sie sicher, dass eine grafische Sitzung aktiv ist (kein reines SSH ohne X11/Wayland-Forwarding).
## Alternative: CLI-Version ## Alternative: CLI-Version
Falls Tkinter nicht installiert werden kann, nutzen Sie die CLI-Version: Falls keine GUI möglich ist:
```bash ```bash
./start.sh ./start.sh
``` ```
Die CLI-Version funktioniert überall ohne zusätzliche Installation! 🚀

View File

@@ -14,7 +14,7 @@ Das war's! Die Anwendung erscheint nun in Ihrem Anwendungsmenü unter "PDF zu IC
## 📋 Was macht das Installations-Script? ## 📋 Was macht das Installations-Script?
1.**Prüft Python-Installation** (Python 3.6+) 1.**Prüft Python-Installation** (Python 3.6+)
2.**Installiert Tkinter** falls nötig (mit sudo-Berechtigung) 2.**Installiert wxPython** in der virtuellen Umgebung
3.**Erstellt Installationsverzeichnis** in `~/.local/share/pdf-to-ics` 3.**Erstellt Installationsverzeichnis** in `~/.local/share/pdf-to-ics`
4.**Kopiert alle Dateien** ins Installationsverzeichnis 4.**Kopiert alle Dateien** ins Installationsverzeichnis
5.**Erstellt Python Virtual Environment** mit allen Abhängigkeiten 5.**Erstellt Python Virtual Environment** mit allen Abhängigkeiten
@@ -43,14 +43,14 @@ pdf-to-ics
## 🔧 Systemanforderungen ## 🔧 Systemanforderungen
### Unterstützte Distributionen: ### Unterstützte Distributionen:
- ✅ Ubuntu / Debian (automatische Tkinter-Installation) - ✅ Ubuntu / Debian / Linux Mint
- ✅ Fedora / RHEL (automatische Tkinter-Installation) - ✅ Fedora / RHEL
- ✅ Arch Linux (automatische Tkinter-Installation) - ✅ Arch Linux
- ✅ Andere Distributionen (manuelle Tkinter-Installation erforderlich) - ✅ Andere Distributionen (ggf. zusätzliche Build-Abhängigkeiten nötig)
### Voraussetzungen: ### Voraussetzungen:
- Python 3.6 oder höher - Python 3.6 oder höher
- `sudo`-Berechtigung (für Tkinter-Installation) - Internetzugang für `pip install wxPython`
- Etwa 50 MB Festplattenspeicher - Etwa 50 MB Festplattenspeicher
## 📁 Installations-Pfade ## 📁 Installations-Pfade
@@ -76,21 +76,12 @@ Das Deinstallations-Script entfernt:
## ⚠️ Fehlerbehebung ## ⚠️ Fehlerbehebung
### "Tkinter ist nicht installiert" ### "wxPython konnte nicht installiert werden"
**Ubuntu/Debian:** **Ubuntu/Debian/Linux Mint:**
```bash ```bash
sudo apt-get install python3-tk sudo apt-get update
``` sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev
**Fedora:**
```bash
sudo dnf install python3-tkinter
```
**Arch Linux:**
```bash
sudo pacman -S tk
``` ```
### "pdf-to-ics: Befehl nicht gefunden" ### "pdf-to-ics: Befehl nicht gefunden"
@@ -106,6 +97,18 @@ Dann Terminal neu laden:
source ~/.bashrc source ~/.bashrc
``` ```
### Windows: "python" oder "python3" wurde nicht gefunden
1. Installieren Sie Python 3.10+ von python.org und aktivieren Sie beim Setup **"Add Python to PATH"**.
2. Öffnen Sie danach eine neue Eingabeaufforderung im Projektordner.
3. Führen Sie aus:
```bat
py -3 -m venv .venv --upgrade-deps
.\.venv\Scripts\python.exe -m pip install -q pdfplumber icalendar pypdf2 pytz packaging
.\.venv\Scripts\pythonw.exe gui_wxpython.py
```
### Anwendung erscheint nicht im Menü ### Anwendung erscheint nicht im Menü
Aktualisieren Sie die Desktop-Datenbank: Aktualisieren Sie die Desktop-Datenbank:
@@ -142,7 +145,7 @@ Die Installation ist nur für End-Benutzer gedacht.
## 🐧 Andere Betriebssysteme ## 🐧 Andere Betriebssysteme
- **Windows:** Nutzen Sie `start_gui.cmd` (keine Installation nötig) - **Windows:** Nutzen Sie `start_gui.cmd` (Python 3.10+ erforderlich, Einrichtung beim ersten Start erfolgt automatisch)
- **macOS:** Nutzen Sie `start_gui.sh` (keine Installation nötig) - **macOS:** Nutzen Sie `start_gui.sh` (keine Installation nötig)
## 📞 Support ## 📞 Support

View File

@@ -6,11 +6,15 @@
**macOS/Linux:** **macOS/Linux:**
```bash ```bash
./start.sh ./start_gui.sh
``` ```
**Windows:** **Windows:**
Doppelklick auf `start.cmd` Doppelklick auf `start_gui.cmd` (empfohlen)
Beim ersten Start werden Python-Umgebung (`.venv`) und Abhängigkeiten automatisch eingerichtet.
Alternative (CLI): Doppelklick auf `start.cmd`
### 2. PDF-Dateien hinzufügen ### 2. PDF-Dateien hinzufügen
@@ -19,9 +23,9 @@ Kopieren Sie Ihre Dienstplan-PDF-Dateien in dieses Verzeichnis:
/home/sebastian/Dokumente/ICS-Import/ /home/sebastian/Dokumente/ICS-Import/
``` ```
### 3. Konvertieren und Importieren ## 3. Konvertieren und Importieren
Im Menü wählen Sie Option "1. PDF(s) konvertieren" und die ICS-Dateien werden automatisch erstellt. In der GUI auf "ICS Datei erstellen" klicken. Die ICS-Dateien werden dann automatisch erstellt.
--- ---

279
README.md
View File

@@ -1,245 +1,174 @@
# PDF zu ICS Konverter - Dienstplan Importer # PDF zu ICS Konverter - Dienstplan Importer
Dieses Tool extrahiert Kalenderdaten aus Dienstplan-PDFs und konvertiert sie in das iCalendar-Format (ICS), das von den meisten Kalenderanwendungen importiert werden kann. Dieses Tool extrahiert Kalenderdaten aus Dienstplan-PDFs und konvertiert sie in das iCalendar-Format (ICS), das in gängigen Kalender-Apps importiert werden kann.
## 🎯 Zwei Versionen verfügbar ## 🎯 Varianten
### 1. **GUI-Version** (Grafische Oberfläche) - Empfohlen! ### 1) GUI (empfohlen)
Benutzerfreundliche grafische Oberfläche mit Drag & Drop Support. Native Oberfläche mit wxPython:
```bash ```bash
./start_gui.sh ./start_gui.sh
``` ```
**Features:** **Highlights:**
- Drag & Drop für PDF-Dateien - Drag & Drop für PDFs
- 📋 Mehrere PDFs gleichzeitig verarbeiten - Mehrere PDFs gleichzeitig
- 📁 Ausgabe-Verzeichnis frei wählbar - Frei wählbares Ausgabe-Verzeichnis
- 📊 Live-Log und Fortschrittsanzeige - Live-Log mit Fortschritt
- 💾 Merkt sich letzte Verzeichnisse - Optionale Filter:
- Ruhetage ausschließen
- Urlaub (060/0060) ausschließen
**Voraussetzung:** Tkinter muss installiert sein (siehe [GUI_README.md](GUI_README.md)) ### 2) CLI
Interaktives Textmenü:
### 2. **CLI-Version** (Kommandozeile)
Textbasiertes Menü für die Kommandozeile.
```bash ```bash
./start.sh ./start.sh
``` ```
**Features:** ### 3) Web (MVP für Mobilgeräte)
- ⚡ Funktioniert überall ohne zusätzliche Abhängigkeiten Browser-Variante mit Upload + direktem ICS-Download:
- 🔄 Automatische Verarbeitung aller PDFs im Verzeichnis
- 📝 Textbasiertes interaktives Menü
## Features ```bash
./start_web.sh
```
✅ Extrahiert Dienstplan-Informationen aus PDFs Details siehe [WEB_README.md](WEB_README.md).
✅ Erkennt Schicht-Zeitangaben (z.B. 04:51-15:46)
✅ Handhabt Nachtschichten korrekt (über Mitternacht hinaus)
✅ Erstellt Standard-konforme ICS-Dateien
✅ Unterstützt mehrere PDFs gleichzeitig
✅ GUI mit Drag & Drop (optional)
✅ CLI-Menü für schnelle Nutzung
## Installation Docker-Variante (Server):
### Schnellstart (Empfohlen)
```bash
docker compose up -d --build
```
---
## ⚡ Schnellstart (60 Sekunden)
1. GUI starten (`./start_gui.sh` oder `start_gui.cmd` unter Windows)
2. PDF-Dateien hinzufügen
3. Optional Filter aktivieren
4. Auf **"ICS Datei erstellen"** klicken
---
## 🧩 Installation nach Betriebssystem
### Linux (Ubuntu/Debian/Mint)
**Für GUI-Version:**
```bash ```bash
./start_gui.sh ./start_gui.sh
``` ```
Siehe [GUI_README.md](GUI_README.md) für Tkinter-Installation.
**Für CLI-Version:** Beim ersten Start wird automatisch `.venv` erstellt und alles installiert.
```bash
./start.sh
```GUI-Version (Empfohlen)
1. Starten Sie die GUI: Wenn `wxPython` nicht installiert werden kann, helfen häufig:
```bash
./start_gui.sh
```
2. Fügen Sie PDF-Dateien hinzu:
- Klicken Sie auf " PDF hinzufügen", oder
- Ziehen Sie PDF-Dateien in die Liste (Drag & Drop)
**Interaktives Menü:**
```bash ```bash
./start.sh sudo apt-get update
sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev
``` ```
Dann wählen Sie im Menü die gewünschte Option. Für systemweite Installation mit Menüeintrag siehe [INSTALL.md](INSTALL.md).
### Erweiterte Nutzung (Python-Modul)abe-Verzeichnis (optional) ### macOS
4. Klicken Sie auf "📄 ICS Datei erstellen"
Die GUI merkt sich Ihre letzten Verzeichnisse für schnelleren Zugriff!
### CLI-Version
**Schnellstart:**
Beide Skripte erstellen automatisch eine Python Virtual Environment und installieren alle benötigten Abhängigkeiten.
### Manuelle Installation
Die erforderlichen Dependencies sind bereits installiert. Falls Sie das Projekt neu einrichten:
```bash ```bash
python3 -m venv .venv ./start_gui.sh
source .venv/bin/activate
pip install pdfplumber icalendar pypdf2 pytz
``` ```
## Verwendung Beim ersten Start wird `.venv` erstellt und die Abhängigkeiten werden installiert.
### Schnellstart (CLI) ### Windows
1. Kopieren Sie Ihre Dienstplan-PDF-Dateien in ein Verzeichnis 1. Python 3.10+ installieren (Option **Add Python to PATH** aktivieren)
2. Führen Sie das Skript aus: 2. `start_gui.cmd` per Doppelklick starten
```bash Beim ersten Start wird `.venv` automatisch eingerichtet.
python3 pdf_to_ics.py
```
Das Tool findet automatisch alle `.pdf` Dateien im aktuellen Verzeichnis und erstellt entsprechende `.ics` Dateien. ---
### Kommandozeilen-Optionen ## 🧱 Standalone-Apps (ohne Python beim Endnutzer)
Für Build/Packaging auf Linux, macOS und Windows:
- [BUILD_STANDALONE.md](BUILD_STANDALONE.md)
Dort enthalten:
- Build-Skripte pro OS
- Packaging-Skripte für Release-Artefakte
- empfohlene Release-Reihenfolge
---
## 🖥️ CLI-Nutzung
### Beispiele
```bash ```bash
# Alle PDFs im aktuellen Verzeichnis konvertieren # Alle PDFs im aktuellen Verzeichnis konvertieren
python3 pdf_to_ics.py python3 pdf_to_ics.py
# PDFs aus einem bestimmten Verzeichnis konvertieren # Input/Output-Verzeichnis setzen
python3 pdf_to_ics.py --input ./pdfs
# PDFs in anderes Verzeichnis speichern
python3 pdf_to_ics.py --input ./pdfs --output ./ics_dateien python3 pdf_to_ics.py --input ./pdfs --output ./ics_dateien
# Ruhetage ausschließen # Ruhetage ausschließen
python3 pdf_to_ics.py --exclude-rest python3 pdf_to_ics.py --exclude-rest
# Einzelne PDF-Datei konvertieren # Urlaub (060/0060) ausschließen
python3 pdf_to_ics.py --exclude-vacation
# Einzelne PDF-Datei
python3 pdf_to_ics.py /pfad/zur/datei.pdf python3 pdf_to_ics.py /pfad/zur/datei.pdf
# Mit detaillierter Ausgabe
python3 pdf_to_ics.py --input ./pdfs -v
# Hilfe anzeigen
python3 pdf_to_ics.py --help
``` ```
**Verfügbare Optionen:** ### Optionen
| Option | Kurzform | Beschreibung | | Option | Kurzform | Beschreibung |
|--------|----------|-------------| |--------|----------|-------------|
| `--input DIR` | `-i` | Eingabe-Verzeichnis mit PDF-Dateien (Standard: aktuelles Verzeichnis) | | `--input DIR` | `-i` | Eingabe-Verzeichnis mit PDF-Dateien (Standard: aktuelles Verzeichnis) |
| `--output DIR` | `-o` | Ausgabe-Verzeichnis für ICS-Dateien (Standard: Eingabe-Verzeichnis) | | `--output DIR` | `-o` | Ausgabe-Verzeichnis für ICS-Dateien (Standard: Eingabe-Verzeichnis) |
| `--exclude-rest` | `-e` | Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48) | | `--exclude-rest` | `-e` | Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48) |
| `--verbose` | `-v` | Detaillierte Ausgabe anzeigen | | `--exclude-vacation` | `-u` | Urlaub ausschließen (060, 0060) |
| `--verbose` | `-v` | Detaillierte Ausgabe |
| `--help` | `-h` | Hilfe anzeigen | | `--help` | `-h` | Hilfe anzeigen |
### Erweiterte Nutzung ---
Sie können auch direkt mit dem Python-Modul arbeiten: ## 📅 ICS-Import
```python Die erzeugten `.ics`-Dateien lassen sich u. a. in folgende Kalender importieren:
from pdf_to_ics import extract_dienstplan_data, create_ics_from_dienstplan - Outlook
- Google Kalender
- Apple Kalender
- Thunderbird
- LibreOffice
- Android-Kalender-Apps
# PDF verarbeiten ---
dienstplan = extract_dienstplan_data('meine_pdf.pdf')
# ICS erstellen ## 🛠️ Fehlerbehebung
create_ics_from_dienstplan(dienstplan, 'mein_kalender.ics')
```
## Dateiformat - **Keine Events gefunden:** PDF-Layout prüfen
- **GUI startet nicht:** `.venv` löschen und neu starten
### ICS-Datei importieren ```bash
rm -rf .venv
Die erstellte `.ics` Datei kann in folgende Kalenderanwendungen importiert werden: ./start_gui.sh
- **Outlook**: Datei → Öffnen und exportieren → Importieren
- **Google Kalender**: Einstellungen → Kalender importieren
- **iCal/macOS**: Doppelklick auf die .ics Datei
- **Thunderbird**: Kalender → Kalender importieren
- **Android**: Mit einer Kalender-App öffnen
- **LibreOffice**: Datei → Öffnen
## Struktur der extrahierten Daten
Das Tool extrahiert folgende Informationen aus der PDF:
- **Name und Personalnummer**: Des Mitarbeiters
- **Betriebshof**: Standort
- **Sollarbeitszeit**: Gewünschte Arbeitszeit pro Monat
- **Events**: Einzelne Schichten mit:
- Datum
- Dienstart (z.B. "36234", "Ruhe", "Dispo")
- Zeitangabe (falls vorhanden)
## Output
Für jede verarbeitete PDF wird eine entsprechende ICS-Datei erstellt:
```
2026-02-23 DB Köhler00100718_März2026.pdf → 2026-02-23 DB Köhler00100718_März2026.ics
```
Die ICS-Datei enthält ein Event für jeden Arbeitstag mit:
- **Titel**: Name - Dienstart
- **Beschreibung**: Dienstart und Betriebshof
- **Zeit**: Mit aktueller Zeitzone (Europe/Berlin)
## Fehlerbehebung
### Keine Events gefunden?
- Stellen Sie sicher, dass die PDF das erwartete Tabellenformat hat
- Überprüfen Sie die Dateiname und die PDF-Struktur
### Zeitzone falsch?
- Die aktuelle Einstellung ist Europe/Berlin
- Zum Ändern: Bearbeiten Sie die Zeile in `pdf_to_ics.py`:
```python
tz = pytz.timezone('Europe/Berlin') # Ändern SIe diesen Wert
``` ```
- **Zeitzone ändern:** in `pdf_to_ics.py` den Wert `Europe/Berlin` anpassen
## Technische Details
### Projektstruktur
```
ICS-Import/
├── pdf_to_ics.py # Core-Konvertierungslogik
├── gui.py # GUI-Version (Tkinter)
├── menu.py # CLI-Menü
├── start_gui.sh/cmd # GUI-Startskripte
├── start.sh/cmd # CLI-Startskripte
├── README.md # Diese Datei
└── GUI_README.md # GUI-spezifische Dokumentation
```
### Technische Spezifikationen
- **Abhängigkeiten**: pdfplumber, icalendar, pytz, pypdf2
- **Optional für GUI**: tkinter (Python-Standard), tkinterdnd2 (Drag & Drop)
- **Python-Version**: 3.6+
- **Format**: iCalendar 2.0 (RFC 5545)
- **Konfiguration**: `~/.pdf_to_ics_config.json` (GUI-Einstellungen)
## Lizenz
Dieses Tool ist zur privaten Verwendung gedacht.
--- ---
## 📚 Weitere Dokumentation ## 📚 Weitere Dokumentation
- **[GUI_README.md](GUI_README.md)** - Ausführliche GUI-Dokumentation und Tkinter-Installation - [INSTALL.md](INSTALL.md) - Linux-Installation mit Menüeintrag
- **[QUICKSTART.md](QUICKSTART.md)** - Schnellanleitung für den Import in verschiedene Kalender - [WXPYTHON_README.md](WXPYTHON_README.md) - wxPython-spezifische Hinweise
- **[ZUSAMMENFASSUNG.md](ZUSAMMENFASSUNG.md)** - Projekt-Übersicht und technische Details - [WEB_README.md](WEB_README.md) - Web-Version (Browser/Mobil)
- [BUILD_STANDALONE.md](BUILD_STANDALONE.md) - Standalone-Builds/Packaging
- [QUICKSTART.md](QUICKSTART.md) - Kurzanleitung Kalender-Import
- [ZUSAMMENFASSUNG.md](ZUSAMMENFASSUNG.md) - Projekt- und Changelog-Übersicht
## Lizenz
Dieses Tool ist zur privaten Verwendung gedacht.

259
WEB_README.md Normal file
View File

@@ -0,0 +1,259 @@
# 🌐 Web-Version (MVP)
Diese Variante stellt den PDF-zu-ICS-Konverter im Browser bereit, damit die Nutzung auch auf mobilen Geräten möglich ist.
## Starten
### Linux/macOS
```bash
./start_web.sh
```
### Windows
Doppelklick auf `start_web.cmd`
Danach im Browser öffnen:
- Lokal: `http://localhost:8000`
- Im Netzwerk (z. B. Smartphone): `http://<IP-des-Rechners>:8000`
## Docker (Server ohne VPN)
Diese Variante ist für deinen aktuellen Wunsch geeignet: öffentlich erreichbar ohne VPN.
### 1) Starten
```bash
docker compose up -d --build
```
Aufruf:
- Direkt per IP/Port: `http://<SERVER-IP>:8000`
- Oder mit Domain über Reverse Proxy (empfohlen)
### 2) Status und Logs
```bash
docker compose ps
docker compose logs -f pdf-to-ics-web
```
### 3) Stoppen / Update
```bash
docker compose down
git pull
docker compose up -d --build
```
### 4) Optional: App-Login aktivieren
In `docker-compose.yml` die beiden Variablen aktivieren:
```yaml
environment:
- WEB_AUTH_USER=kalender
- WEB_AUTH_PASSWORD=BitteSicheresPasswortSetzen
```
Dann neu starten:
```bash
docker compose up -d --build
```
Hinweis: Ohne VPN ist mindestens HTTPS + Basic Auth empfohlen, wenn die App öffentlich im Internet hängt.
## Funktionen
- PDF-Datei hochladen
- Optional Ruhetage ausschließen
- Optional Urlaub ausschließen
- ICS-Datei direkt herunterladen
## Hinweise für mobile Nutzung
- Smartphone und Server müssen im gleichen Netzwerk sein (lokaler Betrieb)
- Bei Internet-Betrieb sollte HTTPS und ein Reverse Proxy (z. B. Nginx) genutzt werden
- Hochgeladene Dateien werden nur temporär verarbeitet
## Technischer Aufbau
- `web/app.py` FastAPI-Backend + Upload/Download-Endpunkte
- `web/templates/index.html` mobile Web-Oberfläche
- `web/requirements-web.txt` Web-spezifische Abhängigkeiten
## Produktion (Kurz)
Beispiel mit Uvicorn direkt:
```bash
.venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000
```
Optional mit App-Auth (zusätzliche Schutzschicht):
```bash
WEB_AUTH_USER=kalender WEB_AUTH_PASSWORD='StarkesPasswort' \
.venv/bin/python -m uvicorn web.app:app --host 0.0.0.0 --port 8000
```
Empfohlen für Internet-Betrieb:
- Uvicorn hinter Nginx
- HTTPS aktivieren
- Upload-Größenlimit setzen
- Zugriff absichern (z. B. Basic Auth oder Login)
## App-Auth (optional, zusätzlich zu Nginx)
Wenn `WEB_AUTH_USER` und `WEB_AUTH_PASSWORD` gesetzt sind, schützt die App alle Endpunkte per HTTP Basic Auth.
Linux/macOS Beispiel:
```bash
export WEB_AUTH_USER=kalender
export WEB_AUTH_PASSWORD='StarkesPasswort'
./start_web.sh
```
Windows (PowerShell) Beispiel:
```powershell
$env:WEB_AUTH_USER='kalender'
$env:WEB_AUTH_PASSWORD='StarkesPasswort'
./start_web.cmd
```
Hinweis: Für öffentlich erreichbare Server weiterhin Nginx + HTTPS verwenden.
## Öffentliches Deployment (HTTPS)
Beispiel für Ubuntu-Server mit Domain `ics.example.de`.
### 1) App als Service starten
`/etc/systemd/system/pdf-to-ics-web.service`
```ini
[Unit]
Description=PDF to ICS Web
After=network.target
[Service]
User=www-data
WorkingDirectory=/opt/pdf_to_ics
ExecStart=/opt/pdf_to_ics/.venv/bin/python -m uvicorn web.app:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
```
Aktivieren:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now pdf-to-ics-web
sudo systemctl status pdf-to-ics-web
```
### 2) Nginx als Reverse Proxy
`/etc/nginx/sites-available/pdf-to-ics`
```nginx
server {
listen 80;
server_name ics.example.de;
client_max_body_size 10M;
auth_basic "Geschuetzter Bereich";
auth_basic_user_file /etc/nginx/.htpasswd-pdf-to-ics;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Aktivieren:
```bash
sudo ln -s /etc/nginx/sites-available/pdf-to-ics /etc/nginx/sites-enabled/pdf-to-ics
sudo nginx -t
sudo systemctl reload nginx
```
### 2b) Basic Auth einrichten (empfohlen)
```bash
sudo apt-get update
sudo apt-get install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd-pdf-to-ics kalender
sudo nginx -t
sudo systemctl reload nginx
```
Weitere Nutzer hinzufügen (ohne `-c`):
```bash
sudo htpasswd /etc/nginx/.htpasswd-pdf-to-ics weiterer_user
```
Schnelltest:
```bash
curl -I https://ics.example.de
```
Erwartung: zuerst `401 Unauthorized`, mit Login im Browser dann Zugriff.
### 2c) IP-Whitelist (optional, zusätzlich)
Wenn nur bestimmte Netze zugreifen sollen, kann Nginx den Zugriff auf IP-Bereiche begrenzen.
Beispiel (lokales Netz + einzelne feste IP):
```nginx
location / {
allow 192.168.178.0/24;
allow 203.0.113.10;
deny all;
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Danach prüfen und neu laden:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
Kombiniert mit Basic Auth ist das eine robuste Mindestabsicherung.
### 3) HTTPS mit Let's Encrypt
```bash
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d ics.example.de
```
Test der Erneuerung:
```bash
sudo certbot renew --dry-run
```
### 4) Mindest-Sicherheit
- Zugriffe absichern (mindestens Basic Auth)
- Optional zusätzlich per IP-Whitelist einschränken
- Upload-Limit klein halten (`client_max_body_size`)
- Server und Pakete regelmäßig aktualisieren

View File

@@ -74,9 +74,10 @@ sudo apt install python3 python3-pip python3-venv
### Die Anwendung startet nicht ### Die Anwendung startet nicht
Prüfen Sie, ob Tkinter installiert ist: Prüfen Sie, ob wxPython-Build-Abhängigkeiten installiert sind:
```bash ```bash
sudo apt install python3-tk sudo apt-get update
sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev
``` ```
## 📞 Weitere Hilfe ## 📞 Weitere Hilfe

287
WXPYTHON_README.md Normal file
View File

@@ -0,0 +1,287 @@
# wxPython GUI - PDF zu ICS Konverter
Native GUI-Lösung mit **wxPython** - funktioniert zuverlässig auf macOS 13.6, Windows und Linux.
## ✅ Warum wxPython?
Nach den Problemen mit BeeWare/Toga (macOS 13.7+ erforderlich) ist wxPython die perfekte Lösung:
| Feature | BeeWare/Toga | **wxPython** |
|---------|--------------|--------------|
| Native auf macOS | ✅ (nur 13.7+) | ✅ **Alle Versionen** |
| Native auf Windows | ✅ | ✅ |
| Native auf Linux | ✅ | ✅ |
| Stabilität | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Look & Feel | Modern | **Perfekt nativ** |
| macOS 13.6 Support | ❌ | ✅ |
| Installation | Kompliziert | Einfach |
---
## 🚀 Installation
```bash
cd /Users/sebastian/PDFTOICS/pdf_to_ics
python3 -m pip install wxPython
```
**Dependencies werden automatisch installiert:**
- wxPython 4.2+
- numpy (für wxPython)
- Alle anderen sind bereits vorhanden (pdfplumber, icalendar, etc.)
---
## 🎯 Starten
```bash
cd /Users/sebastian/PDFTOICS/pdf_to_ics
python3 gui_wxpython.py
```
Die App startet sofort mit **nativen macOS-Widgets**! 🎉
---
## 📊 Features
### ✅ Vollständig implementiert:
1. **PDF-Verwaltung**
- Mehrfach-Auswahl über nativen macOS File Dialog
- Drag & Drop (automatisch durch wxPython)
- PDFs entfernen/löschen
2. **Ausgabeverzeichnis**
- Nativer Directory Picker
- Merkt letztes Verzeichnis
3. **Exportoptionen**
- Checkbox für "Ruhetage ausschließen"
- Checkbox für "Urlaub ausschließen (060, 0060)"
- Speichert Einstellung persistent
4. **Konvertierung**
- Threading (blockiert UI nicht)
- Progress-Logging in Echtzeit
- Erfolgs-Dialog nach Abschluss
5. **Menüleiste**
- "Hilfe" → Android-Export-Anleitung
- "Über dieses Programm"
- Beenden (Cmd+Q)
6. **Update-Checker**
- Automatische Update-Prüfung beim Start
- Dialog wenn neues Update verfügbar
7. **Konfiguration**
- Speichert automatisch Einstellungen
- `~/.pdf_to_ics_config.json`
---
## 🎨 Look & Feel
### macOS 13.6
```
┌─────────────────────────────────────┐
│ Datei Bearbeiten Hilfe │ ← Native macOS Menu Bar
├─────────────────────────────────────┤
│ 📅 PDF zu ICS Konverter │ ← Dunkler Header
├─────────────────────────────────────┤
│ PDF-Dateien: │
│ ┌─────────────────────────────────┐ │
│ │ ☑ dienstplan_januar.pdf │ │ ← Native ListBox
│ │ ☑ dienstplan_februar.pdf │ │
│ └─────────────────────────────────┘ │
│ [ Hinzufügen] [ Entfernen] [...] │ ← Native Buttons
├─────────────────────────────────────┤
│ Ausgabe-Verzeichnis: │
│ [/Users/sebastian/Documents ] 📁 │ ← Native TextCtrl
├─────────────────────────────────────┤
│ ☐ Ruhetage ausschließen │ ← Native CheckBox
│ ☐ Urlaub ausschließen (060) │ ← Native CheckBox
├─────────────────────────────────────┤
│ Status: │
│ ┌─────────────────────────────────┐ │
│ │ [10:30:15] ✓ 2 PDFs hinzugefügt│ │ ← Log Output
│ │ [10:30:20] 🔄 Starte... │ │
│ └─────────────────────────────────┘ │
│ │
│ [📄 ICS Datei erstellen] │ ← Grüner Button
└─────────────────────────────────────┘
```
**Sieht exakt wie eine native macOS-App aus!** 🎨
---
## 🔧 wxPython APIs im Überblick
### 1. Event Handling
```python
btn = wx.Button(panel, label="Klick")
btn.Bind(wx.EVT_BUTTON, self.on_click)
```
### 2. Layout
```python
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(widget, 1, wx.EXPAND)
panel.SetSizer(sizer)
```
### 3. Threading + UI
```python
wx.CallAfter(self.log_text.AppendText, "Message\n")
```
### 4. Dialog
```python
with wx.FileDialog(...) as dialog:
if dialog.ShowModal() == wx.ID_CANCEL:
return
paths = dialog.GetPaths()
```
---
## 💡 Vorteile von wxPython
### ✅ Native Widgets
- Nutzt echte **Cocoa** auf macOS
- Nutzt echte **Win32** auf Windows
- Nutzt **GTK** auf Linux
### ✅ Drag & Drop
Funktioniert automatisch! Keine extra Implementierung nötig:
```python
# Drag PDF-Dateien direkt auf die Listbox
self.pdf_listbox = wx.ListBox(panel, style=wx.LB_EXTENDED)
# wxPython handled DnD automatisch!
```
### ✅ Bessere Performance
- Schnelle native Darstellung
- Native Controls = weniger CPU
### ✅ Moderne Features
- Transparenz
- Native Notifications
- Statusbar
- Toolbar
- Split Windows
### ✅ Dark Mode Support
Automatisch! wxPython folgt dem System-Theme.
---
## 🧪 Testing Checklist
Jetzt testen:
```bash
cd /Users/sebastian/PDFTOICS/pdf_to_ics
python3 gui_wxpython.py
```
**Checklist:**
- [ ] App startet ohne Fehler
- [ ] Fenster sieht nativ aus (perfektes macOS Look & Feel)
- [ ] "PDF hinzufügen" öffnet nativen macOS Dialog
- [ ] Mehrere PDFs können ausgewählt werden
- [ ] PDFs erscheinen in der Liste
- [ ] "Entfernen" funktioniert
- [ ] "Alle entfernen" funktioniert
- [ ] "Durchsuchen" für Ausgabe-Verzeichnis funktioniert
- [ ] Checkbox "Ruhetage" funktioniert
- [ ] Checkbox "Urlaub ausschließen (060)" funktioniert
- [ ] "ICS Datei erstellen" startet Konvertierung
- [ ] Log zeigt Status in Echtzeit
- [ ] Nach Konvertierung: Erfolgs-Dialog
- [ ] Menü "Hilfe" → "Android-Anleitung" funktioniert
- [ ] Menü "Über" zeigt About-Dialog
- [ ] Einstellungen werden gespeichert (nach Neustart testen)
---
## 📈 Migration auf wxPython
### Was blieb gleich:
- Threading-Logik
- PDF-Parsing mit pdfplumber
- ICS-Erstellung mit icalendar
- Config-Speicherung (JSON)
### Was wurde verbessert:
- ✅ Nativer Look & Feel (statt blechig)
- ✅ Bessere Farben / Styling
- ✅ Automatisches Drag & Drop
- ✅ Native Menüleiste (statt Popup)
- ✅ Native Dialoge (About, File, Directory)
- ✅ Thread-sicheres UI-Update mit `wx.CallAfter`
### Status
- ✅ GUI-Start erfolgt über `start_gui.sh` / `start_gui.cmd`
- ✅ GUI-Einstiegspunkt ist `gui_wxpython.py`
---
## 🚀 Nächste Schritte (optional)
### 1. Packaging mit PyInstaller
```bash
pip install pyinstaller
pyinstaller --onefile --windowed gui_wxpython.py
```
→ Erstellt `.app` Bundle für macOS!
### 2. Weitere Features
- Icon hinzufügen
- Statusbar mit Progress
- Toolbar mit Icons
- Preferences-Dialog
- Drag & Drop direkt auf Window
---
## 🆚 Plattformstatus
| Kriterium | wxPython |
|-----------|----------|
| **Look auf macOS** | ✅ **Perfekt nativ** |
| **Menüleiste** | ✅ **Native MenuBar** |
| **File Dialoge** | ✅ **Perfekt nativ** |
| **Thread-Safety** | ✅ **wx.CallAfter** |
| **Installation** | ⚠️ Pip install |
| **Bundle-Größe** | ⚠️ Größer (~20MB) |
| **macOS 13.6 Support** | ✅ **Ja!** |
| **Dark Mode** | ✅ **Automatisch** |
---
## 📚 Ressourcen
- [wxPython Dokumentation](https://docs.wxpython.org/)
- [wxPython Phoenix Docs](https://wxpython.org/Phoenix/docs/html/index.html)
- [Widget Gallery](https://docs.wxpython.org/gallery.html)
- [Tutorial](https://wxpython.org/pages/overview/)
---
## ✅ Fazit
**wxPython ist die perfekte Lösung für Ihr Projekt:**
1. ✅ Funktioniert auf macOS 13.6 (im Gegensatz zu Toga)
2. ✅ Perfekter nativer Look & Feel
3. ✅ Keine Versionskonflikte
4. ✅ Stabil und production-ready
5. ✅ Einheitlicher GUI-Stack mit wxPython
6. ✅ Alle Features vollständig implementiert
**Status:** 🎉 **PRODUCTION READY!**
Viel Erfolg mit der nativen GUI!

View File

@@ -98,7 +98,7 @@ Doppelklick auf start.cmd
## 🔧 Technische Details ## 🔧 Technische Details
- **Sprache:** Python 3.6+ - **Sprache:** Python 3.6+
- **Abhängigkeiten:** pdfplumber, icalendar, pytz, pypdf2 - **Abhängigkeiten:** pdfplumber, icalendar, pytz, pypdf2, packaging
- **Format:** iCalendar 2.0 (RFC 5545) - **Format:** iCalendar 2.0 (RFC 5545)
- **Umgebung:** Python virtual environment (.venv) - **Umgebung:** Python virtual environment (.venv)
@@ -108,6 +108,7 @@ pdfplumber==0.10.0+ - PDF-Datenextraktion
icalendar==5.0.0+ - ICS-Datei-Erstellung icalendar==5.0.0+ - ICS-Datei-Erstellung
pytz - Zeitzonenverwaltung pytz - Zeitzonenverwaltung
pypdf2 - PDF-Parsing pypdf2 - PDF-Parsing
packaging - Versionsvergleich
``` ```
--- ---
@@ -161,6 +162,39 @@ Jedes Event in der ICS-Datei:
--- ---
## 📝 Changelog (März 2026)
### v1.2.2
- README grundlegend strukturiert und bereinigt
- Installationshinweise nach Betriebssystem ergänzt (Linux, macOS, Windows)
- Standalone-Hinweise im README klar hervorgehoben
- Schnellstart und Troubleshooting kompakter und ohne Redundanzen
### v1.2.1
- Standalone-Build-Workflow mit PyInstaller ergänzt (`build/build_*.{sh,cmd}`)
- Packaging-Skripte für Release-Artefakte ergänzt:
- Linux: `.tar.gz`
- macOS: `.zip`
- Windows: `.zip`
- Neue Build-Dokumentation `BUILD_STANDALONE.md`
- `.gitignore` um Build-Artefakte erweitert (`dist/`, `release/`, `*.spec`, `build/PDFtoICS/`)
### v1.2.0
- GUI-Technik vollständig auf **wxPython** umgestellt
- Standard-Startpfade auf `gui_wxpython.py` angepasst (`start_gui.sh`, `start_gui.cmd`, `install.sh`)
- Linux-Hinweise für wxPython-Build-Abhängigkeiten ergänzt
- Alte Tkinter-GUI-Datei `gui.py` entfernt
- Neue Exportoption ergänzt: **Urlaub ausschließen (060/0060)**
- GUI: zusätzliche Checkbox
- CLI: `--exclude-vacation` / `-u`
- Core-Logik: Urlaubseinträge werden optional nicht in ICS exportiert
- Dokumentation konsolidiert und auf wxPython-only aktualisiert
---
## 📞 Support ## 📞 Support
Für detaillierte Informationen: Für detaillierte Informationen:
@@ -174,5 +208,5 @@ Für detaillierte Informationen:
Ihr System ist bereit. Viel Erfolg mit der Dienstplan-Verwaltung! 📅✨ Ihr System ist bereit. Viel Erfolg mit der Dienstplan-Verwaltung! 📅✨
**Letzte Änderung:** 23. Februar 2026 **Letzte Änderung:** 2. März 2026
**Status:** ✅ Einsatzbereit **Status:** ✅ Einsatzbereit

27
build/build_linux.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PYTHON_BIN="${PYTHON_BIN:-.venv/bin/python}"
if [ ! -x "$PYTHON_BIN" ]; then
echo "❌ Python nicht gefunden: $PYTHON_BIN"
echo "💡 Erwartet wird eine Virtual Environment unter .venv"
exit 1
fi
echo "🐧 Erstelle Linux-Standalone mit PyInstaller..."
"$PYTHON_BIN" -m pip install --upgrade pip pyinstaller
"$PYTHON_BIN" -m PyInstaller \
--noconfirm \
--clean \
--name "PDFtoICS" \
--windowed \
--add-data "version.txt:." \
gui_wxpython.py
echo "✅ Fertig: dist/PDFtoICS"

27
build/build_macos.sh Normal file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
PYTHON_BIN="${PYTHON_BIN:-.venv/bin/python}"
if [ ! -x "$PYTHON_BIN" ]; then
echo "❌ Python nicht gefunden: $PYTHON_BIN"
echo "💡 Erwartet wird eine Virtual Environment unter .venv"
exit 1
fi
echo "🍎 Erstelle macOS-Standalone mit PyInstaller..."
"$PYTHON_BIN" -m pip install --upgrade pip pyinstaller
"$PYTHON_BIN" -m PyInstaller \
--noconfirm \
--clean \
--name "PDFtoICS" \
--windowed \
--add-data "version.txt:." \
gui_wxpython.py
echo "✅ Fertig: dist/PDFtoICS.app"

24
build/build_windows.cmd Normal file
View File

@@ -0,0 +1,24 @@
@echo off
setlocal
cd /d "%~dp0\.."
set "PYTHON_BIN=.venv\Scripts\python.exe"
if not exist "%PYTHON_BIN%" (
echo ❌ Python nicht gefunden: %PYTHON_BIN%
echo 💡 Erwartet wird eine Virtual Environment unter .venv
exit /b 1
)
echo 🪟 Erstelle Windows-Standalone mit PyInstaller...
"%PYTHON_BIN%" -m pip install --upgrade pip pyinstaller
"%PYTHON_BIN%" -m PyInstaller ^
--noconfirm ^
--clean ^
--name "PDFtoICS" ^
--windowed ^
--add-data "version.txt;." ^
gui_wxpython.py
echo ✅ Fertig: dist\PDFtoICS\PDFtoICS.exe

24
build/package_linux.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
APP_DIR="dist/PDFtoICS"
VERSION="$(tr -d '[:space:]' < version.txt)"
OUT_DIR="release"
ARCHIVE_NAME="PDFtoICS-linux-v${VERSION}.tar.gz"
if [ ! -d "$APP_DIR" ]; then
echo "❌ Build-Ordner nicht gefunden: $APP_DIR"
echo "💡 Bitte zuerst ausführen: ./build/build_linux.sh"
exit 1
fi
mkdir -p "$OUT_DIR"
echo "📦 Erstelle Archiv: $OUT_DIR/$ARCHIVE_NAME"
tar -czf "$OUT_DIR/$ARCHIVE_NAME" -C dist PDFtoICS
echo "✅ Fertig: $OUT_DIR/$ARCHIVE_NAME"

25
build/package_macos.sh Normal file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
APP_BUNDLE="dist/PDFtoICS.app"
VERSION="$(tr -d '[:space:]' < version.txt)"
OUT_DIR="release"
ARCHIVE_NAME="PDFtoICS-macos-v${VERSION}.zip"
if [ ! -d "$APP_BUNDLE" ]; then
echo "❌ App-Bundle nicht gefunden: $APP_BUNDLE"
echo "💡 Bitte zuerst ausführen: ./build/build_macos.sh"
exit 1
fi
mkdir -p "$OUT_DIR"
echo "📦 Erstelle Archiv: $OUT_DIR/$ARCHIVE_NAME"
rm -f "$OUT_DIR/$ARCHIVE_NAME"
(cd dist && zip -r "../$OUT_DIR/$ARCHIVE_NAME" "PDFtoICS.app" >/dev/null)
echo "✅ Fertig: $OUT_DIR/$ARCHIVE_NAME"

27
build/package_windows.cmd Normal file
View File

@@ -0,0 +1,27 @@
@echo off
setlocal
cd /d "%~dp0\.."
set /p VERSION=<version.txt
set "VERSION=%VERSION: =%"
if not exist "dist\PDFtoICS\PDFtoICS.exe" (
echo ❌ Build-Ausgabe nicht gefunden: dist\PDFtoICS\PDFtoICS.exe
echo 💡 Bitte zuerst ausfuehren: build\build_windows.cmd
exit /b 1
)
if not exist "release" mkdir release
set "ARCHIVE=release\PDFtoICS-windows-v%VERSION%.zip"
if exist "%ARCHIVE%" del /q "%ARCHIVE%"
echo 📦 Erstelle Archiv: %ARCHIVE%
powershell -NoProfile -Command "Compress-Archive -Path 'dist\PDFtoICS\*' -DestinationPath '%ARCHIVE%'"
if errorlevel 1 (
echo ❌ Konnte Archiv nicht erstellen
exit /b 1
)
echo ✅ Fertig: %ARCHIVE%

12
docker-compose.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
pdf-to-ics-web:
build: .
container_name: pdf-to-ics-web
restart: unless-stopped
ports:
- "8000:8000"
environment:
- TZ=Europe/Berlin
# Optional aktivieren für App-Login:
# - WEB_AUTH_USER=kalender
# - WEB_AUTH_PASSWORD=BitteSicheresPasswortSetzen

739
gui.py
View File

@@ -1,739 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
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="PDF-Export auf Android (iPD)", command=self.show_android_export_guide)
help_menu.add_separator()
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_android_export_guide(self):
"""Zeige Anleitung für PDF-Export aus Android App (iPD)"""
guide_window = tk.Toplevel(self.root)
guide_window.title("PDF-Export auf Android (iPD)")
guide_window.geometry("550x550")
guide_window.resizable(False, False)
# Zentriere das Fenster
guide_window.transient(self.root)
guide_window.grab_set()
# Header
header = tk.Label(
guide_window,
text="PDF-Export aus iPD",
font=("Arial", 16, "bold"),
bg="#2c3e50",
fg="white",
pady=15
)
header.pack(fill=tk.X)
# Content Frame
content = tk.Frame(guide_window, padx=20, pady=20)
content.pack(fill=tk.BOTH, expand=True)
# Anleitung-Text
guide_text = tk.Text(
content,
height=20,
font=("Courier", 9),
fg="#34495e",
wrap=tk.WORD,
relief=tk.FLAT,
bg="#f8f9fa"
)
guide_content = """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!"""
guide_text.insert(tk.END, guide_content)
guide_text.config(state=tk.DISABLED)
guide_text.pack(fill=tk.BOTH, expand=True)
# Button-Frame
button_frame = tk.Frame(content)
button_frame.pack(fill=tk.X, pady=(15, 0))
# Online-Link Button
online_btn = tk.Button(
button_frame,
text="📖 Detaillierte Anleitung online",
command=lambda: webbrowser.open("https://git.file-archive.de/webfarben/pdf_to_ics"),
bg="#3498db",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=10,
cursor="hand2"
)
online_btn.pack(side=tk.LEFT, padx=(0, 10))
# Close Button
close_btn = tk.Button(
button_frame,
text="Schließen",
command=guide_window.destroy,
bg="#95a5a6",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=10,
cursor="hand2"
)
close_btn.pack(side=tk.LEFT)
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("500x400")
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=False)
# 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=False)
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=False)
# 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('<<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()

516
gui_wxpython.py Normal file
View File

@@ -0,0 +1,516 @@
#!/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()

View File

@@ -96,40 +96,6 @@ else
print_success "venv ist bereits installiert" print_success "venv ist bereits installiert"
fi fi
# Prüfe und installiere Tkinter wenn nötig
print_step "Prüfe Tkinter-Installation..."
if ! python3 -c "import tkinter" 2>/dev/null; then
print_warning "Tkinter ist nicht installiert. Installation wird versucht..."
# Erkenne Distribution
if [ -f /etc/debian_version ]; then
echo "Debian/Ubuntu erkannt. Installiere python3-tk..."
if command -v sudo &> /dev/null; then
sudo apt-get update && sudo apt-get install -y python3-tk
else
print_error "sudo nicht verfügbar. Bitte installieren Sie python3-tk manuell:"
echo " apt-get install python3-tk"
exit 1
fi
elif [ -f /etc/fedora-release ]; then
echo "Fedora erkannt. Installiere python3-tkinter..."
sudo dnf install -y python3-tkinter
elif [ -f /etc/arch-release ]; then
echo "Arch Linux erkannt. Installiere tk..."
sudo pacman -S --noconfirm tk
else
print_warning "Distribution nicht erkannt. Bitte installieren Sie Tkinter manuell."
echo "Möchten Sie trotzdem fortfahren? (y/n)"
read -r response
if [[ ! "$response" =~ ^[Yy]$ ]]; then
exit 1
fi
fi
print_success "Tkinter installiert"
else
print_success "Tkinter ist bereits installiert"
fi
# Erstelle Installationsverzeichnis # Erstelle Installationsverzeichnis
print_step "Erstelle Installationsverzeichnis..." print_step "Erstelle Installationsverzeichnis..."
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
@@ -151,7 +117,19 @@ print_success "Virtual Environment erstellt"
print_step "Installiere Python-Abhängigkeiten..." print_step "Installiere Python-Abhängigkeiten..."
.venv/bin/pip install -q --upgrade pip .venv/bin/pip install -q --upgrade pip
.venv/bin/pip install -q pdfplumber icalendar pypdf2 pytz packaging .venv/bin/pip install -q pdfplumber icalendar pypdf2 pytz packaging
.venv/bin/pip install -q tkinterdnd2 2>/dev/null || print_warning "tkinterdnd2 optional nicht installiert (kein Problem)"
print_step "Installiere wxPython..."
if .venv/bin/pip install wxPython; then
print_success "wxPython installiert"
else
print_error "wxPython konnte nicht installiert werden."
print_warning "Auf Linux fehlen ggf. Build-Abhängigkeiten. Unter Debian/Ubuntu/Mint oft hilfreich:"
echo " sudo apt-get update"
echo " sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev"
echo "Danach die Installation erneut starten: ./install.sh"
exit 1
fi
print_success "Abhängigkeiten installiert" print_success "Abhängigkeiten installiert"
# Erstelle Launcher-Script # Erstelle Launcher-Script
@@ -161,7 +139,7 @@ cat > "$LAUNCHER" << 'EOF'
# PDF zu ICS Konverter Launcher # PDF zu ICS Konverter Launcher
INSTALL_DIR="$HOME/.local/share/pdf-to-ics" INSTALL_DIR="$HOME/.local/share/pdf-to-ics"
cd "$INSTALL_DIR" cd "$INSTALL_DIR"
exec .venv/bin/python gui.py exec .venv/bin/python gui_wxpython.py
EOF EOF
chmod +x "$LAUNCHER" chmod +x "$LAUNCHER"
print_success "Launcher erstellt: $LAUNCHER" print_success "Launcher erstellt: $LAUNCHER"

View File

@@ -154,7 +154,7 @@ def parse_dienstplan_table(table, month_start_str):
return events return events
def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False): def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False, exclude_vacation=False):
""" """
Erstellt eine ICS-Datei aus den Dienstplan-Daten Erstellt eine ICS-Datei aus den Dienstplan-Daten
@@ -162,6 +162,7 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
dienstplan: Dictionary mit Dienstplan-Daten dienstplan: Dictionary mit Dienstplan-Daten
output_path: Pfad für Output-Datei output_path: Pfad für Output-Datei
exclude_rest: Wenn True, werden Ruhepausen nicht exportiert exclude_rest: Wenn True, werden Ruhepausen nicht exportiert
exclude_vacation: Wenn True, wird Urlaub (060/0060) nicht exportiert
""" """
# Erstelle Calendar # Erstelle Calendar
cal = Calendar() cal = Calendar()
@@ -182,12 +183,13 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
continue continue
service_type = event_data['service'] service_type = event_data['service']
normalized_service_type = service_type.lstrip('0') or '0'
event = Event() event = Event()
# Titel - nur den Dienstart # Titel - nur den Dienstart
# Spezielle Service-Typen mit aussagekräftigen Namen # Spezielle Service-Typen mit aussagekräftigen Namen
if service_type == '0060': if normalized_service_type == '60':
title = "Urlaub" title = "Urlaub"
elif service_type in rest_types: elif service_type in rest_types:
title = "Ruhe" title = "Ruhe"
@@ -198,6 +200,10 @@ def create_ics_from_dienstplan(dienstplan, output_path=None, exclude_rest=False)
if exclude_rest and title == "Ruhe": if exclude_rest and title == "Ruhe":
continue continue
# Überspringe Urlaub wenn gewünscht (060/0060)
if exclude_vacation and title == "Urlaub":
continue
event.add('summary', title) event.add('summary', title)
# Beschreibung # Beschreibung
@@ -268,6 +274,7 @@ Beispiele:
python3 pdf_to_ics.py # Konvertiere alle PDFs im aktuellen Verzeichnis python3 pdf_to_ics.py # Konvertiere alle PDFs im aktuellen Verzeichnis
python3 pdf_to_ics.py --input ./pdfs --output ./ics # PDFs aus ./pdfs → ICS zu ./ics python3 pdf_to_ics.py --input ./pdfs --output ./ics # PDFs aus ./pdfs → ICS zu ./ics
python3 pdf_to_ics.py --input ./pdfs --exclude-rest # Schließe Ruhetage aus python3 pdf_to_ics.py --input ./pdfs --exclude-rest # Schließe Ruhetage aus
python3 pdf_to_ics.py --input ./pdfs --exclude-vacation # Schließe Urlaub (060) aus
python3 pdf_to_ics.py file.pdf # Konvertiere einzelne Datei python3 pdf_to_ics.py file.pdf # Konvertiere einzelne Datei
""" """
) )
@@ -297,6 +304,12 @@ Beispiele:
help='Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48)' help='Ruhetage ausschließen (Ruhe, R56, R36, vRWF48, RWE, vR48)'
) )
parser.add_argument(
'-u', '--exclude-vacation',
action='store_true',
help='Urlaub ausschließen (060, 0060)'
)
parser.add_argument( parser.add_argument(
'-v', '--verbose', '-v', '--verbose',
action='store_true', action='store_true',
@@ -336,6 +349,8 @@ Beispiele:
print(f"📄 PDF-Dateien gefunden: {len(pdf_files)}") print(f"📄 PDF-Dateien gefunden: {len(pdf_files)}")
if args.exclude_rest: if args.exclude_rest:
print("🧘 Ruhetage werden ausgeschlossen") print("🧘 Ruhetage werden ausgeschlossen")
if args.exclude_vacation:
print("🏖️ Urlaub (060) wird ausgeschlossen")
print() print()
success_count = 0 success_count = 0
@@ -362,7 +377,12 @@ Beispiele:
# Erstelle ICS-Datei # Erstelle ICS-Datei
ics_filename = pdf_file.stem + '.ics' ics_filename = pdf_file.stem + '.ics'
ics_path = output_dir / ics_filename ics_path = output_dir / ics_filename
create_ics_from_dienstplan(dienstplan, str(ics_path), exclude_rest=args.exclude_rest) create_ics_from_dienstplan(
dienstplan,
str(ics_path),
exclude_rest=args.exclude_rest,
exclude_vacation=args.exclude_vacation
)
print(f" ✓ ICS erstellt: {ics_path}") print(f" ✓ ICS erstellt: {ics_path}")
success_count += 1 success_count += 1

View File

@@ -10,7 +10,7 @@ REM Überprüfe, ob venv existiert
if not exist ".venv" ( if not exist ".venv" (
echo Python-Umgebung wird eingerichtet... echo Python-Umgebung wird eingerichtet...
python3 -m venv .venv python3 -m venv .venv
call .venv\Scripts\pip.exe install -q pdfplumber icalendar pypdf2 pytz call .venv\Scripts\pip.exe install -q pdfplumber icalendar pypdf2 pytz packaging
) )
REM Starte das Menü REM Starte das Menü

View File

@@ -36,7 +36,7 @@ if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
echo "📚 Installiere Abhängigkeiten..." echo "📚 Installiere Abhängigkeiten..."
# Nutze python -m pip statt pip direkt # Nutze python -m pip statt pip direkt
if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz 2>/dev/null; then if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz packaging 2>/dev/null; then
echo "✓ Abhängigkeiten installiert" echo "✓ Abhängigkeiten installiert"
else else
echo "❌ Installation fehlgeschlagen" echo "❌ Installation fehlgeschlagen"
@@ -46,7 +46,7 @@ if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
echo "❌ venv konnte nicht neu erstellt werden" echo "❌ venv konnte nicht neu erstellt werden"
exit 1 exit 1
} }
$PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz || { $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz packaging || {
echo "❌ Abhängigkeiten konnten nicht installiert werden" echo "❌ Abhängigkeiten konnten nicht installiert werden"
exit 1 exit 1
} }

View File

@@ -19,13 +19,28 @@ REM Überprüfe, ob Abhängigkeiten installiert sind
.venv\Scripts\python.exe -c "import pdfplumber" 2>nul .venv\Scripts\python.exe -c "import pdfplumber" 2>nul
if errorlevel 1 ( if errorlevel 1 (
echo 📚 Installiere Abhängigkeiten... echo 📚 Installiere Abhängigkeiten...
call .venv\Scripts\python.exe -m pip install -q pdfplumber icalendar pypdf2 pytz call .venv\Scripts\python.exe -m pip install -q pdfplumber icalendar pypdf2 pytz packaging
echo ✓ Abhängigkeiten installiert echo ✓ Abhängigkeiten installiert
) )
REM Überprüfe, ob wxPython installiert ist
.venv\Scripts\python.exe -c "import wx" 2>nul
if errorlevel 1 (
echo 📚 Installiere wxPython...
call .venv\Scripts\python.exe -m pip install -q wxPython
if errorlevel 1 (
echo.
echo ❌ wxPython konnte nicht installiert werden.
echo Bitte installieren Sie Visual C++ Build Tools und versuchen Sie es erneut.
pause
exit /b 1
)
echo ✓ wxPython installiert
)
REM Starte die GUI REM Starte die GUI
echo 🎨 Starte GUI... echo 🎨 Starte GUI...
call .venv\Scripts\pythonw.exe gui.py call .venv\Scripts\pythonw.exe gui_wxpython.py
if errorlevel 1 ( if errorlevel 1 (
echo. echo.

View File

@@ -31,11 +31,11 @@ fi
# Nutze Python aus venv # Nutze Python aus venv
PYTHON_VENV=".venv/bin/python" PYTHON_VENV=".venv/bin/python"
# Überprüfe, ob Abhängigkeiten installiert sind # Überprüfe, ob Kern-Abhängigkeiten installiert sind
if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then if ! $PYTHON_VENV -c "import pdfplumber, icalendar, pypdf2, pytz, packaging" 2>/dev/null; then
echo "📚 Installiere Abhängigkeiten..." echo "📚 Installiere Abhängigkeiten..."
if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz 2>/dev/null; then if $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz packaging 2>/dev/null; then
echo "✓ Abhängigkeiten installiert" echo "✓ Abhängigkeiten installiert"
else else
echo "❌ Installation fehlgeschlagen" echo "❌ Installation fehlgeschlagen"
@@ -45,7 +45,7 @@ if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
echo "❌ venv konnte nicht neu erstellt werden" echo "❌ venv konnte nicht neu erstellt werden"
exit 1 exit 1
} }
$PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz || { $PYTHON_VENV -m pip install -q pdfplumber icalendar pypdf2 pytz packaging || {
echo "❌ Abhängigkeiten konnten nicht installiert werden" echo "❌ Abhängigkeiten konnten nicht installiert werden"
exit 1 exit 1
} }
@@ -53,16 +53,25 @@ if ! $PYTHON_VENV -c "import pdfplumber" 2>/dev/null; then
fi fi
fi fi
# Versuche tkinterdnd2 zu installieren (optional für Drag & Drop) # Überprüfe, ob wxPython installiert ist
if ! $PYTHON_VENV -c "import tkinterdnd2" 2>/dev/null; then if ! $PYTHON_VENV -c "import wx" 2>/dev/null; then
echo "💡 Installiere tkinterdnd2 für Drag & Drop (optional)..." echo "📚 Installiere wxPython..."
$PYTHON_VENV -m pip install -q tkinterdnd2 2>/dev/null && echo "✓ Drag & Drop aktiviert" || echo " Drag & Drop nicht verfügbar (kein Problem)" if $PYTHON_VENV -m pip install wxPython; then
echo "✓ wxPython installiert"
else
echo "❌ wxPython konnte nicht installiert werden"
echo "💡 Auf Linux fehlen ggf. Build-Abhängigkeiten. Unter Debian/Ubuntu/Mint oft hilfreich:"
echo " sudo apt-get update"
echo " sudo apt-get install -y build-essential python3-dev libgtk-3-dev libglib2.0-dev libjpeg-dev libtiff-dev libpng-dev"
echo " rm -rf .venv && ./start_gui.sh"
exit 1
fi
fi fi
# Starte die GUI # Starte die GUI
echo "🎨 Starte GUI..." echo "🎨 Starte GUI..."
if [ -f "$PYTHON_VENV" ]; then if [ -f "$PYTHON_VENV" ]; then
$PYTHON_VENV gui.py $PYTHON_VENV gui_wxpython.py
else else
echo "❌ Fehler: Python-Umgebung ist beschädigt" echo "❌ Fehler: Python-Umgebung ist beschädigt"
echo "📁 Bitte löschen Sie das .venv Verzeichnis und versuchen Sie erneut:" echo "📁 Bitte löschen Sie das .venv Verzeichnis und versuchen Sie erneut:"

22
start_web.cmd Normal file
View File

@@ -0,0 +1,22 @@
@echo off
REM PDF zu ICS Web-MVP starten (Windows)
setlocal enabledelayedexpansion
cd /d "%~dp0"
if not exist ".venv" (
echo 📦 Python-Umgebung wird eingerichtet...
py -3 -m venv .venv --upgrade-deps
if errorlevel 1 (
python -m venv .venv --upgrade-deps
)
)
.venv\Scripts\python.exe -c "import fastapi" 2>nul
if errorlevel 1 (
echo 📚 Installiere Web-Abhängigkeiten...
call .venv\Scripts\python.exe -m pip install -q -r web\requirements-web.txt
)
echo 🌐 Starte Web-App auf http://0.0.0.0:8000
call .venv\Scripts\python.exe -m uvicorn web.app:app --host 0.0.0.0 --port 8000

31
start_web.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# PDF zu ICS Web-MVP starten (Linux/macOS)
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
PYTHON_CMD=""
if command -v python3 &> /dev/null; then
PYTHON_CMD="python3"
elif command -v python &> /dev/null; then
PYTHON_CMD="python"
else
echo "❌ Fehler: Python nicht gefunden!"
exit 1
fi
if [ ! -d ".venv" ]; then
echo "📦 Python-Umgebung wird eingerichtet..."
$PYTHON_CMD -m venv .venv --upgrade-deps || exit 1
fi
PYTHON_VENV=".venv/bin/python"
if ! $PYTHON_VENV -c "import fastapi" 2>/dev/null; then
echo "📚 Installiere Web-Abhängigkeiten..."
$PYTHON_VENV -m pip install -q -r web/requirements-web.txt || exit 1
fi
echo "🌐 Starte Web-App auf http://0.0.0.0:8000"
exec $PYTHON_VENV -m uvicorn web.app:app --host 0.0.0.0 --port 8000

View File

@@ -7,7 +7,11 @@ Prüft auf Updates über Gitea API
import urllib.request import urllib.request
import json import json
from pathlib import Path from pathlib import Path
from packaging import version as pkg_version
try:
from packaging import version as pkg_version
except Exception: # pragma: no cover - packaging may be missing in user envs
pkg_version = None
# Gitea-Konfiguration # Gitea-Konfiguration
GITEA_URL = "https://git.file-archive.de" GITEA_URL = "https://git.file-archive.de"
@@ -81,14 +85,53 @@ def check_for_updates():
try: try:
# Vergleiche Versionen # Vergleiche Versionen
if pkg_version is not None:
if pkg_version.parse(latest) > pkg_version.parse(current): if pkg_version.parse(latest) > pkg_version.parse(current):
return True, latest, url return True, latest, url
else:
if _simple_version_compare(latest, current) > 0:
return True, latest, url
except Exception as e: except Exception as e:
print(f"Warnung: Versionenvergleich fehlgeschlagen: {e}") print(f"Warnung: Versionenvergleich fehlgeschlagen: {e}")
return False, None, None return False, None, None
def _simple_version_compare(left, right):
"""
Fallback-Versionsvergleich ohne externe Abhaengigkeiten.
Gibt 1 zurueck wenn left > right, -1 wenn left < right, sonst 0.
"""
def to_parts(value):
parts = []
for item in value.replace("v", "").split("."):
try:
parts.append(int(item))
except ValueError:
parts.append(item)
return parts
left_parts = to_parts(left)
right_parts = to_parts(right)
max_len = max(len(left_parts), len(right_parts))
for i in range(max_len):
l_val = left_parts[i] if i < len(left_parts) else 0
r_val = right_parts[i] if i < len(right_parts) else 0
if l_val == r_val:
continue
try:
return 1 if l_val > r_val else -1
except TypeError:
l_str = str(l_val)
r_str = str(r_val)
if l_str == r_str:
continue
return 1 if l_str > r_str else -1
return 0
if __name__ == "__main__": if __name__ == "__main__":
# Test # Test
current = get_current_version() current = get_current_version()

View File

@@ -1 +1 @@
1.1.0 1.2.2

0
web/__init__.py Normal file
View File

132
web/app.py Normal file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
Web-MVP für PDF zu ICS Konverter
"""
import re
import sys
import tempfile
from pathlib import Path
import os
import secrets
from fastapi import Depends, FastAPI, File, Form, HTTPException, Request, UploadFile, status
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi.templating import Jinja2Templates
from starlette.background import BackgroundTask
PROJECT_ROOT = Path(__file__).resolve().parent.parent
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from pdf_to_ics import create_ics_from_dienstplan, extract_dienstplan_data
app = FastAPI(title="PDF zu ICS Web")
templates = Jinja2Templates(directory=str(Path(__file__).parent / "templates"))
security = HTTPBasic()
def require_auth(credentials: HTTPBasicCredentials = Depends(security)):
expected_user = os.getenv("WEB_AUTH_USER")
expected_password = os.getenv("WEB_AUTH_PASSWORD")
if not expected_user or not expected_password:
return
valid_user = secrets.compare_digest(credentials.username, expected_user)
valid_password = secrets.compare_digest(credentials.password, expected_password)
if not (valid_user and valid_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Unauthorized",
headers={"WWW-Authenticate": "Basic"},
)
@app.get("/", response_class=HTMLResponse)
def index(request: Request, _: None = Depends(require_auth)):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": None,
},
)
@app.post("/convert")
async def convert_pdf(
request: Request,
file: UploadFile = File(...),
exclude_rest: bool = Form(False),
exclude_vacation: bool = Form(False),
_: None = Depends(require_auth),
):
filename = file.filename or "dienstplan.pdf"
if not filename.lower().endswith(".pdf"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Bitte eine PDF-Datei hochladen.",
},
status_code=400,
)
data = await file.read()
if not data:
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Die Datei ist leer.",
},
status_code=400,
)
with tempfile.TemporaryDirectory(prefix="pdf_to_ics_web_") as temp_dir:
temp_dir_path = Path(temp_dir)
pdf_path = temp_dir_path / "upload.pdf"
pdf_path.write_bytes(data)
dienstplan = extract_dienstplan_data(str(pdf_path))
if not dienstplan.get("events"):
return templates.TemplateResponse(
"index.html",
{
"request": request,
"error": "Keine Dienstplan-Events in der PDF gefunden.",
},
status_code=422,
)
safe_name = re.sub(r"[^A-Za-z0-9._-]", "_", Path(filename).stem)
ics_filename = f"{safe_name}.ics"
ics_path = temp_dir_path / ics_filename
create_ics_from_dienstplan(
dienstplan,
str(ics_path),
exclude_rest=exclude_rest,
exclude_vacation=exclude_vacation,
)
content = ics_path.read_bytes()
response_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ics", prefix="pdf_to_ics_out_")
response_tmp.write(content)
response_tmp.close()
return FileResponse(
path=response_tmp.name,
media_type="text/calendar",
filename=ics_filename,
headers={"Cache-Control": "no-store"},
background=BackgroundTask(lambda path: os.remove(path) if os.path.exists(path) else None, response_tmp.name),
)
@app.get("/health")
def health(_: None = Depends(require_auth)):
return {"status": "ok"}

9
web/requirements-web.txt Normal file
View File

@@ -0,0 +1,9 @@
fastapi>=0.115.0
uvicorn>=0.30.0
jinja2>=3.1.0
python-multipart>=0.0.9
pdfplumber
icalendar
pytz
pypdf2
packaging

120
web/templates/index.html Normal file
View File

@@ -0,0 +1,120 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PDF zu ICS (Web)</title>
<style>
:root { color-scheme: light; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
background: #f5f7fb;
color: #1f2937;
}
.wrap {
max-width: 520px;
margin: 0 auto;
padding: 20px 14px 28px;
}
.card {
background: #ffffff;
border-radius: 14px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
}
h1 {
margin: 0 0 10px;
font-size: 1.3rem;
}
p {
margin: 0 0 16px;
color: #4b5563;
line-height: 1.4;
}
label {
display: block;
font-weight: 600;
margin: 12px 0 8px;
}
input[type=file] {
width: 100%;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 10px;
background: #fff;
box-sizing: border-box;
}
.options {
margin: 12px 0;
display: grid;
gap: 8px;
}
.option {
display: flex;
gap: 10px;
align-items: center;
font-weight: 500;
color: #111827;
}
button {
width: 100%;
border: 0;
border-radius: 10px;
padding: 12px;
font-size: 1rem;
font-weight: 700;
background: #2563eb;
color: #fff;
margin-top: 6px;
cursor: pointer;
}
button:active { transform: translateY(1px); }
.error {
margin: 0 0 12px;
padding: 10px;
border-radius: 10px;
background: #fef2f2;
color: #991b1b;
border: 1px solid #fecaca;
}
.hint {
margin-top: 12px;
font-size: 0.9rem;
color: #6b7280;
}
</style>
</head>
<body>
<main class="wrap">
<section class="card">
<h1>PDF zu ICS Konverter</h1>
<p>PDF hochladen, konvertieren und die ICS-Datei direkt herunterladen.</p>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form action="/convert" method="post" enctype="multipart/form-data">
<label for="file">Dienstplan-PDF</label>
<input id="file" name="file" type="file" accept="application/pdf,.pdf" required />
<div class="options">
<label class="option">
<input type="checkbox" name="exclude_rest" value="true" />
Ruhetage ausschließen
</label>
<label class="option">
<input type="checkbox" name="exclude_vacation" value="true" />
Urlaub ausschließen
</label>
</div>
<button type="submit">ICS erstellen</button>
</form>
<div class="hint">Hinweis: Die Datei wird nur temporär für die Konvertierung verarbeitet.</div>
</section>
</main>
</body>
</html>