197 lines
6.7 KiB
JavaScript
197 lines
6.7 KiB
JavaScript
// 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();
|