145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""
|
||
Портал приложений — Flask сервер на порту 5560.
|
||
При старте автогенерирует аватарки для приложений из config/apps.json.
|
||
"""
|
||
|
||
import json
|
||
import hashlib
|
||
from pathlib import Path
|
||
|
||
from flask import Flask, render_template, jsonify, send_from_directory
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
BASE_DIR = Path(__file__).parent
|
||
CONFIG_FILE = BASE_DIR / "config" / "apps.json"
|
||
AVATARS_DIR = BASE_DIR / "static" / "avatars"
|
||
|
||
app = Flask(__name__)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helpers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def load_apps():
|
||
"""Загружает и возвращает активные приложения, отсортированные по order."""
|
||
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
||
apps = json.load(f)
|
||
return sorted(
|
||
[a for a in apps if a.get("enabled", True)],
|
||
key=lambda x: x.get("order", 99),
|
||
)
|
||
|
||
|
||
def _hash_color(text: str, offset: int = 0) -> tuple[int, int, int]:
|
||
"""Детерминированно генерирует RGB-цвет из строки (с optional offset для второго цвета)."""
|
||
digest = hashlib.md5((text + str(offset)).encode()).hexdigest()
|
||
r = int(digest[0:2], 16)
|
||
g = int(digest[2:4], 16)
|
||
b = int(digest[4:6], 16)
|
||
# Сдвигаем яркость так, чтобы фон был насыщенным, не слишком тёмным и не белым
|
||
r = max(60, min(r, 200))
|
||
g = max(60, min(g, 200))
|
||
b = max(60, min(b, 200))
|
||
return (r, g, b)
|
||
|
||
|
||
def _make_gradient(size: int, color1: tuple, color2: tuple) -> Image.Image:
|
||
"""Создаёт квадратное изображение с линейным градиентом (135°)."""
|
||
img = Image.new("RGB", (size, size))
|
||
draw = ImageDraw.Draw(img)
|
||
for y in range(size):
|
||
for x in range(size):
|
||
t = (x + y) / (size * 2) # 0..1 вдоль диагонали 135°
|
||
r = int(color1[0] * (1 - t) + color2[0] * t)
|
||
g = int(color1[1] * (1 - t) + color2[1] * t)
|
||
b = int(color1[2] * (1 - t) + color2[2] * t)
|
||
draw.point((x, y), fill=(r, g, b))
|
||
return img
|
||
|
||
|
||
def _hash_gradient(name: str):
|
||
"""Возвращает два цвета для градиента по хэшу имени."""
|
||
h = int(hashlib.md5(name.encode()).hexdigest()[:8], 16)
|
||
palettes = [
|
||
("#667eea", "#764ba2"),
|
||
("#f093fb", "#f5576c"),
|
||
("#4facfe", "#00f2fe"),
|
||
("#43e97b", "#38f9d7"),
|
||
("#fa709a", "#fee140"),
|
||
("#a18cd1", "#fbc2eb"),
|
||
("#ffecd2", "#fcb69f"),
|
||
("#ff9a9e", "#fad0c4"),
|
||
("#a1c4fd", "#c2e9fb"),
|
||
("#d4fc79", "#96e6a1"),
|
||
]
|
||
return palettes[h % len(palettes)]
|
||
|
||
|
||
def generate_avatar(app_cfg: dict) -> Path:
|
||
"""
|
||
Генерирует аватарку для одного приложения и сохраняет в static/avatars/{id}.png.
|
||
Возвращает путь к файлу.
|
||
"""
|
||
app_id = app_cfg["id"]
|
||
name = app_cfg.get("name", app_id)
|
||
icon = app_cfg.get("icon", name[0].upper())
|
||
|
||
out_path = AVATARS_DIR / f"{app_id}.png"
|
||
if out_path.exists():
|
||
return out_path
|
||
|
||
size = 200
|
||
color1 = _hash_color(name, offset=0)
|
||
color2 = _hash_color(name, offset=1)
|
||
|
||
img = _make_gradient(size, color1, color2)
|
||
_draw_letter(img, name, size)
|
||
|
||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||
img.save(str(out_path), "PNG")
|
||
print(f"[avatar] generated: {out_path}")
|
||
return out_path
|
||
|
||
|
||
def generate_avatars():
|
||
"""Генерирует аватарки для всех приложений из конфига (если файла нет)."""
|
||
AVATARS_DIR.mkdir(parents=True, exist_ok=True)
|
||
apps = load_apps()
|
||
for app_cfg in apps:
|
||
generate_avatar(app_cfg)
|
||
print(f"[avatar] done: {len(apps)} apps processed")
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Routes
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@app.route("/")
|
||
def index():
|
||
apps = load_apps()
|
||
# Добавляем градиент к каждому приложению
|
||
for app in apps:
|
||
c1, c2 = _hash_gradient(app["name"])
|
||
app["gradient"] = f"linear-gradient(135deg, {c1}, {c2})"
|
||
return render_template("index.html", apps=apps, count=len(apps))
|
||
|
||
|
||
@app.route("/api/apps")
|
||
def api_apps():
|
||
return jsonify(load_apps())
|
||
|
||
|
||
@app.route("/static/avatars/<path:filename>")
|
||
def avatar(filename):
|
||
return send_from_directory(str(AVATARS_DIR), filename)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Entry point
|
||
# ---------------------------------------------------------------------------
|
||
|
||
if __name__ == "__main__":
|
||
generate_avatars()
|
||
app.run(host="0.0.0.0", port=5560, debug=False)
|