Files
grotti/scripts/build_icons.py
Dominic Gundel 4ddffbbeb1 Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.
Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
2026-10-18 14:58:32 +02:00

208 lines
5.9 KiB
Python

"""Lucide-Icons als lokales SVG-Sprite bauen.
python scripts/build_icons.py
Schreibt web/static/icons.svg. IDs bleiben i-<grotti-name> (bestehende Templates).
"""
from __future__ import annotations
import re
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "web" / "static" / "icons.svg"
CDN = "https://unpkg.com/[email protected]/icons/{name}.svg"
# grotti-id → lucide dateiname (ohne .svg)
ICONS: dict[str, str] = {
# bestehend
"printer": "printer",
"camera": "camera",
"file": "file",
"spool": "disc-3",
"folder": "folder",
"folder-open": "folder-open",
"list": "list",
"activity": "activity",
"clock": "clock",
"check": "check",
"alert": "triangle-alert",
"flame": "flame",
"pause": "pause",
"play": "play",
"stop": "square",
"layers": "layers",
"x": "x",
"up": "chevron-up",
"down": "chevron-down",
"upload": "upload",
"refresh": "refresh-cw",
"queue": "list-ordered",
"send": "send",
"book": "book-open",
"cube": "box",
"bell": "bell",
"sweep": "brush",
"thermo": "thermometer",
"settings": "settings",
"bulb": "lightbulb",
"search": "search",
"filter": "filter",
"info": "info",
"scan": "scan-line",
"trash": "trash-2",
"inbox": "inbox",
"image": "image",
"grid": "layout-grid",
"menu": "menu",
"panel": "panel-left",
"nozzle": "pipette",
"bed": "rectangle-horizontal",
"chamber": "box",
"fan": "fan",
"fan-chamber": "air-vent",
"fan-aux": "wind",
# neu für Polish
"plus": "plus",
"minus": "minus",
"edit": "pencil",
"save": "save",
"copy": "copy",
"external": "external-link",
"user": "user",
"users": "users",
"key": "key",
"lock": "lock",
"unlock": "lock-open",
"wifi": "wifi",
"wifi-off": "wifi-off",
"power": "power",
"zap": "zap",
"gauge": "gauge",
"chart": "chart-column",
"calendar": "calendar",
"timer": "timer",
"weight": "weight",
"package": "package",
"tag": "tag",
"hash": "hash",
"link": "link",
"download": "download",
"more": "ellipsis",
"chevron-right": "chevron-right",
"chevron-left": "chevron-left",
"home": "house",
"wrench": "wrench",
"hard-drive": "hard-drive",
"server": "server",
"message": "message-circle",
"mail": "mail",
"shield": "shield",
"sliders": "sliders-horizontal",
"palette": "palette",
"moon": "moon",
"sun": "sun",
"monitor": "monitor",
"smartphone": "smartphone",
"keyboard": "keyboard",
"undo": "undo-2",
"redo": "redo-2",
"circle-check": "circle-check",
"circle-x": "circle-x",
"circle-alert": "circle-alert",
"loader": "loader-circle",
"sparkles": "sparkles",
"clipboard": "clipboard-list",
"receipt": "receipt",
"truck": "truck",
"droplet": "droplet",
"percent": "percent",
"euro": "euro",
"arrow-up": "arrow-up",
"arrow-down": "arrow-down",
"history": "history",
"eye": "eye",
"eye-off": "eye-off",
"star": "star",
"heart": "heart",
"ban": "ban",
"rotate-ccw": "rotate-ccw",
"brush": "brush",
}
def fetch(name: str) -> str:
url = CDN.format(name=name)
req = urllib.request.Request(url, headers={"User-Agent": "Grotti-icon-build/1"})
with urllib.request.urlopen(req, timeout=30) as resp:
return resp.read().decode("utf-8")
def to_symbol(grotti: str, svg: str) -> str:
# Lucide: optional comment + <svg ...>paths</svg>
text = re.sub(r"<!--.*?-->", "", svg, flags=re.DOTALL).strip()
match = re.search(r"<svg\b[^>]*>(.*)</svg>\s*$", text, flags=re.DOTALL | re.IGNORECASE)
if not match:
raise ValueError("kein SVG-Root")
inner = match.group(1).strip()
# verschachtelte svg-Attribute/Klassen aus Kind-Elementen nicht nötig
return (
f' <symbol id="i-{grotti}" viewBox="0 0 24 24" fill="none" '
f'stroke="currentColor" stroke-width="2" stroke-linecap="round" '
f'stroke-linejoin="round">\n {inner}\n </symbol>'
)
def main() -> None:
symbols = []
failed = []
for grotti, lucide in sorted(ICONS.items()):
try:
raw = fetch(lucide)
symbols.append(to_symbol(grotti, raw))
print("ok", grotti, "<-", lucide)
except urllib.error.HTTPError as exc:
failed.append((grotti, lucide, str(exc)))
print("FAIL", grotti, lucide, exc.code)
except Exception as exc:
failed.append((grotti, lucide, str(exc)))
print("FAIL", grotti, lucide, type(exc).__name__)
if failed and len(failed) == len(ICONS):
raise SystemExit("Keine Icons geladen.")
# Fallbacks für fehlgeschlagene: leeres symbol vermeiden — check als Ersatz
ok_ids = {s.split('id="i-')[1].split('"')[0] for s in symbols}
for grotti, lucide, _ in failed:
if grotti in ok_ids:
continue
# einfaches X als Platzhalter
symbols.append(
f' <symbol id="i-{grotti}" viewBox="0 0 24 24" fill="none" '
f'stroke="currentColor" stroke-width="2" stroke-linecap="round" '
f'stroke-linejoin="round">\n'
f' <path d="M18 6 6 18"/><path d="m6 6 12 12"/>\n </symbol>'
)
print("placeholder", grotti)
body = "\n".join(symbols)
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(
'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<!-- Lucide icons, built by scripts/build_icons.py — {len(symbols)} symbols -->\n'
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">\n'
f"{body}\n"
"</svg>\n",
encoding="utf-8",
)
print("wrote", OUT, "symbols", len(symbols))
if failed:
print("warnings:", len(failed))
if __name__ == "__main__":
main()