Files
espresso_frame/server/app/static/common.js
T
tfaour eb7127718b
Build and push server image / test (push) Successful in 30s
Build and push server image / build-and-push (push) Successful in 2m4s
Build and push server image / deploy (push) Successful in 50s
Add battery widget (device's own last-reported level, no live upstream)
Shows Frame.battery_percent/battery_as_of, already set by every device
wake-on-battery report, plus routers/common.py's existing
battery_estimate_s time-remaining estimate -- nothing new to fetch or
cache. Compact (icon + percent) or detailed (+ estimate, last report
age) display mode. No button actions.
2026-07-27 19:02:21 +00:00

111 lines
4.7 KiB
JavaScript

// Shared plumbing for every page: CSRF-injecting fetch, theme toggle,
// sidebar toggle (mobile), and small formatting helpers. No framework,
// no build step -- plain scripts, load order handled by <script> tags.
// Session-cookie auth needs CSRF proof on mutating requests. Wrapping
// fetch once means no call site has to remember the header. The token
// rides a <meta> tag emitted only for session-authed pages.
(function () {
var meta = document.querySelector('meta[name="csrf-token"]');
if (!meta || !meta.content) return;
var CSRF = meta.content;
var origFetch = window.fetch;
window.fetch = function (input, init) {
init = init || {};
var method = (init.method || (input && input.method) || 'GET').toUpperCase();
var url = typeof input === 'string' ? input : (input && input.url) || '';
var sameOrigin = url.indexOf('://') === -1 || url.indexOf(location.origin) === 0;
if (sameOrigin && method !== 'GET' && method !== 'HEAD') {
init.headers = new Headers(init.headers || (input && input.headers) || {});
init.headers.set('X-CSRF-Token', CSRF);
}
return origFetch.call(this, input, init);
};
})();
// Theme toggle: explicit choice wins over the OS preference and is
// remembered; with no explicit choice, CSS falls back to
// prefers-color-scheme on its own. (The pre-paint snippet in the page
// <head> applies the stored theme before first render.)
(function () {
var btn = document.getElementById('theme-toggle');
if (!btn) return;
function currentTheme() {
var stored = null;
try { stored = localStorage.getItem('theme'); } catch (e) { /* ignore */ }
if (stored === 'light' || stored === 'dark') return stored;
return (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) ? 'dark' : 'light';
}
btn.addEventListener('click', function () {
var theme = currentTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('theme', theme); } catch (e) { /* ignore */ }
document.dispatchEvent(new CustomEvent('themechange', { detail: { theme: theme } }));
});
})();
// Mobile sidebar: hamburger opens, backdrop or navigation closes.
(function () {
var shell = document.querySelector('.shell');
var toggle = document.getElementById('sidebar-toggle');
var backdrop = document.querySelector('.sidebar-backdrop');
if (!shell || !toggle) return;
toggle.addEventListener('click', function () { shell.classList.toggle('sidebar-open'); });
if (backdrop) {
backdrop.addEventListener('click', function () { shell.classList.remove('sidebar-open'); });
}
})();
// Shared display names for widget_type, everywhere one shows up in the
// UI (Layout canvas, Add-a-widget buttons, button-assignment dropdowns).
const WIDGET_LABELS = {
photos: 'Photos', calendar: 'Calendar', whiteboard: 'Whiteboard (alpha)', tasks: 'Tasks',
static: 'Static image', text: 'Text', weather: 'Weather', battery: 'Battery',
};
function showStatus(ok, message) {
// While a <dialog> is open, its own .dialog-result container gets the
// message instead of the page-level #result -- otherwise it lands
// behind the dialog's backdrop, invisible until the dialog closes
// (e.g. the widget config dialogs on the Layout tab, see
// frame_layout.js). Falls back to #result for everything else.
var openDialog = document.querySelector('dialog[open]');
var el = (openDialog && openDialog.querySelector('.dialog-result')) || document.getElementById('result');
if (!el) return;
el.innerHTML = '<div class="status ' + (ok ? 'ok' : 'err') + '"></div>';
el.firstChild.textContent = message;
}
// A 409 from a control-gated endpoint means someone else holds the
// frame's control lock -- surface who, plus how to take over.
async function apiError(resp) {
var text = await resp.text();
try {
var body = JSON.parse(text);
var detail = body.detail !== undefined ? body.detail : body;
if (detail && detail.error === 'not_controller') {
var holder = detail.holder || 'Someone else';
return holder + ' has control of this frame — use "Take control" to make changes.';
}
if (typeof detail === 'string') return detail;
} catch (e) { /* not JSON */ }
return text;
}
function formatDuration(seconds) {
var d = Math.floor(seconds / 86400);
var h = Math.floor((seconds % 86400) / 3600);
var m = Math.floor((seconds % 3600) / 60);
if (d > 0) return d + 'd ' + h + 'h';
if (h > 0) return h + 'h ' + m + 'm';
return m + 'm';
}
// Reads resolved colors from CSS custom properties rather than
// hardcoding hex values, so canvas drawing matches the current theme.
function themeColor(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}