Add a static image widget (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF upload)
A new widget type showing a single user-uploaded image with no live upstream to poll -- decoded once at upload time (PDF's first page via pypdfium2, BSD-3-Clause/Apache-2.0, no copyleft exposure) into plain RGB PNG bytes, then composed per a crop/stretch/letterbox display mode like the photos widget.
This commit is contained in:
@@ -60,7 +60,10 @@
|
||||
|
||||
// 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' };
|
||||
const WIDGET_LABELS = {
|
||||
photos: 'Photos', calendar: 'Calendar', whiteboard: 'Whiteboard (alpha)', tasks: 'Tasks',
|
||||
static: 'Static image',
|
||||
};
|
||||
|
||||
function showStatus(ok, message) {
|
||||
// While a <dialog> is open, its own .dialog-result container gets the
|
||||
|
||||
@@ -286,11 +286,11 @@ window.addEventListener('resize', () => {
|
||||
// icon was clicked).
|
||||
const DIALOG_INIT = {
|
||||
photos: initPhotosDialog, calendar: initCalendarDialog, whiteboard: initWhiteboardDialog,
|
||||
tasks: initTasksDialog,
|
||||
tasks: initTasksDialog, static: initStaticDialog,
|
||||
};
|
||||
const DIALOG_CLOSE = {
|
||||
photos: closePhotosDialog, calendar: closeCalendarDialog, whiteboard: closeWhiteboardDialog,
|
||||
tasks: closeTasksDialog,
|
||||
tasks: closeTasksDialog, static: closeStaticDialog,
|
||||
};
|
||||
|
||||
let openDialogWidgetType = null;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// Static image widget dialog: upload, display-mode setting, 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 initStaticDialog().
|
||||
|
||||
function loadStaticPreview() {
|
||||
document.getElementById('static-preview').src = `${window.FRAME_API}/preview/static?_=${Date.now()}`;
|
||||
}
|
||||
|
||||
function initStaticDialog() {
|
||||
document.getElementById('static-upload').addEventListener('click', async () => {
|
||||
const input = document.getElementById('static-file');
|
||||
if (!input.files.length) {
|
||||
showStatus(false, 'Pick a file first.');
|
||||
return;
|
||||
}
|
||||
const form = new FormData();
|
||||
form.append('file', input.files[0]);
|
||||
try {
|
||||
const resp = await fetch(`${window.FRAME_API}/static-upload`, { method: 'POST', body: form });
|
||||
if (!resp.ok) throw new Error(await apiError(resp));
|
||||
const result = await resp.json();
|
||||
const currentFileEl = document.getElementById('static-current-file');
|
||||
currentFileEl.textContent = 'Currently showing: ';
|
||||
const nameEl = document.createElement('strong');
|
||||
nameEl.textContent = result.filename;
|
||||
currentFileEl.appendChild(nameEl);
|
||||
input.value = '';
|
||||
showStatus(true, 'Uploaded.');
|
||||
loadStaticPreview();
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('static-config-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const body = new URLSearchParams({
|
||||
display_mode: document.getElementById('display_mode').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.');
|
||||
loadStaticPreview();
|
||||
} catch (e) {
|
||||
showStatus(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('static-preview-refresh').addEventListener('click', loadStaticPreview);
|
||||
loadStaticPreview();
|
||||
}
|
||||
|
||||
function closeStaticDialog() {
|
||||
// Nothing to tear down -- no poll interval, unlike the photos dialog.
|
||||
}
|
||||
Reference in New Issue
Block a user