feat: Tray-Icon fuer Worker GUI (pystray) — Fenster schliessen minimiert ins Tray
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user