Move mode picker below device status bar; fix invisible calendar lines; add CalDAV calendar support
Build and push server image / build-and-push (push) Successful in 50s

Calendar rule/grid lines were light gray, which dithers away to
near-invisible on the 6-color e-ink palette -- now black.

CalDAV accounts (Nextcloud, Fastmail, iCloud, ...) can now be linked
alongside the existing single ICS subscription, since one account can
expose several calendars. A frame's Calendar tab now lists calendars
per person rather than one opt-in per person: your own row shows every
calendar you have available with a full add/remove toggle, while other
linked users' rows show only calendars they've included, toggleable
off (mute) but not on -- only a calendar's owner can add it to a
shared frame. FrameCalendar replaces the old single-boolean
UserFrame.calendar_included; existing opt-ins are migrated forward.
This commit is contained in:
2026-07-22 22:01:05 -04:00
parent 95d69a5512
commit acdb929a99
21 changed files with 525 additions and 109 deletions
+15 -7
View File
@@ -24,19 +24,27 @@ document.getElementById('calendar-config-form').addEventListener('submit', async
}
});
// Each person's own opt-in -- auto-saves on toggle, not batched into the
// form above, since it's the toggling user's own preference (see
// api_frames.py's /calendar-included), not a frame-wide setting.
document.querySelectorAll('.calendar-self-toggle').forEach((el) => {
// Each calendar's own include/mute toggle -- auto-saves on change, not
// batched into the form above, since it's a data-sharing choice (see
// api_frames.py's /calendar-select), not a frame-wide setting. Works the
// same element for your own calendars (full add/remove) and other
// people's (mute only) -- the server enforces which direction is allowed
// and this just reverts the checkbox with an error message if rejected.
document.querySelectorAll('.calendar-toggle').forEach((el) => {
el.addEventListener('change', async () => {
try {
const resp = await fetch(`${window.FRAME_API}/calendar-included`, {
const resp = await fetch(`${window.FRAME_API}/calendar-select`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ included: el.checked }),
body: JSON.stringify({
user_id: Number(el.dataset.userId),
calendar_key: el.dataset.key,
calendar_label: el.dataset.label,
included: el.checked,
}),
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, el.checked ? 'Your calendar is included on this frame.' : 'Your calendar removed from this frame.');
showStatus(true, el.checked ? 'Calendar included on this frame.' : 'Calendar removed from this frame.');
} catch (e) {
el.checked = !el.checked;
showStatus(false, e.message);
+32
View File
@@ -0,0 +1,32 @@
// Settings page: "Discover calendars" against the CalDAV account already
// saved on this form (same idiom as frame_photos.js's Load Albums using
// the frame's already-saved Immich creds) -- so this only works after
// the CalDAV URL/username/password have been saved once.
const discoverBtn = document.getElementById('caldav-discover');
if (discoverBtn) {
discoverBtn.addEventListener('click', async () => {
const list = document.getElementById('caldav-calendar-list');
discoverBtn.disabled = true;
try {
const resp = await fetch('/api/settings/caldav-discover', { method: 'POST' });
if (!resp.ok) throw new Error(await apiError(resp));
const calendars = await resp.json();
list.innerHTML = '';
if (calendars.length === 0) {
list.innerHTML = '<li>No calendars found in this account.</li>';
} else {
for (const c of calendars) {
const li = document.createElement('li');
li.textContent = c.display_name;
list.appendChild(li);
}
}
showStatus(true, `Found ${calendars.length} calendar${calendars.length === 1 ? '' : 's'}.`);
} catch (e) {
showStatus(false, e.message);
} finally {
discoverBtn.disabled = false;
}
});
}
+8
View File
@@ -261,6 +261,11 @@ input:focus, select:focus {
.checkbox-row input { width: auto; margin-top: 0; }
.checkbox-row label { margin-top: 0; font-weight: normal; }
.calendar-user-list { list-style: none; margin: 12px 0 0; padding: 0; }
.calendar-user-list > li { margin-top: 14px; }
.calendar-user-list > li:first-child { margin-top: 0; }
.calendar-user-name { margin: 0; font-size: 13px; font-weight: 600; color: var(--text); }
button {
margin-top: 20px;
padding: 10px 16px;
@@ -540,6 +545,9 @@ code {
.device-status-row { column-gap: 16px; }
}
.mode-picker-row { display: flex; align-items: center; gap: 8px; margin-bottom: 18px; }
.mode-picker-label { font-size: 13px; font-weight: 600; color: var(--text-muted); }
/* Mobile: sidebar off-canvas, hamburger in a slim top bar. */
.mobile-bar { display: none; }
.sidebar-backdrop { display: none; }