// Whiteboard widget dialog: source URL (owner-gated, see // api_widgets.py's api_widget_whiteboard_source), preview, and the file // browser. Not a page-load script -- frame_layout.js fetches this // widget's dialog HTML fragment, injects it into the shared , // points window.FRAME_API at this specific widget (/api/frames/{id}/ // widgets/{widget_id}), then calls initWhiteboardDialog(). // Rewrites #whiteboard-current-source in place instead of telling the // user to reload -- the API always assigns a successful "set" to the // caller (see api_widget_whiteboard_source), so after either action we // already know exactly what the new state is without asking the server // again. function renderWhiteboardCurrentSource(url) { const container = document.getElementById('whiteboard-current-source'); container.innerHTML = ''; const p = document.createElement('p'); p.className = 'sub'; p.style.marginTop = '10px'; if (url) { p.append('Currently showing '); const urlEl = document.createElement('strong'); urlEl.textContent = url; p.append(urlEl, ' using your WebDAV account. '); const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'btn-inline secondary'; clearBtn.id = 'whiteboard-source-clear'; clearBtn.textContent = 'Clear'; clearBtn.addEventListener('click', clearWhiteboardSource); p.append(clearBtn); } else { p.textContent = 'No whiteboard configured yet.'; } container.append(p); const label = document.getElementById('whiteboard-source-form-label'); if (label) { label.textContent = url ? 'Change to one of your own files' : 'Use one of your own files'; } } async function clearWhiteboardSource() { try { const resp = await fetch(`${window.FRAME_API}/whiteboard-source`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: null }), }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'Cleared.'); renderWhiteboardCurrentSource(null); const urlInput = document.getElementById('whiteboard-url-input'); if (urlInput) urlInput.value = ''; loadWhiteboardPreview(false); } catch (e) { showStatus(false, e.message); } } function loadWhiteboardPreview(force) { const forceParam = force ? '&force=1' : ''; document.getElementById('whiteboard-preview').src = `${window.FRAME_API}/preview/whiteboard?_=${Date.now()}${forceParam}`; } function initWhiteboardDialog() { const whiteboardForm = document.getElementById('whiteboard-source-form'); if (whiteboardForm) { whiteboardForm.addEventListener('submit', async (e) => { e.preventDefault(); const url = document.getElementById('whiteboard-url-input').value.trim(); if (!url) return; try { const resp = await fetch(`${window.FRAME_API}/whiteboard-source`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'Saved.'); renderWhiteboardCurrentSource(url); loadWhiteboardPreview(false); } catch (e) { showStatus(false, e.message); } }); } const whiteboardClearBtn = document.getElementById('whiteboard-source-clear'); if (whiteboardClearBtn) { whiteboardClearBtn.addEventListener('click', clearWhiteboardSource); } document.getElementById('whiteboard-config-form').addEventListener('submit', async (e) => { e.preventDefault(); const body = new URLSearchParams({ whiteboard_render_style: document.getElementById('whiteboard_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.'); loadWhiteboardPreview(false); } catch (e) { showStatus(false, e.message); } }); // Shows whatever's already cached (cheap, no refetch) on open; the // button is the one place that means "no really, go check now" -- // bypasses the fetch throttle server-side (see api_widget_preview_ // whiteboard's `force` param). document.getElementById('whiteboard-preview-refresh').addEventListener('click', () => loadWhiteboardPreview(true)); loadWhiteboardPreview(false); initBorderFields(); initButtonActionFields(); // --- file picker (Browse...) --- const browseToggle = document.getElementById('whiteboard-browse-toggle'); if (browseToggle) { const browsePanel = document.getElementById('whiteboard-browser'); const browseList = document.getElementById('whiteboard-browse-list'); const browseCurrent = document.getElementById('whiteboard-browse-current'); const browseUp = document.getElementById('whiteboard-browse-up'); const browseError = document.getElementById('whiteboard-browse-error'); const urlInput = document.getElementById('whiteboard-url-input'); let opened = false; async function browseTo(url) { browseError.style.display = 'none'; browseList.innerHTML = '
  • Loading...
  • '; try { const qs = url ? `?url=${encodeURIComponent(url)}` : ''; const resp = await fetch(`${window.FRAME_API}/whiteboard-browse${qs}`); if (!resp.ok) throw new Error(await apiError(resp)); const data = await resp.json(); browseCurrent.textContent = data.current_url; browseUp.disabled = !data.parent_url; browseUp.onclick = data.parent_url ? () => browseTo(data.parent_url) : null; browseList.innerHTML = ''; if (data.entries.length === 0) { browseList.innerHTML = '
  • (empty folder)
  • '; } for (const entry of data.entries) { const li = document.createElement('li'); const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn-inline secondary'; btn.style.margin = '2px 0'; btn.textContent = (entry.is_dir ? '📁 ' : '📄 ') + entry.name; if (entry.is_dir) { btn.addEventListener('click', () => browseTo(entry.url)); } else { btn.addEventListener('click', () => { urlInput.value = entry.url; browsePanel.style.display = 'none'; }); } li.appendChild(btn); browseList.appendChild(li); } } catch (e) { browseList.innerHTML = ''; browseError.textContent = e.message; browseError.style.display = 'block'; } } browseToggle.addEventListener('click', () => { opened = !opened; browsePanel.style.display = opened ? 'block' : 'none'; if (opened && !browseCurrent.textContent) { browseTo(null); } }); } } function closeWhiteboardDialog() { // Nothing to tear down -- no poll interval, unlike the photos dialog. }