feat(ui): inline SVG icons across all three dialogs
CI / test (3.10) (push) Successful in 6s
CI / test (3.11) (push) Successful in 7s
CI / test (3.12) (push) Successful in 6s
CI / build-pyz (push) Successful in 4s
CI / release (push) Has been skipped

New module cloud_sync/icons.py:
  - CLOUD_SVG, STORAGE_SVG: card glyphs (Material Icons, Apache-2.0)
  - WARNING_BADGE_SVG, PLUS_BADGE_SVG, SYNC_BADGE_SVG: header badges
    (hand-rolled — circle + vector primitives, no glyph font dependency)
  - svg_pixmap(svg, size): QSvgRenderer-backed rasteriser; dual-binding
    helper falls back from PySide6 to PyQt6.QtSvg.

Wired into ui_qt.py:
  - Conflict dialog: warning badge SVG (light disc + dark !), cloud
    card SVG, storage card SVG. Drops the QSS circle hack — entire
    badge is one pixmap now.
  - Login dialog: plus badge SVG (Prism-green disc + dark +).
  - Progress window: sync arrow badge SVG (Prism-green disc + circular
    arrow + head).

QSS shed: no more bg/border-radius/font-size acrobatics for badges
since the SVG includes the disc. Card icons are crisp at any DPI.

52 tests still green; pyz grows ~5 KB for the SVG strings + the new
icons module.
This commit is contained in:
2026-06-05 01:05:54 +02:00
parent 7c9d33f952
commit f1cb9f4b86
2 changed files with 119 additions and 53 deletions
+93
View File
@@ -0,0 +1,93 @@
"""Inline SVG icons + Qt pixmap rasteriser.
Icons are stored as SVG strings so the pyz stays a single file (no
external assets). Rendering uses ``QSvgRenderer`` from whichever Qt
binding loaded — PySide6 or PyQt6.
Color baked into the SVG (Qt's QSvgRenderer ignores stylesheet
``currentColor`` inheritance). To recolour, edit the SVG string here
or call :func:`recolor` to substitute the placeholder ``#FFFFFF``.
Card glyphs from the Material Icons set (Apache-2.0). Badge glyphs
hand-rolled from pure SVG primitives so we don't pull a glyph font.
"""
from __future__ import annotations
from typing import Any
# ---------------------------------------------------------------------------
# Card glyphs (24x24, single-color, drawn over card bg)
# ---------------------------------------------------------------------------
# Material Icons "cloud" path data — outline of a stylised cumulus.
CLOUD_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFFFFF">
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96z"/>
</svg>"""
# Material Icons "storage" — three stacked server racks. Reads as
# "local storage" without the visual ambiguity of a floppy disk.
STORAGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#FFFFFF">
<path d="M2 20h20v-4H2v4zm2-3h2v2H4v-2zM2 4v4h20V4H2zm4 3H4V5h2v2zm-4 7h20v-4H2v4zm2-3h2v2H4v-2z"/>
</svg>"""
# ---------------------------------------------------------------------------
# Header badges (32x32, full disc + glyph)
# ---------------------------------------------------------------------------
# Light-gray disc with a dark exclamation. Used in the conflict dialog.
WARNING_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="16" fill="#D6DDE6"/>
<rect x="14.5" y="7" width="3" height="11" rx="1.5" fill="#313131"/>
<circle cx="16" cy="22.5" r="1.8" fill="#313131"/>
</svg>"""
# Prism-green disc with a dark plus. Used in the login dialog.
PLUS_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="16" fill="#96DB59"/>
<rect x="14.5" y="8" width="3" height="16" rx="1.5" fill="#1A1A1A"/>
<rect x="8" y="14.5" width="16" height="3" rx="1.5" fill="#1A1A1A"/>
</svg>"""
# Prism-green disc with a circular arrow. Used in the progress window.
SYNC_BADGE_SVG = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<circle cx="16" cy="16" r="16" fill="#96DB59"/>
<path d="M 23 17.5 A 7.5 7.5 0 1 1 22 11.5"
stroke="#1A1A1A" stroke-width="2.6" fill="none" stroke-linecap="round"/>
<polygon points="23.5,7.5 23.5,14 17.5,11.2" fill="#1A1A1A"/>
</svg>"""
# ---------------------------------------------------------------------------
# Rasterise
# ---------------------------------------------------------------------------
def svg_pixmap(svg: str, size: int) -> Any:
"""Render an SVG string to a ``QPixmap`` of ``size`` × ``size`` px.
Caller is responsible for keeping the returned pixmap alive (e.g.
by attaching it to a QLabel). Raises ``ImportError`` if no Qt
binding is available.
"""
QtCore, QtGui, QtSvg = _import_qt_for_svg()
pm = QtGui.QPixmap(size, size)
pm.fill(QtCore.Qt.GlobalColor.transparent)
renderer = QtSvg.QSvgRenderer(QtCore.QByteArray(svg.encode("utf-8")))
painter = QtGui.QPainter(pm)
try:
renderer.render(painter)
finally:
painter.end()
return pm
def _import_qt_for_svg() -> tuple[Any, Any, Any]:
try:
from PySide6 import QtCore, QtGui, QtSvg # type: ignore
return QtCore, QtGui, QtSvg
except ImportError:
from PyQt6 import QtCore, QtGui, QtSvg # type: ignore
return QtCore, QtGui, QtSvg