Files
Thomas Faour c1c657c0a9
Build and push server image / test (push) Successful in 29s
Build and push server image / build-and-push (push) Successful in 2m1s
Build and push server image / deploy (push) Successful in 56s
Saved layouts feature
2026-07-25 18:18:48 +00:00

197 lines
6.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();