tg-ws-proxy/ui/tray_icons.py

121 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Иконка tray: загрузка icon.ico или синтез буквы «T» (Pillow), бейдж статуса."""
from __future__ import annotations
from pathlib import Path
from typing import Any, List, Tuple
from PIL import Image, ImageDraw, ImageFont
# Подсказка для tooltip (согласована с цветами бейджа)
BADGE_TOOLTIP_HINT = (
"Бейдж: зелёный — работает, красный — ошибка, жёлтый — запуск/ожидание/остановка"
)
def _resample_lanczos() -> int:
try:
return Image.Resampling.LANCZOS # type: ignore[attr-defined]
except AttributeError:
return Image.LANCZOS
def normalize_tray_icon_image(img: Image.Image, size: int = 64) -> Image.Image:
"""RGBA, единый размер (для стабильного бейджа). ICO — кадр с максимальной площадью."""
im = img
try:
n = getattr(im, "n_frames", 1)
if n > 1:
best: Image.Image | None = None
best_area = 0
for i in range(n):
im.seek(i)
w, h = im.size
a = w * h
if a > best_area:
best_area = a
best = im.copy()
im = best if best is not None else im.copy()
else:
im = im.copy()
except Exception:
im = img.copy()
im = im.convert("RGBA")
if im.size != (size, size):
im = im.resize((size, size), _resample_lanczos())
return im
def badge_rgb_for_phase(phase: str) -> Tuple[int, int, int]:
"""Зелёный — слушает, красный — ошибка, жёлтый — остальное."""
if phase == "listening":
return (34, 197, 94)
if phase == "error":
return (239, 68, 68)
return (234, 179, 8)
def apply_status_badge(base: Image.Image, phase: str) -> Image.Image:
"""Круглый индикатор внизу справа (как бейдж уведомления)."""
img = base.copy()
if img.mode != "RGBA":
img = img.convert("RGBA")
w, h = img.size
rgb = badge_rgb_for_phase(phase)
r = max(4, min(w, h) // 7)
margin = max(1, min(w, h) // 18)
cx = w - margin - r
cy = h - margin - r
draw = ImageDraw.Draw(img)
# лёгкая тень для контраста на светлой панели
draw.ellipse(
[cx - r + 1, cy - r + 1, cx + r + 1, cy + r + 1],
fill=(0, 0, 0, 70),
)
draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=rgb + (255,))
border_w = max(1, r // 5)
draw.ellipse(
[cx - r, cy - r, cx + r, cy + r],
outline=(255, 255, 255, 230),
width=border_w,
)
return img
def _pick_font(size: int, candidates: List[str]) -> Any:
for path in candidates:
try:
return ImageFont.truetype(path, size=int(size * 0.55))
except Exception:
continue
return ImageFont.load_default()
def synthesize_letter_t_icon(size: int, font_candidates: List[str]) -> Image.Image:
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
margin = 2
draw.ellipse(
[margin, margin, size - margin, size - margin],
fill=(0, 136, 204, 255),
)
font = _pick_font(size, font_candidates)
bbox = draw.textbbox((0, 0), "T", font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
tx = (size - tw) // 2 - bbox[0]
ty = (size - th) // 2 - bbox[1]
draw.text((tx, ty), "T", fill=(255, 255, 255, 255), font=font)
return img
def load_ico_or_synthesize(
ico_path: Path,
font_candidates: List[str],
size: int = 64,
) -> Image.Image:
if ico_path.exists():
try:
return Image.open(str(ico_path))
except Exception:
pass
return synthesize_letter_t_icon(size, font_candidates)