This commit is contained in:
2026-05-16 20:33:36 +02:00
parent 96c1029d91
commit 968bf7d102
212 changed files with 954195 additions and 658 deletions

View File

@@ -2,14 +2,33 @@
"""
AzA Empfang Web-Huelle: eigener Desktop-Prozess (pywebview).
Wird vom Desktop per subprocess gestartet, damit keine GUI-Kollision mit Tkinter entsteht.
Argument: erste Start-URL (z.B. GET /empfang/shell/launch?token=...).
Drei Betriebsmodi, klar getrennt:
* ``desktop_shell`` Vom AzA-Hauptprogramm (basis14) per subprocess gestartet
mit /empfang/shell/launch?token=... (KEIN target=...).
Fenstertitel: "AzA-Empfang \xb7 Desktop"
Storage: %APPDATA%\\AzA\\EmpfangWebView
AUMID: ch.aza-medwork.empfang.shellwebview
* ``empfang_chat_shell`` Separat installierbare Huelle (AZA_EmpfangShell.exe via
aza_empfang_chat_setup.exe) auf Empfangs-/MPA-Computern.
Default-URL beim Doppelklick:
https://empfang.aza-medwork.ch/empfang/
?empfang_chat_shell=1&shell_source=empfang_chat_shell
Fenstertitel: "AzA Empfang Chat"
Storage: %APPDATA%\\AzA\\EmpfangChatWebView
AUMID: ch.aza-medwork.empfang.chatshell
* ``minichat`` Kleines Empfang-Popup (?minichat=1). Erbt Storage/AUMID
des Parent-Modus, damit die Login-Session erhalten bleibt.
Fenstertitel: "AzA MiniChat"
WebView2-Profil: pywebview nutzt standardmaessig ``private_mode=True`` (ephemeral) und
speichert keine Site-Permissions zwischen Sessions. Für die Huelle setzen wir einen
festen ``storage_path`` unter %APPDATA%\\AzA\\EmpfangWebView und ``private_mode=False``,
damit z. B. Mikrofon-Erlaubnis für die App-URL persistiert (wie im normalen Edge-Profil),
ohne den Systembrowser zu verwenden.
speichert keine Site-Permissions zwischen Sessions. Wir setzen je Modus einen festen
``storage_path`` und ``private_mode=False``, damit Mikrofon-Erlaubnis etc. fuer die
App-URL persistieren (wie im normalen Edge-Profil) ohne den Systembrowser zu nutzen.
Getrennte Storage-Pfade verhindern, dass Haupt- und Empfang-Chat-Huelle sich beim
parallelen Coldstart gegenseitig sperren ("Keine Rueckmeldung").
"""
from __future__ import annotations
@@ -107,8 +126,13 @@ def _apply_win32_topmost(value: bool) -> bool:
return False
def _focus_other_empfang_host_window() -> bool:
"""Bringt ein anderes AzA-Empfang-Fenster (anderer Prozess) in den Vordergrund."""
def _focus_other_empfang_host_window(title_prefixes: tuple[str, ...] = ("AzA-Empfang",)) -> bool:
"""Bringt ein anderes AzA-Empfang-Fenster (anderer Prozess) in den Vordergrund.
``title_prefixes`` schraenkt das Match auf den eigenen Modus ein. Default ist
die Desktop-Huelle ("AzA-Empfang"/"AzA-Empfang \xb7 Desktop"), damit Cross-Mode-
Fokus (Chat-Huelle <-> Desktop-Huelle) NICHT passiert.
"""
if sys.platform != "win32":
return False
try:
@@ -118,6 +142,7 @@ def _focus_other_empfang_host_window() -> bool:
user32 = ctypes.windll.user32
my_pid = int(os.getpid())
handles: list[int] = []
prefixes = tuple(p for p in title_prefixes if p)
@ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
def _enum(hwnd, _lp):
@@ -125,7 +150,8 @@ def _focus_other_empfang_host_window() -> bool:
return True
buf = ctypes.create_unicode_buffer(260)
user32.GetWindowTextW(hwnd, buf, 260)
if not (buf.value == "AzA-Empfang" or buf.value.startswith("AzA-Empfang")):
t = buf.value or ""
if not any(t == p or t.startswith(p) for p in prefixes):
return True
pid = wintypes.DWORD()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
@@ -146,25 +172,172 @@ def _focus_other_empfang_host_window() -> bool:
def _empfang_shell_icon_path() -> str | None:
"""Windows/pywebview: WinForms laedt Fenstericon aus .ico neben diesem Skript."""
p = Path(__file__).resolve().parent / "logo.ico"
return str(p) if p.is_file() else None
"""Windows/pywebview: WinForms laedt Fenstericon aus .ico neben diesem Skript.
Im PyInstaller-Bundle liegt logo.ico unter sys._MEIPASS (bzw. neben sys.executable).
"""
candidates: list[Path] = []
try:
if getattr(sys, "frozen", False):
candidates.append(Path(sys.executable).resolve().parent / "logo.ico")
meip = getattr(sys, "_MEIPASS", "")
if meip:
candidates.append(Path(meip) / "logo.ico")
except Exception:
pass
candidates.append(Path(__file__).resolve().parent / "logo.ico")
for c in candidates:
try:
if c.is_file():
return str(c)
except Exception:
continue
return None
def _empfang_webview_storage_dir() -> str:
"""Eigener User-Data-Ordner, isoliert vom normalen Browser."""
# ---------------------------------------------------------------------------
# Modus-Klassifizierung (Desktop-Huelle / separate Empfang-Chat-Huelle /
# MiniChat-Popup) - bestimmt Titel, Storage, AppUserModelID.
# ---------------------------------------------------------------------------
_MODE_DESKTOP_SHELL = "desktop_shell"
_MODE_EMPFANG_CHAT_SHELL = "empfang_chat_shell"
_MODE_MINICHAT = "minichat"
def _classify_shell_mode(start_url: str, *, parent_hint: str = "") -> str:
"""Bestimmt den Betriebsmodus aus der Start-URL.
Eindeutige Marker (per Reihenfolge):
1. ?minichat=1 -> minichat (Parent-Modus folgt unten)
2. target=empfang_chat_shell (im /shell/launch) -> empfang_chat_shell
3. shell_source=aza_desktop / ?desktop_shell=1 -> desktop_shell
4. shell_source=empfang_chat_shell / ?empfang_chat_shell=1 -> empfang_chat_shell
5. /empfang/shell/launch?token=... (ohne target) -> desktop_shell (legacy AzA Office)
6. Default (kein Argument, Doppelklick) -> empfang_chat_shell
``parent_hint`` ist nur fuer MiniChat relevant: minichat erbt den Modus
seines Parent, damit das WebView2-Profil (und damit die Login-Session)
konsistent bleibt.
"""
u = (start_url or "").strip()
low = u.lower()
if "minichat=1" in low or "mode=minichat" in low:
return _MODE_MINICHAT
if "target=empfang_chat_shell" in low or "target%3dempfang_chat_shell" in low:
return _MODE_EMPFANG_CHAT_SHELL
if (
"shell_source=aza_desktop" in low
or "shell_source%3daza_desktop" in low
or "desktop_shell=1" in low
):
return _MODE_DESKTOP_SHELL
if (
"shell_source=empfang_chat_shell" in low
or "shell_source%3dempfang_chat_shell" in low
or "empfang_chat_shell=1" in low
):
return _MODE_EMPFANG_CHAT_SHELL
if "/empfang/shell/launch" in low and "token=" in low:
# /shell/launch ohne target=... -> historischer AzA-Desktop-Pfad.
return _MODE_DESKTOP_SHELL
_ph = (parent_hint or "").strip().lower()
if _ph in (_MODE_DESKTOP_SHELL, _MODE_EMPFANG_CHAT_SHELL):
return _ph
return _MODE_EMPFANG_CHAT_SHELL
def _split_argv_for_shell_mode(argv: list[str]) -> tuple[list[str], str]:
"""Filtert ``--shell-mode=<x>`` aus argv heraus.
Wird vom MiniChat-Subprocess gesetzt, damit der Kind-Prozess das gleiche
WebView2-Profil/AUMID wie sein Parent verwendet (Login bleibt erhalten).
Andere Modi setzen das Flag nicht; sie werden aus der URL klassifiziert.
"""
rest: list[str] = []
mode = ""
for a in argv:
s = str(a or "")
ls = s.strip()
if ls.startswith("--shell-mode="):
mode = ls.split("=", 1)[1].strip().lower()
continue
rest.append(s)
return rest, mode
def _window_title_for_mode(mode: str) -> str:
if mode == _MODE_DESKTOP_SHELL:
return "AzA-Empfang \u00b7 Desktop"
if mode == _MODE_MINICHAT:
return "AzA MiniChat"
return "AzA Empfang Chat"
def _aumid_for_mode(mode: str) -> str:
if mode == _MODE_DESKTOP_SHELL:
# Kompatibel zur bisherigen Haupt-Huelle (Taskleisten-Icon).
return "ch.aza-medwork.empfang.shellwebview"
if mode == _MODE_MINICHAT:
return "ch.aza-medwork.empfang.minichat"
return "ch.aza-medwork.empfang.chatshell"
def _effective_profile_mode(mode: str, parent_mode_hint: str = "") -> str:
"""WebView2-Profil-Mode (Cookies/Login-Session) - MiniChat erbt Parent.
Storage darf MiniChat NICHT entkoppeln, sonst geht die Login-Session verloren.
Das Fenster selbst hat trotzdem eigenen Titel/AUMID/Pin (logische Trennung).
"""
m = (mode or "").strip().lower()
if m == _MODE_MINICHAT:
ph = (parent_mode_hint or "").strip().lower()
if ph in (_MODE_DESKTOP_SHELL, _MODE_EMPFANG_CHAT_SHELL):
return ph
# Default-Parent fuer MiniChat: Chat-Huelle (Standalone-Empfang-Chat-Shell)
return _MODE_EMPFANG_CHAT_SHELL
return m or _MODE_DESKTOP_SHELL
def _empfang_webview_storage_dir(mode: str = _MODE_DESKTOP_SHELL, parent_mode_hint: str = "") -> str:
"""WebView2-User-Data-Ordner pro Modus, damit Haupt- und Chat-Huelle nicht
dasselbe WebView2-Profil sperren.
MiniChat ERBT den Storage seines Parent-Modus, damit die Login-Session
geteilt wird (Cookies, LocalStorage).
- desktop_shell -> %APPDATA%\\AzA\\EmpfangWebView (kompatibel zur Hauptprogramm-Huelle)
- empfang_chat_shell -> %APPDATA%\\AzA\\EmpfangChatWebView
- minichat (Parent=Desktop) -> EmpfangWebView
- minichat (Parent=ChatShell) -> EmpfangChatWebView
"""
eff = _effective_profile_mode(mode, parent_mode_hint)
appdata = (os.environ.get("APPDATA") or "").strip()
if appdata:
return str(Path(appdata) / "AzA" / "EmpfangWebView")
return str(Path.home() / ".aza_empfang_webview")
base = Path(appdata) / "AzA" if appdata else Path.home() / ".aza_empfang_webview"
if eff == _MODE_EMPFANG_CHAT_SHELL:
return str(base / "EmpfangChatWebView") if appdata else str(base.parent / ".aza_empfang_chat_webview")
return str(base / "EmpfangWebView") if appdata else str(base)
def _shell_pin_state_path() -> Path:
return Path(_empfang_webview_storage_dir()) / "shell_pin_on_top.json"
def _shell_pin_state_path(mode: str = _MODE_DESKTOP_SHELL, parent_mode_hint: str = "") -> Path:
"""Pin-State pro Fenster-Modus (MiniChat hat eigenes Pin-Verhalten),
liegt aber im Profil-Storage des effektiven Parent-Modus.
"""
storage = _empfang_webview_storage_dir(mode, parent_mode_hint)
if (mode or "").strip().lower() == _MODE_MINICHAT:
return Path(storage) / "minichat_pin_on_top.json"
return Path(storage) / "shell_pin_on_top.json"
def _load_shell_pin_on_top() -> bool:
p = _shell_pin_state_path()
def _load_shell_pin_on_top(mode: str = _MODE_DESKTOP_SHELL, parent_mode_hint: str = "") -> bool:
p = _shell_pin_state_path(mode, parent_mode_hint)
try:
if p.is_file():
data = json.loads(p.read_text(encoding="utf-8"))
@@ -174,8 +347,8 @@ def _load_shell_pin_on_top() -> bool:
return False
def _save_shell_pin_on_top(value: bool) -> None:
p = _shell_pin_state_path()
def _save_shell_pin_on_top(value: bool, mode: str = _MODE_DESKTOP_SHELL, parent_mode_hint: str = "") -> None:
p = _shell_pin_state_path(mode, parent_mode_hint)
try:
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(
@@ -189,9 +362,11 @@ def _save_shell_pin_on_top(value: bool) -> None:
class EmpfangWebviewApi:
"""pywebview JS-API: globaler Patienten-Pinsel (Desktop), Fenster-Pin (always on top)."""
def __init__(self) -> None:
def __init__(self, mode: str = _MODE_DESKTOP_SHELL, parent_mode_hint: str = "") -> None:
self._window = None
self._on_top = _load_shell_pin_on_top()
self._mode = (mode or _MODE_DESKTOP_SHELL).strip().lower()
self._parent_mode_hint = (parent_mode_hint or "").strip().lower()
self._on_top = _load_shell_pin_on_top(self._mode, self._parent_mode_hint)
self._pin_lock = threading.Lock()
def bind_window(self, window) -> None:
@@ -213,9 +388,9 @@ class EmpfangWebviewApi:
finally:
self._pin_lock.release()
_save_shell_pin_on_top(new_val)
_save_shell_pin_on_top(new_val, self._mode, self._parent_mode_hint)
print(
f"[EMPFANG_TOPMOST] stage=manual_toggle requested={new_val}",
f"[EMPFANG_TOPMOST] stage=manual_toggle mode={self._mode} requested={new_val}",
flush=True,
)
@@ -331,6 +506,10 @@ class EmpfangWebviewApi:
Nicht-blockierend: Popen in Daemon-Thread. Damit hat das MiniChat-Fenster
eine eigene pywebview-Instanz inkl. Pin (on_top), ohne window.open-Popup ohne API.
Wichtig: Wir reichen den Parent-Modus via ``--shell-mode=<mode>`` durch.
Damit teilt sich MiniChat das WebView2-Profil seines Parents (Desktop-Huelle
ODER Chat-Huelle), und die Login-Session bleibt erhalten.
"""
url = str(href or "").strip()
if not url:
@@ -343,15 +522,18 @@ class EmpfangWebviewApi:
except Exception:
full = url
w, h = 600, 820
# MiniChat erbt das effektive Profil seines Parents (nicht "minichat" selbst).
parent_mode = _effective_profile_mode(self._mode, self._parent_mode_hint)
def _spawn() -> None:
try:
script = Path(__file__).resolve()
mode_arg = f"--shell-mode={parent_mode}"
if getattr(sys, "frozen", False):
cmd = [sys.executable, full, str(w), str(h)]
cmd = [sys.executable, mode_arg, full, str(w), str(h)]
cwd = str(Path(sys.executable).resolve().parent)
else:
cmd = [sys.executable, str(script), full, str(w), str(h)]
cmd = [sys.executable, str(script), mode_arg, full, str(w), str(h)]
cwd = str(script.parent)
subprocess.Popen(
cmd,
@@ -365,22 +547,31 @@ class EmpfangWebviewApi:
return {"ok": True}
def open_browser_chat(self, href: str) -> dict:
"""Volle Empfang-Ansicht ohne minichat: erst anderes Fenster fokussieren, sonst neuer Prozess."""
"""Volle Empfang-Ansicht ohne minichat: erst anderes Fenster fokussieren, sonst neuer Prozess.
Cross-Mode-Fokus wird vermieden: nur Fenster mit dem gleichen Titel-Praefix
wie der eigene Modus werden in den Vordergrund geholt.
"""
url = _strip_minichat_query(href)
if not url:
return {"ok": False, "reason": "empty"}
w_main, h_main = 1180, 820
# Beim "Browser-Chat oeffnen" aus dem MiniChat das volle Empfang-Fenster
# im effektiven Parent-Modus oeffnen, nicht erneut als MiniChat.
parent_mode = _effective_profile_mode(self._mode, self._parent_mode_hint)
prefixes = (_window_title_for_mode(parent_mode),)
def _work() -> None:
if _focus_other_empfang_host_window():
if _focus_other_empfang_host_window(prefixes):
return
try:
script = Path(__file__).resolve()
mode_arg = f"--shell-mode={parent_mode}"
if getattr(sys, "frozen", False):
cmd = [sys.executable, url, str(w_main), str(h_main)]
cmd = [sys.executable, mode_arg, url, str(w_main), str(h_main)]
cwd = str(Path(sys.executable).resolve().parent)
else:
cmd = [sys.executable, str(script), url, str(w_main), str(h_main)]
cmd = [sys.executable, str(script), mode_arg, url, str(w_main), str(h_main)]
cwd = str(script.parent)
subprocess.Popen(
cmd,
@@ -475,6 +666,42 @@ class EmpfangWebviewApi:
stop_global_patient_nr_pick()
return {"ok": True}
def start_region_snapshot_capture(self) -> dict:
"""Startet nativen Bereichs-Screenshot (Tk-Overlay + PIL) fuer die WebView-Huelle.
Rueckgabe an JS erfolgt asynchron per evaluate_js -> shellReceiveSnapshotFromDesktop.
Keine Bilddaten in Logs.
"""
from aza_empfang_region_snapshot import start_region_snapshot_capture as _region_snap_start
win = self._window
if win is None:
return {"ok": False, "reason": "no_window"}
if sys.platform != "win32":
return {"ok": False, "reason": "unsupported_os"}
def on_done(data_url: str | None) -> None:
if not data_url:
self._pinsel_eval_js(
win,
"try{if(window.shellReceiveSnapshotFromDesktopCancel)"
"window.shellReceiveSnapshotFromDesktopCancel();}catch(_e){}",
"snapshot_cancel",
)
return
self._pinsel_eval_js(
win,
"try{if(window.shellReceiveSnapshotFromDesktop)"
"window.shellReceiveSnapshotFromDesktop(%s);}catch(_e){}"
% json.dumps(data_url),
"shellReceiveSnapshotFromDesktop",
)
rc = _region_snap_start(on_done)
if rc == "busy":
return {"ok": False, "reason": "busy"}
return {"ok": True}
def play_notification_sound(self, kind: str = "") -> dict:
"""Spielt einen kurzen Systemton ab (Windows: winsound.MessageBeep).
@@ -509,9 +736,13 @@ class EmpfangWebviewApi:
werden in einen Daemon-Thread ausgelagert, damit der WebView-Worker
nicht haengt ("AzA-Empfang reagiert nicht").
Sucht nur nach Fenstern, deren Titel zum eigenen Modus passt - keine
Cross-Mode-Fokussierung (Desktop-Huelle <-> Chat-Huelle).
Keine Chat-/Patientendaten werden geloggt oder verarbeitet.
"""
win = self._window
own_title = _window_title_for_mode(self._mode)
def _do() -> None:
try:
@@ -538,7 +769,7 @@ class EmpfangWebviewApi:
buf = ctypes.create_unicode_buffer(260)
user32.GetWindowTextW(hwnd, buf, 260)
t = buf.value or ""
if not (t == "AzA-Empfang" or t.startswith("AzA-Empfang")):
if not (t == own_title or t.startswith(own_title)):
return True
pid = ctypes.c_ulong()
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
@@ -575,17 +806,37 @@ def _append_query_marker(url: str, key: str, value: str) -> str:
return f"{url}{sep}{key}={value}"
_EMPFANG_CHAT_SHELL_DEFAULT_BASE = "https://empfang.aza-medwork.ch/empfang/"
def _build_default_empfang_chat_shell_url() -> str:
"""Standard-URL der separaten Empfang-Chat-Huelle (kein Arzt-Desktop):
nutzt aza_empfang_app._empfang_url() bzw. den oeffentlichen Fallback und setzt
den Marker ?empfang_chat_shell=1 fuer empfang.html (Login-/Handoff-UI).
"""Standard-URL der separaten Empfang-Chat-Huelle (kein Arzt-Desktop).
Setzt BEIDE Marker fuer empfang.html (data-empfang-chat-shell + Login-/Handoff-UI):
* empfang_chat_shell=1
* shell_source=empfang_chat_shell
Optional ueberschreibbar per Umgebungsvariable
``AZA_EMPFANG_CHAT_SHELL_URL`` (z.B. Staging). Es wird absichtlich NICHT
aus aza_empfang_app._empfang_url() / backend_url.txt gelesen, damit die
Chat-Huelle unabhaengig von einer optionalen Datei neben der EXE arbeitet
und keine alte/abweichende Server-URL erbt.
"""
base = (os.environ.get("AZA_EMPFANG_CHAT_SHELL_URL") or "").strip()
if not base:
base = _EMPFANG_CHAT_SHELL_DEFAULT_BASE
# Sicherstellen, dass /empfang/ Pfad enthalten ist
try:
from aza_empfang_app import _empfang_url # noqa: WPS433 (lazy)
base = _empfang_url()
p = urlparse(base)
if not (p.path or "").rstrip("/").endswith("/empfang"):
path = (p.path or "").rstrip("/")
new_path = (path + "/empfang/") if not path.endswith("/empfang/") else path
base = urlunparse(p._replace(path=new_path or "/empfang/"))
except Exception:
base = "https://empfang.aza-medwork.ch/empfang/"
return _append_query_marker(base, "empfang_chat_shell", "1")
pass
url = _append_query_marker(base, "empfang_chat_shell", "1")
url = _append_query_marker(url, "shell_source", "empfang_chat_shell")
return url
def _normalize_handoff_code(raw: str) -> str:
@@ -666,11 +917,16 @@ def _split_argv_for_handoff(argv: list[str]) -> tuple[list[str], str, str]:
def main(argv: list[str] | None = None) -> int:
argv = list(argv if argv is not None else sys.argv[1:])
argv, handoff_token, handoff_code = _split_argv_for_handoff(argv)
argv, parent_mode_hint = _split_argv_for_shell_mode(argv)
standalone_default = False
if not argv or not str(argv[0]).strip():
# Standalone / Doppelklick: separate Empfang-Chat-Huelle. Marker fuer empfang.html setzen.
# Standalone / Doppelklick: separate Empfang-Chat-Huelle.
# Beide Marker (empfang_chat_shell=1 & shell_source=empfang_chat_shell)
# werden gesetzt, damit empfang.html die Chat-Shell-UI aktiviert.
url = _build_default_empfang_chat_shell_url()
argv = [url]
standalone_default = True
url = str(argv[0]).strip()
if handoff_token and "/empfang/shell/launch" not in url:
@@ -697,10 +953,39 @@ def main(argv: list[str] | None = None) -> int:
file=sys.stderr, flush=True,
)
w = int(argv[1]) if len(argv) > 1 and str(argv[1]).isdigit() else 1180
h = int(argv[2]) if len(argv) > 2 and str(argv[2]).isdigit() else 820
# Modus zuerst klassifizieren, damit wir mode-abhaengige Default-Groessen
# vergeben koennen (Empfang-Chat-Huelle startet kompakter als Desktop-Huelle).
mode = _classify_shell_mode(url, parent_hint=parent_mode_hint)
if standalone_default and mode == _MODE_DESKTOP_SHELL:
# Default-Doppelklick darf nicht versehentlich als Desktop-Huelle starten.
mode = _MODE_EMPFANG_CHAT_SHELL
storage_path = _empfang_webview_storage_dir()
# Default-Groessen pro Modus:
# desktop_shell -> 1180 x 820 (unveraendert)
# empfang_chat_shell -> 900 x 650 (~70% Flaeche; Chat/Eingabe weiter brauchbar)
# minichat -> 600 x 820 (wie bisher; nur ueber subprocess gesetzt)
if mode == _MODE_EMPFANG_CHAT_SHELL:
default_w, default_h = 900, 650
elif mode == _MODE_MINICHAT:
default_w, default_h = 600, 820
else:
default_w, default_h = 1180, 820
# Explizit per argv uebergebene Groesse (z.B. open_minichat) hat Vorrang.
w = int(argv[1]) if len(argv) > 1 and str(argv[1]).isdigit() else default_w
h = int(argv[2]) if len(argv) > 2 and str(argv[2]).isdigit() else default_h
# AppUserModelID je Modus (vor webview.create_window setzen, damit Taskleisten-
# Gruppierung sauber bleibt). Idempotent: spaeter im __main__ wieder gesetzt.
if sys.platform == "win32":
try:
import ctypes # noqa: WPS433
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
_aumid_for_mode(mode),
)
except Exception:
pass
storage_path = _empfang_webview_storage_dir(mode, parent_mode_hint)
try:
Path(storage_path).mkdir(parents=True, exist_ok=True)
except OSError as exc:
@@ -720,26 +1005,21 @@ def main(argv: list[str] | None = None) -> int:
)
return 11
def _window_title_for_start_url(start_url: str) -> str:
"""Titel nach Start-URL: Arzt-Desktop-Shell vs. separate Chat-Huelle vs. manueller Test."""
u = (start_url or "")
low = u.lower()
if "/empfang/shell/launch" in u and "token=" in low:
if "target=empfang_chat_shell" in low or "target%3Dempfang_chat_shell" in low:
return "AzA-Empfang \u00b7 Chat-H\u00fclle"
return "AzA-Empfang \u00b7 Desktop"
if "shell_source=aza_desktop" in u or "shell_source%3Daza_desktop" in low:
return "AzA-Empfang \u00b7 Desktop"
return "AzA-Empfang"
window_title = _window_title_for_mode(mode)
print(
f"[EMPFANG_SHELL] start mode={mode} parent_hint={parent_mode_hint or '-'} "
f"title={window_title!r} storage_tail={Path(storage_path).name}",
flush=True,
)
api = EmpfangWebviewApi()
api = EmpfangWebviewApi(mode=mode, parent_mode_hint=parent_mode_hint)
start_kw = {"storage_path": storage_path, "private_mode": False}
_ico = _empfang_shell_icon_path()
if _ico:
start_kw["icon"] = _ico
try:
win = webview.create_window(
_window_title_for_start_url(url), url, width=w, height=h, js_api=api,
window_title, url, width=w, height=h, js_api=api,
)
api.bind_window(win)
webview.start(**start_kw)
@@ -750,6 +1030,8 @@ def main(argv: list[str] | None = None) -> int:
if __name__ == "__main__":
# AUMID wird zusaetzlich in main() pro Modus gesetzt. Hier ein konservativer
# Default-Wert, damit ein eventueller Crash vor main() korrekt gruppiert wird.
if sys.platform == "win32":
try:
import ctypes