Add a per-widget text-size picker for calendar/tasks legibility
Build and push server image / test (push) Successful in 42s
Build and push server image / build-and-push (push) Successful in 3m33s
Build and push server image / deploy (push) Failing after 1m24s

Calendar and tasks pack the most body text at the smallest default
sizes, so those two gear-icon dialogs get a "Text size" card (Normal/
Large/X-Large) alongside the existing Border card -- a new Widget-level
font_scale column with its own POST .../font-scale endpoint, same
Widget-property-not-config-field shape as border_style. Threaded through
every classic (calendar_render.py) and modern (html_render.py/
calendar_html_render.py) size calc via one shared panel_style.
scaled_size() so row heights/max_rows already derived from font size
re-fit around the bigger text automatically.
This commit is contained in:
2026-08-02 03:23:27 +00:00
parent 09119e775f
commit 2868087467
22 changed files with 426 additions and 60 deletions
+33 -6
View File
@@ -26,7 +26,7 @@ from pydantic import BaseModel
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from .. import calendar_render, grid, photo_queue, quiet_hours, weather, weather_render, webdav_client
from .. import calendar_render, grid, panel_style, photo_queue, quiet_hours, weather, weather_render, webdav_client
from ..auth import require_frame_control, require_frame_view, require_user_api
from ..db import frame_locked, get_db, widget_locked
from ..image_pipeline import (
@@ -96,7 +96,7 @@ def _widget_dict(w: Widget, locked: bool = False) -> dict:
return {"id": w.id, "widget_type": w.widget_type, "x": w.x, "y": w.y, "w": w.w, "h": w.h,
"sort_order": w.sort_order, "border_style": w.border_style,
"border_thickness": w.border_thickness, "border_color_index": w.border_color_index,
"locked": locked}
"font_scale": w.font_scale, "locked": locked}
def require_widget_view(
@@ -280,6 +280,31 @@ def api_widget_border(
return _widget_dict(widget)
class WidgetFontScaleRequest(BaseModel):
font_scale: float
@router.post("/api/frames/{frame_id}/widgets/{widget_id}/font-scale")
def api_widget_font_scale(
body: WidgetFontScaleRequest, frame_widget: tuple[Frame, Widget] = Depends(require_widget_control),
db: Session = Depends(get_db),
):
"""Sets this widget's text-size multiplier (the Layout dialog's "Text
size" picker) -- a shared Widget-level property (see models.Widget.
font_scale), not a per-type config field, since any widget type with
body text can use it. Its own endpoint for the same reason
api_widget_border has one: api_widget_config_save's per-type dispatch
edits a config row via widget_locked, and font_scale lives on Widget
itself, not any per-type config table."""
frame, widget = frame_widget
if body.font_scale not in panel_style.FONT_SCALE_CHOICES:
raise HTTPException(400, f"font_scale must be one of {panel_style.FONT_SCALE_CHOICES}")
with frame_locked(db, frame.id):
widget.font_scale = body.font_scale
db.commit()
return _widget_dict(widget)
@router.delete("/api/frames/{frame_id}/widgets/{widget_id}")
def api_widget_delete(
widget_id: int, frame: Frame = Depends(require_frame_control), db: Session = Depends(get_db)
@@ -876,7 +901,7 @@ def api_widget_preview_calendar(
img = calendar_html_render.build(
events, ccfg.view, ccfg.browse_offset, target_w, target_h, tz, ccfg.week_start, frame.palette_rgb,
weather_cities, ccfg.weather_units, ccfg.week_days, ccfg.week_layout, ccfg.week_start_offset,
frame.theme,
frame.theme, widget.font_scale,
)
quantized = _quantize(img, frame.palette_rgb, dither_strength=1.0)
png = _png_bytes(quantized)
@@ -887,7 +912,7 @@ def api_widget_preview_calendar(
week_start=ccfg.week_start,
weather_cities=weather_cities, weather_units=ccfg.weather_units,
week_days=ccfg.week_days, week_layout=ccfg.week_layout,
week_start_offset=ccfg.week_start_offset,
week_start_offset=ccfg.week_start_offset, font_scale=widget.font_scale,
)
return Response(content=png, media_type="image/png")
@@ -912,12 +937,14 @@ def api_widget_preview_tasks(
from .. import html_render
target_w, target_h = logical_render_size(frame.orientation)
img = html_render.build_tasks(tasks, target_w, target_h, frame.palette_rgb, title, frame.theme)
img = html_render.build_tasks(tasks, target_w, target_h, frame.palette_rgb, title, frame.theme,
widget.font_scale)
quantized = _quantize(img, frame.palette_rgb, dither_strength=1.0)
png = _png_bytes(quantized)
else:
png = calendar_render.render_tasks_preview_png(tasks, orientation=frame.orientation,
palette_rgb=frame.palette_rgb, title=title)
palette_rgb=frame.palette_rgb, title=title,
font_scale=widget.font_scale)
return Response(content=png, media_type="image/png")
+13 -3
View File
@@ -17,7 +17,7 @@ from fastapi.templating import Jinja2Templates
from sqlalchemy import select
from sqlalchemy.orm import Session
from .. import theme_tokens, weather
from .. import panel_style, theme_tokens, weather
from ..auth import can_view_frame, current_user
from ..calendar_render import CALENDAR_VIEW_LABELS
from ..db import get_db
@@ -258,6 +258,16 @@ def widget_dialog(frame_id: int, widget_id: int, request: Request, db: Session =
"back_button_action": bindings.get("back", ""),
}
# The shared "Text size" card (_widget_font_scale_fields.html,
# models.Widget.font_scale) -- only included on the dialogs whose
# on-panel content is mostly body text (calendar/tasks; the other
# types are either image-only or, for text, already have their own
# richer per-widget font_size control -- see TextWidgetConfig).
font_scale_labels = {1.0: "Normal", 1.25: "Large", 1.5: "X-Large"}
font_scale_ctx = {
"font_scale_choices": [(v, font_scale_labels[v]) for v in panel_style.FONT_SCALE_CHOICES],
}
if widget.widget_type == "photos":
photo_cfg = db.get(PhotoWidgetConfig, widget.id)
return templates.TemplateResponse("_widget_dialog_photos.html", {
@@ -273,7 +283,7 @@ def widget_dialog(frame_id: int, widget_id: int, request: Request, db: Session =
"calendar_users": _calendar_users_for_widget(db, frame.id, widget.id, user.id),
"week_start_labels": WEEK_START_LABELS,
"calendar_color_labels": PALETTE_LABELS,
**border_ctx, **button_ctx,
**border_ctx, **button_ctx, **font_scale_ctx,
})
if widget.widget_type == "tasks":
@@ -282,7 +292,7 @@ def widget_dialog(frame_id: int, widget_id: int, request: Request, db: Session =
"request": request, "frame": frame, "widget": widget, "task_cfg": task_cfg, "user": user,
"task_users": _task_users_for_widget(db, frame.id, widget.id, user.id),
"task_color_labels": PALETTE_LABELS,
**border_ctx, **button_ctx,
**border_ctx, **button_ctx, **font_scale_ctx,
})
if widget.widget_type == "static":