Add battery widget (device's own last-reported level, no live upstream)
Shows Frame.battery_percent/battery_as_of, already set by every device wake-on-battery report, plus routers/common.py's existing battery_estimate_s time-remaining estimate -- nothing new to fetch or cache. Compact (icon + percent) or detailed (+ estimate, last report age) display mode. No button actions.
This commit is contained in:
+44
-13
@@ -2,9 +2,9 @@
|
||||
|
||||
A frame's panel isn't one fixed "mode" anymore -- it holds N independently
|
||||
placed/sized widgets (photos/calendar/whiteboard/tasks/static image/text/
|
||||
weather), 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).
|
||||
weather/battery), 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
|
||||
full-panel renderer; that column (and the other now-dead per-mode `Frame`
|
||||
columns it left behind -- `album_id`, `calendar_*`, `whiteboard_*`, etc.)
|
||||
@@ -21,15 +21,15 @@ a button press does.
|
||||
|
||||
- `Widget` (`server/app/models.py`): `id`, `frame_id`, `widget_type`
|
||||
(`"photos"` | `"calendar"` | `"whiteboard"` | `"tasks"` | `"static"` |
|
||||
`"text"` | `"weather"`), `x`/`y`/`w`/`h`
|
||||
`"text"` | `"weather"` | `"battery"`), `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`,
|
||||
`StaticWidgetConfig`, `TextWidgetConfig`, `WeatherWidgetConfig`, each
|
||||
keyed by `widget_id` with
|
||||
`StaticWidgetConfig`, `TextWidgetConfig`, `WeatherWidgetConfig`,
|
||||
`BatteryWidgetConfig`, each keyed by `widget_id` with
|
||||
`ondelete="CASCADE"` -- rather than one wide table with every type's
|
||||
mostly-irrelevant columns. `TextWidgetConfig.content` is parsed rich
|
||||
text (paragraphs of styled runs), never raw HTML -- see
|
||||
@@ -46,7 +46,12 @@ a button press does.
|
||||
`WeatherWidgetConfig` similarly lifts `CalendarWidgetConfig`'s embedded
|
||||
weather strip (still present and unchanged, `weather_*` columns) out
|
||||
into its own placeable widget type (migration 24) -- see "Weather
|
||||
widget" below.
|
||||
widget" below. `BatteryWidgetConfig` (migration 25) is the odd one out
|
||||
-- its actual content (`Frame.battery_percent`/`battery_as_of`) isn't
|
||||
in this table at all, already existing frame-level state set by
|
||||
`routers/device.py`'s `frame_battery` regardless of whether a battery
|
||||
widget is even placed; the config row only holds a display-mode
|
||||
setting (`"compact"` | `"detailed"`).
|
||||
- `FrameCalendar`/`FrameTaskList` are keyed by `widget_id` (not
|
||||
`frame_id`) since a frame can now have more than one independent
|
||||
calendar/tasks widget, each with its own included set. Identical
|
||||
@@ -73,8 +78,11 @@ 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, text 2x1,
|
||||
weather 2x2 (its hourly/daily strips need the room; current/multi_city
|
||||
modes would tolerate smaller, but every mode shares one footprint value).
|
||||
Enforced both client-side
|
||||
modes would tolerate smaller, but every mode shares one footprint value),
|
||||
battery 1x1 (just an icon + a percent, legible even at a single cell,
|
||||
like photos/static -- though see `MIN_FOOTPRINT`'s own comment in
|
||||
`grid.py` on a mobile-width gear-icon click-target gap at that size,
|
||||
already pre-existing for photos/static too). 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
|
||||
alone.
|
||||
@@ -83,7 +91,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`, `text.py`, `weather.py`), each exposing:
|
||||
`static_image.py`, `text.py`, `weather.py`, `battery.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
|
||||
@@ -94,9 +102,10 @@ alone.
|
||||
- `ACTIONS: dict[str, Callable]` -- named button actions this type
|
||||
supports (`"advance"`/`"back"` for photos and calendar, `"check_now"`
|
||||
for whiteboard and weather -- both throttled external fetches with a
|
||||
forced-refetch action). 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.
|
||||
forced-refetch action). Empty for tasks, static image, text, and
|
||||
battery -- nothing to advance/back/force for a passive checklist, a
|
||||
fixed uploaded image, a fixed block of authored text, or a number the
|
||||
device itself pushes on every wake.
|
||||
- `ACTION_LABELS: dict[str, str]` -- human labels for the button-
|
||||
assignment UI.
|
||||
|
||||
@@ -265,6 +274,28 @@ by actually running one through the real quantize pass during
|
||||
development). Used for every provider's rendering, not just when EC is
|
||||
selected as the provider.
|
||||
|
||||
## Battery widget
|
||||
|
||||
The simplest widget type (`models.BatteryWidgetConfig`, `app/widgets/
|
||||
battery.py`): shows this frame's own last-reported battery level. Unlike
|
||||
every other widget type, there's no live upstream to poll and nothing to
|
||||
cache -- the content is `Frame.battery_percent`/`battery_as_of`, set by
|
||||
`routers/device.py`'s `frame_battery` on every device wake-on-battery
|
||||
report, which already existed for the Device panel's own history chart
|
||||
regardless of whether a battery widget is placed anywhere. The widget's
|
||||
own config is just a display mode: `"compact"` (icon + percent) or
|
||||
`"detailed"` (default, adds `routers/common.py`'s existing
|
||||
`battery_estimate_s` time-remaining estimate and the last report's age).
|
||||
`render()` falls back to a "No reports yet" placeholder for a frame that
|
||||
has never reported (never run on battery, or not yet claimed by a
|
||||
device) rather than showing a stale or fabricated number. The battery
|
||||
icon fill color (red/yellow/green by percent) uses the same exact-panel-
|
||||
ink-RGB approach as the weather icons above and `manage_overlay.py`'s own
|
||||
battery glyph on the "scan to manage" overlay -- a separate, unrelated
|
||||
piece of code with its own fixed small size, not shared with this
|
||||
widget, but drawing from the same thresholds/colors so a battery glyph
|
||||
reads the same wherever one shows up on a panel.
|
||||
|
||||
## Known gaps (Phase 6, not yet done)
|
||||
|
||||
The original 8-phase rollout plan's last phase is still open:
|
||||
|
||||
Reference in New Issue
Block a user