71 lines
2.1 KiB
Python
71 lines
2.1 KiB
Python
"""Иконка tray: бейдж статуса на базе основной иконки."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Tuple
|
|
|
|
from PIL import Image, ImageDraw
|
|
|
|
|
|
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 и единый размер для стабильного отрисовывания бейджа."""
|
|
im = img
|
|
try:
|
|
n = getattr(im, "n_frames", 1)
|
|
if n > 1:
|
|
best = None
|
|
best_area = 0
|
|
for i in range(n):
|
|
im.seek(i)
|
|
w, h = im.size
|
|
area = w * h
|
|
if area > best_area:
|
|
best_area = area
|
|
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
|