add: Installer funktionen mit auto Update check

This commit is contained in:
Rob committed 2026-05-08 14:12:03 +02:00
1 parent 102b485794
commit 1fb6444444
8 files changed
+422 -10

No files matched your search

+133 -1
View File
@@ -2,13 +2,15 @@
# Ergebnis: dist/AnwesenheitPDF/AnwesenheitPDF.exe + alle Abhaengigkeiten
# Build-Artefakte: build/ (temporaer, nicht committen)
import json
import os
import re
import shutil
import sys
import threading
from datetime import date, datetime
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
import customtkinter as ctk
import tkinter as tk
@@ -99,6 +101,78 @@ def _count_output_pdfs(config: Dict[str, Any]) -> int:
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]:
"""Return deduplicated month keys from config (preserves insertion order)."""
seen: set = set()
@@ -178,6 +252,7 @@ class AnwesenheitApp(ctk.CTk):
self._tooltip_label: Optional[tk.Label] = None
# Build UI sections
self._build_update_banner()
self._build_input_section()
self._build_button_section()
self._build_preview_section()
@@ -185,11 +260,68 @@ class AnwesenheitApp(ctk.CTk):
# Auto-sync after event loop starts
self.after(100, self._start_initial_sync)
# Daily update check
if _should_check_update():
self.after(200, self._start_update_check)
# ------------------------------------------------------------------
# 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:
frame = ctk.CTkFrame(self)
frame.pack(fill="x", padx=16, pady=(16, 0))