- manifest + icones, service worker (precache, network-first, Background Sync)
- module client FlowOffline (IndexedDB, queue, delta, flush) + hook editeur
- endpoints /api/v2/sync/{delta,batch,status} + moteur de sync (conflits LWW/orpheline/copie offline)
- migrations offline_sync_queue + sync_version (triggers)
- UI offline (banner, badge sync, toasts, icone dirty) + doc /help
- tests pytest (sync, migrations, SW, offline) + E2E Playwright; bump 6.0.0
70 lines
2.2 KiB
Python
70 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate FlowDeck PWA icons as PNG (72→512 px) from the favicon design.
|
|
|
|
Draws the same logo as static/favicon.svg (blue rounded square + white "FD")
|
|
at every size required by the web manifest. No cairosvg dependency required —
|
|
pure Pillow.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
ICON_BLUE = "#2383E2"
|
|
ICON_WHITE = "#FFFFFF"
|
|
SIZES = [72, 96, 128, 144, 152, 192, 384, 512]
|
|
OUT_DIR = Path(__file__).resolve().parent.parent / "static" / "icons"
|
|
|
|
|
|
def _font_at(px: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
|
"""Pick the best available bold font for the requested icon size."""
|
|
candidates = [
|
|
"C:/Windows/Fonts/arialbd.ttf",
|
|
"C:/Windows/Fonts/Arialbd.ttf",
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
|
"/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf",
|
|
]
|
|
for path in candidates:
|
|
if Path(path).exists():
|
|
try:
|
|
return ImageFont.truetype(path, size=int(px * 0.42))
|
|
except OSError:
|
|
continue
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def draw_icon(size: int) -> Image.Image:
|
|
"""Render the FlowDeck rounded-square logo at `size` x `size` px."""
|
|
radius = max(2, round(size * 0.1875)) # rx=6 on a 32 viewBox → ~18.75%
|
|
img = Image.new("RGB", (size, size), ICON_BLUE)
|
|
draw = ImageDraw.Draw(img)
|
|
draw.rounded_rectangle([0, 0, size - 1, size - 1], radius=radius, fill=ICON_BLUE)
|
|
font = _font_at(size)
|
|
bbox = draw.textbbox((0, 0), "FD", font=font)
|
|
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
draw.text(
|
|
((size - tw) / 2 - bbox[0], (size - th) / 2 - bbox[1]),
|
|
"FD",
|
|
font=font,
|
|
fill=ICON_WHITE,
|
|
)
|
|
return img
|
|
|
|
|
|
def main() -> None:
|
|
OUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
for size in SIZES:
|
|
path = OUT_DIR / f"icon-{size}x{size}.png"
|
|
draw_icon(size).save(path, "PNG", optimize=True)
|
|
print(f"Generated {path}")
|
|
|
|
# Apple touch icon: standalone 180 px (not part of the manifest set).
|
|
apple = OUT_DIR / "apple-touch-icon.png"
|
|
draw_icon(180).save(apple, "PNG", optimize=True)
|
|
print(f"Generated {apple}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|