Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

187
app/icons.py Normal file
View File

@@ -0,0 +1,187 @@
"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji).
`icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert,
save, export, import, settings, star, star-off, warning, ab, loop, search, wave.
`app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images).
"""
import math
import os
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
ACCENT = "#b0242c"
STEEL = "#c4c4c4"
TEXT = "#e6e6e6"
DANGER = "#e0533d"
MUTED = "#9a9a9a"
_cache = {}
def _arrow_arc(p, r, start, span, head=True):
rect = QRectF(-r, -r, 2 * r, 2 * r)
path = QPainterPath()
path.arcMoveTo(rect, start)
path.arcTo(rect, start, span)
p.drawPath(path)
if head:
a = math.radians(start + span)
tip = QPointF(r * math.cos(a), -r * math.sin(a))
d = 1 if span > 0 else -1
tang = math.radians(start + span + 90 * d)
back = QPointF(math.cos(tang), -math.sin(tang))
nrm = QPointF(math.cos(a), -math.sin(a))
L = r * 0.55
p.drawLine(tip, tip - back * L + nrm * L * 0.6)
p.drawLine(tip, tip - back * L - nrm * L * 0.6)
def _draw(p, name, c, s):
"""Draw icon `name` in a box of size s (origin top-left)."""
pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
p.setPen(pen)
p.setBrush(Qt.BrushStyle.NoBrush)
m = s * 0.18
if name == "play":
path = QPainterPath()
path.moveTo(s * 0.30, s * 0.20)
path.lineTo(s * 0.80, s * 0.50)
path.lineTo(s * 0.30, s * 0.80)
path.closeSubpath()
p.setBrush(QColor(c))
p.drawPath(path)
elif name == "stop":
p.setBrush(QColor(c))
p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06)
elif name == "record":
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor(DANGER))
p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56))
elif name == "open":
path = QPainterPath()
path.moveTo(m, s * 0.30)
path.lineTo(s * 0.40, s * 0.30)
path.lineTo(s * 0.48, s * 0.38)
path.lineTo(s - m, s * 0.38)
path.lineTo(s - m, s * 0.78)
path.lineTo(m, s * 0.78)
path.closeSubpath()
p.drawPath(path)
elif name == "apply":
path = QPainterPath()
path.moveTo(s * 0.20, s * 0.52)
path.lineTo(s * 0.42, s * 0.74)
path.lineTo(s * 0.82, s * 0.28)
p.drawPath(path)
elif name in ("revert", "loop"):
p.translate(s / 2, s / 2)
if name == "revert":
_arrow_arc(p, s * 0.30, 60, 270)
else:
_arrow_arc(p, s * 0.30, 20, 150)
_arrow_arc(p, s * 0.30, 200, 150)
p.translate(-s / 2, -s / 2)
elif name == "save": # disk
path = QPainterPath()
path.moveTo(m, m)
path.lineTo(s * 0.70, m)
path.lineTo(s - m, s * 0.30)
path.lineTo(s - m, s - m)
path.lineTo(m, s - m)
path.closeSubpath()
p.drawPath(path)
p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20))
p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26))
elif name in ("export", "import"):
p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m))
p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m))
p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62))
up = name == "export"
a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66)
p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b))
d = -1 if up else 1
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16))
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16))
elif name == "settings":
p.translate(s / 2, s / 2)
path = QPainterPath()
teeth = 8
for i in range(teeth * 2):
a = math.pi * i / teeth
r = s * (0.36 if i % 2 == 0 else 0.27)
pt = QPointF(r * math.cos(a), r * math.sin(a))
path.moveTo(pt) if i == 0 else path.lineTo(pt)
path.closeSubpath()
p.drawPath(path)
p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11)
p.translate(-s / 2, -s / 2)
elif name in ("star", "star-off"):
path = QPainterPath()
for i in range(10):
a = -math.pi / 2 + math.pi * i / 5
r = s * (0.40 if i % 2 == 0 else 0.17)
pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a))
path.moveTo(pt) if i == 0 else path.lineTo(pt)
path.closeSubpath()
if name == "star":
p.setBrush(QColor(c))
p.drawPath(path)
elif name == "warning":
path = QPainterPath()
path.moveTo(s / 2, s * 0.14)
path.lineTo(s * 0.88, s * 0.84)
path.lineTo(s * 0.12, s * 0.84)
path.closeSubpath()
p.drawPath(path)
p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60))
p.drawPoint(QPointF(s / 2, s * 0.72))
elif name == "ab": # compare: half-filled circle
rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64)
half = QPainterPath()
half.moveTo(s / 2, s * 0.18)
half.arcTo(rect, 90, 180)
half.closeSubpath()
p.setBrush(QColor(c))
p.drawPath(half)
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawEllipse(rect)
elif name == "search":
p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46))
p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84))
elif name == "wave": # fallback app glyph: waveform bars
p.setPen(Qt.PenStyle.NoPen)
p.setBrush(QColor(c))
for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)):
p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03)
def icon(name, color=TEXT):
key = (name, color)
if key not in _cache:
ic = QIcon()
for sz in (16, 20, 24, 32, 48, 64, 128, 256):
pm = QPixmap(sz, sz)
pm.fill(Qt.GlobalColor.transparent)
p = QPainter(pm)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
_draw(p, name, color, sz)
p.end()
ic.addPixmap(pm)
_cache[key] = ic
return _cache[key]
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
def asset(name):
p = os.path.join(ASSETS, name)
return p if os.path.exists(p) else None
def app_icon():
"""The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing."""
p = asset("app_icon.ico") or asset("app_icon.png")
return QIcon(p) if p else icon("wave", ACCENT)