Add a static image widget (PNG/JPEG/GIF/BMP/WEBP/TIFF/PDF upload)
Build and push server image / test (push) Successful in 26s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 59s

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:
Thomas Faour
2026-07-25 08:39:09 +00:00
parent 4a2b1f3795
commit 35e80c6d1c
19 changed files with 618 additions and 8 deletions
+62
View File
@@ -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.
}