9911151d8d0003006c71d73af9c6cd9817efaaf6
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9911151d8d |
Add per-photo-widget lock (freezes current photo until unlocked)
A "Lock this photo" button in the photos widget's dialog toggles PhotoWidgetConfig.locked, which suppresses both the timer-elapsed auto-advance and the advance/back button actions until unlocked. The Layout tab canvas shows a lock badge on any locked photo widget's box. |
||
|
|
eb7127718b |
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. |
||
|
|
52ebafab78 |
Add standalone weather widget (current/hourly/daily/multi-city, pluggable providers)
New widget type with four display modes -- current conditions, an hourly forecast strip, a multi-day forecast, and several cities' current day side by side -- backed by a pluggable provider registry (app/weather/, mirroring the app/widgets/ dispatch pattern): Open-Meteo (worldwide) and NWS (US-only) both wired up now, Environment Canada documented as the next one to add given its more involved station/grid-lookup API. The calendar widget's existing embedded weather strip is untouched and still Open-Meteo-only; this lifts the same underlying icon-drawing primitives (now shared via app/weather_render.py, calendar_render.py still imports draw_weather_row unchanged) into a widget that can be placed and sized on its own. Icons are redrawn in the panel's actual ink colors (yellow sun/bolt, blue rain/snow) instead of flat black, and build_multi_city's icon/font sizing now scales with how many cities need to fit rather than the box's height alone -- both fixed after catching them via live browser verification, along with a mode-switch cache-shape crash and a mobile-width dialog overflow. New WeatherWidgetConfig table (migration 24), grid footprint, widget module, common.py fetch/cache helper, router endpoints (location set/ clear, city add/remove, preview), dialog template + JS, and full test coverage (providers, widget render, HTTP endpoints, migration replay). docs/widgets.md and CLAUDE.md's TODO updated accordingly. |
||
|
|
edbd90745b |
Add font family choice to the text widget, move toolbar below the editor
Six more vendored families alongside the existing Noto Sans (Inter, Source Sans 3, Noto Serif, Crimson Text, Arvo, IBM Plex Mono -- sans/ serif/slab/mono variety), all OFL-licensed with their own per-family license file in app/fonts/ since each has a different copyright holder. Static Regular/Bold/Italic/BoldItalic builds only -- variable-font-only families (Inter and Source Sans's current Google Fonts releases, plus Playfair Display/Lora/Merriweather) were skipped in favor of static builds from their own upstream repos, keeping every family's loading code uniform with what was already there. Considered but deliberately left out: Georgia -- a proprietary Microsoft core font, not freely redistributable, unlike everything else vendored here. Also moves the bold/italic/underline/color toolbar below the contenteditable box per request, and reorders the dialog's Settings card to a more natural family-then-size order. Fixes a latent migration bug this surfaced: migration 20 (static image widget) used Base.metadata.create_all, which creates every table declared in Base.metadata that's missing, not just its own new one -- harmless when nothing else pending, but once TextWidgetConfig existed it would silently pre-create text_widget_configs (in whatever shape models.py currently declares) before migration 21 got a turn, so migration 21's own CREATE TABLE (or a later ALTER TABLE adding font_family) would collide with a table create_all had already leaked into existence. Both migrations 20 and 21 now use raw, frozen CREATE TABLE SQL instead, matching migration 17's existing precedent for exactly this reason. |
||
|
|
3735c5bfa7 |
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.
|
||
|
|
35e80c6d1c |
Add a static image widget (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF upload)
A new widget type showing a single user-uploaded image with no live upstream to poll -- decoded once at upload time (PDF's first page via pypdfium2, BSD-3-Clause/Apache-2.0, no copyleft exposure) into plain RGB PNG bytes, then composed per a crop/stretch/letterbox display mode like the photos widget. |
||
|
|
4a2b1f3795 |
Let a tasks widget have a custom on-panel name
Adds TaskWidgetConfig.name (migration 19, plain column add) shown at the top of the widget on the actual panel instead of the hardcoded "Tasks" header -- e.g. "Chores" or "Mom's list". The only widget type with its own on-panel title at all, since it's the only one where "which list is this" isn't already obvious from a calendar/photo/ whiteboard's own content. Threaded through calendar_render's _draw_tasks/_build_tasks/ render_tasks/render_tasks_preview_png as a `title` param (default "Tasks", truncated to fit -- a long custom name shouldn't be able to overflow the widget's box), the config-save endpoint (tasks_name, truncated server-side to a sane header length rather than rejected), and a new "Settings" section in the tasks dialog. Verified live in the browser: the name actually renders at the top of the real composited panel (not just the dialog preview, which stays gated on having a configured source), and persists correctly on both desktop and mobile. Full suite (196 tests) passes. |
||
|
|
14c47aa2a0 |
Let a tasks widget merge multiple task lists, checkbox+color like calendar
Tasks widgets could only ever point at one CalDAV task list (a radio- button picker, owner-only). Now they merge any number of included task lists across every linked user, same checkbox-inclusion + optional pinned-color shape a calendar widget already has for its calendars -- FrameTaskList mirrors FrameCalendar exactly, down to the same owner- adds/anyone-mutes permission split (api_widget_task_list_select/ api_widget_task_list_color). Reused calendar_render._event_colors/ _draw_color_bar as-is for the per-task color bar -- a task dict's owner_display_name/color_index is exactly that function's single- source fallback shape. Also added an opt-in "show tasks completed in the last 24 hours" toggle (TaskWidgetConfig.show_completed): caldav_client.fetch_tasks now accepts a completed_since cutoff and returns completed VTODOs (with their completion time) instead of silently dropping them, and _draw_tasks gives a completed task a filled checkbox + muted text instead of the normal empty-box/due-date row. Migration 18 splits the single-source TaskWidgetConfig columns (added by 17, splitting tasks out of the calendar widget in the first place) into frame_task_lists, carrying forward each widget's existing single source as its first included list -- same shape migration 9 used carrying forward frame_calendars' old single opt-in. Verified live in the browser (desktop + mobile): the new "Included task lists" + "Recently completed" dialog sections, the show_completed toggle actually persisting through a real HTTP round-trip, and no regression in the calendar widget's own "Included calendars" dialog. Full suite (192 tests, including new merge_tasks/config_save/migration coverage) passes. |
||
|
|
b5c52004c8 |
Split the tasks feature out of the calendar widget into its own widget type
Task lists used to be a week-view-only sub-feature bolted onto calendar widgets (CalendarWidgetConfig.tasks_*), so a task list could only exist tied to a calendar's view and only inside its footprint. Tasks are now a standalone widget type (TaskWidgetConfig, app/widgets/tasks.py) that can be placed and sized independently, same as photos/calendar/ whiteboard -- no separate "enabled" flag either, since being on the grid at all is the on/off switch, matching every other widget type. Migration 17 creates task_widget_configs, extracts any existing calendar widget's configured task source into a new sibling tasks widget (auto-placed in open grid space, source dropped+logged if truly none is left), then drops calendar_widget_configs' now-dead tasks_* columns in the same migration -- this project's usual same-migration- drop convention. Also handles the rarer case of a database jumping straight from before the widget system existed to after this split in one boot, via the legacy Frame.calendar_tasks_* columns. Verified live in the browser at desktop and mobile widths: adding a Tasks widget, its own dialog (task-list source picker + preview), and confirming the calendar widget's dialog no longer mentions tasks at all. Full test suite (180 tests, including new coverage for the widget render/actions, the migration's data-extraction path, and the permission-boundary shape for tasks-source) passes. |
||
|
|
99069ba5fe |
Widget system Phase 3: calendar widgets become size-aware
calendar_render.py's _build_* functions now take a real target box and pick font sizes/margins from three discrete size tiers (nearest pixel-area fit) instead of always laying out at full panel size and resizing after the fact -- a calendar widget placed smaller than the full panel gets an actually-legible layout instead of shrunk text. Month view falls back to agenda below the smallest tier, where 7 columns can no longer stay readable. The old photo-inlay split (inlay_region/_content_region/_paste_inlay) is deleted along with it -- arbitrary widget placement already subsumes what a fixed half-panel split did, and every call site has passed photo_inlay=None since the Phase 2 cutover. Also adds HTTP-level test coverage for GET .../preview/calendar, which had none before this -- it's what caught a stale photo_inlay kwarg left over from the _build signature change that would have TypeError'd on every request. |
||
|
|
37bd657299 |
Widget system Phase 2: full cutover to widget-based rendering
device.py's mode-keyed dispatch is replaced by a real compositor: load a frame's widgets, compute pixel rects via app/grid.py, render each through its widget module, and composite with render_panel. Physical NEXT/BACK buttons now execute each frame's assigned FrameButtonAction rows instead of one hardcoded per-mode action. api_frames.py, manage.py, and common.py's build_manage_content are repointed to read/write the frame's widget config rows instead of the old Frame columns, and every settings page (Photos/Calendar/ Whiteboard tabs) now pre-fills its form from the same widget config the write endpoints actually save to -- previously the read and write sides would have silently diverged. The old mode selector and photo-inlay checkbox are removed along with their now-inert wiring; arbitrary widget placement subsumes what the fixed inlay split did. Ships together with Phase 1 (per-type render/action modules) since splitting the read/write cutover across deploys would have left settings changes with no visible effect. |
||
|
|
f48daa71c8 |
Widget system Phase 1: per-type render/action modules
New app/widgets/ package (photos.py, calendar.py, whiteboard.py, plus the WIDGET_TYPES registry) -- the widget-system analogue of routers/device.py's old RENDERERS/ADVANCE_RENDERERS/BACK_RENDERERS, generalized from "one mode owns the whole panel" to "each widget renders into its own region and responds to named button actions." Each module exposes render(db, frame, widget, target_w, target_h) -> Image.Image (never raises -- a widget's own fetch hiccup falls back to a small placeholder rather than taking the whole panel's render down) and an ACTIONS registry for NEXT/BACK button assignment. Supporting changes needed to give the widget modules something to call, all mechanical/behavior-preserving for every existing caller: - image_pipeline.py: render_panel(regions, ...) generalizes render_frame's tail (paste, enhance once, overlay once, quantize once, pack once) from one photo to N regions -- not a restructuring, since calendar mode's photo-inlay feature already pastes a second composed image onto the canvas before that single shared pipeline runs. - photo_queue.py: advance_forced/back_forced/remove_from_rotation/ get_current take an explicit `frame` param now that `cfg` won't always be the Frame itself once photo-queue state moves to PhotoWidgetConfig -- caught a real latent bug while doing this: get_current was reading refresh_interval_s off `cfg`, but that's a frame-level wake-cadence setting, not something that becomes per-widget, so it now reads that off `frame` explicitly instead. - routers/common.py: list_assets/fetch_source_and_faces take album_id/ display_mode directly instead of a whole Frame (both only ever read that one attribute off it); new get_or_refresh_*_for_widget siblings of the existing calendar/weather/tasks/whiteboard cache helpers, read/ writing the new per-widget config tables -- the Frame-scoped originals are untouched and still what routers/device.py's actual dispatch calls until the Phase 2 cutover. 26 new tests (95 total): render_panel size/placement/orientation coverage, and per-widget-type render/action tests (unconfigured -> placeholder, a fetch failure -> placeholder not a crash, actions mutate the right state). Full suite passes; diff-reviewed to confirm device.py's actual RENDERERS dispatch and the old Frame-scoped get_or_refresh_* bodies are unchanged, so this is safe to deploy on its own despite being step 1 of a two-step cutover (see the project plan on why the *next* step, not this one, has to ship atomically). |