// Layout tab's "Saved layouts" card: save the current widget arrangement // (placement, settings, button assignments -- see routers/api_layouts.py) // under a name, then switch back to it later. Layouts are owned by the // logged-in user, not this frame, so this always hits window.FRAME_BASE_API // (the frame-level base) rather than window.FRAME_API, which the widget // dialog machinery in frame_layout.js temporarily repoints at a specific // widget while its gear-icon dialog is open. let savedLayouts = []; let editingLayoutId = null; // inline rename in progress, same pattern as frame_header.js's name pencil-edit function renderSavedLayouts() { const list = document.getElementById('saved-layout-list'); const emptyHint = document.getElementById('saved-layout-empty-hint'); list.innerHTML = ''; emptyHint.style.display = savedLayouts.length ? 'none' : ''; savedLayouts.forEach((layout) => { const li = document.createElement('li'); li.className = 'saved-layout-row'; if (editingLayoutId === layout.id) { const input = document.createElement('input'); input.type = 'text'; input.maxLength = 60; input.value = layout.name; input.className = 'saved-layout-rename-input'; const saveBtn = document.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'btn-inline'; saveBtn.textContent = 'Save'; saveBtn.addEventListener('click', () => renameSavedLayout(layout, input.value)); const cancelBtn = document.createElement('button'); cancelBtn.type = 'button'; cancelBtn.className = 'btn-inline secondary'; cancelBtn.textContent = 'Cancel'; cancelBtn.addEventListener('click', () => { editingLayoutId = null; renderSavedLayouts(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') renameSavedLayout(layout, input.value); if (e.key === 'Escape') { editingLayoutId = null; renderSavedLayouts(); } }); li.appendChild(input); li.appendChild(saveBtn); li.appendChild(cancelBtn); list.appendChild(li); input.focus(); input.select(); return; } const nameWrap = document.createElement('span'); nameWrap.className = 'saved-layout-name'; const nameText = document.createElement('span'); nameText.textContent = layout.name; nameWrap.appendChild(nameText); if (!layout.compatible) { const badge = document.createElement('span'); badge.className = 'saved-layout-badge'; badge.textContent = 'different orientation'; nameWrap.appendChild(badge); } li.appendChild(nameWrap); const controls = document.createElement('span'); controls.className = 'saved-layout-controls'; const applyBtn = document.createElement('button'); applyBtn.type = 'button'; applyBtn.className = 'btn-inline'; applyBtn.textContent = 'Apply'; applyBtn.disabled = !layout.compatible; applyBtn.title = layout.compatible ? `Replace the current arrangement with "${layout.name}"` : "This layout was saved for a different orientation's grid"; applyBtn.addEventListener('click', () => applySavedLayout(layout)); controls.appendChild(applyBtn); const renameBtn = document.createElement('button'); renameBtn.type = 'button'; renameBtn.className = 'icon-btn'; renameBtn.textContent = '✎'; // pencil renameBtn.title = 'Rename'; renameBtn.addEventListener('click', () => { editingLayoutId = layout.id; renderSavedLayouts(); }); controls.appendChild(renameBtn); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'icon-btn'; deleteBtn.textContent = '×'; deleteBtn.title = 'Delete'; deleteBtn.addEventListener('click', () => deleteSavedLayout(layout)); controls.appendChild(deleteBtn); li.appendChild(controls); list.appendChild(li); }); } async function loadSavedLayouts() { try { const resp = await fetch(`${window.FRAME_BASE_API}/layouts`); if (!resp.ok) throw new Error(await apiError(resp)); savedLayouts = (await resp.json()).layouts; renderSavedLayouts(); } catch (e) { showStatus(false, e.message); } } async function saveCurrentLayout() { const input = document.getElementById('saved-layout-name'); const name = input.value.trim(); if (!name) { showStatus(false, 'Give this layout a name first.'); return; } const existing = savedLayouts.find((l) => l.name === name); if (existing && !confirm(`You already have a saved layout named "${name}" -- overwrite it with the current arrangement?`)) { return; } try { const resp = await fetch(`${window.FRAME_BASE_API}/layouts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!resp.ok) throw new Error(await apiError(resp)); input.value = ''; showStatus(true, `Saved layout "${name}".`); } catch (e) { showStatus(false, e.message); } finally { loadSavedLayouts(); } } document.getElementById('save-layout-btn').addEventListener('click', saveCurrentLayout); document.getElementById('saved-layout-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') saveCurrentLayout(); }); async function applySavedLayout(layout) { if (!confirm(`Replace the current arrangement with "${layout.name}"? Widgets not in that layout will be removed.`)) { return; } try { const resp = await fetch(`${window.FRAME_BASE_API}/layouts/${layout.id}/apply`, { method: 'POST' }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, `Applied "${layout.name}".`); } catch (e) { showStatus(false, e.message); } finally { loadWidgets(); // see frame_layout.js -- reloads the placement canvas } } async function renameSavedLayout(layout, rawName) { const name = rawName.trim(); if (!name || name === layout.name) { editingLayoutId = null; renderSavedLayouts(); return; } try { const resp = await fetch(`/api/layouts/${layout.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'Renamed.'); editingLayoutId = null; } catch (e) { showStatus(false, e.message); } finally { loadSavedLayouts(); } } async function deleteSavedLayout(layout) { if (!confirm(`Delete the saved layout "${layout.name}"? This can't be undone.`)) return; try { const resp = await fetch(`/api/layouts/${layout.id}`, { method: 'DELETE' }); if (!resp.ok) throw new Error(await apiError(resp)); showStatus(true, 'Deleted.'); } catch (e) { showStatus(false, e.message); } finally { loadSavedLayouts(); } } loadSavedLayouts();