Let a tasks widget have a custom on-panel name
Build and push server image / test (push) Successful in 25s
Build and push server image / build-and-push (push) Successful in 1m59s
Build and push server image / deploy (push) Successful in 53s

Adds TaskWidgetConfig.name (migration 19, plain column add) shown at
the top of the widget on the actual panel instead of the hardcoded
"Tasks" header -- e.g. "Chores" or "Mom's list". The only widget type
with its own on-panel title at all, since it's the only one where
"which list is this" isn't already obvious from a calendar/photo/
whiteboard's own content.

Threaded through calendar_render's _draw_tasks/_build_tasks/
render_tasks/render_tasks_preview_png as a `title` param (default
"Tasks", truncated to fit -- a long custom name shouldn't be able to
overflow the widget's box), the config-save endpoint (tasks_name,
truncated server-side to a sane header length rather than rejected),
and a new "Settings" section in the tasks dialog.

Verified live in the browser: the name actually renders at the top of
the real composited panel (not just the dialog preview, which stays
gated on having a configured source), and persists correctly on both
desktop and mobile. Full suite (196 tests) passes.
This commit is contained in:
Thomas Faour
2026-07-25 04:05:36 +00:00
parent 14c47aa2a0
commit 4a2b1f3795
10 changed files with 120 additions and 20 deletions
+14 -9
View File
@@ -530,8 +530,12 @@ def _draw_agenda_day(img: Image.Image, draw: ImageDraw.ImageDraw, day: date, eve
def _draw_tasks(img: Image.Image, draw: ImageDraw.ImageDraw, region: tuple[int, int, int, int],
tasks: list[dict], title_font: ImageFont.ImageFont, body_font: ImageFont.ImageFont,
palette_rgb: list | None = None) -> None:
"""A simple checklist filling `region` (x0, y0, w, h) -- a color bar
palette_rgb: list | None = None, title: str = "Tasks") -> None:
"""A simple checklist filling `region` (x0, y0, w, h) -- a header
(`title`, truncated to fit -- TaskWidgetConfig.name or the "Tasks"
default; the only widget type with its own on-panel title, since
it's the only one where "which list is this" isn't obvious from its
content the way a calendar/photo/whiteboard's is), then a color bar
(reusing _event_colors/_draw_color_bar as-is: a task dict's
top-level owner_display_name/color_index is exactly _event_colors'
single-source fallback shape, since caldav_client.merge_tasks
@@ -551,7 +555,7 @@ def _draw_tasks(img: Image.Image, draw: ImageDraw.ImageDraw, region: tuple[int,
x0, y0, w, h = region
text_x0, text_y0 = x0 + MARGIN, y0 + MARGIN
text_w = w - MARGIN * 2
draw_text(img, (text_x0, text_y0), "Tasks", title_font)
draw_text(img, (text_x0, text_y0), _truncate_to_width(draw, title or "Tasks", title_font, text_w), title_font)
y = text_y0 + title_font.size + 12
draw.line([(text_x0, y), (text_x0 + text_w, y)], fill=RULE)
y += 12
@@ -894,7 +898,8 @@ def render_calendar_preview_png(events: list[dict], view: str, browse_offset: in
_TASKS_FONTS = {"large": (24, 18), "medium": (20, 16), "small": (16, 13)}
def _build_tasks(tasks: list[dict], target_w: int, target_h: int, palette_rgb: list | None = None) -> Image.Image:
def _build_tasks(tasks: list[dict], target_w: int, target_h: int, palette_rgb: list | None = None,
title: str = "Tasks") -> Image.Image:
"""A tasks widget's entire region is the checklist -- unlike the old
week-view slot, there's no day columns/header to share space with,
so this is just _draw_tasks over the whole box."""
@@ -903,29 +908,29 @@ def _build_tasks(tasks: list[dict], target_w: int, target_h: int, palette_rgb: l
title_size, body_size = _TASKS_FONTS[_size_tier(target_w, target_h)]
title_font = ImageFont.load_default(size=title_size)
body_font = ImageFont.load_default(size=body_size)
_draw_tasks(img, draw, (0, 0, target_w, target_h), tasks, title_font, body_font, palette_rgb)
_draw_tasks(img, draw, (0, 0, target_w, target_h), tasks, title_font, body_font, palette_rgb, title)
return img
def render_tasks(tasks: list[dict], orientation: str, palette_rgb: list | None,
manage: dict | None = None) -> bytes:
manage: dict | None = None, title: str = "Tasks") -> bytes:
"""Renders the tasks widget full-panel to the panel's packed format.
Always returns exactly EPD_WIDTH*EPD_HEIGHT/2 bytes, same invariant
every other renderer honors."""
target_w, target_h = logical_render_size(orientation)
img = _build_tasks(tasks, target_w, target_h, palette_rgb)
img = _build_tasks(tasks, target_w, target_h, palette_rgb, title)
img = _apply_manage_overlay(img, manage)
quantized = _quantize(img, palette_rgb, dither_strength=1.0)
return _transpose_and_pack(quantized, orientation)
def render_tasks_preview_png(tasks: list[dict], orientation: str, palette_rgb: list | None,
manage: dict | None = None) -> bytes:
manage: dict | None = None, title: str = "Tasks") -> bytes:
"""Same pipeline as render_tasks, but a normal browser-viewable PNG
in logical (upright) orientation -- mirrors render_calendar_preview_
png's relationship to render_calendar."""
target_w, target_h = logical_render_size(orientation)
img = _build_tasks(tasks, target_w, target_h, palette_rgb)
img = _build_tasks(tasks, target_w, target_h, palette_rgb, title)
img = _apply_manage_overlay(img, manage)
quantized = _quantize(img, palette_rgb, dither_strength=1.0)
buf = io.BytesIO()