Calendar mode polish batch + Today & Tomorrow view

Responds to post-launch feedback on calendar mode: configurable
week-start day for week/month views, crisper non-antialiased text
(threshold-masked instead of drawn straight, so Floyd-Steinberg
dithering doesn't speckle glyph edges), a color-coded/proportionally
filled battery icon on the manage overlay, word-wrapped placeholder
text so "Calendar isn't set up yet" no longer clips in portrait, photo
inlay support extended from agenda-only to every view, and a fix so
manage-overlay face labels reposition correctly when a photo inlay is
active (they previously assumed the photo filled the whole canvas).

Also adds a fourth calendar view, "Today & Tomorrow" -- a two-day
agenda that reuses the same per-day row-layout helper the single-day
agenda view already has.
This commit is contained in:
2026-07-22 21:20:44 -04:00
parent 716776c3f2
commit aa194be09a
9 changed files with 326 additions and 115 deletions
+7 -3
View File
@@ -99,6 +99,7 @@ def api_config_save(
mode: str | None = Form(None),
calendar_view: str | None = Form(None),
calendar_photo_inlay: bool | None = Form(None),
calendar_week_start: int | None = Form(None),
frame: Frame = Depends(require_frame_control),
db: Session = Depends(get_db),
):
@@ -174,6 +175,8 @@ def api_config_save(
cfg.calendar_view = new_view
if calendar_photo_inlay is not None:
cfg.calendar_photo_inlay = calendar_photo_inlay
if calendar_week_start is not None:
cfg.calendar_week_start = max(0, min(6, calendar_week_start))
cfg.stats_config_saves += 1
return {"status": "saved"}
@@ -436,13 +439,13 @@ def api_calendar_included(
def _calendar_photo_inlay(frame: Frame, db: Session):
"""The agenda view's optional photo-inlay source image, or None if
inlay is off, not agenda view, or the frame's photos-mode album isn't
"""The photo-inlay's source image (any view now, not just agenda), or
None if inlay is off or the frame's photos-mode album isn't
configured. Shared shape between the live render (routers/device.py's
_render_calendar_mode) and this preview endpoint; small enough that
duplicating rather than factoring out is fine, since the two call
sites differ slightly in error handling."""
if not (frame.calendar_view == "agenda" and frame.calendar_photo_inlay):
if not frame.calendar_photo_inlay:
return None
url, key = immich_creds(frame)
if not (url and key and frame.album_id):
@@ -477,6 +480,7 @@ def api_preview_calendar(frame: Frame = Depends(require_frame_view), db: Session
png = calendar_render.render_calendar_preview_png(
events, view=view, browse_offset=frame.calendar_browse_offset, orientation=frame.orientation,
palette_rgb=frame.palette_rgb, timezone=frame.timezone, photo_inlay=photo_inlay, fetch_summary=summary,
week_start=frame.calendar_week_start,
)
return Response(content=png, media_type="image/png")
+24 -7
View File
@@ -244,15 +244,29 @@ def _format_taken_at(exif: dict) -> str | None:
def _manage_content_asset_id(frame: Frame) -> str | None:
"""Whether frame.current_asset_id refers to a photo actually visible
right now, for whichever mode is active -- always true in photos
mode; only true in calendar mode when the agenda view's photo inlay
is on (otherwise current_asset_id could be stale, left over from
whenever photos mode last ran, and showing its location/date/share
info on a manage overlay over a view with no visible photo at all
would be actively misleading, not just unhelpful)."""
relevant = frame.mode != "calendar" or (frame.calendar_view == "agenda" and frame.calendar_photo_inlay)
mode; only true in calendar mode when that view's photo inlay is on
(otherwise current_asset_id could be stale, left over from whenever
photos mode last ran, and showing its location/date/share info on a
manage overlay over a view with no visible photo at all would be
actively misleading, not just unhelpful)."""
relevant = frame.mode != "calendar" or frame.calendar_photo_inlay
return frame.current_asset_id if relevant and frame.current_asset_id else None
def _manage_content_region(frame: Frame) -> tuple[int, int, int, int] | None:
"""Where the photo behind _manage_content_asset_id actually landed in
the logical canvas -- None (the whole canvas) in photos mode, or
calendar_render.inlay_region(...) when a calendar view's photo inlay
is what's showing. Needed so face labels (and, if ever added, other
photo-relative overlay positioning) land on the actual inlaid photo
instead of where a full-panel photo would have been."""
if frame.mode == "calendar" and frame.calendar_photo_inlay:
from ..calendar_render import inlay_region
return inlay_region(frame.orientation)
return None
def build_manage_content(db: Session, frame: Frame, request) -> dict:
"""Gathers everything manage_overlay.compose() needs -- what used to
be two separate device-facing endpoints (/frame/photo-info,
@@ -293,7 +307,10 @@ def build_manage_content(db: Session, frame: Frame, request) -> dict:
preview_bytes = client.download_asset_preview(asset_id)
from ..face_labels import compute_face_labels
content["face_labels"] = compute_face_labels(preview_bytes, faces, frame.display_mode, frame.orientation)
content["face_labels"] = compute_face_labels(
preview_bytes, faces, frame.display_mode, frame.orientation,
region=_manage_content_region(frame),
)
except httpx.HTTPError as e:
logger.warning("Could not download asset %s for face-label mapping: %s", asset_id, e)
+3 -2
View File
@@ -144,7 +144,8 @@ def _render_calendar_mode(db: Session, frame: Frame, request: Request, manage: d
locked.calendar_browse_offset = 0
browse_offset = locked.calendar_browse_offset
view = locked.calendar_view if locked.calendar_view in calendar_render.CALENDAR_VIEWS else "agenda"
inlay_wanted = locked.calendar_photo_inlay and view == "agenda"
week_start = locked.calendar_week_start
inlay_wanted = locked.calendar_photo_inlay
events, summary = get_or_refresh_calendar_events(db, frame)
@@ -169,7 +170,7 @@ def _render_calendar_mode(db: Session, frame: Frame, request: Request, manage: d
return calendar_render.render_calendar(
events, view=view, browse_offset=browse_offset, orientation=frame.orientation,
palette_rgb=frame.palette_rgb, timezone=frame.timezone,
photo_inlay=photo_inlay, fetch_summary=summary, manage=manage,
photo_inlay=photo_inlay, fetch_summary=summary, manage=manage, week_start=week_start,
)