feat: Tray-Icon fuer Worker GUI (pystray) — Fenster schliessen minimiert ins Tray

This commit is contained in:
Hitonabi
2026-07-26 22:33:54 +02:00
parent 264b2c77f0
commit a221a161f4
+78
View File
@@ -8,6 +8,14 @@ import json
import webbrowser import webbrowser
import flet as ft 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 # Environment configuration
BASIS = os.path.dirname(os.path.abspath(__file__)) BASIS = os.path.dirname(os.path.abspath(__file__))
RIPPY_HOST = os.getenv("RIPPY_TRAY_HOST", "") or os.getenv("RIPPY_HOST", "") 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") 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 # Colors
c_bg = "#0f172a" c_bg = "#0f172a"
c_panel = "#1e293b" c_panel = "#1e293b"
@@ -460,5 +525,18 @@ def main(page: ft.Page):
threading.Thread(target=poll_loop, daemon=True).start() threading.Thread(target=poll_loop, daemon=True).start()
threading.Thread(target=_schluessel_wache, args=(_bruecke_bauen(), add_log), daemon=True, name="schluessel-wache").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__": if __name__ == "__main__":
ft.app(target=main) ft.app(target=main)