Files
wiki/tasks/apps-portal/server.py
2026-04-12 21:55:33 +03:00

145 lines
4.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.
"""
Портал приложений — 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)