Add calendar frame mode + server-side manage overlay (server)
Build and push server image / build-and-push (push) Successful in 42s

calendar_feed.py/calendar_render.py: fetch/merge per-user ICS feeds,
render agenda/week/month views. manage_overlay.py: composites the
manage-button overlay server-side (QR, battery, location/date,
share-QR, face labels), reused by every render mode. device.py/common.py
wire both together: mode dispatch for /frame/image+advance+back, and
the &manage=1 flag. Plus UI (frame_config.html Calendar card, settings
calendar URL field) and the icalendar/recurring-ical-events deps.
This commit is contained in:
2026-07-22 19:06:49 -04:00
parent 15e37c77cd
commit c007acde75
17 changed files with 1317 additions and 265 deletions
+64
View File
@@ -6,6 +6,7 @@
async function saveConfig() {
const minutes = parseInt(document.getElementById('refresh_interval_minutes').value, 10) || 60;
const body = new URLSearchParams({
mode: document.getElementById('frame_mode').value,
name: document.getElementById('frame_name').value || '',
order: document.getElementById('order').value,
orientation: document.getElementById('orientation').value,
@@ -36,6 +37,69 @@ document.getElementById('config-form').addEventListener('submit', async (e) => {
}
});
// ---- Calendar card: mode/view toggling, its own save, self opt-in, preview ----
const calendarCard = document.getElementById('calendar-card');
if (calendarCard) {
document.getElementById('frame_mode').addEventListener('change', () => {
calendarCard.style.display = document.getElementById('frame_mode').value === 'calendar' ? 'block' : 'none';
});
const inlayRow = document.getElementById('calendar-inlay-row');
const inlayHint = document.getElementById('calendar-inlay-hint');
document.getElementById('calendar_view').addEventListener('change', () => {
const isAgenda = document.getElementById('calendar_view').value === 'agenda';
inlayRow.style.display = isAgenda ? 'flex' : 'none';
inlayHint.style.display = isAgenda ? 'block' : 'none';
});
document.getElementById('calendar-config-form').addEventListener('submit', async (e) => {
e.preventDefault();
const body = new URLSearchParams({
calendar_view: document.getElementById('calendar_view').value,
calendar_photo_inlay: String(document.getElementById('calendar_photo_inlay').checked),
});
try {
const resp = await fetch(`${window.FRAME_API}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Saved.');
loadCalendarPreview();
} catch (e) {
showStatus(false, e.message);
}
});
// 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) => {
el.addEventListener('change', async () => {
try {
const resp = await fetch(`${window.FRAME_API}/calendar-included`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 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.');
} catch (e) {
el.checked = !el.checked;
showStatus(false, e.message);
}
});
});
function loadCalendarPreview() {
document.getElementById('calendar-preview').src = `${window.FRAME_API}/preview/calendar?_=${Date.now()}`;
}
document.getElementById('calendar-preview-refresh').addEventListener('click', loadCalendarPreview);
loadCalendarPreview();
}
async function takeControl() {
try {
const resp = await fetch(`${window.FRAME_API}/take-control`, { method: 'POST' });