// Calendar widget dialog: view/week-start settings, per-user opt-in, // weather, and the rendered preview. Not a page-load script -- // frame_layout.js fetches this widget's dialog HTML fragment, injects // it into the shared , points window.FRAME_API at this specific // widget (/api/frames/{id}/widgets/{widget_id}), then calls // initCalendarDialog(). Checkboxes are always sent explicitly as // "true"/"false". // Week-view-only settings (days/layout/start-offset) only matter when // View is actually "Week"; "Week starts on" also matters for Month, so // it gets its own, slightly looser condition. The start-offset row is // further gated on the day count -- it's meaningless at the default 7 // days, where "Week starts on" governs instead (see // calendar_render.py's _build_week). function updateCalendarFieldVisibility() { const view = document.getElementById('calendar_view').value; const days = Number(document.getElementById('calendar_week_days').value); const isWeek = view === 'week'; document.getElementById('calendar-week-start-row').style.display = (view === 'week' || view === 'month') ? '' : 'none'; document.getElementById('calendar-week-days-row').style.display = isWeek ? '' : 'none'; document.getElementById('calendar-week-layout-row').style.display = isWeek ? '' : 'none'; document.getElementById('calendar-week-offset-row').style.display = (isWeek && days !== 7) ? '' : 'none'; } function addWeatherCityRow(label) { const list = document.getElementById('weather-city-list'); const empty = document.getElementById('weather-city-empty'); if (empty) empty.remove(); const li = document.createElement('li'); li.className = 'checkbox-row'; li.style.cssText = 'justify-content: space-between; margin-top: 6px;'; const span = document.createElement('span'); span.textContent = label; const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn-inline secondary weather-city-remove'; btn.dataset.label = label; btn.textContent = 'Remove'; btn.addEventListener('click', removeWeatherCity); li.appendChild(span); li.appendChild(btn); list.appendChild(li); } async function removeWeatherCity(e) { const label = e.target.dataset.label; try { const resp = await fetch(`${window.FRAME_API}/weather-cities/remove`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ label }), }); if (!resp.ok) throw new Error(await apiError(resp)); e.target.closest('li').remove(); const list = document.getElementById('weather-city-list'); if (!list.querySelector('li')) { list.innerHTML = '
  • No cities added yet.
  • '; } showStatus(true, `${label} removed.`); loadCalendarPreview(); } catch (e) { showStatus(false, e.message); } } function loadCalendarPreview() { document.getElementById('calendar-preview').src = `${window.FRAME_API}/preview/calendar?_=${Date.now()}`; } function initCalendarDialog() { document.getElementById('calendar_view').addEventListener('change', updateCalendarFieldVisibility); document.getElementById('calendar_week_days').addEventListener('input', updateCalendarFieldVisibility); updateCalendarFieldVisibility(); document.getElementById('calendar-config-form').addEventListener('submit', async (e) => { e.preventDefault(); const body = new URLSearchParams({ calendar_view: document.getElementById('calendar_view').value, calendar_week_start: document.getElementById('calendar_week_start').value, calendar_week_days: document.getElementById('calendar_week_days').value, calendar_week_layout: document.getElementById('calendar_week_layout').value, calendar_week_start_offset: document.getElementById('calendar_week_start_offset').value, calendar_render_style: document.getElementById('calendar_render_style').value, }); 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 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_widget_calendar_select), not a widget-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-select`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, 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 ? 'Calendar included on this widget.' : 'Calendar removed from this widget.'); } catch (e) { el.checked = !el.checked; showStatus(false, e.message); } }); }); // Per-calendar color pin -- owner-only (the server enforces it; these // buttons only ever render for the viewer's own calendars anyway). // Clicking the currently-selected swatch again has no special // "toggle off" behavior -- use the explicit Auto button. document.querySelectorAll('.calendar-color-picker').forEach((picker) => { const key = picker.dataset.key; picker.querySelectorAll('.color-swatch').forEach((btn) => { btn.addEventListener('click', async () => { const colorIndex = btn.dataset.index === '' ? null : Number(btn.dataset.index); try { const resp = await fetch(`${window.FRAME_API}/calendar-color`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ calendar_key: key, color_index: colorIndex }), }); if (!resp.ok) throw new Error(await apiError(resp)); picker.querySelectorAll('.color-swatch').forEach((el) => el.classList.remove('selected')); btn.classList.add('selected'); showStatus(true, 'Color saved.'); loadCalendarPreview(); } catch (e) { showStatus(false, e.message); } }); }); }); document.getElementById('weather-config-form').addEventListener('submit', async (e) => { e.preventDefault(); const body = new URLSearchParams({ calendar_weather_enabled: String(document.getElementById('weather_enabled').checked), calendar_weather_units: document.getElementById('weather_units').value, }); 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); } }); document.querySelectorAll('.weather-city-remove').forEach((el) => el.addEventListener('click', removeWeatherCity)); document.getElementById('weather-city-add').addEventListener('click', async () => { const input = document.getElementById('weather-city-input'); const name = input.value.trim(); if (!name) return; try { const resp = await fetch(`${window.FRAME_API}/weather-cities/add`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!resp.ok) throw new Error(await apiError(resp)); const data = await resp.json(); addWeatherCityRow(data.city.label); input.value = ''; showStatus(true, `Added ${data.city.label}.`); loadCalendarPreview(); } catch (e) { showStatus(false, e.message); } }); document.getElementById('calendar-preview-refresh').addEventListener('click', loadCalendarPreview); loadCalendarPreview(); initBorderFields(); initFontScaleFields(); initButtonActionFields(); } function closeCalendarDialog() { // Nothing to tear down -- no poll interval, unlike the photos dialog. }