578 lines
20 KiB
Python
578 lines
20 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
AZA Desktop – Launcher / Startseite.
|
|||
|
|
Premium-Medizinprodukt-Design mit 6 Modulkacheln, KI-Kapazitätsanzeige
|
|||
|
|
und verstecktem Admin-Zugang (Doppelklick auf Logo).
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import tkinter as tk
|
|||
|
|
|
|||
|
|
from aza_config import (
|
|||
|
|
LAUNCHER_MODULES,
|
|||
|
|
LAUNCHER_MODULE_LABELS,
|
|||
|
|
)
|
|||
|
|
from aza_persistence import (
|
|||
|
|
load_launcher_prefs,
|
|||
|
|
save_launcher_prefs,
|
|||
|
|
get_remaining_tokens,
|
|||
|
|
get_capacity_fraction,
|
|||
|
|
estimated_reports_remaining,
|
|||
|
|
is_capacity_low,
|
|||
|
|
)
|
|||
|
|
from aza_ui_helpers import save_toplevel_geometry, load_toplevel_geometry
|
|||
|
|
from aza_style import (
|
|||
|
|
BG, CARD_BG, CARD_HOVER_BG, CARD_BORDER, CARD_HOVER_BORDER,
|
|||
|
|
ACCENT, TEXT, SUBTLE,
|
|||
|
|
CAPACITY_BLUE, TURQUOISE, WARNING_AMBER, DANGER,
|
|||
|
|
FONT_FAMILY, SPACING,
|
|||
|
|
format_number_de,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
_MODULE_DESCRIPTIONS = {
|
|||
|
|
"ki": "Medizinische Fragen stellen,\nBefunde besprechen, Zweitmeinung einholen",
|
|||
|
|
"kg": "Diktat aufnehmen, transkribieren\nund Krankengeschichte erstellen",
|
|||
|
|
"notizen": "Sprachaufnahmen und Notizen\nfür den Praxisalltag",
|
|||
|
|
"translator": "Medizinische Fachtexte übersetzen\nund Begriffe nachschlagen",
|
|||
|
|
"medwork_chat": "Kollegialer Austausch mit\nÄrzten und Fachpersonal",
|
|||
|
|
"praxis_chat": "Nachrichten und Aufgaben\nim eigenen Praxisteam",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_MODULE_ICON_COLORS = {
|
|||
|
|
"ki": "#0984E3",
|
|||
|
|
"kg": "#00B894",
|
|||
|
|
"notizen": "#6C5CE7",
|
|||
|
|
"translator": "#0078D7",
|
|||
|
|
"medwork_chat": "#2D3436",
|
|||
|
|
"praxis_chat": "#636E72",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_ICON_SZ = 38
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _draw_module_icon(c: tk.Canvas, key: str):
|
|||
|
|
"""Draw a minimal white line-art icon (38×38) for the given module."""
|
|||
|
|
s = _ICON_SZ
|
|||
|
|
m = s // 2
|
|||
|
|
fg = "#FFFFFF"
|
|||
|
|
|
|||
|
|
if key == "ki":
|
|||
|
|
c.create_polygon(m, 7, m + 5, m, m, s - 7, m - 5, m,
|
|||
|
|
fill=fg, outline="")
|
|||
|
|
c.create_polygon(7, m, m, m - 5, s - 7, m, m, m + 5,
|
|||
|
|
fill=fg, outline="")
|
|||
|
|
|
|||
|
|
elif key == "kg":
|
|||
|
|
c.create_rectangle(10, 12, s - 10, s - 7, outline=fg, width=2)
|
|||
|
|
c.create_rectangle(14, 8, s - 14, 15, outline=fg, width=1.5)
|
|||
|
|
c.create_line(m, 18, m, s - 10, fill=fg, width=2.5)
|
|||
|
|
c.create_line(14, m + 2, s - 14, m + 2, fill=fg, width=2.5)
|
|||
|
|
|
|||
|
|
elif key == "notizen":
|
|||
|
|
c.create_oval(m - 5, 7, m + 5, 19, outline=fg, width=2)
|
|||
|
|
c.create_arc(m - 9, 13, m + 9, 27, start=180, extent=180,
|
|||
|
|
outline=fg, width=2, style="arc")
|
|||
|
|
c.create_line(m, 27, m, s - 9, fill=fg, width=2)
|
|||
|
|
c.create_line(m - 5, s - 9, m + 5, s - 9, fill=fg, width=2)
|
|||
|
|
|
|||
|
|
elif key == "translator":
|
|||
|
|
c.create_rectangle(7, 8, m + 1, s - 10, outline=fg, width=1.5)
|
|||
|
|
c.create_rectangle(m - 1, 10, s - 7, s - 8, outline=fg, width=1.5)
|
|||
|
|
c.create_text(14, m, text="A",
|
|||
|
|
font=("Segoe UI", 9, "bold"), fill=fg)
|
|||
|
|
c.create_text(s - 14, m + 1, text="\u6587",
|
|||
|
|
font=("Segoe UI", 8), fill=fg)
|
|||
|
|
|
|||
|
|
elif key == "medwork_chat":
|
|||
|
|
nodes = [(m, 9), (9, s - 11), (s - 9, s - 11)]
|
|||
|
|
for i in range(3):
|
|||
|
|
for j in range(i + 1, 3):
|
|||
|
|
c.create_line(*nodes[i], *nodes[j], fill=fg, width=1.5)
|
|||
|
|
for x, y in nodes:
|
|||
|
|
c.create_oval(x - 4, y - 4, x + 4, y + 4, fill=fg, outline="")
|
|||
|
|
|
|||
|
|
elif key == "praxis_chat":
|
|||
|
|
c.create_oval(8, 7, s - 8, s - 13, outline=fg, width=2)
|
|||
|
|
bg = c.cget("bg")
|
|||
|
|
c.create_polygon(12, s - 14, 10, s - 7, 18, s - 14,
|
|||
|
|
fill=bg, outline=bg)
|
|||
|
|
c.create_line(12, s - 14, 10, s - 7, fill=fg, width=2)
|
|||
|
|
c.create_line(10, s - 7, 17, s - 14, fill=fg, width=2)
|
|||
|
|
|
|||
|
|
_GRID_COLS = 2
|
|||
|
|
_BAR_H = 6
|
|||
|
|
_WIN_W = 620
|
|||
|
|
_WIN_MIN_W = 520
|
|||
|
|
_WIN_MIN_H = 500
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _Tooltip:
|
|||
|
|
"""Dezentes Hover-Tooltip im Glas-Stil."""
|
|||
|
|
|
|||
|
|
def __init__(self, widget, text: str):
|
|||
|
|
self._widget = widget
|
|||
|
|
self._text = text
|
|||
|
|
self._tip = None
|
|||
|
|
widget.bind("<Enter>", self._show, add="+")
|
|||
|
|
widget.bind("<Leave>", self._hide, add="+")
|
|||
|
|
|
|||
|
|
def _show(self, event):
|
|||
|
|
if self._tip:
|
|||
|
|
return
|
|||
|
|
x = self._widget.winfo_rootx() + 20
|
|||
|
|
y = self._widget.winfo_rooty() + self._widget.winfo_height() + 4
|
|||
|
|
self._tip = tw = tk.Toplevel(self._widget)
|
|||
|
|
tw.wm_overrideredirect(True)
|
|||
|
|
tw.wm_geometry(f"+{x}+{y}")
|
|||
|
|
tw.configure(bg="#2D3436")
|
|||
|
|
tk.Label(
|
|||
|
|
tw, text=self._text,
|
|||
|
|
font=(FONT_FAMILY, 9), fg="#F0F0F0", bg="#2D3436",
|
|||
|
|
padx=12, pady=8, justify="left",
|
|||
|
|
).pack()
|
|||
|
|
|
|||
|
|
def _hide(self, event):
|
|||
|
|
if self._tip:
|
|||
|
|
self._tip.destroy()
|
|||
|
|
self._tip = None
|
|||
|
|
|
|||
|
|
def update_text(self, text: str):
|
|||
|
|
self._text = text
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AzaLauncher(tk.Tk):
|
|||
|
|
"""Premium-Startseite mit Modulauswahl, KI-Kapazität und verstecktem Admin."""
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
super().__init__()
|
|||
|
|
self.title("AZA \u2013 Digitales Praxis-Cockpit")
|
|||
|
|
self.configure(bg=BG)
|
|||
|
|
self.resizable(True, True)
|
|||
|
|
self.minsize(_WIN_MIN_W, _WIN_MIN_H)
|
|||
|
|
|
|||
|
|
self.attributes("-topmost", True)
|
|||
|
|
|
|||
|
|
self._logo_img = None
|
|||
|
|
try:
|
|||
|
|
logo_path = os.path.join(
|
|||
|
|
os.path.dirname(os.path.abspath(__file__)), "logo.png"
|
|||
|
|
)
|
|||
|
|
if os.path.exists(logo_path):
|
|||
|
|
from PIL import Image, ImageTk
|
|||
|
|
img = Image.open(logo_path).resize((44, 44), Image.Resampling.LANCZOS)
|
|||
|
|
self._logo_img = ImageTk.PhotoImage(img)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
self._selected_module = None
|
|||
|
|
prefs = load_launcher_prefs()
|
|||
|
|
self._auto_open_var = tk.BooleanVar(value=prefs.get("auto_open", False))
|
|||
|
|
|
|||
|
|
self._build_ui()
|
|||
|
|
self._apply_geometry()
|
|||
|
|
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
|||
|
|
|
|||
|
|
def _apply_geometry(self):
|
|||
|
|
saved_geom = load_toplevel_geometry("launcher")
|
|||
|
|
if saved_geom:
|
|||
|
|
try:
|
|||
|
|
self.geometry(saved_geom)
|
|||
|
|
return
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self.update_idletasks()
|
|||
|
|
req_w = max(self.winfo_reqwidth(), _WIN_W)
|
|||
|
|
req_h = self.winfo_reqheight() + 20
|
|||
|
|
sw = self.winfo_screenwidth()
|
|||
|
|
sh = self.winfo_screenheight()
|
|||
|
|
win_w = min(req_w, sw - 40)
|
|||
|
|
win_h = min(req_h, sh - 80)
|
|||
|
|
self.minsize(_WIN_MIN_W, min(win_h, _WIN_MIN_H))
|
|||
|
|
x = (sw - win_w) // 2
|
|||
|
|
y = max(20, (sh - win_h) // 2)
|
|||
|
|
self.geometry(f"{win_w}x{win_h}+{x}+{y}")
|
|||
|
|
|
|||
|
|
# ── UI Aufbau ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_ui(self):
|
|||
|
|
outer = tk.Frame(self, bg=BG)
|
|||
|
|
outer.pack(fill="both", expand=True, padx=36, pady=(28, 20))
|
|||
|
|
|
|||
|
|
# ── Header ──
|
|||
|
|
header = tk.Frame(outer, bg=BG)
|
|||
|
|
header.pack(fill="x")
|
|||
|
|
|
|||
|
|
title_row = tk.Frame(header, bg=BG)
|
|||
|
|
title_row.pack(fill="x")
|
|||
|
|
|
|||
|
|
if self._logo_img:
|
|||
|
|
logo_lbl = tk.Label(title_row, image=self._logo_img, bg=BG, cursor="hand2")
|
|||
|
|
logo_lbl.pack(side="left", padx=(0, 14))
|
|||
|
|
logo_lbl.bind("<Double-Button-1>", self._open_admin)
|
|||
|
|
|
|||
|
|
title_block = tk.Frame(title_row, bg=BG)
|
|||
|
|
title_block.pack(side="left", anchor="w")
|
|||
|
|
aza_lbl = tk.Label(title_block, text="AZA",
|
|||
|
|
font=(FONT_FAMILY, 24, "bold"), fg=ACCENT, bg=BG,
|
|||
|
|
cursor="hand2")
|
|||
|
|
aza_lbl.pack(anchor="w")
|
|||
|
|
aza_lbl.bind("<Double-Button-1>", self._open_admin)
|
|||
|
|
tk.Label(title_block, text="Medizinischer KI-Arbeitsplatz",
|
|||
|
|
font=(FONT_FAMILY, 10), fg=SUBTLE, bg=BG
|
|||
|
|
).pack(anchor="w")
|
|||
|
|
|
|||
|
|
self._build_capacity_bar(header)
|
|||
|
|
|
|||
|
|
# ── Separator ──
|
|||
|
|
tk.Frame(outer, bg="#E2E8F0", height=1).pack(fill="x", pady=(14, 16))
|
|||
|
|
|
|||
|
|
# ── Card Grid ──
|
|||
|
|
grid = tk.Frame(outer, bg=BG)
|
|||
|
|
grid.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
for c in range(_GRID_COLS):
|
|||
|
|
grid.columnconfigure(c, weight=1, uniform="col")
|
|||
|
|
num_rows = (len(LAUNCHER_MODULES) + _GRID_COLS - 1) // _GRID_COLS
|
|||
|
|
for r in range(num_rows):
|
|||
|
|
grid.rowconfigure(r, weight=1)
|
|||
|
|
|
|||
|
|
for i, mod_key in enumerate(LAUNCHER_MODULES):
|
|||
|
|
row, col = divmod(i, _GRID_COLS)
|
|||
|
|
card = self._create_card(grid, mod_key)
|
|||
|
|
card.grid(row=row, column=col,
|
|||
|
|
padx=SPACING // 2, pady=SPACING // 2,
|
|||
|
|
sticky="nsew")
|
|||
|
|
|
|||
|
|
# ── Auto-Open Bereich ──
|
|||
|
|
self._build_auto_open_section(outer)
|
|||
|
|
|
|||
|
|
# ── Footer ──
|
|||
|
|
_FOOTER_BG = "#EDF2F7"
|
|||
|
|
footer_wrap = tk.Frame(outer, bg=_FOOTER_BG, highlightthickness=0)
|
|||
|
|
footer_wrap.pack(fill="x", pady=(16, 0), ipady=8)
|
|||
|
|
|
|||
|
|
footer = tk.Frame(footer_wrap, bg=_FOOTER_BG)
|
|||
|
|
footer.pack(fill="x", padx=12)
|
|||
|
|
|
|||
|
|
lbl_status = tk.Label(
|
|||
|
|
footer, text="Systemstatus",
|
|||
|
|
font=(FONT_FAMILY, 9, "bold"), fg=ACCENT, bg=_FOOTER_BG,
|
|||
|
|
cursor="hand2",
|
|||
|
|
)
|
|||
|
|
lbl_status.pack(side="left")
|
|||
|
|
lbl_status.bind("<Button-1>", self._open_systemstatus)
|
|||
|
|
|
|||
|
|
self._build_key_status(footer, _FOOTER_BG)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from aza_version import APP_VERSION
|
|||
|
|
tk.Label(
|
|||
|
|
footer, text=f"v{APP_VERSION}",
|
|||
|
|
font=(FONT_FAMILY, 8), fg="#A0AEC0", bg=_FOOTER_BG,
|
|||
|
|
).pack(side="right")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Auto-Open Bereich ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_auto_open_section(self, parent):
|
|||
|
|
prefs = load_launcher_prefs()
|
|||
|
|
default_mod = prefs.get("default_module", "")
|
|||
|
|
|
|||
|
|
section = tk.Frame(parent, bg=BG)
|
|||
|
|
section.pack(fill="x", pady=(14, 0))
|
|||
|
|
|
|||
|
|
tk.Frame(section, bg="#E2E8F0", height=1).pack(fill="x", pady=(0, 10))
|
|||
|
|
|
|||
|
|
row1 = tk.Frame(section, bg=BG)
|
|||
|
|
row1.pack(fill="x")
|
|||
|
|
|
|||
|
|
tk.Checkbutton(
|
|||
|
|
row1,
|
|||
|
|
text="Nächste Auswahl als Standardstart merken",
|
|||
|
|
variable=self._auto_open_var,
|
|||
|
|
font=(FONT_FAMILY, 9), fg=TEXT, bg=BG,
|
|||
|
|
activebackground=BG, selectcolor=CARD_BG,
|
|||
|
|
command=self._on_auto_open_toggle,
|
|||
|
|
).pack(side="left")
|
|||
|
|
|
|||
|
|
row2 = tk.Frame(section, bg=BG)
|
|||
|
|
row2.pack(fill="x", pady=(4, 0))
|
|||
|
|
|
|||
|
|
self._auto_open_status = tk.Label(row2, font=(FONT_FAMILY, 9), bg=BG)
|
|||
|
|
self._auto_open_status.pack(side="left")
|
|||
|
|
|
|||
|
|
self._auto_open_reset = tk.Label(
|
|||
|
|
row2, text="Zurücksetzen",
|
|||
|
|
font=(FONT_FAMILY, 8, "underline"), fg=ACCENT, bg=BG,
|
|||
|
|
cursor="hand2",
|
|||
|
|
)
|
|||
|
|
self._auto_open_reset.bind("<Button-1>", self._reset_auto_open)
|
|||
|
|
|
|||
|
|
self._auto_open_help = tk.Label(
|
|||
|
|
section, font=(FONT_FAMILY, 8), fg=SUBTLE, bg=BG,
|
|||
|
|
)
|
|||
|
|
self._auto_open_help.pack(anchor="w", pady=(2, 0))
|
|||
|
|
|
|||
|
|
self._update_auto_open_display(default_mod)
|
|||
|
|
|
|||
|
|
def _update_auto_open_display(self, mod_key: str):
|
|||
|
|
if mod_key and mod_key in LAUNCHER_MODULES:
|
|||
|
|
label = LAUNCHER_MODULE_LABELS.get(mod_key, mod_key)
|
|||
|
|
self._auto_open_status.configure(
|
|||
|
|
text=f"Standardstart: {label}",
|
|||
|
|
fg=ACCENT,
|
|||
|
|
)
|
|||
|
|
self._auto_open_reset.pack(side="left", padx=(10, 0))
|
|||
|
|
self._auto_open_help.configure(
|
|||
|
|
text="Dieses Modul öffnet sich beim nächsten Programmstart automatisch.",
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
self._auto_open_status.configure(
|
|||
|
|
text="Kein Standardstart festgelegt",
|
|||
|
|
fg=SUBTLE,
|
|||
|
|
)
|
|||
|
|
self._auto_open_reset.pack_forget()
|
|||
|
|
self._auto_open_help.configure(
|
|||
|
|
text="Aktivieren Sie die Option und wählen Sie ein Modul, um einen Standardstart festzulegen.",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _on_auto_open_toggle(self):
|
|||
|
|
if not self._auto_open_var.get():
|
|||
|
|
save_launcher_prefs("", False)
|
|||
|
|
self._update_auto_open_display("")
|
|||
|
|
|
|||
|
|
def _reset_auto_open(self, event=None):
|
|||
|
|
self._auto_open_var.set(False)
|
|||
|
|
save_launcher_prefs("", False)
|
|||
|
|
self._update_auto_open_display("")
|
|||
|
|
|
|||
|
|
# ── Admin-Zugang ──────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _open_systemstatus(self, event=None):
|
|||
|
|
try:
|
|||
|
|
from aza_systemstatus import show_systemstatus
|
|||
|
|
show_systemstatus(self)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _open_admin(self, event=None):
|
|||
|
|
try:
|
|||
|
|
from aza_admin import show_admin_login, show_admin_panel
|
|||
|
|
if show_admin_login(self):
|
|||
|
|
show_admin_panel(self)
|
|||
|
|
self._refresh_capacity()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _refresh_capacity(self):
|
|||
|
|
"""Aktualisiert Kapazitätsanzeige nach Admin-Aktion."""
|
|||
|
|
try:
|
|||
|
|
remaining = get_remaining_tokens()
|
|||
|
|
pct = get_capacity_fraction()
|
|||
|
|
low = is_capacity_low()
|
|||
|
|
est = estimated_reports_remaining()
|
|||
|
|
|
|||
|
|
self._cap_label.configure(
|
|||
|
|
text=f"Ihre KI-Kapazität: {format_number_de(remaining)} Einheiten verbleibend",
|
|||
|
|
fg=WARNING_AMBER if low else SUBTLE,
|
|||
|
|
)
|
|||
|
|
self._cap_canvas.delete("all")
|
|||
|
|
self._cap_canvas.update_idletasks()
|
|||
|
|
self._draw_gradient_bar(self._cap_canvas, pct, _BAR_H)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── KI-Kapazitätsanzeige ──────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_capacity_bar(self, parent):
|
|||
|
|
frame = tk.Frame(parent, bg=BG)
|
|||
|
|
frame.pack(fill="x", pady=(12, 0))
|
|||
|
|
|
|||
|
|
remaining = get_remaining_tokens()
|
|||
|
|
pct = get_capacity_fraction()
|
|||
|
|
low = is_capacity_low()
|
|||
|
|
est = estimated_reports_remaining()
|
|||
|
|
|
|||
|
|
label_text = f"KI-Kapazität: {format_number_de(remaining)} Einheiten"
|
|||
|
|
color = WARNING_AMBER if low else "#A0AEC0"
|
|||
|
|
self._cap_label = tk.Label(
|
|||
|
|
frame, text=label_text,
|
|||
|
|
font=(FONT_FAMILY, 8), fg=color, bg=BG, anchor="e",
|
|||
|
|
)
|
|||
|
|
self._cap_label.pack(anchor="e", pady=(0, 3))
|
|||
|
|
|
|||
|
|
self._cap_canvas = canvas = tk.Canvas(
|
|||
|
|
frame, height=_BAR_H, bg=BG, highlightthickness=0,
|
|||
|
|
)
|
|||
|
|
canvas.pack(fill="x")
|
|||
|
|
canvas.bind("<Configure>", lambda e: self._draw_gradient_bar(canvas, pct, _BAR_H))
|
|||
|
|
|
|||
|
|
tooltip_text = (
|
|||
|
|
f"Entspricht ca. {est} weiteren Berichten\n"
|
|||
|
|
"(Basierend auf Ihrem Durchschnittsverbrauch)"
|
|||
|
|
)
|
|||
|
|
if low:
|
|||
|
|
tooltip_text += (
|
|||
|
|
"\n\n\u26A0 Kapazität fast aufgebraucht.\n"
|
|||
|
|
"Guthaben unter aza-medwork.ch nachfüllen."
|
|||
|
|
)
|
|||
|
|
_Tooltip(canvas, tooltip_text)
|
|||
|
|
_Tooltip(self._cap_label, tooltip_text)
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _draw_gradient_bar(canvas, pct: float, bar_h: int):
|
|||
|
|
canvas.delete("all")
|
|||
|
|
w = canvas.winfo_width()
|
|||
|
|
if w <= 1:
|
|||
|
|
return
|
|||
|
|
filled_w = max(0, min(w, int(w * pct)))
|
|||
|
|
|
|||
|
|
canvas.create_rectangle(0, 0, w, bar_h, fill="#EDF2F7", outline="")
|
|||
|
|
|
|||
|
|
if filled_w <= 0:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if pct <= 0.02:
|
|||
|
|
canvas.create_rectangle(0, 0, filled_w, bar_h, fill=DANGER, outline="")
|
|||
|
|
elif pct <= 0.10:
|
|||
|
|
canvas.create_rectangle(0, 0, filled_w, bar_h, fill=WARNING_AMBER, outline="")
|
|||
|
|
else:
|
|||
|
|
r1, g1, b1 = 0x00, 0x78, 0xD7
|
|||
|
|
r2, g2, b2 = 0x00, 0xCE, 0xC9
|
|||
|
|
steps = min(filled_w, 120)
|
|||
|
|
step_w = filled_w / steps
|
|||
|
|
for i in range(steps):
|
|||
|
|
t = i / max(1, steps - 1)
|
|||
|
|
r = int(r1 + (r2 - r1) * t)
|
|||
|
|
g = int(g1 + (g2 - g1) * t)
|
|||
|
|
b = int(b1 + (b2 - b1) * t)
|
|||
|
|
x1 = int(i * step_w)
|
|||
|
|
x2 = int((i + 1) * step_w)
|
|||
|
|
canvas.create_rectangle(
|
|||
|
|
x1, 0, x2, bar_h,
|
|||
|
|
fill=f"#{r:02x}{g:02x}{b:02x}", outline="",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ── Key-Status ────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_key_status(self, parent, bg_color=None):
|
|||
|
|
bg = bg_color or BG
|
|||
|
|
try:
|
|||
|
|
from security_vault import has_vault_key, get_masked_key
|
|||
|
|
if has_vault_key():
|
|||
|
|
masked = get_masked_key()
|
|||
|
|
tk.Label(
|
|||
|
|
parent,
|
|||
|
|
text=f"Schlüssel aktiv ({masked})",
|
|||
|
|
font=(FONT_FAMILY, 8), fg="#00B894", bg=bg,
|
|||
|
|
).pack(side="left", padx=(12, 0))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Card Rendering ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _create_card(self, parent, mod_key: str) -> tk.Frame:
|
|||
|
|
label = LAUNCHER_MODULE_LABELS.get(mod_key, mod_key)
|
|||
|
|
desc = _MODULE_DESCRIPTIONS.get(mod_key, "")
|
|||
|
|
icon_color = _MODULE_ICON_COLORS.get(mod_key, ACCENT)
|
|||
|
|
|
|||
|
|
card = tk.Frame(parent, bg=CARD_BG, cursor="hand2",
|
|||
|
|
highlightthickness=1, highlightbackground=CARD_BORDER)
|
|||
|
|
|
|||
|
|
inner = tk.Frame(card, bg=CARD_BG, cursor="hand2")
|
|||
|
|
inner.pack(fill="both", expand=True, padx=20, pady=18)
|
|||
|
|
|
|||
|
|
top_row = tk.Frame(inner, bg=CARD_BG, cursor="hand2")
|
|||
|
|
top_row.pack(fill="x", pady=(0, 10))
|
|||
|
|
|
|||
|
|
icon_cv = tk.Canvas(top_row, width=_ICON_SZ, height=_ICON_SZ,
|
|||
|
|
bg=icon_color, highlightthickness=0, cursor="hand2")
|
|||
|
|
icon_cv.pack(side="left")
|
|||
|
|
_draw_module_icon(icon_cv, mod_key)
|
|||
|
|
|
|||
|
|
tk.Label(inner, text=label, font=(FONT_FAMILY, 12, "bold"),
|
|||
|
|
fg=TEXT, bg=CARD_BG, anchor="w", cursor="hand2"
|
|||
|
|
).pack(anchor="w")
|
|||
|
|
|
|||
|
|
if desc:
|
|||
|
|
tk.Label(inner, text=desc, font=(FONT_FAMILY, 9),
|
|||
|
|
fg=SUBTLE, bg=CARD_BG, anchor="w",
|
|||
|
|
justify="left", cursor="hand2"
|
|||
|
|
).pack(anchor="w", pady=(4, 0))
|
|||
|
|
|
|||
|
|
def on_enter(e):
|
|||
|
|
card.configure(highlightbackground=CARD_HOVER_BORDER, highlightthickness=2)
|
|||
|
|
for w in _deep_children(card):
|
|||
|
|
try:
|
|||
|
|
if isinstance(w, tk.Canvas):
|
|||
|
|
continue
|
|||
|
|
w.configure(bg=CARD_HOVER_BG)
|
|||
|
|
except tk.TclError:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def on_leave(e):
|
|||
|
|
card.configure(highlightbackground=CARD_BORDER, highlightthickness=1)
|
|||
|
|
for w in _deep_children(card):
|
|||
|
|
try:
|
|||
|
|
if isinstance(w, tk.Canvas):
|
|||
|
|
continue
|
|||
|
|
w.configure(bg=CARD_BG)
|
|||
|
|
except tk.TclError:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def on_click(e, key=mod_key):
|
|||
|
|
self._select(key)
|
|||
|
|
|
|||
|
|
for w in _deep_children(card):
|
|||
|
|
w.bind("<Enter>", on_enter)
|
|||
|
|
w.bind("<Leave>", on_leave)
|
|||
|
|
w.bind("<Button-1>", on_click)
|
|||
|
|
|
|||
|
|
return card
|
|||
|
|
|
|||
|
|
# ── Modul-Auswahl / Schliessen ────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _save_geom(self):
|
|||
|
|
try:
|
|||
|
|
save_toplevel_geometry("launcher", self.geometry())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _select(self, mod_key: str):
|
|||
|
|
self._selected_module = mod_key
|
|||
|
|
if self._auto_open_var.get():
|
|||
|
|
save_launcher_prefs(mod_key, True)
|
|||
|
|
else:
|
|||
|
|
save_launcher_prefs("", False)
|
|||
|
|
self._save_geom()
|
|||
|
|
self.destroy()
|
|||
|
|
|
|||
|
|
def _on_close(self):
|
|||
|
|
self._selected_module = None
|
|||
|
|
self._save_geom()
|
|||
|
|
self.destroy()
|
|||
|
|
|
|||
|
|
def run(self) -> str | None:
|
|||
|
|
self.mainloop()
|
|||
|
|
return self._selected_module
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _deep_children(widget):
|
|||
|
|
"""Widget + alle verschachtelten Kinder."""
|
|||
|
|
result = [widget]
|
|||
|
|
for child in widget.winfo_children():
|
|||
|
|
result.extend(_deep_children(child))
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
|
|||
|
|
def should_skip_launcher() -> tuple[bool, str]:
|
|||
|
|
"""Prüft ob der Launcher übersprungen werden soll."""
|
|||
|
|
prefs = load_launcher_prefs()
|
|||
|
|
mod = prefs.get("default_module", "")
|
|||
|
|
auto = prefs.get("auto_open", False)
|
|||
|
|
if auto and mod and mod in LAUNCHER_MODULES:
|
|||
|
|
return True, mod
|
|||
|
|
return False, ""
|