Files
media-player-server/media_server/tray.py
T
alexei.dolgolyov 3f14512e5d
Lint & Test / test (push) Successful in 24s
Release / create-release (push) Successful in 1s
Release / build-linux (push) Successful in 31s
Release / build-windows (push) Successful in 1m13s
feat: add Restart and Shutdown tray actions with confirmation dialogs
2026-03-24 14:19:15 +03:00

135 lines
3.6 KiB
Python

"""System tray icon for Media Server."""
import io
import logging
import os
import signal
import subprocess
import sys
import threading
import webbrowser
from PIL import Image, ImageDraw
logger = logging.getLogger(__name__)
# pystray is optional — tray silently disabled when missing
try:
import pystray
except ImportError:
pystray = None
# tkinter is optional — used for confirmation dialogs
try:
import tkinter as tk
from tkinter import messagebox
except ImportError:
tk = None
messagebox = None
def _confirm(title: str, message: str) -> bool:
"""Show a Yes/No confirmation dialog. Returns True if user clicks Yes."""
if messagebox is None:
return True # No tkinter — skip confirmation
root = tk.Tk()
root.withdraw()
root.attributes("-topmost", True)
result = messagebox.askyesno(title, message, parent=root)
root.destroy()
return result
def _create_icon_image(size: int = 64) -> Image.Image:
"""Create a tray icon: green circle with white play triangle."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Green circle background
padding = 2
draw.ellipse(
[padding, padding, size - padding, size - padding],
fill=(29, 185, 84, 255),
)
# White play triangle
cx, cy = size // 2, size // 2
r = size * 0.28
triangle = [
(cx - r * 0.6, cy - r),
(cx - r * 0.6, cy + r),
(cx + r * 0.9, cy),
]
draw.polygon(triangle, fill=(255, 255, 255, 255))
return img
def _load_icon_image() -> Image.Image:
"""Load the SVG app icon, falling back to a generated image."""
try:
import cairosvg
svg_path = os.path.join(
os.path.dirname(__file__), "static", "icons", "icon.svg"
)
if os.path.exists(svg_path):
png_data = cairosvg.svg2png(url=svg_path, output_width=64, output_height=64)
return Image.open(io.BytesIO(png_data))
except Exception:
pass
return _create_icon_image()
def start_tray(host: str, port: int) -> "pystray.Icon | None":
"""Start system tray icon in a background thread.
Returns the Icon instance (call icon.stop() to remove), or None if
pystray is not installed.
"""
if pystray is None:
logger.info("pystray not installed — tray icon disabled")
return None
url = f"http://{'localhost' if host == '0.0.0.0' else host}:{port}"
def on_show_ui(_icon, _item):
webbrowser.open(url)
def on_restart(_icon, _item):
if not _confirm("Restart Media Server", "Are you sure you want to restart the server?"):
return
logger.info("Restart requested from tray")
_icon.stop()
# Re-launch the same process, then exit current
subprocess.Popen([sys.executable] + sys.argv)
os.kill(os.getpid(), signal.SIGINT)
def on_exit(_icon, _item):
if not _confirm("Exit Media Server", "Are you sure you want to shut down the server?"):
return
logger.info("Exit requested from tray")
_icon.stop()
os.kill(os.getpid(), signal.SIGINT)
menu = pystray.Menu(
pystray.MenuItem("Show UI", on_show_ui, default=True),
pystray.Menu.SEPARATOR,
pystray.MenuItem("Restart", on_restart),
pystray.MenuItem("Shutdown", on_exit),
)
icon = pystray.Icon(
name="media-server",
icon=_load_icon_image(),
title="Media Server",
menu=menu,
)
thread = threading.Thread(target=icon.run, daemon=True)
thread.start()
logger.info("System tray icon started")
return icon