From a221a161f45ac55a38ca24efe0ffe7c0ed310ba5 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 26 Jul 2026 22:33:54 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Tray-Icon=20fuer=20Worker=20GUI=20(pyst?= =?UTF-8?q?ray)=20=E2=80=94=20Fenster=20schliessen=20minimiert=20ins=20Tra?= =?UTF-8?q?y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/worker/gui.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docker/worker/gui.py b/docker/worker/gui.py index 5498586..91b549e 100644 --- a/docker/worker/gui.py +++ b/docker/worker/gui.py @@ -8,6 +8,14 @@ import json import webbrowser import flet as ft +# Tray-Icon: pystray + PIL fuer das Icon-Bild +try: + import pystray + from PIL import Image as PILImage + TRAY_VERFUEGBAR = True +except ImportError: + TRAY_VERFUEGBAR = False + # Environment configuration BASIS = os.path.dirname(os.path.abspath(__file__)) RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") or os.getenv("RIPPY_HOST", "") @@ -200,6 +208,63 @@ def main(page: ft.Page): } page.theme = ft.Theme(font_family="Inter") + # --- Tray-Icon ----------------------------------------------------------- + tray_icon_ref = {"icon": None} + + def _tray_icon_bild(): + """Lade rippy.ico oder erzeuge ein Fallback-Bild.""" + ico_pfad = os.path.join(BASIS, "rippy.ico") + if os.path.isfile(ico_pfad): + try: + return PILImage.open(ico_pfad) + except Exception: + pass + # Fallback: kleines lila Quadrat + img = PILImage.new("RGB", (64, 64), (99, 102, 241)) + return img + + def tray_zeigen(icon, item): + """Tray-Menue: Fenster wieder anzeigen.""" + page.window.visible = True + page.window.minimized = False + page.update() + + def tray_beenden(icon, item): + """Tray-Menue: App komplett beenden.""" + if tray_icon_ref["icon"]: + tray_icon_ref["icon"].stop() + worker_stoppen() + page.window.destroy() + + def tray_starten(): + """Erstellt und startet das System-Tray-Icon.""" + if not TRAY_VERFUEGBAR: + return + menu = pystray.Menu( + pystray.MenuItem("Rippy Worker anzeigen", tray_zeigen, default=True), + pystray.Menu.SEPARATOR, + pystray.MenuItem("Beenden", tray_beenden), + ) + icon = pystray.Icon("rippy_worker", _tray_icon_bild(), + f"Rippy Worker — {WORKER_NAME}", menu) + tray_icon_ref["icon"] = icon + threading.Thread(target=icon.run, daemon=True, name="tray-icon").start() + + def on_window_event(e): + """Fenster-Schliessen minimiert ins Tray statt die App zu beenden.""" + if e.data == "close": + if TRAY_VERFUEGBAR and tray_icon_ref["icon"]: + page.window.visible = False + page.update() + else: + # Kein Tray verfuegbar — wirklich beenden + worker_stoppen() + page.window.destroy() + + page.window.prevent_close = True + page.window.on_event = on_window_event + tray_starten() + # Colors c_bg = "#0f172a" c_panel = "#1e293b" @@ -460,5 +525,18 @@ def main(page: ft.Page): threading.Thread(target=poll_loop, daemon=True).start() threading.Thread(target=_schluessel_wache, args=(_bruecke_bauen(), add_log), daemon=True, name="schluessel-wache").start() +def worker_stoppen(): + """Celery-Prozess sauber beenden.""" + global prozess + if prozess and prozess.poll() is None: + try: + prozess.terminate() + prozess.wait(timeout=5) + except Exception: + try: + prozess.kill() + except Exception: + pass + if __name__ == "__main__": ft.app(target=main)