diff --git a/cloud_sync/icons.py b/cloud_sync/icons.py
new file mode 100644
index 0000000..4771882
--- /dev/null
+++ b/cloud_sync/icons.py
@@ -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 = """"""
+
+# Material Icons "storage" — three stacked server racks. Reads as
+# "local storage" without the visual ambiguity of a floppy disk.
+STORAGE_SVG = """"""
+
+
+# ---------------------------------------------------------------------------
+# Header badges (32x32, full disc + glyph)
+# ---------------------------------------------------------------------------
+
+# Light-gray disc with a dark exclamation. Used in the conflict dialog.
+WARNING_BADGE_SVG = """"""
+
+# Prism-green disc with a dark plus. Used in the login dialog.
+PLUS_BADGE_SVG = """"""
+
+# Prism-green disc with a circular arrow. Used in the progress window.
+SYNC_BADGE_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
diff --git a/cloud_sync/ui_qt.py b/cloud_sync/ui_qt.py
index d12d5c8..fa0cab8 100644
--- a/cloud_sync/ui_qt.py
+++ b/cloud_sync/ui_qt.py
@@ -93,18 +93,6 @@ QLabel#title {
font-weight: bold;
letter-spacing: 2px;
}
-QLabel#badge {
- color: #313131;
- background: #96db59;
- border-radius: 16px;
- font-size: 18pt;
- font-weight: bold;
- qproperty-alignment: AlignCenter;
- min-width: 32px;
- max-width: 32px;
- min-height: 32px;
- max-height: 32px;
-}
QLabel#status {
color: #c8c8c8;
font-size: 10pt;
@@ -166,10 +154,12 @@ class QtProgressWindow:
outer.setContentsMargins(28, 24, 28, 20)
outer.setSpacing(14)
+ from . import icons
header = QtWidgets.QHBoxLayout()
header.setSpacing(12)
- badge = QtWidgets.QLabel("↻")
- badge.setObjectName("badge")
+ badge = QtWidgets.QLabel()
+ badge.setPixmap(icons.svg_pixmap(icons.SYNC_BADGE_SVG, 32))
+ badge.setFixedSize(32, 32)
header.addWidget(badge)
self._title_label = QtWidgets.QLabel("CLOUD SYNC")
self._title_label.setObjectName("title")
@@ -259,18 +249,6 @@ QLabel#title {
font-weight: bold;
letter-spacing: 2px;
}
-QLabel#badge {
- color: #313131;
- background: #96db59;
- border-radius: 16px;
- font-size: 20pt;
- font-weight: bold;
- qproperty-alignment: AlignCenter;
- min-width: 32px;
- max-width: 32px;
- min-height: 32px;
- max-height: 32px;
-}
QLabel#body {
color: #c8c8c8;
font-size: 10pt;
@@ -341,6 +319,7 @@ def prompt_login_qt() -> str | None:
focus accent, primary "Save and continue" button in Prism green.
"""
QtWidgets, QtCore, _ = import_qt()
+ from . import icons
app_existed = QtWidgets.QApplication.instance() is not None
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
if not app_existed:
@@ -357,8 +336,9 @@ def prompt_login_qt() -> str | None:
header = QtWidgets.QHBoxLayout()
header.setSpacing(12)
- badge = QtWidgets.QLabel("+")
- badge.setObjectName("badge")
+ badge = QtWidgets.QLabel()
+ badge.setPixmap(icons.svg_pixmap(icons.PLUS_BADGE_SVG, 32))
+ badge.setFixedSize(32, 32)
header.addWidget(badge)
title = QtWidgets.QLabel("CONNECT CLOUD SAVE")
title.setObjectName("title")
@@ -449,18 +429,6 @@ QLabel#title {
font-weight: bold;
letter-spacing: 2px;
}
-QLabel#warning {
- color: #313131;
- background: #d6dde6;
- border-radius: 16px;
- font-size: 18pt;
- font-weight: bold;
- qproperty-alignment: AlignCenter;
- min-width: 32px;
- max-width: 32px;
- min-height: 32px;
- max-height: 32px;
-}
QLabel#body {
color: #c8c8c8;
font-size: 10pt;
@@ -489,11 +457,6 @@ QLabel#cardSubtitle {
font-size: 9pt;
background: transparent;
}
-QLabel#cardIcon {
- color: white;
- font-size: 24pt;
- background: transparent;
-}
QPushButton#cancel {
background: #303030;
color: white;
@@ -525,6 +488,7 @@ def prompt_conflict_qt(
Cancel bottom-right. Card click commits and dismisses.
"""
QtWidgets, QtCore, Signal = import_qt()
+ from . import icons
app_existed = QtWidgets.QApplication.instance() is not None
app = QtWidgets.QApplication.instance() or QtWidgets.QApplication(sys.argv)
if not app_existed:
@@ -533,7 +497,7 @@ def prompt_conflict_qt(
class _Card(QtWidgets.QFrame):
clicked = Signal()
- def __init__(self, glyph: str, title: str, subtitle: str) -> None:
+ def __init__(self, pixmap: Any, title: str, subtitle: str) -> None:
super().__init__()
self.setObjectName("card")
self.setCursor(QtCore.Qt.CursorShape.PointingHandCursor)
@@ -544,9 +508,9 @@ def prompt_conflict_qt(
row = QtWidgets.QHBoxLayout(self)
row.setContentsMargins(18, 14, 18, 14)
row.setSpacing(16)
- icon = QtWidgets.QLabel(glyph)
- icon.setObjectName("cardIcon")
- icon.setFixedWidth(40)
+ icon = QtWidgets.QLabel()
+ icon.setPixmap(pixmap)
+ icon.setFixedSize(40, 40)
icon.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
row.addWidget(icon)
text_col = QtWidgets.QVBoxLayout()
@@ -573,8 +537,9 @@ def prompt_conflict_qt(
header = QtWidgets.QHBoxLayout()
header.setSpacing(12)
- warning = QtWidgets.QLabel("!")
- warning.setObjectName("warning")
+ warning = QtWidgets.QLabel()
+ warning.setPixmap(icons.svg_pixmap(icons.WARNING_BADGE_SVG, 32))
+ warning.setFixedSize(32, 32)
header.addWidget(warning)
title = QtWidgets.QLabel("CLOUD CONFLICT")
title.setObjectName("title")
@@ -593,8 +558,16 @@ def prompt_conflict_qt(
outer.addSpacing(6)
- cloud_card = _Card("☁", "Cloud Save", f"Modified {remote_modified}")
- local_card = _Card("▢", "Local Save", f"Modified {local_modified}")
+ cloud_card = _Card(
+ icons.svg_pixmap(icons.CLOUD_SVG, 32),
+ "Cloud Save",
+ f"Modified {remote_modified}",
+ )
+ local_card = _Card(
+ icons.svg_pixmap(icons.STORAGE_SVG, 32),
+ "Local Save",
+ f"Modified {local_modified}",
+ )
outer.addWidget(cloud_card)
outer.addWidget(local_card)