Calendar and tasks pack the most body text at the smallest default sizes, so those two gear-icon dialogs get a "Text size" card (Normal/ Large/X-Large) alongside the existing Border card -- a new Widget-level font_scale column with its own POST .../font-scale endpoint, same Widget-property-not-config-field shape as border_style. Threaded through every classic (calendar_render.py) and modern (html_render.py/ calendar_html_render.py) size calc via one shared panel_style. scaled_size() so row heights/max_rows already derived from font size re-fit around the bigger text automatically.
209 lines
8.5 KiB
JavaScript
209 lines
8.5 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,
|
|
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.
|
|
}
|