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