Week view flexibility: configurable day count, layout, and a CalDAV task list
Build and push server image / build-and-push (push) Successful in 52s

- Day count (2-10, was fixed at 7) -- 5 days trims the weekend clutter
  without losing the grid format.
- Layout choice: days side by side (original behavior) or stacked
  vertically as full agenda-style sections (reuses _draw_agenda_day,
  same approach _build_today_tomorrow already used for a fixed 2 days).
- Optional task list (CalDAV VTODO collections only -- a plain ICS
  subscription doesn't meaningfully have one) that takes the space of
  one day slot instead of adding an extra one. Same owner-controls-
  their-own-data permission split as calendar sources: only the
  calendar's owner can point a frame's task list at it, but anyone
  linked to the frame can clear it.

Browse-offset paging now moves by N days (was hardcoded to weeks),
identical to the old behavior when days=7. Changing the day count
resets the browse offset, same reasoning as changing views already did.
This commit is contained in:
2026-07-23 07:58:13 -04:00
parent 01b9e9f1d0
commit ce8525bee8
10 changed files with 436 additions and 25 deletions
+52
View File
@@ -44,6 +44,7 @@ from .common import (
calendar_sources_for_frame,
fetch_source_and_faces,
get_or_refresh_calendar_events,
get_or_refresh_tasks,
get_or_refresh_weather,
immich_client_for,
immich_creds,
@@ -101,8 +102,11 @@ def api_config_save(
calendar_view: str | None = Form(None),
calendar_photo_inlay: bool | None = Form(None),
calendar_week_start: int | None = Form(None),
calendar_week_days: int | None = Form(None),
calendar_week_layout: str | None = Form(None),
calendar_weather_enabled: bool | None = Form(None),
calendar_weather_units: str | None = Form(None),
calendar_tasks_enabled: bool | None = Form(None),
frame: Frame = Depends(require_frame_control),
db: Session = Depends(get_db),
):
@@ -180,6 +184,16 @@ def api_config_save(
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))
if calendar_week_days is not None:
new_days = max(2, min(10, calendar_week_days))
if new_days != cfg.calendar_week_days:
# A stale offset counts a different-sized page under the
# old day count -- same reasoning as calendar_view's own
# reset below.
cfg.calendar_browse_offset = 0
cfg.calendar_week_days = new_days
if calendar_week_layout is not None:
cfg.calendar_week_layout = calendar_week_layout if calendar_week_layout in ("horizontal", "vertical") else "horizontal"
if calendar_weather_enabled is not None:
cfg.calendar_weather_enabled = calendar_weather_enabled
if calendar_weather_units is not None and calendar_weather_units in ("fahrenheit", "celsius"):
@@ -188,6 +202,8 @@ def api_config_save(
# rather than showing stale numbers under a new unit label.
cfg.calendar_weather_checked_at = 0.0
cfg.calendar_weather_units = calendar_weather_units
if calendar_tasks_enabled is not None:
cfg.calendar_tasks_enabled = calendar_tasks_enabled
cfg.stats_config_saves += 1
return {"status": "saved"}
@@ -548,15 +564,51 @@ def api_preview_calendar(frame: Frame = Depends(require_frame_view), db: Session
photo_inlay = _calendar_photo_inlay(frame, db)
view = frame.calendar_view if frame.calendar_view in calendar_render.CALENDAR_VIEWS else "agenda"
weather_cities = get_or_refresh_weather(db, frame)
tasks = get_or_refresh_tasks(db, frame) if (view == "week" and frame.calendar_tasks_enabled) else None
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,
weather_cities=weather_cities, weather_units=frame.calendar_weather_units,
week_days=frame.calendar_week_days, week_layout=frame.calendar_week_layout, tasks=tasks,
)
return Response(content=png, media_type="image/png")
class TasksSourceRequest(BaseModel):
calendar_key: str | None # None clears the source
@router.post("/api/frames/{frame_id}/tasks-source")
def api_tasks_source(
body: TasksSourceRequest,
request: Request,
frame: Frame = Depends(require_frame_view),
db: Session = Depends(get_db),
):
"""Points this frame's week-view task list at one of the calling
user's own CalDAV calendars -- same owner-controls-their-own-data
permission split as calendar-select's included=True, since this is
volunteering personal calendar data, not a frame-wide display
setting a controller should get to pick on someone else's behalf.
None clears the source; clearing (unlike setting) isn't
ownership-gated -- like muting a shared calendar, anyone linked to
the frame can turn off a task list they'd rather not see, but only
its owner can point the frame at one of their calendars to begin
with."""
user = require_user_api(request, db)
with frame_locked(db, frame.id) as cfg:
if body.calendar_key is None:
cfg.calendar_tasks_user_id = None
cfg.calendar_tasks_calendar_key = None
cfg.calendar_tasks_cached = None
else:
cfg.calendar_tasks_user_id = user.id
cfg.calendar_tasks_calendar_key = body.calendar_key
cfg.calendar_tasks_checked_at = 0.0 # pick up the change promptly
return {"status": "saved", "calendar_key": body.calendar_key}
class WeatherCityAddRequest(BaseModel):
name: str