2026-05-23 21:31:34 +02:00
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
|
|
|
|
|
AzA – Einheitliches Startpanel (WebView-Huelle mit MP4-Hintergrund).
|
|
|
|
|
|
|
|
|
|
|
|
Sichtbar: AzA Office | Praxis Chat
|
|
|
|
|
|
Technisch Praxischat = bisheriger MiniChat-Modus (minichat=1), unveraendert.
|
|
|
|
|
|
|
|
|
|
|
|
UI: pywebview + lokalem HTTP (127.0.0.1) fuer MP4/PNG/Logo.
|
|
|
|
|
|
WebView2 blockiert file:// in html= about:blank — daher kein reines as_uri() im Panel.
|
|
|
|
|
|
Fallback nur bei fehlendem pywebview: Tkinter + PNG (kein laufendes Video).
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import html
|
|
|
|
|
|
import mimetypes
|
|
|
|
|
|
import os
|
|
|
|
|
|
import subprocess
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import threading
|
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
2026-05-28 18:58:38 +02:00
|
|
|
|
|
|
|
|
|
|
_POPEN_NO_CONSOLE: dict = (
|
|
|
|
|
|
{"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}
|
|
|
|
|
|
)
|
2026-05-23 21:31:34 +02:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from tkinter import messagebox
|
|
|
|
|
|
from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from PIL import Image, ImageEnhance, ImageFilter, ImageTk
|
|
|
|
|
|
|
|
|
|
|
|
_HAS_PIL = True
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
_HAS_PIL = False
|
|
|
|
|
|
|
|
|
|
|
|
_ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
_W, _H = 940, 625
|
|
|
|
|
|
_CARD_W = 270 # Feinschliff 235 px + 15 %
|
|
|
|
|
|
|
|
|
|
|
|
_MP4 = _ROOT / "assets" / "matternhorn-aza-2.mp4"
|
|
|
|
|
|
_PNG = _ROOT / "assets" / "matternhorn.png"
|
|
|
|
|
|
_LOGO = _ROOT / "logo.png"
|
|
|
|
|
|
_WIN_APP_ID = "ch.aza-medwork.startpanel.v1"
|
|
|
|
|
|
|
|
|
|
|
|
_FALLBACK_BG = "#d4e3ef"
|
|
|
|
|
|
_CARD_BG = "#f5f8fb"
|
|
|
|
|
|
_ACCENT = "#1a5f8a"
|
|
|
|
|
|
_ACCENT_HOVER = "#164f73"
|
|
|
|
|
|
_TEXT = "#1a2a3a"
|
|
|
|
|
|
_SUBTLE = "#5a6d7d"
|
|
|
|
|
|
_FONT_TITLE = ("Segoe UI", 18, "bold")
|
|
|
|
|
|
_FONT_SUB = ("Segoe UI", 10)
|
|
|
|
|
|
_FONT_BTN = ("Segoe UI", 10, "bold")
|
|
|
|
|
|
_FONT_FOOT = ("Segoe UI", 8)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _file_uri(path: Path) -> str:
|
|
|
|
|
|
if path.is_file():
|
|
|
|
|
|
return path.resolve().as_uri()
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _short_uri(uri: str, max_len: int = 72) -> str:
|
|
|
|
|
|
if len(uri) <= max_len:
|
|
|
|
|
|
return uri
|
|
|
|
|
|
return uri[: max_len - 1] + "…"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _log_asset_status(*, asset_mode: str = "", start_url: str = "") -> None:
|
|
|
|
|
|
video_exists = _MP4.is_file()
|
|
|
|
|
|
video_size = int(_MP4.stat().st_size) if video_exists else 0
|
|
|
|
|
|
print(f"[AzA Startpanel] VIDEO_EXISTS={video_exists}")
|
|
|
|
|
|
print(f"[AzA Startpanel] VIDEO_SIZE_BYTES={video_size}")
|
|
|
|
|
|
print(f"[AzA Startpanel] VIDEO_URI={_short_uri(_file_uri(_MP4))}")
|
|
|
|
|
|
print(f"[AzA Startpanel] PNG_EXISTS={_PNG.is_file()}")
|
|
|
|
|
|
print(f"[AzA Startpanel] LOGO_EXISTS={_LOGO.is_file()}")
|
|
|
|
|
|
if asset_mode:
|
|
|
|
|
|
print(f"[AzA Startpanel] ASSET_MODE={asset_mode}")
|
|
|
|
|
|
if start_url:
|
|
|
|
|
|
print(f"[AzA Startpanel] START_URL={_short_uri(start_url, 96)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_start_panel_handler(root_dir: Path, panel_html_supplier):
|
|
|
|
|
|
"""Liefert nur /start (HTML) und statische Dateien unter Projektroot."""
|
|
|
|
|
|
|
|
|
|
|
|
class Handler(BaseHTTPRequestHandler):
|
|
|
|
|
|
def log_message(self, _format, *_args) -> None:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def do_GET(self) -> None:
|
|
|
|
|
|
path = unquote(urlparse(self.path).path)
|
|
|
|
|
|
if path in ("/start", "/"):
|
|
|
|
|
|
panel_html = panel_html_supplier()
|
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
|
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
|
|
|
|
self.send_header("Content-Length", str(len(panel_html)))
|
|
|
|
|
|
self.send_header("Cache-Control", "no-store")
|
|
|
|
|
|
self.end_headers()
|
|
|
|
|
|
self.wfile.write(panel_html)
|
|
|
|
|
|
return
|
|
|
|
|
|
rel = path.lstrip("/").replace("/", os.sep)
|
|
|
|
|
|
if not rel or ".." in rel.split(os.sep):
|
|
|
|
|
|
self.send_error(404)
|
|
|
|
|
|
return
|
|
|
|
|
|
fp = (root_dir / rel).resolve()
|
|
|
|
|
|
try:
|
|
|
|
|
|
fp.relative_to(root_dir.resolve())
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
self.send_error(403)
|
|
|
|
|
|
return
|
|
|
|
|
|
if not fp.is_file():
|
|
|
|
|
|
self.send_error(404)
|
|
|
|
|
|
return
|
|
|
|
|
|
data = fp.read_bytes()
|
|
|
|
|
|
ctype = mimetypes.guess_type(str(fp))[0] or "application/octet-stream"
|
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
|
self.send_header("Content-Type", ctype)
|
|
|
|
|
|
self.send_header("Content-Length", str(len(data)))
|
|
|
|
|
|
self.send_header("Cache-Control", "no-store")
|
|
|
|
|
|
self.end_headers()
|
|
|
|
|
|
self.wfile.write(data)
|
|
|
|
|
|
|
|
|
|
|
|
return Handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _LocalAssetServer:
|
|
|
|
|
|
"""localhost-only HTTP fuer WebView2-Assets (MP4/PNG/Logo)."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, root_dir: Path) -> None:
|
|
|
|
|
|
self._root_dir = root_dir
|
|
|
|
|
|
self._panel_html: bytes = b""
|
|
|
|
|
|
self._httpd: ThreadingHTTPServer | None = None
|
|
|
|
|
|
self.base_url = ""
|
|
|
|
|
|
|
|
|
|
|
|
def _panel_html_bytes(self) -> bytes:
|
|
|
|
|
|
if not self._panel_html and self.base_url:
|
|
|
|
|
|
self._panel_html = _build_panel_html(self.base_url).encode("utf-8")
|
|
|
|
|
|
return self._panel_html
|
|
|
|
|
|
|
|
|
|
|
|
def start(self) -> str:
|
|
|
|
|
|
handler = _make_start_panel_handler(self._root_dir, self._panel_html_bytes)
|
|
|
|
|
|
self._httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
|
|
|
|
|
host, port = self._httpd.server_address
|
|
|
|
|
|
self.base_url = f"http://{host}:{port}"
|
|
|
|
|
|
self._panel_html_bytes()
|
|
|
|
|
|
threading.Thread(target=self._httpd.serve_forever, daemon=True).start()
|
|
|
|
|
|
return f"{self.base_url}/start"
|
|
|
|
|
|
|
|
|
|
|
|
def stop(self) -> None:
|
|
|
|
|
|
if self._httpd is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._httpd.shutdown()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._httpd.server_close()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._httpd = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _screen_center_xy(width: int, height: int) -> tuple[int, int]:
|
|
|
|
|
|
"""Bildschirmmitte fuer pywebview x/y und Win32-Fallback."""
|
|
|
|
|
|
sw, sh = 1920, 1080
|
|
|
|
|
|
if sys.platform == "win32":
|
|
|
|
|
|
try:
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
|
|
|
|
user32 = ctypes.windll.user32
|
|
|
|
|
|
sw = int(user32.GetSystemMetrics(0))
|
|
|
|
|
|
sh = int(user32.GetSystemMetrics(1))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
x = max(0, (sw - width) // 2)
|
|
|
|
|
|
y = max(0, (sh - height) // 2)
|
|
|
|
|
|
return x, y
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _center_window(win: tk.Tk, width: int, height: int) -> None:
|
|
|
|
|
|
win.update_idletasks()
|
|
|
|
|
|
x, y = _screen_center_xy(width, height)
|
|
|
|
|
|
win.geometry(f"{width}x{height}+{x}+{y}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _win32_center_window(hwnd: int, width: int, height: int) -> bool:
|
|
|
|
|
|
if sys.platform != "win32" or not hwnd:
|
|
|
|
|
|
return False
|
|
|
|
|
|
try:
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
|
|
|
|
user32 = ctypes.windll.user32
|
|
|
|
|
|
x, y = _screen_center_xy(width, height)
|
|
|
|
|
|
SWP_NOSIZE = 0x0001
|
|
|
|
|
|
SWP_NOZORDER = 0x0004
|
|
|
|
|
|
user32.SetWindowPos(hwnd, 0, x, y, 0, 0, SWP_NOSIZE | SWP_NOZORDER)
|
|
|
|
|
|
return True
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _minichat_url() -> str:
|
|
|
|
|
|
"""Technischer URL-Modus minichat=1 (Praxischat)."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_empfang_webview import _build_default_empfang_chat_shell_url
|
|
|
|
|
|
|
|
|
|
|
|
base = _build_default_empfang_chat_shell_url()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
base = (
|
|
|
|
|
|
os.environ.get("AZA_EMPFANG_CHAT_SHELL_URL") or ""
|
|
|
|
|
|
).strip() or (
|
|
|
|
|
|
"https://empfang.aza-medwork.ch/empfang/"
|
|
|
|
|
|
"?empfang_chat_shell=1&shell_source=empfang_chat_shell"
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
p = urlparse(base)
|
|
|
|
|
|
qs = parse_qs(p.query, keep_blank_values=True)
|
|
|
|
|
|
qs["minichat"] = ["1"]
|
|
|
|
|
|
return urlunparse(p._replace(query=urlencode(qs, doseq=True)))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
sep = "&" if "?" in base else "?"
|
|
|
|
|
|
return f"{base}{sep}minichat=1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_office_executable() -> Path | None:
|
|
|
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
|
|
exe_dir = Path(sys.executable).resolve().parent
|
|
|
|
|
|
for name in ("aza_desktop.exe", "AzA.exe", "AzA_Start.exe"):
|
|
|
|
|
|
p = exe_dir / name
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
meipass = getattr(sys, "_MEIPASS", "")
|
|
|
|
|
|
if meipass:
|
|
|
|
|
|
p = Path(meipass) / "aza_desktop.exe"
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
for p in (_ROOT / "aza_desktop.exe", _ROOT / "dist" / "aza_desktop" / "aza_desktop.exe"):
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_empfang_shell_executable() -> Path | None:
|
|
|
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
|
|
exe_dir = Path(sys.executable).resolve().parent
|
|
|
|
|
|
p = exe_dir / "AZA_EmpfangShell.exe"
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
meipass = getattr(sys, "_MEIPASS", "")
|
|
|
|
|
|
if meipass:
|
|
|
|
|
|
p = Path(meipass) / "AZA_EmpfangShell.exe"
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
for p in (_ROOT / "AZA_EmpfangShell.exe", _ROOT / "dist" / "AZA_EmpfangShell.exe"):
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_office() -> tuple[bool, str]:
|
|
|
|
|
|
exe = _resolve_office_executable()
|
|
|
|
|
|
try:
|
|
|
|
|
|
if exe is not None:
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
[str(exe)],
|
|
|
|
|
|
cwd=str(exe.parent),
|
|
|
|
|
|
close_fds=(sys.platform != "win32"),
|
2026-05-28 18:58:38 +02:00
|
|
|
|
**_POPEN_NO_CONSOLE,
|
2026-05-23 21:31:34 +02:00
|
|
|
|
)
|
|
|
|
|
|
return True, f"Gestartet: {exe.name}"
|
|
|
|
|
|
script = _ROOT / "basis14.py"
|
|
|
|
|
|
if not script.is_file():
|
|
|
|
|
|
return False, "basis14.py und aza_desktop.exe wurden nicht gefunden."
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
[sys.executable, str(script)],
|
|
|
|
|
|
cwd=str(_ROOT),
|
|
|
|
|
|
close_fds=(sys.platform != "win32"),
|
2026-05-28 18:58:38 +02:00
|
|
|
|
**_POPEN_NO_CONSOLE,
|
2026-05-23 21:31:34 +02:00
|
|
|
|
)
|
|
|
|
|
|
return True, "AzA Office wird gestartet."
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
return False, f"Office konnte nicht gestartet werden: {exc}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start_praxischat() -> tuple[bool, str]:
|
|
|
|
|
|
"""Praxischat = bestehender MiniChat-WebView-Starter (minichat=1)."""
|
|
|
|
|
|
url = _minichat_url()
|
|
|
|
|
|
mode_arg = "--shell-mode=empfang_chat_shell"
|
|
|
|
|
|
w, h = "350", "700"
|
|
|
|
|
|
exe = _resolve_empfang_shell_executable()
|
|
|
|
|
|
try:
|
|
|
|
|
|
if exe is not None:
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
[str(exe), mode_arg, url, w, h],
|
|
|
|
|
|
cwd=str(exe.parent),
|
|
|
|
|
|
close_fds=(sys.platform != "win32"),
|
2026-05-28 18:58:38 +02:00
|
|
|
|
**_POPEN_NO_CONSOLE,
|
2026-05-23 21:31:34 +02:00
|
|
|
|
)
|
|
|
|
|
|
return True, f"Praxischat wird gestartet ({exe.name})."
|
|
|
|
|
|
script = _ROOT / "aza_empfang_webview.py"
|
|
|
|
|
|
if not script.is_file():
|
|
|
|
|
|
return (
|
|
|
|
|
|
False,
|
|
|
|
|
|
"aza_empfang_webview.py und AZA_EmpfangShell.exe wurden nicht gefunden.",
|
|
|
|
|
|
)
|
|
|
|
|
|
subprocess.Popen(
|
|
|
|
|
|
[sys.executable, str(script), mode_arg, url, w, h],
|
|
|
|
|
|
cwd=str(_ROOT),
|
|
|
|
|
|
close_fds=(sys.platform != "win32"),
|
2026-05-28 18:58:38 +02:00
|
|
|
|
**_POPEN_NO_CONSOLE,
|
2026-05-23 21:31:34 +02:00
|
|
|
|
)
|
|
|
|
|
|
return True, "Praxischat wird gestartet."
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
return False, f"Praxischat konnte nicht gestartet werden: {exc}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Alias fuer aeltere HTML-API-Namen
|
|
|
|
|
|
start_minichat = start_praxischat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class StartPanelApi:
|
|
|
|
|
|
"""pywebview JS-API (Chromium/WebView2)."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
self._window = None
|
|
|
|
|
|
|
|
|
|
|
|
def bind_window(self, window) -> None:
|
|
|
|
|
|
self._window = window
|
|
|
|
|
|
|
|
|
|
|
|
def _close_on_success(self, ok: bool) -> None:
|
|
|
|
|
|
if ok and self._window is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
self._window.destroy()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def open_office(self) -> dict:
|
|
|
|
|
|
ok, msg = start_office()
|
|
|
|
|
|
self._close_on_success(ok)
|
|
|
|
|
|
return {"ok": ok, "message": msg}
|
|
|
|
|
|
|
|
|
|
|
|
def open_praxischat(self) -> dict:
|
|
|
|
|
|
ok, msg = start_praxischat()
|
|
|
|
|
|
self._close_on_success(ok)
|
|
|
|
|
|
return {"ok": ok, "message": msg}
|
|
|
|
|
|
|
|
|
|
|
|
# Kompatibilitaet aelterer HTML-Handler
|
|
|
|
|
|
launch_office = open_office
|
|
|
|
|
|
launch_minichat = open_praxischat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_panel_html(asset_base: str) -> str:
|
|
|
|
|
|
"""HTML mit http://127.0.0.1:PORT/... Asset-URLs (WebView2-kompatibel)."""
|
|
|
|
|
|
base = asset_base.rstrip("/")
|
|
|
|
|
|
png = html.escape(f"{base}/assets/matternhorn.png", quote=True)
|
|
|
|
|
|
mp4 = ""
|
|
|
|
|
|
if _MP4.is_file():
|
|
|
|
|
|
mp4 = html.escape(f"{base}/assets/matternhorn-aza-2.mp4", quote=True)
|
|
|
|
|
|
favicon = ""
|
|
|
|
|
|
if _LOGO.is_file():
|
|
|
|
|
|
favicon = (
|
|
|
|
|
|
f'<link rel="icon" type="image/png" href="'
|
|
|
|
|
|
f'{html.escape(f"{base}/logo.png", quote=True)}"/>'
|
|
|
|
|
|
)
|
|
|
|
|
|
logo_block = ""
|
|
|
|
|
|
if _LOGO.is_file():
|
|
|
|
|
|
logo_u = html.escape(f"{base}/logo.png", quote=True)
|
|
|
|
|
|
logo_block = (
|
|
|
|
|
|
f'<img class="logo" id="logoImg" src="{logo_u}" alt="" '
|
|
|
|
|
|
f'onerror="this.remove()"/>'
|
|
|
|
|
|
)
|
|
|
|
|
|
video_block = ""
|
|
|
|
|
|
if mp4:
|
|
|
|
|
|
video_block = f"""
|
|
|
|
|
|
<div class="bg-wrap">
|
|
|
|
|
|
<video id="bgVideo" class="bg-video" autoplay muted loop playsinline preload="auto" poster="{png}">
|
|
|
|
|
|
<source src="{mp4}" type="video/mp4"/>
|
|
|
|
|
|
</video>
|
|
|
|
|
|
<img class="bg-fallback" src="{png}" alt=""/>
|
|
|
|
|
|
</div>"""
|
|
|
|
|
|
else:
|
|
|
|
|
|
video_block = f"""
|
|
|
|
|
|
<div class="bg-wrap">
|
|
|
|
|
|
<img class="bg-fallback show-fallback" src="{png}" alt=""/>
|
|
|
|
|
|
</div>"""
|
|
|
|
|
|
return f"""<!DOCTYPE html>
|
|
|
|
|
|
<html lang="de">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="utf-8"/>
|
|
|
|
|
|
<title>AzA</title>
|
|
|
|
|
|
{favicon}
|
|
|
|
|
|
<style>
|
|
|
|
|
|
* {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
|
|
|
|
|
html, body {{
|
|
|
|
|
|
width: 100%; height: 100%; overflow: hidden;
|
|
|
|
|
|
font-family: "Segoe UI", system-ui, sans-serif;
|
|
|
|
|
|
background: #b8c9d8;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.bg-wrap {{
|
|
|
|
|
|
position: fixed; inset: 0; z-index: 0; overflow: hidden;
|
|
|
|
|
|
width: 100vw; height: 100vh;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.bg-video, .bg-fallback {{
|
|
|
|
|
|
position: absolute; inset: 0; width: 100%; height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.bg-fallback {{ display: none; }}
|
|
|
|
|
|
.bg-fallback.show-fallback {{ display: block; }}
|
|
|
|
|
|
body.no-video .bg-video {{ display: none; }}
|
|
|
|
|
|
body.no-video .bg-fallback {{ display: block; }}
|
|
|
|
|
|
.fog {{
|
|
|
|
|
|
position: fixed; inset: 0; z-index: 1;
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
180deg,
|
|
|
|
|
|
rgba(255, 255, 255, 0.34) 0%,
|
|
|
|
|
|
rgba(240, 248, 255, 0.58) 50%,
|
|
|
|
|
|
rgba(230, 240, 250, 0.68) 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.stage {{
|
|
|
|
|
|
position: fixed; inset: 0; z-index: 2;
|
|
|
|
|
|
display: flex; align-items: center; justify-content: center;
|
|
|
|
|
|
padding: 25px;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.card {{
|
|
|
|
|
|
width: min({_CARD_W}px, 88vw);
|
|
|
|
|
|
padding: 18px 18px 15px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.51);
|
|
|
|
|
|
border: 1px solid rgba(255, 255, 255, 0.64);
|
|
|
|
|
|
box-shadow: 0 9px 33px rgba(20, 40, 60, 0.14);
|
|
|
|
|
|
backdrop-filter: blur(16px);
|
|
|
|
|
|
-webkit-backdrop-filter: blur(16px);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.logo {{
|
|
|
|
|
|
width: 43px; height: 43px; object-fit: contain;
|
|
|
|
|
|
margin: 0 auto 7px; display: block;
|
|
|
|
|
|
}}
|
|
|
|
|
|
h1 {{ font-size: 1.38rem; font-weight: 700; color: #1a2a3a; }}
|
|
|
|
|
|
.sub {{ margin: 6px 0 15px; font-size: 0.83rem; color: #5a6d7d; }}
|
|
|
|
|
|
.btn {{
|
|
|
|
|
|
display: block; width: 100%; margin-bottom: 9px;
|
|
|
|
|
|
padding: 12px 15px; border: none; border-radius: 9px;
|
|
|
|
|
|
font-size: 0.86rem; font-weight: 600; color: #fff;
|
|
|
|
|
|
background: #1a5f8a; cursor: pointer;
|
|
|
|
|
|
box-shadow: 0 3px 13px rgba(26, 95, 138, 0.28);
|
|
|
|
|
|
transition: background 0.15s ease, transform 0.1s ease;
|
|
|
|
|
|
}}
|
|
|
|
|
|
.btn:last-of-type {{ margin-bottom: 0; }}
|
|
|
|
|
|
.btn:hover {{ background: #164f73; }}
|
|
|
|
|
|
.btn:active {{ transform: scale(0.98); }}
|
|
|
|
|
|
.btn:disabled {{ opacity: 0.65; cursor: wait; }}
|
|
|
|
|
|
.foot {{ margin-top: 13px; font-size: 0.67rem; color: #6a7d8c; }}
|
|
|
|
|
|
.err {{ display: none; margin-top: 9px; font-size: 0.71rem; color: #b42318; }}
|
|
|
|
|
|
.err.show {{ display: block; }}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
{video_block}
|
|
|
|
|
|
<div class="fog"></div>
|
|
|
|
|
|
<div class="stage">
|
|
|
|
|
|
<div class="card">
|
|
|
|
|
|
{logo_block}
|
|
|
|
|
|
<h1>AzA</h1>
|
|
|
|
|
|
<p class="sub">Was möchten Sie öffnen?</p>
|
|
|
|
|
|
<button type="button" class="btn" id="btnOffice">AzA Office</button>
|
|
|
|
|
|
<button type="button" class="btn" id="btnPraxischat">Praxis Chat</button>
|
|
|
|
|
|
<p class="err" id="errBox"></p>
|
|
|
|
|
|
<p class="foot">AzA Medwork</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
|
|
|
(function () {{
|
|
|
|
|
|
function useFallback(reason) {{
|
|
|
|
|
|
document.body.classList.add("no-video");
|
|
|
|
|
|
if (reason) console.warn("START_PANEL_VIDEO_FALLBACK", reason);
|
|
|
|
|
|
}}
|
|
|
|
|
|
function initVideo() {{
|
|
|
|
|
|
var v = document.getElementById("bgVideo");
|
|
|
|
|
|
if (!v) {{
|
|
|
|
|
|
useFallback("no_video_element");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
v.muted = true;
|
|
|
|
|
|
v.playsInline = true;
|
|
|
|
|
|
v.setAttribute("playsinline", "");
|
|
|
|
|
|
v.addEventListener("error", function () {{
|
|
|
|
|
|
useFallback("error code=" + (v.error ? v.error.code : "?"));
|
|
|
|
|
|
}});
|
|
|
|
|
|
v.addEventListener("playing", function () {{
|
|
|
|
|
|
document.body.classList.remove("no-video");
|
|
|
|
|
|
}});
|
|
|
|
|
|
v.addEventListener("canplay", function () {{
|
|
|
|
|
|
document.body.classList.remove("no-video");
|
|
|
|
|
|
v.play().catch(function () {{ useFallback("play_rejected"); }});
|
|
|
|
|
|
}});
|
|
|
|
|
|
setTimeout(function () {{
|
|
|
|
|
|
if (v.error) useFallback("timeout_error");
|
|
|
|
|
|
else if (v.readyState < 2 && v.networkState === 3) useFallback("timeout_no_source");
|
|
|
|
|
|
}}, 15000);
|
|
|
|
|
|
try {{ v.load(); }} catch (e) {{ useFallback("load_failed"); }}
|
|
|
|
|
|
v.play().catch(function () {{}});
|
|
|
|
|
|
}}
|
|
|
|
|
|
if (document.readyState === "loading") {{
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", initVideo);
|
|
|
|
|
|
}} else {{
|
|
|
|
|
|
initVideo();
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
|
|
|
|
function showErr(msg) {{
|
|
|
|
|
|
var el = document.getElementById("errBox");
|
|
|
|
|
|
el.textContent = msg || "";
|
|
|
|
|
|
el.classList.toggle("show", !!msg);
|
|
|
|
|
|
}}
|
|
|
|
|
|
function setBusy(busy) {{
|
|
|
|
|
|
document.getElementById("btnOffice").disabled = busy;
|
|
|
|
|
|
document.getElementById("btnPraxischat").disabled = busy;
|
|
|
|
|
|
}}
|
|
|
|
|
|
async function callApi(method) {{
|
|
|
|
|
|
if (!window.pywebview || !pywebview.api || !pywebview.api[method]) {{
|
|
|
|
|
|
showErr("Start-API nicht verfügbar.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}}
|
|
|
|
|
|
setBusy(true);
|
|
|
|
|
|
showErr("");
|
|
|
|
|
|
try {{
|
|
|
|
|
|
var r = await pywebview.api[method]();
|
|
|
|
|
|
if (r && r.ok) return;
|
|
|
|
|
|
showErr((r && r.message) ? r.message : "Start fehlgeschlagen.");
|
|
|
|
|
|
}} catch (e) {{
|
|
|
|
|
|
showErr(String(e && e.message ? e.message : e));
|
|
|
|
|
|
}} finally {{
|
|
|
|
|
|
setBusy(false);
|
|
|
|
|
|
}}
|
|
|
|
|
|
}}
|
|
|
|
|
|
document.getElementById("btnOffice").addEventListener("click", function () {{
|
|
|
|
|
|
callApi("open_office");
|
|
|
|
|
|
}});
|
|
|
|
|
|
document.getElementById("btnPraxischat").addEventListener("click", function () {{
|
|
|
|
|
|
callApi("open_praxischat");
|
|
|
|
|
|
}});
|
|
|
|
|
|
}})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _webview_storage_dir() -> str:
|
|
|
|
|
|
appdata = os.environ.get("APPDATA") or os.environ.get("LOCALAPPDATA") or ""
|
|
|
|
|
|
base = Path(appdata) / "AzA" / "StartPanelWebView" if appdata else _ROOT / ".aza_start_panel_webview"
|
|
|
|
|
|
try:
|
|
|
|
|
|
base.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
except OSError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return str(base)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _has_pywebview() -> bool:
|
|
|
|
|
|
try:
|
|
|
|
|
|
import webview # noqa: F401
|
|
|
|
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _should_use_webview() -> bool:
|
|
|
|
|
|
if os.environ.get("AZA_START_PANEL_FORCE_TK", "").strip().lower() in (
|
|
|
|
|
|
"1",
|
|
|
|
|
|
"true",
|
|
|
|
|
|
"yes",
|
|
|
|
|
|
):
|
|
|
|
|
|
return False
|
|
|
|
|
|
return _has_pywebview()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _icon_path() -> str | None:
|
|
|
|
|
|
return _resolve_native_icon_path()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ensure_win32_app_user_model_id() -> None:
|
|
|
|
|
|
"""Eigener Taskleisten-Eintrag statt python.exe (muss vor dem ersten Fenster)."""
|
|
|
|
|
|
if sys.platform != "win32":
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
|
|
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(_WIN_APP_ID)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _resolve_native_icon_path() -> str | None:
|
|
|
|
|
|
"""logo.ico bevorzugt; sonst logo.png -> temporaeres .ico (nur fuer Fenster-Icon)."""
|
|
|
|
|
|
ico = _ROOT / "logo.ico"
|
|
|
|
|
|
if ico.is_file():
|
|
|
|
|
|
return str(ico.resolve())
|
|
|
|
|
|
png = _ROOT / "logo.png"
|
|
|
|
|
|
if png.is_file() and _HAS_PIL:
|
|
|
|
|
|
cache = _ROOT / ".aza_start_panel_icon_cache.ico"
|
|
|
|
|
|
try:
|
|
|
|
|
|
if (not cache.is_file()) or cache.stat().st_mtime < png.stat().st_mtime:
|
|
|
|
|
|
img = Image.open(png)
|
|
|
|
|
|
img.save(
|
|
|
|
|
|
cache,
|
|
|
|
|
|
format="ICO",
|
|
|
|
|
|
sizes=[(16, 16), (32, 32), (48, 48)],
|
|
|
|
|
|
)
|
|
|
|
|
|
return str(cache.resolve())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _webview_hwnd(window) -> int | None:
|
|
|
|
|
|
native = getattr(window, "native", None)
|
|
|
|
|
|
if native is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
handle = getattr(native, "Handle", None)
|
|
|
|
|
|
if handle is not None:
|
|
|
|
|
|
return int(handle.ToInt32())
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _win32_find_window_hwnd(title: str = "AzA") -> int | None:
|
|
|
|
|
|
if sys.platform != "win32":
|
|
|
|
|
|
return None
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
|
|
|
|
user32 = ctypes.windll.user32
|
|
|
|
|
|
|
|
|
|
|
|
found: list[int] = []
|
|
|
|
|
|
|
|
|
|
|
|
def _enum(hwnd, _lparam) -> bool:
|
|
|
|
|
|
if not user32.IsWindowVisible(hwnd):
|
|
|
|
|
|
return True
|
|
|
|
|
|
buf = ctypes.create_unicode_buffer(256)
|
|
|
|
|
|
if user32.GetWindowTextW(hwnd, buf, 256) and buf.value == title:
|
|
|
|
|
|
found.append(int(hwnd))
|
|
|
|
|
|
return False
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
|
|
|
|
|
|
user32.EnumWindows(WNDENUMPROC(_enum), 0)
|
|
|
|
|
|
return found[0] if found else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _win32_set_window_icon(hwnd: int, icon_path: str) -> bool:
|
|
|
|
|
|
"""WM_SETICON + Klassen-Icon fuer Titelzeile und Taskleiste."""
|
|
|
|
|
|
if sys.platform != "win32":
|
|
|
|
|
|
return False
|
|
|
|
|
|
import ctypes
|
|
|
|
|
|
|
|
|
|
|
|
user32 = ctypes.windll.user32
|
|
|
|
|
|
path = os.path.abspath(icon_path)
|
|
|
|
|
|
if not os.path.isfile(path):
|
|
|
|
|
|
return False
|
|
|
|
|
|
WM_SETICON = 0x0080
|
|
|
|
|
|
ICON_SMALL = 0
|
|
|
|
|
|
ICON_BIG = 1
|
|
|
|
|
|
IMAGE_ICON = 1
|
|
|
|
|
|
LR_LOADFROMFILE = 0x00000010
|
|
|
|
|
|
GCLP_HICON = -14
|
|
|
|
|
|
GCLP_HICONSM = -34
|
|
|
|
|
|
set_class = getattr(user32, "SetClassLongPtrW", None) or user32.SetClassLongW
|
|
|
|
|
|
ok = False
|
|
|
|
|
|
h_default = user32.LoadImageW(None, path, IMAGE_ICON, 0, 0, LR_LOADFROMFILE)
|
|
|
|
|
|
h_small = user32.LoadImageW(None, path, IMAGE_ICON, 16, 16, LR_LOADFROMFILE)
|
|
|
|
|
|
h_big = user32.LoadImageW(None, path, IMAGE_ICON, 32, 32, LR_LOADFROMFILE) or h_default
|
|
|
|
|
|
if h_small:
|
|
|
|
|
|
user32.SendMessageW(hwnd, WM_SETICON, ICON_SMALL, h_small)
|
|
|
|
|
|
set_class(hwnd, GCLP_HICONSM, h_small)
|
|
|
|
|
|
ok = True
|
|
|
|
|
|
if h_big:
|
|
|
|
|
|
user32.SendMessageW(hwnd, WM_SETICON, ICON_BIG, h_big)
|
|
|
|
|
|
set_class(hwnd, GCLP_HICON, h_big)
|
|
|
|
|
|
ok = True
|
|
|
|
|
|
return ok
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_native_window_icon(window) -> bool:
|
|
|
|
|
|
path = _resolve_native_icon_path()
|
|
|
|
|
|
if not path:
|
|
|
|
|
|
print("[AzA Startpanel] WINDOW_ICON=missing")
|
|
|
|
|
|
return False
|
|
|
|
|
|
hwnd = _webview_hwnd(window) or _win32_find_window_hwnd("AzA")
|
|
|
|
|
|
applied = bool(hwnd and _win32_set_window_icon(hwnd, path))
|
|
|
|
|
|
print(
|
|
|
|
|
|
f"[AzA Startpanel] WINDOW_ICON={'ok' if applied else 'pending'} "
|
|
|
|
|
|
f"file={Path(path).name} hwnd={hwnd or 0}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return applied
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _schedule_native_window_icon(window) -> None:
|
|
|
|
|
|
for delay in (0.15, 0.45, 1.0, 2.5):
|
|
|
|
|
|
threading.Timer(delay, lambda: _apply_native_window_icon(window)).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bind_native_icon_on_loaded(window) -> None:
|
|
|
|
|
|
def _on_loaded() -> None:
|
|
|
|
|
|
_apply_native_window_icon(window)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
window.events.loaded += _on_loaded
|
|
|
|
|
|
window.events.shown += _on_loaded
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
_schedule_native_window_icon(window)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _bind_window_center_on_shown(window) -> None:
|
|
|
|
|
|
def _center() -> None:
|
|
|
|
|
|
hwnd = _webview_hwnd(window) or _win32_find_window_hwnd("AzA")
|
|
|
|
|
|
if hwnd:
|
|
|
|
|
|
_win32_center_window(hwnd, _W, _H)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
window.events.shown += _center
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
for delay in (0.05, 0.25, 0.6):
|
|
|
|
|
|
threading.Timer(delay, _center).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _prime_pywebview_icon(icon_path: str | None) -> None:
|
|
|
|
|
|
"""Icon vor create_window in pywebview-State (winforms liest _state['icon'])."""
|
|
|
|
|
|
if not icon_path:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
import webview
|
|
|
|
|
|
|
|
|
|
|
|
webview._state["icon"] = os.path.abspath(icon_path)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_webview_panel() -> int:
|
|
|
|
|
|
import webview
|
|
|
|
|
|
|
|
|
|
|
|
_ensure_win32_app_user_model_id()
|
|
|
|
|
|
icon_path = _icon_path()
|
|
|
|
|
|
_prime_pywebview_icon(icon_path)
|
|
|
|
|
|
_log_asset_status()
|
|
|
|
|
|
if not _MP4.is_file():
|
|
|
|
|
|
print("[AzA Startpanel] START_PANEL_VIDEO_FALLBACK reason=mp4_missing")
|
|
|
|
|
|
server = _LocalAssetServer(_ROOT)
|
|
|
|
|
|
start_url = server.start()
|
|
|
|
|
|
_log_asset_status(asset_mode="http_localhost", start_url=start_url)
|
|
|
|
|
|
|
|
|
|
|
|
api = StartPanelApi()
|
|
|
|
|
|
win_x, win_y = _screen_center_xy(_W, _H)
|
|
|
|
|
|
try:
|
|
|
|
|
|
win = webview.create_window(
|
|
|
|
|
|
"AzA",
|
|
|
|
|
|
url=start_url,
|
|
|
|
|
|
width=_W,
|
|
|
|
|
|
height=_H,
|
|
|
|
|
|
x=win_x,
|
|
|
|
|
|
y=win_y,
|
|
|
|
|
|
resizable=False,
|
|
|
|
|
|
js_api=api,
|
|
|
|
|
|
background_color="#b8c9d8",
|
|
|
|
|
|
)
|
|
|
|
|
|
api.bind_window(win)
|
|
|
|
|
|
_bind_native_icon_on_loaded(win)
|
|
|
|
|
|
_bind_window_center_on_shown(win)
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_updater import run_startup_update_check_in_background
|
|
|
|
|
|
|
2026-05-28 18:58:38 +02:00
|
|
|
|
run_startup_update_check_in_background(
|
|
|
|
|
|
delay_seconds=1.5,
|
|
|
|
|
|
use_native_dialog=True,
|
|
|
|
|
|
)
|
2026-05-23 21:31:34 +02:00
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[AzA Startpanel] UPDATE_CHECK init skipped: {exc}")
|
|
|
|
|
|
webview.start(
|
|
|
|
|
|
private_mode=False,
|
|
|
|
|
|
storage_path=_webview_storage_dir(),
|
|
|
|
|
|
icon=icon_path,
|
|
|
|
|
|
)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
server.stop()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_logo_photo(master: tk.Misc, size: int = 44) -> tk.PhotoImage | None:
|
|
|
|
|
|
if not _LOGO.is_file():
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
if _HAS_PIL:
|
|
|
|
|
|
img = Image.open(_LOGO).convert("RGBA")
|
|
|
|
|
|
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
|
|
|
|
|
return ImageTk.PhotoImage(img, master=master)
|
|
|
|
|
|
return tk.PhotoImage(file=str(_LOGO), master=master)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _build_background_photo(master: tk.Misc) -> tk.PhotoImage | None:
|
|
|
|
|
|
if not _PNG.is_file() or not _HAS_PIL:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
img = Image.open(_PNG).convert("RGB")
|
|
|
|
|
|
img = ImageEnhance.Brightness(img).enhance(1.08)
|
|
|
|
|
|
img = img.resize((_W, _H), Image.Resampling.LANCZOS)
|
|
|
|
|
|
img = img.filter(ImageFilter.GaussianBlur(radius=1.6))
|
|
|
|
|
|
fog = Image.new("RGBA", (_W, _H), (245, 248, 252, 140))
|
|
|
|
|
|
img = Image.alpha_composite(img.convert("RGBA"), fog).convert("RGB")
|
|
|
|
|
|
return ImageTk.PhotoImage(img, master=master)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AzAStartPanelTk:
|
|
|
|
|
|
"""Nur wenn pywebview fehlt: statisches PNG, kein MP4."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
|
self.root = tk.Tk()
|
|
|
|
|
|
self.root.title("AzA")
|
|
|
|
|
|
self.root.resizable(False, False)
|
|
|
|
|
|
self.root.configure(bg=_FALLBACK_BG)
|
|
|
|
|
|
_center_window(self.root, _W, _H)
|
|
|
|
|
|
|
|
|
|
|
|
self._bg_photo: tk.PhotoImage | None = None
|
|
|
|
|
|
self._logo_photo: tk.PhotoImage | None = None
|
|
|
|
|
|
|
|
|
|
|
|
self.canvas = tk.Canvas(
|
|
|
|
|
|
self.root, width=_W, height=_H, highlightthickness=0, bd=0
|
|
|
|
|
|
)
|
|
|
|
|
|
self.canvas.pack(fill="both", expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
self._bg_photo = _build_background_photo(self.root)
|
|
|
|
|
|
if self._bg_photo is not None:
|
|
|
|
|
|
self.canvas.create_image(0, 0, anchor="nw", image=self._bg_photo)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.canvas.configure(bg=_FALLBACK_BG)
|
|
|
|
|
|
|
|
|
|
|
|
card = tk.Frame(
|
|
|
|
|
|
self.canvas,
|
|
|
|
|
|
bg=_CARD_BG,
|
|
|
|
|
|
highlightbackground="#c8d6e4",
|
|
|
|
|
|
highlightthickness=1,
|
|
|
|
|
|
)
|
|
|
|
|
|
self.canvas.create_window(_W // 2, _H // 2, window=card, width=_CARD_W)
|
|
|
|
|
|
|
|
|
|
|
|
header = tk.Frame(card, bg=_CARD_BG)
|
|
|
|
|
|
header.pack(pady=(18, 6))
|
|
|
|
|
|
|
|
|
|
|
|
self._logo_photo = _load_logo_photo(card, 40)
|
|
|
|
|
|
if self._logo_photo is not None:
|
|
|
|
|
|
tk.Label(header, image=self._logo_photo, bg=_CARD_BG).pack()
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(header, text="AzA", font=_FONT_TITLE, fg=_TEXT, bg=_CARD_BG).pack(
|
|
|
|
|
|
pady=(8, 2)
|
|
|
|
|
|
)
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
header,
|
|
|
|
|
|
text="Was möchten Sie öffnen?",
|
|
|
|
|
|
font=_FONT_SUB,
|
|
|
|
|
|
fg=_SUBTLE,
|
|
|
|
|
|
bg=_CARD_BG,
|
|
|
|
|
|
).pack(pady=(0, 13))
|
|
|
|
|
|
|
|
|
|
|
|
btn_frame = tk.Frame(card, bg=_CARD_BG)
|
|
|
|
|
|
btn_frame.pack(padx=21, pady=(0, 6))
|
|
|
|
|
|
|
|
|
|
|
|
self._mk_btn(btn_frame, "AzA Office", self._on_office).pack(
|
|
|
|
|
|
fill="x", pady=(0, 9)
|
|
|
|
|
|
)
|
|
|
|
|
|
self._mk_btn(btn_frame, "Praxis Chat", self._on_praxischat).pack(fill="x")
|
|
|
|
|
|
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
card, text="AzA Medwork", font=_FONT_FOOT, fg=_SUBTLE, bg=_CARD_BG
|
|
|
|
|
|
).pack(pady=(15, 15))
|
|
|
|
|
|
|
|
|
|
|
|
ico = _icon_path()
|
|
|
|
|
|
if ico and sys.platform == "win32":
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.root.iconbitmap(ico)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if not _has_pywebview():
|
|
|
|
|
|
tk.Label(
|
|
|
|
|
|
card,
|
|
|
|
|
|
text="Hinweis: Video-Hintergrund benötigt pywebview.",
|
|
|
|
|
|
font=("Segoe UI", 8),
|
|
|
|
|
|
fg=_SUBTLE,
|
|
|
|
|
|
bg=_CARD_BG,
|
|
|
|
|
|
wraplength=_CARD_W - 24,
|
|
|
|
|
|
).pack(pady=(0, 8))
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from aza_updater import run_startup_update_check_in_background
|
|
|
|
|
|
|
2026-05-28 18:58:38 +02:00
|
|
|
|
self.root.after(
|
|
|
|
|
|
1500,
|
|
|
|
|
|
lambda: run_startup_update_check_in_background(
|
|
|
|
|
|
parent=self.root,
|
|
|
|
|
|
schedule_on_main=lambda fn: self.root.after(0, fn),
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
2026-05-23 21:31:34 +02:00
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[AzA Startpanel] UPDATE_CHECK init skipped: {exc}")
|
|
|
|
|
|
|
|
|
|
|
|
def _mk_btn(self, parent: tk.Misc, text: str, cmd) -> tk.Button:
|
|
|
|
|
|
btn = tk.Button(
|
|
|
|
|
|
parent,
|
|
|
|
|
|
text=text,
|
|
|
|
|
|
font=_FONT_BTN,
|
|
|
|
|
|
fg="#ffffff",
|
|
|
|
|
|
bg=_ACCENT,
|
|
|
|
|
|
activebackground=_ACCENT_HOVER,
|
|
|
|
|
|
activeforeground="#ffffff",
|
|
|
|
|
|
relief="flat",
|
|
|
|
|
|
bd=0,
|
|
|
|
|
|
padx=16,
|
|
|
|
|
|
pady=9,
|
|
|
|
|
|
cursor="hand2",
|
|
|
|
|
|
command=cmd,
|
|
|
|
|
|
)
|
|
|
|
|
|
btn.bind("<Enter>", lambda _e: btn.configure(bg=_ACCENT_HOVER))
|
|
|
|
|
|
btn.bind("<Leave>", lambda _e: btn.configure(bg=_ACCENT))
|
|
|
|
|
|
return btn
|
|
|
|
|
|
|
|
|
|
|
|
def _on_office(self) -> None:
|
|
|
|
|
|
ok, msg = start_office()
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
messagebox.showerror("AzA Office", msg, parent=self.root)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.root.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
def _on_praxischat(self) -> None:
|
|
|
|
|
|
ok, msg = start_praxischat()
|
|
|
|
|
|
if not ok:
|
|
|
|
|
|
messagebox.showerror("Praxischat", msg, parent=self.root)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.root.destroy()
|
|
|
|
|
|
|
|
|
|
|
|
def run(self) -> None:
|
|
|
|
|
|
self.root.mainloop()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> int:
|
|
|
|
|
|
_ensure_win32_app_user_model_id()
|
|
|
|
|
|
if _should_use_webview():
|
|
|
|
|
|
try:
|
|
|
|
|
|
return run_webview_panel()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
print(f"[AzA Startpanel] WebView-Fehler: {exc}", file=sys.stderr)
|
|
|
|
|
|
try:
|
|
|
|
|
|
root = tk.Tk()
|
|
|
|
|
|
root.withdraw()
|
|
|
|
|
|
messagebox.showerror(
|
|
|
|
|
|
"AzA Startpanel",
|
|
|
|
|
|
"WebView-Huelle konnte nicht starten.\n"
|
|
|
|
|
|
f"{exc}\n\nEs wird das statische Fallback geöffnet (ohne Video).",
|
|
|
|
|
|
parent=root,
|
|
|
|
|
|
)
|
|
|
|
|
|
root.destroy()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
AzAStartPanelTk().run()
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
_ensure_win32_app_user_model_id()
|
|
|
|
|
|
raise SystemExit(main())
|