add: Installer funktionen mit auto Update check
This commit is contained in:
1 parent
102b485794
commit
1fb6444444
8 files changed
+422
-10
No files matched your search
@@ -61,3 +61,38 @@ Excel-Datei → excel_reader → MonthlyAttendance → pdf_filler → PDF-Datei
|
|||||||
| Numerisch | DD.MM.YYYY | X | Wert (dt. Format) | "6" | "1" |
|
| Numerisch | DD.MM.YYYY | X | Wert (dt. Format) | "6" | "1" |
|
||||||
| "K"/"KK"/etc| DD.MM.YYYY | Code | leer | leer | "0" |
|
| "K"/"KK"/etc| DD.MM.YYYY | Code | leer | leer | "0" |
|
||||||
| Leer | (überspringen) | — | — | — | — |
|
| Leer | (überspringen) | — | — | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows-Installer (Inno Setup)
|
||||||
|
|
||||||
|
### Ziel
|
||||||
|
Installer-EXE erstellen, die:
|
||||||
|
- Von einem Windows SMB-Share aus gestartet werden kann (self-contained .exe)
|
||||||
|
- Das Programm lokal unter `%ProgramFiles%\AnwesenheitPDF\` installiert
|
||||||
|
- Einen Startmenü-Eintrag anlegt (optional: Desktop-Verknüpfung)
|
||||||
|
- Einen Uninstaller registriert (Programme und Features)
|
||||||
|
|
||||||
|
### Tool: Inno Setup 6
|
||||||
|
- Free, open-source, weit verbreitet
|
||||||
|
- Erzeugt eine einzelne selbstextrahierende .exe (ideal für SMB-Share)
|
||||||
|
- Unterstützt German.isl (deutschsprachiger Wizard)
|
||||||
|
- ISCC.exe (Compiler) muss auf Build-Maschine installiert sein
|
||||||
|
- Installation: `winget install --id JRSoftware.InnoSetup -e -s winget -i`
|
||||||
|
|
||||||
|
### Dateien
|
||||||
|
- `installer\AnwesenheitPDF.iss` — Inno Setup Script
|
||||||
|
- `build.bat` — um Installer-Schritt erweitert (ruft ISCC auf)
|
||||||
|
- Ausgabe: `dist\AnwesenheitPDF-Setup-x.y.z.exe`
|
||||||
|
|
||||||
|
### Ablauf build.bat (neu: Schritt 4)
|
||||||
|
1. pip install
|
||||||
|
2. update_version.py (liest Version aus config.json)
|
||||||
|
3. PyInstaller → dist\AnwesenheitPDF\
|
||||||
|
4. Version aus config.json extrahieren → ISCC aufrufen → dist\AnwesenheitPDF-Setup-x.y.z.exe
|
||||||
|
|
||||||
|
### Install-Ziel
|
||||||
|
- `{autopf}\AnwesenheitPDF\` (= Program Files, UAC-geschützt)
|
||||||
|
- Requires admin (PrivilegesRequired=admin)
|
||||||
|
- Startmenü: `{group}\AnwesenheitPDF`
|
||||||
|
- Optional Desktop-Verknüpfung (Checkbox im Wizard)
|
||||||
@@ -13,3 +13,4 @@ tests/fixtures/**/*.xlsx
|
|||||||
tests/fixtures/**/*.xlsm
|
tests/fixtures/**/*.xlsm
|
||||||
tests/fixtures/**/reference/*.pdf
|
tests/fixtures/**/reference/*.pdf
|
||||||
tests/fixtures/**/diff/
|
tests/fixtures/**/diff/
|
||||||
|
*.log
|
||||||
@@ -198,3 +198,48 @@ Das Programm prüft vor jeder Generierung:
|
|||||||
7. Headerzeile mit `Name, Vorname` gefunden
|
7. Headerzeile mit `Name, Vorname` gefunden
|
||||||
8. Tagesspalten ab Spaltenindex 9 vorhanden
|
8. Tagesspalten ab Spaltenindex 9 vorhanden
|
||||||
9. PDF-Template unter `template/sab.pdf` erreichbar
|
9. PDF-Template unter `template/sab.pdf` erreichbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Windows-Installer erstellen
|
||||||
|
|
||||||
|
Der Build-Prozess erzeugt neben dem Programm auch einen Windows-Installer (`.exe`), der vom SMB-Netzlaufwerk gestartet werden kann.
|
||||||
|
|
||||||
|
### Voraussetzung: Inno Setup 6
|
||||||
|
|
||||||
|
Inno Setup 6 muss auf der Build-Maschine installiert sein. Installation über `winget`:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
winget install --id JRSoftware.InnoSetup -e -s winget -i
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternativ manueller Download: <https://jrsoftware.org/isinfo.php>
|
||||||
|
|
||||||
|
### Installer bauen
|
||||||
|
|
||||||
|
```bat
|
||||||
|
build.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
`build.bat` führt alle vier Schritte automatisch aus:
|
||||||
|
|
||||||
|
1. Abhängigkeiten installieren (`pip install`)
|
||||||
|
2. Version aktualisieren (`update_version.py`)
|
||||||
|
3. Programm packen (`PyInstaller` → `dist\AnwesenheitPDF\`)
|
||||||
|
4. Installer kompilieren (`ISCC.exe` → `dist\AnwesenheitPDF-Setup-x.y.z.exe`)
|
||||||
|
|
||||||
|
Ist Inno Setup **nicht** installiert, wird Schritt 4 mit einer Warnung übersprungen — der PyInstaller-Output in `dist\AnwesenheitPDF\` ist trotzdem nutzbar.
|
||||||
|
|
||||||
|
### Installer verteilen
|
||||||
|
|
||||||
|
Die fertige Datei `dist\AnwesenheitPDF-Setup-x.y.z.exe` auf das SMB-Netzlaufwerk kopieren. Von dort kann sie direkt gestartet werden — ohne vorherige lokale Kopie.
|
||||||
|
|
||||||
|
### Was der Installer macht
|
||||||
|
|
||||||
|
| Aktion | Details |
|
||||||
|
|---|---|
|
||||||
|
| Installationspfad | `%ProgramFiles%\AnwesenheitPDF\` (Admin erforderlich) |
|
||||||
|
| Startmenü-Eintrag | `AnwesenheitPDF` |
|
||||||
|
| Desktop-Verknüpfung | Optional (Checkbox im Wizard, Standard: deaktiviert) |
|
||||||
|
| Uninstaller | Registriert unter *Programme und Features* |
|
||||||
|
| Windows-Version | Windows 10 oder höher |
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
@echo off
|
@echo off
|
||||||
REM Build-Script fuer AnwesenheitPDF.exe
|
REM Build-Script fuer AnwesenheitPDF.exe + Windows-Installer
|
||||||
REM
|
REM
|
||||||
REM Ergebnis:
|
REM Ergebnis:
|
||||||
REM build/ - temporaere PyInstaller-Artefakte
|
REM build/ - temporaere PyInstaller-Artefakte
|
||||||
REM dist/ - fertiges Programmverzeichnis (AnwesenheitPDF.exe + alle Abhaengigkeiten)
|
REM dist/ - fertiges Programmverzeichnis (AnwesenheitPDF.exe + alle Abhaengigkeiten)
|
||||||
|
REM + AnwesenheitPDF-Setup-x.y.z.exe (Windows-Installer)
|
||||||
REM
|
REM
|
||||||
REM Voraussetzungen:
|
REM Voraussetzungen:
|
||||||
REM pip install -r requirements-dev.txt
|
REM pip install -r requirements-dev.txt
|
||||||
|
REM Inno Setup 6 installiert (https://jrsoftware.org/isinfo.php)
|
||||||
|
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ echo.
|
|||||||
echo === AnwesenheitPDF Build ===
|
echo === AnwesenheitPDF Build ===
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
echo [1/3] Abhaengigkeiten installieren...
|
echo [1/4] Abhaengigkeiten installieren...
|
||||||
pip install -r requirements.txt --quiet && pip install -r requirements-dev.txt --quiet
|
pip install -r requirements.txt --quiet && pip install -r requirements-dev.txt --quiet
|
||||||
if %ERRORLEVEL% neq 0 (
|
if %ERRORLEVEL% neq 0 (
|
||||||
echo [FEHLER] pip install fehlgeschlagen.
|
echo [FEHLER] pip install fehlgeschlagen.
|
||||||
@@ -22,7 +24,7 @@ if %ERRORLEVEL% neq 0 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [2/3] Version aktualisieren...
|
echo [2/4] Version aktualisieren...
|
||||||
python update_version.py
|
python update_version.py
|
||||||
if %ERRORLEVEL% neq 0 (
|
if %ERRORLEVEL% neq 0 (
|
||||||
echo [FEHLER] Version-Update fehlgeschlagen.
|
echo [FEHLER] Version-Update fehlgeschlagen.
|
||||||
@@ -30,7 +32,7 @@ if %ERRORLEVEL% neq 0 (
|
|||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo [3/3] Build starten...
|
echo [3/4] Build starten...
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
python -m PyInstaller ^
|
python -m PyInstaller ^
|
||||||
@@ -59,11 +61,104 @@ if not exist "dist\AnwesenheitPDF\template" mkdir "dist\AnwesenheitPDF\template"
|
|||||||
if not exist "dist\AnwesenheitPDF\output" mkdir "dist\AnwesenheitPDF\output"
|
if not exist "dist\AnwesenheitPDF\output" mkdir "dist\AnwesenheitPDF\output"
|
||||||
if exist "template\sab.pdf" copy /Y "template\sab.pdf" "dist\AnwesenheitPDF\template\sab.pdf" >nul
|
if exist "template\sab.pdf" copy /Y "template\sab.pdf" "dist\AnwesenheitPDF\template\sab.pdf" >nul
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [4/4] Windows-Installer erstellen...
|
||||||
|
|
||||||
|
REM Version aus config.json lesen (Format: "x.y.z+hash" -> nur "x.y.z" verwenden)
|
||||||
|
for /f "tokens=2 delims=:," %%v in ('findstr /c:"\"version\"" config.json') do (
|
||||||
|
set RAW_VERSION=%%v
|
||||||
|
)
|
||||||
|
REM Fuehrende/nachfolgende Leerzeichen und Anfuehrungszeichen entfernen, Hash abschneiden
|
||||||
|
set RAW_VERSION=%RAW_VERSION: =%
|
||||||
|
set RAW_VERSION=%RAW_VERSION:"=%
|
||||||
|
for /f "tokens=1 delims=+" %%c in ("%RAW_VERSION%") do set APP_VERSION=%%c
|
||||||
|
|
||||||
|
echo Version: %APP_VERSION%
|
||||||
|
|
||||||
|
REM Inno Setup Compiler suchen (Standard-Installationspfade)
|
||||||
|
set ISCC_PATH=
|
||||||
|
if exist "%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" (
|
||||||
|
set "ISCC_PATH=%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe"
|
||||||
|
) else if exist "%ProgramFiles%\Inno Setup 6\ISCC.exe" (
|
||||||
|
set "ISCC_PATH=%ProgramFiles%\Inno Setup 6\ISCC.exe"
|
||||||
|
) else if exist "%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe" (
|
||||||
|
set "ISCC_PATH=%LOCALAPPDATA%\Programs\Inno Setup 6\ISCC.exe"
|
||||||
|
) else (
|
||||||
|
for /f "delims=" %%i in ('where ISCC.exe 2^>nul') do set "ISCC_PATH=%%i"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%ISCC_PATH%"=="" (
|
||||||
|
echo.
|
||||||
|
echo [WARNUNG] Inno Setup 6 nicht gefunden. Installer wird NICHT erstellt.
|
||||||
|
echo Download: https://jrsoftware.org/isinfo.php
|
||||||
|
echo Nach der Installation Inno Setup 6 neu starten und build.bat erneut ausfuehren.
|
||||||
|
echo.
|
||||||
|
goto build_done
|
||||||
|
)
|
||||||
|
|
||||||
|
"%ISCC_PATH%" /DMyAppVersion=%APP_VERSION% installer\AnwesenheitPDF.iss > build_iscc.log 2>&1
|
||||||
|
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo [FEHLER] Installer-Erstellung fehlgeschlagen.
|
||||||
|
echo.
|
||||||
|
type build_iscc.log
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [OK] Installer erstellt: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo [5/5] Installer zu update_check_path deployen...
|
||||||
|
|
||||||
|
REM update_check_path aus config.json auslesen
|
||||||
|
for /f "tokens=2 delims=:," %%d in ('findstr /c:"\"update_check_path\"" config.json') do (
|
||||||
|
set UPDATE_PATH=%%d
|
||||||
|
)
|
||||||
|
REM Leerzeichen und Anfuehrungszeichen entfernen
|
||||||
|
set UPDATE_PATH=%UPDATE_PATH: =%
|
||||||
|
set UPDATE_PATH=%UPDATE_PATH:"=%
|
||||||
|
|
||||||
|
if "%UPDATE_PATH%"=="" (
|
||||||
|
echo [WARNUNG] update_check_path nicht in config.json gefunden. Deployment uebersprungen.
|
||||||
|
goto build_done
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Zielverzeichnis pruefen und ggf. erstellen
|
||||||
|
if not exist "%UPDATE_PATH%" (
|
||||||
|
echo [INFO] Erstelle Zielverzeichnis: %UPDATE_PATH%
|
||||||
|
mkdir "%UPDATE_PATH%"
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo [WARNUNG] Konnte Zielverzeichnis nicht erstellen. Deployment uebersprungen.
|
||||||
|
goto build_done
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Neuen Installer kopieren
|
||||||
|
echo [INFO] Kopiere Installer nach: %UPDATE_PATH%
|
||||||
|
copy /Y "dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe" "%UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe" >nul
|
||||||
|
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo [WARNUNG] Installer-Deployment fehlgeschlagen (Schreibrechte?).
|
||||||
|
goto build_done
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [OK] Installer deployed zu: %UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
||||||
|
|
||||||
|
:build_done
|
||||||
echo.
|
echo.
|
||||||
echo [OK] Build abgeschlossen.
|
echo [OK] Build abgeschlossen.
|
||||||
echo Programm: dist\AnwesenheitPDF\AnwesenheitPDF.exe
|
echo Programm: dist\AnwesenheitPDF\AnwesenheitPDF.exe
|
||||||
echo Template: dist\AnwesenheitPDF\template\sab.pdf
|
echo Template: dist\AnwesenheitPDF\template\sab.pdf
|
||||||
echo Output: dist\AnwesenheitPDF\output
|
echo Output: dist\AnwesenheitPDF\output
|
||||||
|
if not "%ISCC_PATH%"=="" (
|
||||||
|
echo Installer: dist\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
||||||
|
)
|
||||||
|
if not "%UPDATE_PATH%"=="" (
|
||||||
|
echo Deployed: %UPDATE_PATH%\AnwesenheitPDF-Setup-%APP_VERSION%.exe
|
||||||
|
)
|
||||||
echo.
|
echo.
|
||||||
echo ... Programm wird gestartet ...
|
echo ... Programm wird gestartet ...
|
||||||
start "" "dist\AnwesenheitPDF\AnwesenheitPDF.exe"
|
start "" "dist\AnwesenheitPDF\AnwesenheitPDF.exe"
|
||||||
+3
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.1+250a473",
|
"version": "1.0.8+47220f9",
|
||||||
"antragsnummer": "100740439",
|
"antragsnummer": "100740439",
|
||||||
"ort": "Dresden",
|
"ort": "Dresden",
|
||||||
"template_pdf": "template/sab.pdf",
|
"template_pdf": "template/sab.pdf",
|
||||||
@@ -42,5 +42,6 @@
|
|||||||
"pauschale_present_value": "1",
|
"pauschale_present_value": "1",
|
||||||
"pdf_use_display_values": true,
|
"pdf_use_display_values": true,
|
||||||
"pdf_need_appearances": false,
|
"pdf_need_appearances": false,
|
||||||
"excel_network_source": "J:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\01_Anwesenheit\\Anwesenheitsliste\\Anwesenheitsliste 2026.xlsx"
|
"excel_network_source": "J:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\01_Anwesenheit\\Anwesenheitsliste\\Anwesenheitsliste 2026.xlsx",
|
||||||
|
"update_check_path": "j:\\Fachbereiche\\BO\\Produktionsschule\\Maßnahmeunterlagen\\30_Teilnehmende\\02_Abrechnung Aufwand und Fahrtgeld\\Automatisierung"
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,15 @@
|
|||||||
# Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten
|
# Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten
|
||||||
# Build-Artefakte: build/ (temporaer, nicht committen)
|
# Build-Artefakte: build/ (temporaer, nicht committen)
|
||||||
|
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -99,6 +101,78 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int:
|
|||||||
return len(list(out_dir.glob("*.pdf")))
|
return len(list(out_dir.glob("*.pdf")))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Update-check helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_UPDATE_CHECK_FILE = _runtime_dir() / "update_check.json"
|
||||||
|
_INSTALLER_PATTERN = re.compile(r"^AnwesenheitPDF-Setup-(\d+\.\d+\.\d+)\.exe$", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_semver(version_str: str) -> Tuple[int, int, int]:
|
||||||
|
"""Parse semantic version string, ignoring build metadata after '+'."""
|
||||||
|
base = version_str.split("+")[0].strip()
|
||||||
|
match = re.match(r"^(\d+)\.(\d+)\.(\d+)$", base)
|
||||||
|
if not match:
|
||||||
|
return (0, 0, 0)
|
||||||
|
return int(match.group(1)), int(match.group(2)), int(match.group(3))
|
||||||
|
|
||||||
|
|
||||||
|
def _should_check_update() -> bool:
|
||||||
|
"""Return True if no check has been recorded for today."""
|
||||||
|
try:
|
||||||
|
if _UPDATE_CHECK_FILE.exists():
|
||||||
|
with open(_UPDATE_CHECK_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data.get("last_check") != date.today().isoformat()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _record_update_check() -> None:
|
||||||
|
"""Persist today's date as the last check timestamp."""
|
||||||
|
try:
|
||||||
|
with open(_UPDATE_CHECK_FILE, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"last_check": date.today().isoformat()}, f)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _find_latest_installer(config: Dict[str, Any]) -> Optional[Tuple[str, Path]]:
|
||||||
|
"""Scan the configured SMB update path for the newest installer.
|
||||||
|
|
||||||
|
Returns (version_str, installer_path) if a newer version exists, else None.
|
||||||
|
"""
|
||||||
|
update_path_str = str(config.get("update_check_path", "")).strip()
|
||||||
|
if not update_path_str:
|
||||||
|
return None
|
||||||
|
|
||||||
|
update_dir = Path(update_path_str)
|
||||||
|
if not update_dir.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_ver = _parse_semver(str(config.get("version", "0.0.0")))
|
||||||
|
best_ver = current_ver
|
||||||
|
best_file: Optional[Path] = None
|
||||||
|
best_ver_str: Optional[str] = None
|
||||||
|
|
||||||
|
for f in update_dir.glob("AnwesenheitPDF-Setup-*.exe"):
|
||||||
|
m = _INSTALLER_PATTERN.match(f.name)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
ver_str = m.group(1)
|
||||||
|
ver = _parse_semver(ver_str)
|
||||||
|
if ver > best_ver:
|
||||||
|
best_ver = ver
|
||||||
|
best_file = f
|
||||||
|
best_ver_str = ver_str
|
||||||
|
|
||||||
|
if best_file and best_ver_str:
|
||||||
|
return (best_ver_str, best_file)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
def _build_month_keys(config: Dict[str, Any]) -> List[str]:
|
||||||
"""Return deduplicated month keys from config (preserves insertion order)."""
|
"""Return deduplicated month keys from config (preserves insertion order)."""
|
||||||
seen: set = set()
|
seen: set = set()
|
||||||
@@ -178,6 +252,7 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
self._tooltip_label: Optional[tk.Label] = None
|
self._tooltip_label: Optional[tk.Label] = None
|
||||||
|
|
||||||
# Build UI sections
|
# Build UI sections
|
||||||
|
self._build_update_banner()
|
||||||
self._build_input_section()
|
self._build_input_section()
|
||||||
self._build_button_section()
|
self._build_button_section()
|
||||||
self._build_preview_section()
|
self._build_preview_section()
|
||||||
@@ -185,11 +260,68 @@ class AnwesenheitApp(ctk.CTk):
|
|||||||
|
|
||||||
# Auto-sync after event loop starts
|
# Auto-sync after event loop starts
|
||||||
self.after(100, self._start_initial_sync)
|
self.after(100, self._start_initial_sync)
|
||||||
|
# Daily update check
|
||||||
|
if _should_check_update():
|
||||||
|
self.after(200, self._start_update_check)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# UI builders
|
# UI builders
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_update_banner(self) -> None:
|
||||||
|
"""Create the update-available banner (hidden until an update is found)."""
|
||||||
|
self._update_banner = ctk.CTkFrame(self, fg_color="#854D0E")
|
||||||
|
# Banner is NOT packed here — only shown when an update is detected
|
||||||
|
self._update_banner_label = ctk.CTkLabel(
|
||||||
|
self._update_banner,
|
||||||
|
text="",
|
||||||
|
text_color="#FEF9C3",
|
||||||
|
font=ctk.CTkFont(weight="bold"),
|
||||||
|
anchor="w",
|
||||||
|
)
|
||||||
|
self._update_banner_label.pack(side="left", padx=12, pady=6, fill="x", expand=True)
|
||||||
|
self._update_banner_btn = ctk.CTkButton(
|
||||||
|
self._update_banner,
|
||||||
|
text="Installieren",
|
||||||
|
width=110,
|
||||||
|
height=28,
|
||||||
|
fg_color="#CA8A04",
|
||||||
|
hover_color="#A16207",
|
||||||
|
text_color="white",
|
||||||
|
command=self._open_update_folder,
|
||||||
|
)
|
||||||
|
self._update_banner_btn.pack(side="right", padx=(0, 12), pady=6)
|
||||||
|
self._update_installer_path: Optional[Path] = None
|
||||||
|
|
||||||
|
def _show_update_banner(self, version: str, installer: Path) -> None:
|
||||||
|
"""Display the update banner with the new version info."""
|
||||||
|
self._update_installer_path = installer
|
||||||
|
current = _CONFIG.get("version", "?").split("+")[0]
|
||||||
|
self._update_banner_label.configure(
|
||||||
|
text=f"Update verfügbar: v{current} → v{version} "
|
||||||
|
f"({installer.name})"
|
||||||
|
)
|
||||||
|
# Insert banner above everything else
|
||||||
|
self._update_banner.pack(fill="x", padx=16, pady=(8, 0), before=self.winfo_children()[1])
|
||||||
|
|
||||||
|
def _open_update_folder(self) -> None:
|
||||||
|
"""Open the installer directory in Explorer."""
|
||||||
|
if self._update_installer_path and self._update_installer_path.parent.exists():
|
||||||
|
os.startfile(str(self._update_installer_path.parent))
|
||||||
|
|
||||||
|
def _start_update_check(self) -> None:
|
||||||
|
threading.Thread(target=self._run_update_check, daemon=True).start()
|
||||||
|
|
||||||
|
def _run_update_check(self) -> None:
|
||||||
|
try:
|
||||||
|
result = _find_latest_installer(_CONFIG)
|
||||||
|
except Exception:
|
||||||
|
result = None
|
||||||
|
_record_update_check()
|
||||||
|
if result:
|
||||||
|
version, installer = result
|
||||||
|
self.after(0, self._show_update_banner, version, installer)
|
||||||
|
|
||||||
def _build_input_section(self) -> None:
|
def _build_input_section(self) -> None:
|
||||||
frame = ctk.CTkFrame(self)
|
frame = ctk.CTkFrame(self)
|
||||||
frame.pack(fill="x", padx=16, pady=(16, 0))
|
frame.pack(fill="x", padx=16, pady=(16, 0))
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
; Inno Setup Script fuer AnwesenheitPDF
|
||||||
|
; Compiler: Inno Setup 6.x (https://jrsoftware.org/isinfo.php)
|
||||||
|
;
|
||||||
|
; Aufruf durch build.bat:
|
||||||
|
; iscc.exe /DMyAppVersion=1.0.1 installer\AnwesenheitPDF.iss
|
||||||
|
;
|
||||||
|
; Ausgabe: dist\AnwesenheitPDF-Setup-{version}.exe
|
||||||
|
|
||||||
|
#ifndef MyAppVersion
|
||||||
|
#define MyAppVersion "0.0.0"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define MyAppName "AnwesenheitPDF"
|
||||||
|
#define MyAppPublisher "SAB Anwesenheitslisten"
|
||||||
|
#define MyAppExeName "AnwesenheitPDF.exe"
|
||||||
|
#define MyAppID "{{A3F2C1D7-84BE-4E9A-B5D0-2C7F3A1E9B46}"
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
; Eindeutige App-ID – niemals aendern (sonst gelten Updates als Neuinstallation)
|
||||||
|
AppId={#MyAppID}
|
||||||
|
AppName={#MyAppName}
|
||||||
|
AppVersion={#MyAppVersion}
|
||||||
|
AppVerName={#MyAppName} {#MyAppVersion}
|
||||||
|
AppPublisher={#MyAppPublisher}
|
||||||
|
AppComments=Anwesenheitslisten aus Excel-Daten als PDF generieren
|
||||||
|
|
||||||
|
; Installations-Zielverzeichnis (Programme-Ordner)
|
||||||
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
|
DefaultGroupName={#MyAppName}
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
|
||||||
|
; Installer-Ausgabe
|
||||||
|
OutputDir=..\dist
|
||||||
|
OutputBaseFilename=AnwesenheitPDF-Setup-{#MyAppVersion}
|
||||||
|
|
||||||
|
; Komprimierung
|
||||||
|
Compression=lzma2/ultra64
|
||||||
|
SolidCompression=yes
|
||||||
|
LZMAUseSeparateProcess=yes
|
||||||
|
|
||||||
|
; Erscheinungsbild
|
||||||
|
WizardStyle=modern
|
||||||
|
SetupIconFile=..\assets\icon.ico
|
||||||
|
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||||||
|
|
||||||
|
; Berechtigungen: Admin erforderlich fuer Installation in Programme-Ordner
|
||||||
|
PrivilegesRequired=admin
|
||||||
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
|
|
||||||
|
; Mindest-Windows-Version: Windows 10 (10.0)
|
||||||
|
MinVersion=10.0
|
||||||
|
|
||||||
|
; Installer kann direkt vom Netzlaufwerk (SMB-Share) ausgefuehrt werden
|
||||||
|
; (kein lokales Entpacken des Installers noetig)
|
||||||
|
AllowNetworkDrive=yes
|
||||||
|
AllowUNCPath=yes
|
||||||
|
|
||||||
|
; Versionsinformation fuer die EXE-Eigenschaften
|
||||||
|
VersionInfoVersion={#MyAppVersion}
|
||||||
|
VersionInfoCompany={#MyAppPublisher}
|
||||||
|
VersionInfoDescription={#MyAppName} Setup
|
||||||
|
VersionInfoProductName={#MyAppName}
|
||||||
|
VersionInfoProductVersion={#MyAppVersion}
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "startmenuicon"; \
|
||||||
|
Description: "Startmenü-Einträge erstellen"; \
|
||||||
|
GroupDescription: "Startmenü:"
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
; Gesamten PyInstaller-Output in das Installationsverzeichnis kopieren
|
||||||
|
Source: "..\dist\AnwesenheitPDF\*"; \
|
||||||
|
DestDir: "{app}"; \
|
||||||
|
Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
; Startmenü-Eintrag (nur wenn Task ausgewaehlt)
|
||||||
|
Name: "{group}\{#MyAppName}"; \
|
||||||
|
Filename: "{app}\{#MyAppExeName}"; \
|
||||||
|
Comment: "Anwesenheitslisten als PDF generieren"; \
|
||||||
|
Tasks: startmenuicon
|
||||||
|
|
||||||
|
; Startmenü: Uninstaller (nur wenn Task ausgewaehlt)
|
||||||
|
Name: "{group}\{#MyAppName} deinstallieren"; \
|
||||||
|
Filename: "{uninstallexe}"; \
|
||||||
|
Tasks: startmenuicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
; Optionaler Start nach der Installation
|
||||||
|
Filename: "{app}\{#MyAppExeName}"; \
|
||||||
|
Description: "{#MyAppName} jetzt starten"; \
|
||||||
|
Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[UninstallDelete]
|
||||||
|
; Beim Deinstallieren: nur generierte Dateien loeschen, keine Nutzerdaten
|
||||||
|
Type: files; Name: "{app}\*.log"
|
||||||
@@ -100,6 +100,10 @@
|
|||||||
"excel_network_source": {
|
"excel_network_source": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Optionaler SMB- oder Dateipfad zur Excel-Datei, die beim GUI-Start nach input/ kopiert und bei Bedarf aktualisiert wird"
|
"description": "Optionaler SMB- oder Dateipfad zur Excel-Datei, die beim GUI-Start nach input/ kopiert und bei Bedarf aktualisiert wird"
|
||||||
|
},
|
||||||
|
"update_check_path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "SMB-Pfad zum Verzeichnis mit Installer-Dateien (AnwesenheitPDF-Setup-x.y.z.exe) fuer automatische Update-Pruefung"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user