FEAT-NETATMO-02: Komplettes Redesign auf 4x4 WarmNews-Layout
* Indoor: 5 Sensoren (1x Main + 4x NAModule4) prominent mit Tag, Temp, CO2-Bar * Outdoor: Big-Temp + 12h-Verlaufsgraph + Min/Max/Luftfeuchte * Wind/Regen: je mit Mini-Verlauf + 1h/24h-Bars * Forecast: 3 Tage mit Min/Max + Regen-Bar * Batterie-Indikatoren mit %-Anzeige an Aussen, Wind, Regen und allen 5 Indoor-Sensoren * CO2-Bars mit ppm-Wert + Grün/Gelb/Rot-Schwellen * Sans-Schrift only, WarmNews-Farbpalette (warm-weiss, Tinte-schwarz, Akzent-Farben) * Subtile Box-Rahmen + Accent-Strips für visuelle Trennung designbase.py neu hinzugefügt als Template-Modul. tools/netatmo_auth.py: Auth-Flow leicht angepasst (Redirect-URI auf Pi-IP statt localhost).
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
"""DesignBase — Plugin Design Template / Schablone.
|
||||
|
||||
Dieses Modul ist KEIN eigenständiges Widget, sondern eine SAMMLUNG
|
||||
von Design-Konzepten, Layout-Helfern und Theme-Definitionen,
|
||||
die als Vorlage für alle anderen Plugin-Renderer dienen.
|
||||
|
||||
ANATOMIE EINES DESIGN-TEMPLATES
|
||||
=================================
|
||||
|
||||
Jedes Plugin-render() folgt diesem Schema:
|
||||
|
||||
def render(self, draw, fonts, x, y, w, h):
|
||||
theme = self._theme() # Theme-Objekt mit Farben + Schriften
|
||||
data = self.fetch() # Daten holen
|
||||
if "_error" in data:
|
||||
render_error_banner(...)
|
||||
return
|
||||
layout = self._pick_layout(w, h) # Layout-Strategie wählen
|
||||
self._render_header(draw, theme, x, y, w, header_h)
|
||||
self._render_body(draw, theme, data, x, body_y, w, body_h, layout)
|
||||
if self._show_footer(w, h):
|
||||
self._render_footer(draw, theme, data, x, y+h-footer_h, w, footer_h)
|
||||
|
||||
Die 4 LAYOUT-STRATEGIEN
|
||||
========================
|
||||
|
||||
is_small (1x1): Eine einzige große Zahl / ein Icon
|
||||
is_wide (4x1/2x1): Horizontale Teilung in 2-3 Spalten
|
||||
is_tall (1x4/1x2): Vertikale Teilung in Zeilen
|
||||
standard (2x2+): Header + Content + Footer (oder Karten)
|
||||
|
||||
Die 4 THEMES
|
||||
=============
|
||||
|
||||
THEME_LIGHT — Weiß, schwarz, eine Akzentfarbe (z.B. Netatmo: blau)
|
||||
THEME_DARK — Fast schwarz, helle Farben auf dunklem Grund
|
||||
THEME_RETRO — Gelb/Orange-Schwarz (80er-Terminal-Feeling)
|
||||
THEME_MAG — Minimal, typografisch, viel Weißraum
|
||||
|
||||
Farben werden IMMER über theme.xxx bezogen, NIEMALS direkt als RGB-Tuple.
|
||||
Das erlaubt komplettes Umstyling ohne den Renderer-Code anzufassen.
|
||||
|
||||
Farben-Schema pro Theme (Palette: FG, BG, ACCENT, OK, WARN, ALERT, INFO):
|
||||
|
||||
THEME_LIGHT: BG=WHITE, FG=BLACK, ACCENT=BLUE, OK=GREEN, WARN=YELLOW, ALERT=RED
|
||||
THEME_DARK: BG=BLACK, FG=WHITE, ACCENT=CYAN, OK=GREEN, WARN=ORANGE,ALERT=RED
|
||||
THEME_RETRO: BG=BLACK, FG=YELLOW,ACCENT=ORANGE, OK=GREEN, WARN=YELLOW, ALERT=RED
|
||||
THEME_MAG: BG=WHITE, FG=BLACK, ACCENT=ORANGE, OK=GREEN, WARN=ORANGE, ALERT=BLUE
|
||||
|
||||
Beispiel-Implementierung: widgets/netatmo.py (theme='light', accent='blue')
|
||||
Beispiel-Implementierung: widgets/weather.py (theme='light', accent='orange')
|
||||
Beispiel-Implementierung: widgets/system.py (theme='light', accent='green')
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal
|
||||
|
||||
# Palette-Aliase (für direkten Zugriff in Templates)
|
||||
# noqa: E402 — diese Imports funktionieren weil plugins/ im Python-Path liegt
|
||||
from palette import (
|
||||
FG, BG, WHITE, BLACK, GREEN, BLUE, RED, YELLOW, ORANGE,
|
||||
INFO, OK, WARN, ALERT, ACCENT,
|
||||
measure, fit_font, centered_text, hbar, parse_thresholds,
|
||||
is_small, is_wide, is_tall,
|
||||
)
|
||||
|
||||
# noqa: E402
|
||||
from plugins.base import Widget, render_error_banner
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# THEME DEFINITIONS
|
||||
# ============================================================================
|
||||
|
||||
ThemeName = Literal["light", "dark", "retro", "mag"]
|
||||
LayoutName = Literal["small", "wide", "tall", "standard"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Theme:
|
||||
"""Alle visuellen Eigenschaften eines Designs.
|
||||
|
||||
Ein Theme-Objekt wird in render() erzeugt und an alle
|
||||
_render_*-Methoden weitergegeben. Nie direkt Farbwerte hardcoden.
|
||||
"""
|
||||
name: ThemeName
|
||||
bg: tuple[int, int, int] # Hintergrund
|
||||
fg: tuple[int, int, int] # Primärtext
|
||||
accent: tuple[int, int, int] # Akzentfarbe (Platzierung je nach Theme)
|
||||
ok: tuple[int, int, int]
|
||||
warn: tuple[int, int, int]
|
||||
alert: tuple[int, int, int]
|
||||
info: tuple[int, int, int]
|
||||
header_font_key: str = "24" # Font-Schlüssel für Headlines
|
||||
label_font_key: str = "20" # Font-Schlüssel für Modul-Labels
|
||||
body_font_key: str = "32" # Font-Schlüssel für Hauptwerte
|
||||
mono_font_key: str = "20" # Font-Schlüssel für Metadaten
|
||||
pad: int = 8 # Innenabstand
|
||||
border_w: int = 2 # Rahmendicke
|
||||
corner_r: int = 0 # Eckenradius (0 = scharf)
|
||||
|
||||
# ---- Farb-Helfer ----
|
||||
def temp_color(self, t: float | None) -> tuple[int, int, int]:
|
||||
"""Temperaturanzeige: kalt→info, warm→accent, heiß→alert."""
|
||||
if t is None:
|
||||
return self.fg
|
||||
if t >= 30:
|
||||
return self.alert
|
||||
if t >= 22:
|
||||
return self.accent
|
||||
if t <= 5:
|
||||
return self.info
|
||||
if t <= 12:
|
||||
return self.info
|
||||
return self.fg
|
||||
|
||||
def value_color(self, value: float, warn_at: float, alert_at: float) -> tuple[int, int, int]:
|
||||
"""Ampel-Helfer: value + Schwellen → passende Farbe."""
|
||||
if value >= alert_at:
|
||||
return self.alert
|
||||
if value >= warn_at:
|
||||
return self.warn
|
||||
return self.ok
|
||||
|
||||
def text(self, draw, text: str, x: int, y: int,
|
||||
font_key: str | None = None, color=None, max_w: int | None = None):
|
||||
"""Short-hand: Text zeichnen mit Theme-Farbe."""
|
||||
font = draw.font if hasattr(draw, 'font') else None
|
||||
# Actual implementation uses fonts dict from render scope
|
||||
pass # see render helpers below
|
||||
|
||||
|
||||
# Vordefinierte Themes
|
||||
THEMES: dict[ThemeName, Theme] = {
|
||||
"light": Theme(
|
||||
name="light",
|
||||
bg=WHITE, fg=BLACK,
|
||||
accent=BLUE, ok=GREEN, warn=YELLOW, alert=RED, info=BLUE,
|
||||
header_font_key="24", label_font_key="20",
|
||||
body_font_key="32", mono_font_key="20",
|
||||
pad=8, border_w=2, corner_r=0,
|
||||
),
|
||||
"dark": Theme(
|
||||
name="dark",
|
||||
bg=BLACK, fg=WHITE,
|
||||
accent=(0, 200, 220), ok=GREEN, warn=ORANGE, alert=RED, info=(0, 180, 255),
|
||||
header_font_key="24", label_font_key="20",
|
||||
body_font_key="32", mono_font_key="20",
|
||||
pad=8, border_w=2, corner_r=0,
|
||||
),
|
||||
"retro": Theme(
|
||||
name="retro",
|
||||
bg=BLACK, fg=YELLOW,
|
||||
accent=ORANGE, ok=GREEN, warn=YELLOW, alert=RED, info=ORANGE,
|
||||
header_font_key="24", label_font_key="20",
|
||||
body_font_key="32", mono_font_key="20",
|
||||
pad=8, border_w=2, corner_r=0,
|
||||
),
|
||||
"mag": Theme(
|
||||
name="mag",
|
||||
bg=WHITE, fg=BLACK,
|
||||
accent=ORANGE, ok=GREEN, warn=ORANGE, alert=RED, info=BLUE,
|
||||
header_font_key="24", label_font_key="20",
|
||||
body_font_key="32", mono_font_key="20",
|
||||
pad=16, border_w=1, corner_r=0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# LAYOUT HELPERS
|
||||
# ============================================================================
|
||||
|
||||
def pick_layout(w: int, h: int) -> LayoutName:
|
||||
"""Wähle Layout-Strategie basierend auf Slot-Größe.
|
||||
|
||||
Reihenfolge ist wichtig: is_wide/is_tall VOR is_small prüfen,
|
||||
weil is_small zu eager matcht (w<280 or h<180).
|
||||
"""
|
||||
if is_wide(w, h):
|
||||
return "wide"
|
||||
if is_tall(w, h):
|
||||
return "tall"
|
||||
if is_small(w, h):
|
||||
return "small"
|
||||
return "standard"
|
||||
|
||||
|
||||
def header_height(layout: LayoutName) -> int:
|
||||
"""Höhe des Header-Bereichs (Label + Titel)."""
|
||||
return 32 if layout == "standard" else 28
|
||||
|
||||
|
||||
def footer_height(layout: LayoutName) -> int:
|
||||
"""Höhe des Footer-Bereichs (Metadaten, Timestamp)."""
|
||||
if layout == "small":
|
||||
return 0
|
||||
if layout in ("wide", "tall"):
|
||||
return 20
|
||||
return 18
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# RENDER HELPERS (Theme-bewusst)
|
||||
# ============================================================================
|
||||
|
||||
def th_text(draw, fonts, theme: Theme, text: str, x: int, y: int,
|
||||
font_key: str | None = None, color=None, max_w: int | None = None):
|
||||
"""Text mit Theme-Default zeichnen."""
|
||||
key = font_key or theme.header_font_key
|
||||
font = fonts.get(key) or fonts.get("default")
|
||||
c = color if color is not None else theme.fg
|
||||
draw.text((x, y), text, font=font, fill=c)
|
||||
|
||||
|
||||
def th_centered(draw, fonts, theme: Theme, text: str,
|
||||
x: int, y: int, w: int, h: int,
|
||||
font_key: str | None = None, color=None):
|
||||
"""Text in Box zentrieren mit Theme-Defaults."""
|
||||
key = font_key or theme.body_font_key
|
||||
font = fonts.get(key) or fonts.get("default")
|
||||
c = color if color is not None else theme.fg
|
||||
centered_text(draw, text, x, y, w, h, font, c)
|
||||
|
||||
|
||||
def th_rect(draw, theme: Theme, x: int, y: int, w: int, h: int,
|
||||
fill=None, outline=None, width: int = 1):
|
||||
"""Rechteck mit Theme-Defaults."""
|
||||
f = fill if fill is not None else None
|
||||
o = outline if outline is not None else theme.fg
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1),
|
||||
fill=f, outline=o, width=width)
|
||||
|
||||
|
||||
def th_header_bar(draw, fonts, theme: Theme,
|
||||
x: int, y: int, w: int, h: int,
|
||||
label: str, value: str = "", value_color=None):
|
||||
"""Header-Leiste: links Label, rechts optionaler Wert.
|
||||
|
||||
Layout: [LABEL] [VALUE]
|
||||
Farbe: accent-Bg fg
|
||||
"""
|
||||
pad = theme.pad
|
||||
# Hintergrund links: Label-Bereich
|
||||
label_w = min(measure(draw, label, fonts.get(theme.label_font_key) or fonts.get("default"))[0] + pad * 2, w // 2)
|
||||
draw.rectangle((x, y, x + label_w, y + h - 1), fill=theme.accent)
|
||||
draw.text((x + pad, y + (h - 20) // 2), label,
|
||||
font=fonts.get(theme.label_font_key) or fonts.get("default"),
|
||||
fill=theme.bg)
|
||||
# Wert rechts
|
||||
if value:
|
||||
vc = value_color if value_color is not None else theme.fg
|
||||
font_v = fonts.get(theme.label_font_key) or fonts.get("default")
|
||||
vw, _ = measure(draw, value, font_v)
|
||||
draw.text((x + w - vw - pad, y + (h - 20) // 2), value, font=font_v, fill=vc)
|
||||
|
||||
|
||||
def th_big_value(draw, fonts, theme: Theme,
|
||||
x: int, y: int, w: int, h: int,
|
||||
value: str, unit: str = "",
|
||||
color=None, align: str = "left"):
|
||||
"""Die große zentrale Kennzahl (z.B. "22°", "45%", "12.4 km").
|
||||
|
||||
Der Wert wird so groß wie möglich dargestellt, das Unit darunter oder
|
||||
daneben in kleinerer Schrift.
|
||||
"""
|
||||
c = color if color is not None else theme.fg
|
||||
candidates = [theme.body_font_key, "80", "60", "48", "36", "28", "24"]
|
||||
font_v = fit_font(draw, value, fonts, w - 2 * theme.pad,
|
||||
h - 2 * theme.pad, candidates=candidates)
|
||||
|
||||
if align == "center":
|
||||
th_centered(draw, fonts, theme, value, x, y, w, h,
|
||||
font_key=None, color=c)
|
||||
else:
|
||||
# Linksbündig, groß
|
||||
tw, th_f = measure(draw, value, font_v)
|
||||
draw.text((x + theme.pad, y + max(0, (h - th_f) // 2)),
|
||||
value, font=font_v, fill=c)
|
||||
|
||||
# Unit darunter oder daneben
|
||||
if unit:
|
||||
unit_font = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
if align == "center":
|
||||
# Unter dem Wert
|
||||
uw, uh = measure(draw, unit, unit_font)
|
||||
draw.text((x + max(0, (w - uw) // 2), y + h // 2 + 4), unit,
|
||||
font=unit_font, fill=theme.fg)
|
||||
else:
|
||||
uw, uh = measure(draw, unit, unit_font)
|
||||
draw.text((x + theme.pad + tw + 6,
|
||||
y + max(0, (h - uh) // 2)),
|
||||
unit, font=unit_font, fill=theme.fg)
|
||||
|
||||
|
||||
def th_mini_bar(draw, fonts, theme: Theme,
|
||||
x: int, y: int, w: int, h: int,
|
||||
pct: float,
|
||||
thresholds: list | None = None,
|
||||
gradient: bool = True):
|
||||
"""Kompakter Fortschrittsbalken mit Theme-Styling.
|
||||
|
||||
thresholds: [(pct, color), ...] — z.B. [(50, ok), (80, warn), (100, alert)]
|
||||
gradient: True = zeigt alle Farbstufen gleichzeitig (e-Paper-nativ)
|
||||
"""
|
||||
if thresholds is None:
|
||||
thresholds = [(50, theme.ok), (80, theme.warn), (100, theme.alert)]
|
||||
hbar(draw, x, y, w, h, pct,
|
||||
thresholds=thresholds, gradient=gradient)
|
||||
|
||||
|
||||
def th_divider(draw, theme: Theme, x: int, y: int, w: int,
|
||||
style: str = "solid"):
|
||||
"""Horizontale Trennlinie."""
|
||||
if style == "solid":
|
||||
draw.line((x, y, x + w, y), fill=theme.fg, width=1)
|
||||
elif style == "dashed":
|
||||
# Dashed: 4px dash, 4px gap
|
||||
for dx in range(0, w, 8):
|
||||
draw.line((x + dx, y, min(x + dx + 4, x + w), y),
|
||||
fill=theme.fg, width=1)
|
||||
elif style == "accent":
|
||||
draw.line((x, y, x + w, y), fill=theme.accent, width=2)
|
||||
|
||||
|
||||
def th_corner_marker(draw, theme: Theme, x: int, y: int, size: int = 8):
|
||||
"""Kleiner Eck-Marker (L-Form) oben-links — markiert den Slot-Ursprung.
|
||||
|
||||
Optionaler visueller Anker der zeigt: "hier beginnt dieses Widget".
|
||||
"""
|
||||
draw.line((x, y, x + size, y), fill=theme.accent, width=2)
|
||||
draw.line((x, y, x, y + size), fill=theme.accent, width=2)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# LAYOUT PATTERN TEMPLATES
|
||||
# ============================================================================
|
||||
|
||||
def layout_1x1(draw, fonts, theme: Theme,
|
||||
value: str, unit: str = "",
|
||||
label: str = "", color=None):
|
||||
"""ONE BIG NUMBER — für 1x1 Slots.
|
||||
|
||||
Große zentrierte Zahl, darunter kleines Label.
|
||||
Beispiel: Gmail ungelesen, Strava Jahr-km, System CPU%
|
||||
"""
|
||||
pad = theme.pad
|
||||
# Rahmen
|
||||
th_rect(draw, theme, x=0, y=0, w=200, h=120,
|
||||
outline=theme.fg, width=theme.border_w)
|
||||
|
||||
c = color if color is not None else theme.fg
|
||||
font_val = fit_font(draw, value, fonts, 200 - 2 * pad, 80,
|
||||
candidates=[theme.body_font_key, "80", "60", "48"])
|
||||
th_centered(draw, fonts, theme, value,
|
||||
x=0, y=10, w=200, h=90, color=c)
|
||||
|
||||
if label:
|
||||
font_l = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
lw = measure(draw, label, font_l)[0]
|
||||
draw.text(((200 - lw) // 2, 96), label, font=font_l, fill=theme.fg)
|
||||
|
||||
|
||||
def layout_wide_strip(draw, fonts, theme: Theme,
|
||||
cols: list[dict],
|
||||
header_h: int = 30):
|
||||
"""WIDE STRIP — für 4x1 / 2x1 Slots.
|
||||
|
||||
Horizontale Teilung in gleichbreite Spalten.
|
||||
Jede Spalte: [LABEL] über [VALUE] über [UNIT]
|
||||
|
||||
cols = [
|
||||
{"label": "TEMP", "value": "22°", "unit": "", "color": theme.temp_color(22)},
|
||||
{"label": "HUM", "value": "65%", "unit": "Feuchte", "color": theme.ok},
|
||||
{"label": "CO₂", "value": "820", "unit": "ppm", "color": theme.warn},
|
||||
]
|
||||
"""
|
||||
n = len(cols)
|
||||
col_w = 200 // n if n > 0 else 200
|
||||
pad = theme.pad
|
||||
|
||||
for i, col in enumerate(cols):
|
||||
cx = i * col_w
|
||||
c = col.get("color", theme.fg)
|
||||
|
||||
# Label
|
||||
lbl = col.get("label", "")
|
||||
if lbl:
|
||||
draw.text((cx + pad, 4), lbl,
|
||||
font=fonts.get(theme.label_font_key) or fonts.get("default"),
|
||||
fill=theme.accent)
|
||||
|
||||
# Value (groß)
|
||||
val = col.get("value", "—")
|
||||
font_v = fit_font(draw, val, fonts, col_w - 2 * pad, header_h + 30,
|
||||
candidates=[theme.body_font_key, "48", "36", "28"])
|
||||
draw.text((cx + pad, header_h), val, font=font_v, fill=c)
|
||||
|
||||
# Unit (klein darunter)
|
||||
unit = col.get("unit", "")
|
||||
if unit:
|
||||
font_u = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
draw.text((cx + pad, header_h + font_v.size + 2),
|
||||
unit, font=font_u, fill=theme.fg)
|
||||
|
||||
|
||||
def layout_card_grid(draw, fonts, theme: Theme,
|
||||
cards: list[dict],
|
||||
x: int, y: int, w: int, h: int,
|
||||
cols: int = 2):
|
||||
"""CARD GRID — für 2x2+ Slots.
|
||||
|
||||
Teil den verfügbaren Raum in gleichmäßige Karten auf.
|
||||
Jede Karte hat: Border + Header-Akzent + Label + Wert + optional Bar.
|
||||
|
||||
cards = [
|
||||
{
|
||||
"label": "🏠 Indoor",
|
||||
"value": "22.5°",
|
||||
"unit": "",
|
||||
"color": theme.temp_color(22.5),
|
||||
"bar": {"pct": 45, "thresholds": [...], "gradient": True},
|
||||
"meta": "min 18° / max 26°",
|
||||
},
|
||||
...
|
||||
]
|
||||
"""
|
||||
pad = theme.pad
|
||||
rows = (len(cards) + cols - 1) // cols
|
||||
card_w = (w - (cols + 1) * pad) // cols
|
||||
card_h = (h - (rows + 1) * pad) // rows
|
||||
|
||||
for i, card in enumerate(cards):
|
||||
row = i // cols
|
||||
col = i % cols
|
||||
cx = x + pad + col * (card_w + pad)
|
||||
cy = y + pad + row * (card_h + pad)
|
||||
|
||||
# Border
|
||||
th_rect(draw, theme, cx, cy, card_w, card_h,
|
||||
outline=theme.fg, width=theme.border_w)
|
||||
|
||||
# Label-Balken oben
|
||||
lbl = card.get("label", "")
|
||||
if lbl:
|
||||
lbl_h = 24
|
||||
draw.rectangle((cx, cy, cx + card_w - 1, cy + lbl_h),
|
||||
fill=theme.accent)
|
||||
font_l = fonts.get(theme.label_font_key) or fonts.get("default")
|
||||
draw.text((cx + pad, cy + 4), lbl, font=font_l, fill=theme.bg)
|
||||
|
||||
# Value
|
||||
inner_y = cy + 28
|
||||
inner_h = card_h - 30
|
||||
val = card.get("value", "—")
|
||||
c = card.get("color", theme.fg)
|
||||
font_v = fit_font(draw, val, fonts, card_w - 2 * pad,
|
||||
inner_h // 2,
|
||||
candidates=[theme.body_font_key, "48", "36", "28"])
|
||||
draw.text((cx + pad, inner_y), val, font=font_v, fill=c)
|
||||
|
||||
# Bar (optional)
|
||||
bar = card.get("bar")
|
||||
if bar and inner_h > 60:
|
||||
bar_pct = bar.get("pct", 0)
|
||||
bar_thresholds = bar.get("thresholds") or [
|
||||
(50, theme.ok), (80, theme.warn), (100, theme.alert)]
|
||||
bar_y = cy + card_h - 28
|
||||
th_mini_bar(draw, fonts, theme,
|
||||
cx + pad, bar_y, card_w - 2 * pad, 12,
|
||||
bar_pct, bar_thresholds, bar.get("gradient", True))
|
||||
|
||||
# Meta (optional)
|
||||
meta = card.get("meta", "")
|
||||
if meta:
|
||||
font_m = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
draw.text((cx + pad, cy + card_h - 18), meta,
|
||||
font=font_m, fill=theme.fg)
|
||||
|
||||
|
||||
def layout_poster(draw, fonts, theme: Theme,
|
||||
label: str, value: str, unit: str = "",
|
||||
sub: str = "", color=None):
|
||||
"""POSTER — eine einzelne Aussage, maximal typografisch.
|
||||
|
||||
Für 2x2+ Slots die WIRKLICH nur eine Zahl zeigen wollen.
|
||||
Beispiel: Eine gigantische Uhrzeit, eine große Temperatur.
|
||||
"""
|
||||
pad = theme.pad
|
||||
c = color if color is not None else theme.fg
|
||||
|
||||
# Dünne Rahmenlinie
|
||||
th_rect(draw, theme, x=0, y=0, w=400, h=240,
|
||||
outline=theme.accent, width=1)
|
||||
|
||||
# Label oben links
|
||||
if label:
|
||||
font_l = fonts.get(theme.label_font_key) or fonts.get("default")
|
||||
draw.text((pad, pad), label, font=font_l, fill=theme.accent)
|
||||
|
||||
# Value zentriert, RIESIG
|
||||
font_v = fit_font(draw, value, fonts, 400 - 2 * pad, 180,
|
||||
candidates=["80", "60", "48", theme.body_font_key])
|
||||
tw, th_f = measure(draw, value, font_v)
|
||||
draw.text(((400 - tw) // 2, 40 + max(0, (180 - th_f) // 2)),
|
||||
value, font=font_v, fill=c)
|
||||
|
||||
# Unit darunter
|
||||
if unit:
|
||||
font_u = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
uw, _ = measure(draw, unit, font_u)
|
||||
draw.text(((400 - uw) // 2, 40 + 180 - th_f // 2 + 4),
|
||||
unit, font=font_u, fill=theme.fg)
|
||||
|
||||
# Sub / Metatext unten
|
||||
if sub:
|
||||
font_s = fonts.get(theme.mono_font_key) or fonts.get("default")
|
||||
sw, _ = measure(draw, sub, font_s)
|
||||
draw.text(((400 - sw) // 2, 220), sub, font=font_s, fill=theme.fg)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# EXAMPLE: So wird ein Plugin-Design daraus gebaut
|
||||
# ============================================================================
|
||||
#
|
||||
# class Widget(Widget):
|
||||
# name = "myplugin"
|
||||
# default_config = {
|
||||
# "theme": "light", # light | dark | retro | mag
|
||||
# "accent_color": "blue", # blue | green | orange | red
|
||||
# }
|
||||
#
|
||||
# def render(self, draw, fonts, x, y, w, h):
|
||||
# theme = THEMES[self.cfg("theme", "light")]
|
||||
# data = self.fetch()
|
||||
# if "_error" in data:
|
||||
# render_error_banner(draw, fonts, x, y, w, h, self.label, data["_error"])
|
||||
# return
|
||||
#
|
||||
# layout = pick_layout(w, h)
|
||||
# hdr_h = header_height(layout)
|
||||
#
|
||||
# if layout == "small":
|
||||
# th_corner_marker(draw, theme, x, y)
|
||||
# layout_1x1(draw, fonts, theme, data["value"], data.get("unit", ""))
|
||||
# elif layout == "wide":
|
||||
# layout_wide_strip(draw, fonts, theme, data["cols"])
|
||||
# else:
|
||||
# # standard: Card-Grid
|
||||
# cards = [
|
||||
# {"label": k, "value": v, "color": theme.fg}
|
||||
# for k, v in data["cards"].items()
|
||||
# ]
|
||||
# layout_card_grid(draw, fonts, theme, cards, x, y, w, h)
|
||||
#
|
||||
# ============================================================================
|
||||
+609
-794
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user