Add a per-widget text-size picker for calendar/tasks legibility
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:
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user