Add Layouts modal (save/load/delete named scene+playlist profiles)
This commit is contained in:
@@ -30,17 +30,23 @@ document.head.insertAdjacentHTML('beforeend', `<style>${styles}
|
||||
.badge { font-size:.7rem; opacity:.6; }
|
||||
#legend { position:absolute; left:10px; bottom:10px; font-size:.72rem; background:rgba(6,8,15,.6);
|
||||
padding:6px 10px; border-radius:8px; line-height:1.5; pointer-events:none; }
|
||||
#setModal { position:fixed; inset:0; background:rgba(4,6,12,.7); display:none; z-index:50;
|
||||
.modal { position:fixed; inset:0; background:rgba(4,6,12,.7); display:none; z-index:50;
|
||||
align-items:center; justify-content:center; }
|
||||
#setModal.open { display:flex; }
|
||||
#setCard { background:#111730; border:1px solid #2a3a6e; border-radius:10px; padding:16px;
|
||||
.modal.open { display:flex; }
|
||||
.modalCard { background:#111730; border:1px solid #2a3a6e; border-radius:10px; padding:16px;
|
||||
width:min(420px,92vw); max-height:80vh; display:flex; flex-direction:column; }
|
||||
#setCard h2 { margin:0 0 4px; font-size:1rem; color:#8fb8ff; }
|
||||
#setCard .hint { font-size:.72rem; opacity:.6; margin:0 0 10px; }
|
||||
#setList { overflow:auto; display:flex; flex-direction:column; gap:4px; }
|
||||
.modalCard h2 { margin:0 0 4px; font-size:1rem; color:#8fb8ff; }
|
||||
.modalCard .hint { font-size:.72rem; opacity:.6; margin:0 0 10px; }
|
||||
.modalList { overflow:auto; display:flex; flex-direction:column; gap:4px; }
|
||||
#setList button { text-align:left; background:#141a33; }
|
||||
#setList button:hover { background:#1c2650; }
|
||||
#setCard .close { margin-top:12px; align-self:flex-end; }
|
||||
.modalCard .close { margin-top:12px; align-self:flex-end; }
|
||||
.layoutRow { display:flex; gap:6px; align-items:center; background:#141a33; border-radius:6px; padding:4px 6px; }
|
||||
.layoutRow .nm { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.layoutRow .active { color:#51eaf1; font-size:.7rem; }
|
||||
.layoutRow button { padding:4px 10px; }
|
||||
#layoutSaveRow { display:flex; gap:6px; margin:10px 0 4px; }
|
||||
#layoutSaveRow input { flex:1; min-width:0; }
|
||||
</style>`);
|
||||
|
||||
await requireAuth();
|
||||
@@ -53,6 +59,7 @@ app.innerHTML = `${nav('layout')}
|
||||
<button data-add="spawn">+ Spawn</button>
|
||||
<button data-add="path">+ Path</button>
|
||||
<button id="tableBtn">Table…</button>
|
||||
<button id="layoutsBtn">Layouts…</button>
|
||||
<button id="topView">Top view</button>
|
||||
<button onclick="location.href='/admin/plan.html'">Print plan ↗</button>
|
||||
<span style="flex:1"></span>
|
||||
@@ -69,11 +76,23 @@ app.innerHTML = `${nav('layout')}
|
||||
</div>
|
||||
<div id="side"></div>
|
||||
</div>
|
||||
<div id="setModal"><div id="setCard">
|
||||
<div id="setModal" class="modal"><div class="modalCard">
|
||||
<h2>Import Hidden Side set</h2>
|
||||
<p class="hint">Adds a to-scale footprint (a building box) at the view centre. Drag it into place,
|
||||
then set its height/yaw in the panel. Footprints in cm; not affiliated with the LEGO Group.</p>
|
||||
<div id="setList"></div>
|
||||
<div id="setList" class="modalList"></div>
|
||||
<button class="close">Close</button>
|
||||
</div></div>
|
||||
<div id="layoutsModal" class="modal"><div class="modalCard">
|
||||
<h2>Layouts</h2>
|
||||
<p class="hint">Each layout stores this scene <b>and</b> its playlist. Save the current arrangement
|
||||
under a name, then load any saved layout to switch the live exhibit (all viewers update).
|
||||
Save your current edits first — loading replaces the live scene.</p>
|
||||
<div id="layoutSaveRow">
|
||||
<input id="layoutName" placeholder="Layout name (e.g. Small demo)">
|
||||
<button id="layoutSave" class="primary">Save current</button>
|
||||
</div>
|
||||
<div id="layoutList" class="modalList"></div>
|
||||
<button class="close">Close</button>
|
||||
</div></div>`;
|
||||
|
||||
@@ -311,6 +330,57 @@ setList.querySelectorAll('[data-fp]').forEach(b => b.onclick = () => {
|
||||
buildAll(); panel();
|
||||
});
|
||||
|
||||
// ---------- layouts (save / load / delete) ----------
|
||||
const layoutsModal = document.getElementById('layoutsModal');
|
||||
const layoutList = document.getElementById('layoutList');
|
||||
async function refreshLayouts() {
|
||||
let data;
|
||||
try { data = await api('/api/layouts'); }
|
||||
catch { layoutList.innerHTML = '<em style="opacity:.6">Could not load layouts.</em>'; return; }
|
||||
const { layouts, activeSlug } = data;
|
||||
if (!layouts.length) { layoutList.innerHTML = '<em style="opacity:.6">No saved layouts yet. Name one above and Save current.</em>'; return; }
|
||||
layoutList.innerHTML = layouts.map(l => `<div class="layoutRow" data-slug="${l.slug}">
|
||||
<span class="nm">${l.name}${l.slug === activeSlug ? ' <span class="active">● active</span>' : ''}</span>
|
||||
<button data-act="load">Load</button>
|
||||
<button data-act="del" class="danger">✕</button>
|
||||
</div>`).join('');
|
||||
layoutList.querySelectorAll('.layoutRow').forEach(row => {
|
||||
const slug = row.dataset.slug;
|
||||
row.querySelector('[data-act="load"]').onclick = async () => {
|
||||
if (!confirm('Load this layout? The current live scene will be replaced (save it first if unsaved).')) return;
|
||||
try {
|
||||
const r = await api('/api/layouts/' + slug + '/load', 'POST');
|
||||
scene = r.scene;
|
||||
scene.anchors ||= []; scene.buildings ||= []; scene.spawns ||= []; scene.paths ||= [];
|
||||
scene.table = Object.assign({ width: 120, depth: 100, offsetX: 0, offsetZ: 0, show: true }, scene.table || {});
|
||||
sel = null; gizmo.detach(); buildAll(); panel();
|
||||
status.textContent = 'Loaded layout';
|
||||
refreshLayouts();
|
||||
} catch (e) { alert('Load failed: ' + e.message); }
|
||||
};
|
||||
row.querySelector('[data-act="del"]').onclick = async () => {
|
||||
if (!confirm('Delete this saved layout? (Does not affect the live scene.)')) return;
|
||||
try { await api('/api/layouts/' + slug, 'DELETE'); refreshLayouts(); }
|
||||
catch (e) { alert('Delete failed: ' + e.message); }
|
||||
};
|
||||
});
|
||||
}
|
||||
document.getElementById('layoutsBtn').onclick = () => { layoutsModal.classList.add('open'); refreshLayouts(); };
|
||||
layoutsModal.querySelector('.close').onclick = () => layoutsModal.classList.remove('open');
|
||||
layoutsModal.onclick = e => { if (e.target === layoutsModal) layoutsModal.classList.remove('open'); };
|
||||
document.getElementById('layoutSave').onclick = async () => {
|
||||
const name = document.getElementById('layoutName').value.trim();
|
||||
if (!name) return alert('Give the layout a name first.');
|
||||
try {
|
||||
// persist current edits into the live scene, then snapshot as a layout
|
||||
scene = await api('/api/scene', 'PUT', scene);
|
||||
await api('/api/layouts', 'POST', { name });
|
||||
document.getElementById('layoutName').value = '';
|
||||
status.textContent = 'Saved layout "' + name + '"';
|
||||
refreshLayouts();
|
||||
} catch (e) { alert('Save failed: ' + e.message); }
|
||||
};
|
||||
|
||||
// ---------- property panel ----------
|
||||
const side = document.getElementById('side');
|
||||
function field(label, val, oncommit, opts) {
|
||||
|
||||
Reference in New Issue
Block a user