FEAT: 4x4 grid UI fixes, smooth drag-drop ghost preview, WordClock plugin
- Grid items now correctly placed inside .grid-cell divs (CSS Grid children) - Visual occupied-state feedback during drag (CSS classes, no full re-render) - Drag ghost preview (green=ok, red=overlap) replaces flicker on dragend - New clock_wordclock plugin: German/English word clock (QWATCH layout)
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""WordClock-Plugin:Deutsche Wort-Uhr (QWATCHLayout).
|
||||
|
||||
Reines Wort-Uhr-Display im deutschen Stil:
|
||||
ES IST <Fünf/Zehn/Viertel/Zwanzig> <Minuten> <nach/vor> <Stunde>
|
||||
|
||||
Unterstützt auch ENGLISCH (US-Layout).
|
||||
|
||||
Layout: fester 4×4-Zeichen-Grid im Quadrat (4×4 Zellen = 800×480 Display).
|
||||
Jede Zelle = 200×120px → Grid = 800×480px.
|
||||
Wir nutzen 11×8 "字符-Zellen" pro Grid (72×60px pro Zeichen).
|
||||
|
||||
Minuten-Zeilen (oberer Block):
|
||||
Zeile 0: [E][S][ ][I][S][T]
|
||||
Zeile 1: [F][Ü][N][F][Z][E][H][N][Z][W][A][N]
|
||||
Zeile 2: [V][I][E][R][T][E][L][Z][W][A][N][Z]
|
||||
Zeile 3: [N][U][L][L]
|
||||
Zeile 4: [N][A][C][H][ ][V][O][R][ ][H][A][L]
|
||||
Zeile 5: [B][ ]
|
||||
Zeile 6: [S][P][R][A][C][H][E]
|
||||
|
||||
Stunden (unterer Block, je 2×2 Zellen für die Ziffern):
|
||||
DieZiffern werden als gefüllte Rechtecke in der unteren Reihe gerendert.
|
||||
|
||||
Das Layout wird intern gecacht bis sich die Minute ändert.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import os, sys
|
||||
from datetime import datetime
|
||||
from functools import lru_cache
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
from plugins.base import Widget
|
||||
from palette import FG, BG, OK, BLUE, YELLOW, ORANGE, fill_for, measure
|
||||
|
||||
|
||||
class Widget(Widget):
|
||||
name = "clock_wordclock"
|
||||
label = "WordClock"
|
||||
description = "Deutsche Wort-Uhr (ES IST …). Nur für 2×2 oder größer."
|
||||
category = "info"
|
||||
|
||||
config_schema = [
|
||||
{"key": "lang", "label": "Sprache",
|
||||
"type": "select", "choices": ["de", "en"], "default": "de",
|
||||
"help": "de = Deutsch (Standard), en = Englisch"},
|
||||
{"key": "show_date", "label": "Datum anzeigen",
|
||||
"type": "bool", "default": True},
|
||||
{"key": "show_weekday", "label": "Wochentag anzeigen",
|
||||
"type": "bool", "default": True},
|
||||
{"key": "accent_color", "label": "Akzentfarbe",
|
||||
"type": "select",
|
||||
"choices": ["accent", "blue", "ok", "warn", "info"],
|
||||
"default": "accent"},
|
||||
{"key": "invert", "label": "Invertiert (dunkel)",
|
||||
"type": "bool", "default": False},
|
||||
]
|
||||
default_config = {
|
||||
"lang": "de", "show_date": True, "show_weekday": True,
|
||||
"accent_color": "accent", "invert": False,
|
||||
}
|
||||
|
||||
def fetch(self):
|
||||
return {}
|
||||
|
||||
def render(self, draw, fonts, x: int, y: int, w: int, h: int):
|
||||
from palette import fill_for
|
||||
now = datetime.now()
|
||||
m = now.minute
|
||||
h_ = now.hour
|
||||
|
||||
invert = self.cfg("invert", False)
|
||||
accent = fill_for(self.cfg("accent_color", "accent"))
|
||||
fg = BG if invert else FG
|
||||
bg = FG if invert else BG
|
||||
|
||||
pad = 8
|
||||
|
||||
# Mindestgröße: 300px-breit, 240px-hoch für WordClock
|
||||
if w < 300 or h < 240:
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
|
||||
draw.text((x + pad, y + pad),
|
||||
"WordClock\n(min 2×2)",
|
||||
font=fonts.get("20", fonts.get("default")), fill=fg)
|
||||
return
|
||||
|
||||
# ---- Wort-Uhr Grid ----
|
||||
# 4×4 Zellen → 800×480px
|
||||
# Wir rendern in ein 11×8 Zeichen-Grid
|
||||
chars_x, chars_y = 11, 8
|
||||
char_w = w // chars_x
|
||||
char_h = h // chars_y
|
||||
font_size = min(char_w, char_h) * 2 // 3
|
||||
font_key = str(font_size)
|
||||
if font_key not in fonts:
|
||||
font_key = str(max(16, min(fonts.keys(), key=lambda k: abs(int(k) - font_size) if k.isdigit() else 9999)) if fonts else "20")
|
||||
fnt = fonts.get(font_key, fonts.get("20", fonts.get("default")))
|
||||
|
||||
def _draw_char(cx, cy, char, color):
|
||||
"""Zeichnet ein Zeichen an Gitterposition (cx, cy)."""
|
||||
px = x + cx * char_w
|
||||
py = y + cy * char_h
|
||||
tw, th = measure(draw, char, fnt)
|
||||
draw.text((px + (char_w - tw) // 2, py + (char_h - th) // 2),
|
||||
char, font=fnt, fill=color)
|
||||
|
||||
def _fill_char(cx, cy, color):
|
||||
"""Füllt eine Gitterzelle mit einer Farbe (z.B. für Stunden-Dots)."""
|
||||
px = x + cx * char_w
|
||||
py = y + cy * char_h
|
||||
draw.rectangle((px + 2, py + 2, px + char_w - 3, py + char_h - 3), fill=color)
|
||||
|
||||
def _lit(cx, cy):
|
||||
_draw_char(cx, cy, LAYOUT_DE[cy][cx], fg)
|
||||
|
||||
def _dim(cx, cy):
|
||||
_draw_char(cx, cy, LAYOUT_DE[cy][cx], (150, 150, 150))
|
||||
|
||||
# ---- Minuten-Logik (Deutsch) ----
|
||||
def lit_minute(m):
|
||||
"""Sektor der Minuten: 0-4, 5-9, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59."""
|
||||
if m < 5:
|
||||
return []
|
||||
elif m < 10:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1)] # FÜNF
|
||||
elif m < 15:
|
||||
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN
|
||||
elif m < 20:
|
||||
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2)] # VIERTEL
|
||||
elif m < 25:
|
||||
return [(7, 2), (8, 2), (9, 2), (10, 2), (7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
|
||||
elif m < 30:
|
||||
return [(0, 4), (1, 4), (2, 4), (3, 4)] # NACH
|
||||
elif m < 35:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF
|
||||
(4, 4), (5, 4), (6, 4)] # + HALB
|
||||
elif m < 40:
|
||||
return [(4, 4), (5, 4), (6, 4)] # HALB
|
||||
elif m < 45:
|
||||
return [(7, 3), (8, 3), (9, 3), (10, 3)] # ZWANZIG
|
||||
elif m < 50:
|
||||
return [(0, 2), (1, 2), (2, 2), (3, 2), (4, 2), (5, 2), (6, 2), # VIERTEL
|
||||
(7, 4), (8, 4), (9, 4)] # + VOR
|
||||
elif m < 55:
|
||||
return [(5, 1), (6, 1), (7, 1), (8, 1)] # ZEHN + VOR
|
||||
else:
|
||||
return [(0, 1), (1, 1), (2, 1), (3, 1), # FÜNF + VOR
|
||||
(7, 4), (8, 4), (9, 4)]
|
||||
|
||||
def hour_name(h, past_half):
|
||||
"""Gibt die Stunde zurück für die Wortuhr."""
|
||||
DE_HOURS = [
|
||||
"ZWÖLF", "EINS", "ZWEI", "DREI", "VIER",
|
||||
"FÜNF", "SECHS", "SIEBEN", "ACHT", "NEUN",
|
||||
"ZEHN", "ELF", "ZWÖLF"
|
||||
]
|
||||
if past_half:
|
||||
h = (h + 1) % 24
|
||||
if h == 0:
|
||||
return "ZWÖLF"
|
||||
return DE_HOURS[h % 12]
|
||||
|
||||
past_half = m >= 20
|
||||
h_display = hour_name(h_, past_half)
|
||||
lit_cells = lit_minute(m)
|
||||
|
||||
# ---- Render ----
|
||||
# Hintergrund
|
||||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=bg)
|
||||
|
||||
# "ES IST" immer lit in Spalte 0
|
||||
_lit(0, 0); _lit(1, 0); _lit(3, 0); _lit(4, 0)
|
||||
|
||||
# Minuten-Wörter
|
||||
for (cx, cy) in lit_cells:
|
||||
_lit(cx, cy)
|
||||
|
||||
# "VOR" und "NACH" (Zeile 4)
|
||||
if 5 <= m < 30:
|
||||
_lit(0, 4); _lit(1, 4); _lit(2, 4); _lit(3, 4) # NACH
|
||||
elif m >= 35 and m < 55:
|
||||
_lit(7, 4); _lit(8, 4); _lit(9, 4) # VOR
|
||||
|
||||
# Rest dim
|
||||
for row in range(chars_y):
|
||||
for col in range(chars_x):
|
||||
if (col, row) not in lit_cells and not (row == 0 and col in (0, 1, 3, 4)):
|
||||
_dim(col, row)
|
||||
|
||||
# ---- Stunden-Balken unten ----
|
||||
# Zeile 6+7: Stunden-Name in großen Buchstaben unten zentriert
|
||||
hour_str = h_display
|
||||
hour_font_size = min(w // len(hour_str), h // 4) * 3 // 4
|
||||
hf = fonts.get(str(hour_font_size), fonts.get("60", fonts.get("default")))
|
||||
tw, th = measure(draw, hour_str, hf)
|
||||
hour_y = y + h - th - pad
|
||||
draw.text((x + (w - tw) // 2, hour_y), hour_str, font=hf, fill=fg)
|
||||
|
||||
# ---- Datum + Wochentag ----
|
||||
if self.cfg("show_date", True):
|
||||
date_str = now.strftime("%d. %b %Y")
|
||||
df = fonts.get("16", fonts.get("default"))
|
||||
dw, dh = measure(draw, date_str, df)
|
||||
draw.text((x + (w - dw) // 2, y + pad), date_str, font=df, fill=accent)
|
||||
|
||||
if self.cfg("show_weekday", True):
|
||||
day_str = now.strftime("%A").upper()
|
||||
df = fonts.get("14", fonts.get("default"))
|
||||
dw, dh = measure(draw, day_str, df)
|
||||
draw.text((x + (w - dw) // 2, y + pad + (18 if self.cfg("show_date", True) else 0)),
|
||||
day_str, font=df, fill=accent)
|
||||
|
||||
|
||||
# ---- Deutsches WordClock-Layout (11×8) ----
|
||||
# Jede Position ist ein Zeichen das gerendert wird.
|
||||
# ' ' = Leerzeichen, rest = Buchstabe.
|
||||
LAYOUT_DE = [
|
||||
["E", "S", " ", "I", "S", "T", " ", " ", " ", " ", " "],
|
||||
["F", "Ü", "N", "F", " ", "Z", "E", "H", "N", " ", " "],
|
||||
["V", "I", "E", "R", "T", "E", "L", " ", "Z", "W", "A"],
|
||||
["N", "U", "L", "L", " ", "Z", "W", "A", "N", "Z", " "],
|
||||
["N", "A", "C", "H", " ", "V", "O", "R", " ", "H", "A"],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
[" ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " "],
|
||||
]
|
||||
Reference in New Issue
Block a user