Add a text widget (rich text: bold/italic/underline, per-run color/highlight)
A new self-contained widget type showing user-authored rich text -- no
live upstream to poll, like the static image widget, just word-wrapped
styled text instead of an uploaded image.
The dialog's contenteditable HTML is never stored or replayed as HTML:
app/text_content.py parses it server-side (on save) into a plain
paragraphs-of-styled-runs structure -- the actual sanitization
boundary, since raw HTML never round-trips back into any browser DOM
(the dialog rebuilds its editor from that same JSON via
createElement/textContent). app/widgets/text.py renders it with a
custom word-wrap/shrink-to-fit layout, using real vendored font weights
(app/fonts/NotoSans-{Regular,Bold,Italic,BoldItalic}.ttf, OFL-licensed
like the emoji fonts already there) rather than every other widget's
single ImageFont.load_default() -- the one widget type where that
distinction matters.
This commit is contained in:
+14
-9
@@ -1,8 +1,8 @@
|
||||
# Widget system
|
||||
|
||||
A frame's panel isn't one fixed "mode" anymore -- it holds N independently
|
||||
placed/sized widgets (photos/calendar/whiteboard/tasks/static image), like arranging
|
||||
icons on an Android home screen. A frame can hold several widgets of the
|
||||
placed/sized widgets (photos/calendar/whiteboard/tasks/static image/text), like
|
||||
arranging icons on an Android home screen. A frame can hold several widgets of the
|
||||
same type (e.g. two photo widgets pointed at different Immich albums side
|
||||
by side).
|
||||
This replaced an earlier design where `Frame.mode` picked exactly one
|
||||
@@ -20,16 +20,20 @@ a button press does.
|
||||
## Data model
|
||||
|
||||
- `Widget` (`server/app/models.py`): `id`, `frame_id`, `widget_type`
|
||||
(`"photos"` | `"calendar"` | `"whiteboard"` | `"tasks"` | `"static"`), `x`/`y`/`w`/`h`
|
||||
(`"photos"` | `"calendar"` | `"whiteboard"` | `"tasks"` | `"static"` | `"text"`), `x`/`y`/`w`/`h`
|
||||
(grid cells), `sort_order`. Widgets never overlap (enforced server-side in
|
||||
`routers/api_widgets.py`, re-validated regardless of what the client
|
||||
already checked) -- that's what keeps compositing simple: no z-order,
|
||||
no blending, just N independent regions pasted onto one shared canvas.
|
||||
- Per-type 1:1 extension tables -- `PhotoWidgetConfig`,
|
||||
`CalendarWidgetConfig`, `WhiteboardWidgetConfig`, `TaskWidgetConfig`,
|
||||
(plus `StaticWidgetConfig`) each keyed by `widget_id` with
|
||||
`StaticWidgetConfig`, `TextWidgetConfig`, each keyed by `widget_id` with
|
||||
`ondelete="CASCADE"` -- rather than one wide table with every type's
|
||||
mostly-irrelevant columns. `PhotoWidgetConfig`
|
||||
mostly-irrelevant columns. `TextWidgetConfig.content` is parsed rich
|
||||
text (paragraphs of styled runs), never raw HTML -- see
|
||||
`server/app/text_content.py`'s module docstring for why that parse
|
||||
step is the widget's actual stored-XSS sanitization boundary.
|
||||
`PhotoWidgetConfig`
|
||||
mirrors `app/photo_queue.py`'s attribute names exactly, so that module's
|
||||
advance/back/queue logic ports across widget instances unchanged.
|
||||
`TaskWidgetConfig` used to be a handful of `tasks_*` columns bolted onto
|
||||
@@ -61,7 +65,7 @@ orientation change rather than trying to remap coordinates.
|
||||
|
||||
Each widget type has a minimum grid footprint (`grid.MIN_FOOTPRINT`):
|
||||
photos 1x1, calendar 3x2 (a crammed calendar is illegible regardless of
|
||||
size-tier scaling), whiteboard 2x2, tasks 2x2, static image 1x1.
|
||||
size-tier scaling), whiteboard 2x2, tasks 2x2, static image 1x1, text 2x1.
|
||||
Enforced both client-side
|
||||
(UX, in the Layout tab's drag/resize canvas -- `static/frame_layout.js`)
|
||||
and server-side (`routers/api_widgets.py`) -- the client is never trusted
|
||||
@@ -71,7 +75,7 @@ alone.
|
||||
|
||||
`app/widgets/` is the render/action registry -- one module per
|
||||
`widget_type` (`photos.py`, `calendar.py`, `whiteboard.py`, `tasks.py`,
|
||||
`static_image.py`), each exposing:
|
||||
`static_image.py`, `text.py`), each exposing:
|
||||
|
||||
- `render(db, frame, widget, target_w, target_h, is_normal_wake) -> Image`:
|
||||
an unquantized RGB image exactly `target_w x target_h`, the widget's
|
||||
@@ -81,8 +85,9 @@ alone.
|
||||
bad moment doesn't blank the whole panel.
|
||||
- `ACTIONS: dict[str, Callable]` -- named button actions this type
|
||||
supports (`"advance"`/`"back"` for photos and calendar, `"check_now"`
|
||||
for whiteboard). Empty for tasks and static image -- nothing to
|
||||
advance/back/force for a passive checklist or a fixed uploaded image.
|
||||
for whiteboard). Empty for tasks, static image, and text -- nothing to
|
||||
advance/back/force for a passive checklist, a fixed uploaded image, or
|
||||
a fixed block of authored text.
|
||||
- `ACTION_LABELS: dict[str, str]` -- human labels for the button-
|
||||
assignment UI.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user