Files
espresso_frame/server/app/static/widget_dialog_calendar.js
T
tfaour b15747a604
Build and push server image / test (push) Has been cancelled
Build and push server image / build-and-push (push) Has been cancelled
Build and push server image / deploy (push) Has been cancelled
Add per-widget border option (style, thickness, palette color)
A Widget-level property (border_style/border_thickness/border_color_index),
not a per-type config field, since every widget type can have one -- drawn
once centrally in device.py's _render_widgets before compositing, using
an exact panel palette color so it never dithers. Styles: solid, dashed,
dotted, and a fancy double-line picture-frame-mat look. Configurable from
a shared "Border" card in every widget's gear-icon dialog.
2026-07-27 19:51:04 +00:00

206 lines
8.4 KiB
JavaScript

// 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 <dialog>, 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 = '<li class="sub" id="weather-city-empty">No cities added yet.</li>';
}
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,
});
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();
}
function closeCalendarDialog() {
// Nothing to tear down -- no poll interval, unlike the photos dialog.
}