Files
aza/AzA march 2026/aza_start_panel.py

976 lines
30 KiB
Python
Raw Permalink Normal View History

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())