Move button actions to per-widget config, add hold-for-global-action
Build and push server image / test (push) Successful in 36s
Firmware build check / build-check (push) Successful in 2m4s
Build and push server image / build-and-push (push) Successful in 3m12s
Build and push server image / deploy (push) Successful in 58s

Next/back button assignment moves from a frame-level "Button
assignments" card into each widget's own gear-icon dialog, prefilled
with a sane default at creation (photos/calendar -> advance/back,
whiteboard/weather -> check_now, others -> none). At most one binding
per (widget, button) now -- cross-widget execution order never
mattered since each widget's action only touches its own state.

New firmware capability: holding NEXT or BACK past a configurable
duration (min 3s, server-side default) triggers a frame-wide action
instead of the per-widget short-press one -- cycling saved layouts,
refreshing all widgets, or freezing/unfreezing every photo widget (see
app/global_actions.py). Firmware next/back checks gain the same
hold-duration polling the combo button already had; the threshold
comes from the previous wake's /frame/config fetch (persisted in NVS),
since this wake's button decision happens before that request.

Not done here: firmware/version.txt is intentionally left unbumped --
this hasn't been built or hardware-tested (no ESP-IDF toolchain in this
environment), so no firmware release build should be triggered yet.
This commit is contained in:
2026-07-27 22:09:33 +00:00
parent 9911151d8d
commit fcf3aec4c0
47 changed files with 1351 additions and 602 deletions
+51 -1
View File
@@ -45,6 +45,7 @@ from ..image_upload import decode_upload
from ..models import (
CalendarWidgetConfig,
Frame,
FrameButtonAction,
FrameCalendar,
FrameTaskList,
PhotoWidgetConfig,
@@ -57,7 +58,7 @@ from ..models import (
Widget,
)
from ..text_content import has_text, parse_rich_text
from ..widgets import WIDGET_TYPES
from ..widgets import WIDGET_TYPES, default_button_actions
from ..widgets import battery as battery_widget
from ..widgets import text as text_widget
from .common import (
@@ -214,6 +215,7 @@ def api_widget_create(
db.add(widget)
db.flush()
db.add(WIDGET_CONFIG_MODELS[body.widget_type](widget_id=widget.id))
db.add_all(default_button_actions(frame.id, widget.id, body.widget_type))
db.commit()
return _widget_dict(widget)
@@ -476,6 +478,54 @@ def api_widget_config_save(
return {"status": "saved"}
class WidgetButtonActionsRequest(BaseModel):
next_button_action: str
back_button_action: str
@router.post("/api/frames/{frame_id}/widgets/{widget_id}/button-actions")
def api_widget_button_actions(
body: WidgetButtonActionsRequest, frame_widget: tuple[Frame, Widget] = Depends(require_widget_control),
db: Session = Depends(get_db),
):
"""Sets this widget's NEXT/BACK button bindings (models.FrameButtonAction)
-- its own endpoint, not folded into api_widget_config_save, same
reasoning as api_widget_border above: these rows live in their own
table, not this widget's per-type config table. Replaces the old
frame-level "Button assignments" card (routers/api_frames.py's
api_buttons_get/api_buttons_save, now removed) -- each widget's own
dialog edits its own binding directly, prefilled at widget-creation
time with a sane default (see widgets.default_button_actions).
An empty string clears the binding for that button. Unlike
api_widget_config_save's silent-ignore-unrecognized-value posture,
a value outside this widget type's own ACTIONS is a 400 -- this
request body is specifically about button actions, so a bad value
here is a real client bug worth surfacing, not a stray field to
shrug off."""
frame, widget = frame_widget
valid_actions = set(WIDGET_TYPES[widget.widget_type].ACTIONS)
for value in (body.next_button_action, body.back_button_action):
if value != "" and value not in valid_actions:
raise HTTPException(400, f"{widget.widget_type} widgets don't support the {value!r} action")
with frame_locked(db, frame.id):
for button, value in (("next", body.next_button_action), ("back", body.back_button_action)):
existing = db.scalars(
select(FrameButtonAction).where(
FrameButtonAction.widget_id == widget.id, FrameButtonAction.button == button
)
).first()
if value == "":
if existing is not None:
db.delete(existing)
elif existing is not None:
existing.action = value
else:
db.add(FrameButtonAction(frame_id=frame.id, button=button, widget_id=widget.id, action=value))
db.commit()
return {"status": "saved"}
# --- Photos: queue/thumbnail/preview ------------------------------------
@router.get("/api/frames/{frame_id}/widgets/{widget_id}/queue")