Files
aza/AzA march 2026/aza_start_panel.py
2026-05-28 18:58:38 +02:00

976 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
_POPEN_NO_CONSOLE: dict = (
{"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}
)
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"),
**_POPEN_NO_CONSOLE,
)
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"),
**_POPEN_NO_CONSOLE,
)
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"),
**_POPEN_NO_CONSOLE,
)
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"),
**_POPEN_NO_CONSOLE,
)
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
run_startup_update_check_in_background(
delay_seconds=1.5,
use_native_dialog=True,
)
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
self.root.after(
1500,
lambda: run_startup_update_check_in_background(
parent=self.root,
schedule_on_main=lambda fn: self.root.after(0, fn),
),
)
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())