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
@@ -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))
|
||||
|
||||
Reference in new issue
Block a user