Фронт: без бейджа «Обе недели», недели на страницах преподов/аудиторий, иконка приложения
- пары с одинаковыми ч/з показываются одним списком без пометки недели (группы, преподаватели, аудитории); раньше на страницах преподавателей и аудиторий такое занятие дублировалось в обеих колонках - бейдж «Обе недели» убран как информационный шум (отсутствие пометки и означает «любая неделя») - favicon: квадрат с градиентом боковой панели (#1b2dd8 → #167cf2) и совой - PWA: manifest.json, sw.js, apple-touch-icon и иконки 192/512 — при добавлении на главный экран смартфона появляется логотип - tools/make_icons.py — генерация иконок из логотипа (rsvg-convert) - удалён неиспользуемый roomPeriodCell и мёртвый favicon.png
This commit is contained in:
121
tools/make_icons.py
Normal file
121
tools/make_icons.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Генерация иконок приложения (фавиконка + PWA) из логотипа факультета.
|
||||
|
||||
Квадрат с градиентом боковой панели сайта (#1b2dd8 → #167cf2), внутрь
|
||||
помещается белая сова. Результат:
|
||||
img/app-icon.svg — самодостаточный svg (favicon)
|
||||
img/icon-192.png — иконка манифеста
|
||||
img/icon-512.png — иконка манифеста (maskable)
|
||||
img/apple-touch-icon.png — 180x180 для iOS
|
||||
img/favicon-32.png — растровая запасная фавиконка
|
||||
|
||||
Запуск (нужен rsvg-convert из librsvg2-bin):
|
||||
python3 tools/make_icons.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
IMG = ROOT / "src" / "frontend" / "static" / "img"
|
||||
|
||||
SVG_NS = "http://www.w3.org/2000/svg"
|
||||
SODIPODI_NS = "http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
INKSCAPE_NS = "http://www.inkscape.org/namespaces/inkscape"
|
||||
|
||||
SIZE = 512
|
||||
GRADIENT = (("#1b2dd8", "0%"), ("#167cf2", "100%")) # как .side в светлой теме
|
||||
OWL_SCALE = 0.62 # доля стороны иконки под логотип
|
||||
|
||||
|
||||
def strip_ns(el: ET.Element) -> None:
|
||||
"""Убирает inkscape/sodipodi-атрибуты (в чужом документе они не нужны)."""
|
||||
for key in list(el.attrib):
|
||||
if key.startswith(f"{{{SODIPODI_NS}}}") or key.startswith(f"{{{INKSCAPE_NS}}}"):
|
||||
del el.attrib[key]
|
||||
for child in el:
|
||||
strip_ns(child)
|
||||
|
||||
|
||||
def build_icon_svg() -> str:
|
||||
owl_root = ET.parse(IMG / "owl-white.svg").getroot()
|
||||
view_box = owl_root.get("viewBox", "0 0 109.34 109.87")
|
||||
_, _, ow, oh = (float(v) for v in view_box.replace(",", " ").split())
|
||||
|
||||
# размеры и положение логотипа внутри квадрата
|
||||
target = SIZE * OWL_SCALE
|
||||
scale = target / max(ow, oh)
|
||||
tx = (SIZE - ow * scale) / 2
|
||||
ty = (SIZE - oh * scale) / 2
|
||||
|
||||
ET.register_namespace("", SVG_NS)
|
||||
icon = ET.Element(
|
||||
f"{{{SVG_NS}}}svg",
|
||||
{
|
||||
"viewBox": f"0 0 {SIZE} {SIZE}",
|
||||
"width": str(SIZE),
|
||||
"height": str(SIZE),
|
||||
},
|
||||
)
|
||||
defs = ET.SubElement(icon, f"{{{SVG_NS}}}defs")
|
||||
grad = ET.SubElement(
|
||||
defs,
|
||||
f"{{{SVG_NS}}}linearGradient",
|
||||
{"id": "bg", "x1": "0", "y1": "0", "x2": "0", "y2": "1"},
|
||||
)
|
||||
for color, offset in GRADIENT:
|
||||
ET.SubElement(
|
||||
grad, f"{{{SVG_NS}}}stop", {"offset": offset, "stop-color": color}
|
||||
)
|
||||
ET.SubElement(
|
||||
icon, f"{{{SVG_NS}}}rect", {"width": str(SIZE), "height": str(SIZE), "fill": "url(#bg)"}
|
||||
)
|
||||
|
||||
group = ET.SubElement(
|
||||
icon,
|
||||
f"{{{SVG_NS}}}g",
|
||||
{"transform": f"translate({tx:.2f} {ty:.2f}) scale({scale:.6f})"},
|
||||
)
|
||||
for child in owl_root:
|
||||
tag = child.tag
|
||||
if tag.endswith("namedview") or tag.endswith("metadata"):
|
||||
continue
|
||||
clone = ET.fromstring(ET.tostring(child))
|
||||
strip_ns(clone)
|
||||
group.append(clone)
|
||||
|
||||
xml = ET.tostring(icon, encoding="unicode")
|
||||
return '<?xml version="1.0" encoding="UTF-8"?>\n' + xml + "\n"
|
||||
|
||||
|
||||
def rasterize(svg_path: Path, size: int, out_path: Path) -> None:
|
||||
subprocess.run(
|
||||
["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(out_path), str(svg_path)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
svg = build_icon_svg()
|
||||
icon = IMG / "app-icon.svg"
|
||||
icon.write_text(svg, encoding="utf-8")
|
||||
|
||||
if not subprocess.run(["which", "rsvg-convert"], capture_output=True).stdout:
|
||||
print("нет rsvg-convert — svg создан, png не сгенерированы", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rasterize(icon, 192, IMG / "icon-192.png")
|
||||
rasterize(icon, 512, IMG / "icon-512.png")
|
||||
rasterize(icon, 180, IMG / "apple-touch-icon.png")
|
||||
rasterize(icon, 32, IMG / "favicon-32.png")
|
||||
for name in ("app-icon.svg", "icon-192.png", "icon-512.png", "apple-touch-icon.png", "favicon-32.png"):
|
||||
print("готово:", (IMG / name).relative_to(ROOT))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user