import { api } from '../api.js'; import { showToast } from '../components/toast.js'; const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json()); export async function render(container) { const hash = window.location.hash; if (hash.startsWith('#/layout/')) { const id = hash.split('#/layout/')[1]; return renderEditor(container, id); } return renderList(container); } async function renderList(container) { container.innerHTML = `

Templates

My Layouts

`; document.getElementById('newLayoutBtn').onclick = async () => { const name = prompt('Layout name:'); if (!name) return; const layout = await API('/layouts', { method: 'POST', body: JSON.stringify({ name, zones: [{ name: 'Main', x_percent: 0, y_percent: 0, width_percent: 100, height_percent: 100 }] }) }); window.location.hash = `#/layout/${layout.id}`; }; try { const layouts = await API('/layouts'); const templates = layouts.filter(l => l.is_template); const custom = layouts.filter(l => !l.is_template); document.getElementById('templateGrid').innerHTML = templates.map(l => renderLayoutCard(l, true)).join(''); document.getElementById('layoutGrid').innerHTML = custom.length ? custom.map(l => renderLayoutCard(l, false)).join('') : '

No custom layouts yet

'; // Use template click container.querySelectorAll('[data-use-template]').forEach(btn => { btn.onclick = async () => { const layout = await API(`/layouts/${btn.dataset.useTemplate}/duplicate`, { method: 'POST', body: '{}' }); window.location.hash = `#/layout/${layout.id}`; }; }); // Edit layout click container.querySelectorAll('[data-edit-layout]').forEach(btn => { btn.onclick = () => { window.location.hash = `#/layout/${btn.dataset.editLayout}`; }; }); // Delete layout click container.querySelectorAll('[data-delete-layout]').forEach(btn => { btn.onclick = async (e) => { e.stopPropagation(); const name = btn.dataset.layoutName; if (!confirm(`Delete layout "${name}"? This cannot be undone.`)) return; try { await API(`/layouts/${btn.dataset.deleteLayout}`, { method: 'DELETE' }); showToast('Layout deleted'); renderList(container); } catch (err) { showToast(err.message || 'Failed to delete layout', 'error'); } }; }); } catch (err) { showToast(err.message, 'error'); } } function renderLayoutCard(layout, isTemplate) { return `
${(layout.zones || []).map(z => `
${z.name}
`).join('')}
${layout.name}
${layout.zones?.length || 0} zone(s) ${isTemplate ? '• Template' : ''}
${isTemplate ? `` : `` }
`; } async function renderEditor(container, layoutId) { let layout; try { layout = await API(`/layouts/${layoutId}`); } catch { container.innerHTML = '

Layout not found

'; return; } container.innerHTML = ` Back to Layouts

Zones

`; let zones = layout.zones || []; let selectedZone = null; let dragging = null; function renderZones() { const canvas = document.getElementById('canvas'); // Clear only zone divs canvas.querySelectorAll('.zone-el').forEach(z => z.remove()); zones.forEach((z, i) => { const el = document.createElement('div'); el.className = 'zone-el'; el.dataset.index = i; el.style.cssText = `position:absolute;left:${z.x_percent}%;top:${z.y_percent}%;width:${z.width_percent}%;height:${z.height_percent}%; background:${selectedZone === i ? 'rgba(59,130,246,0.3)' : 'rgba(59,130,246,0.1)'}; border:2px solid ${selectedZone === i ? 'var(--accent)' : 'rgba(59,130,246,0.4)'}; cursor:move;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--text-secondary); user-select:none;z-index:${z.z_index || 0}`; el.textContent = z.name; // Drag to move el.onmousedown = (e) => { if (e.target !== el) return; e.preventDefault(); selectedZone = i; renderZones(); updateProperties(); const rect = canvas.getBoundingClientRect(); const startX = e.clientX; const startY = e.clientY; const origX = z.x_percent; const origY = z.y_percent; const onMove = (e2) => { const dx = (e2.clientX - startX) / rect.width * 100; const dy = (e2.clientY - startY) / rect.height * 100; z.x_percent = Math.max(0, Math.min(100 - z.width_percent, Math.round((origX + dx) * 10) / 10)); z.y_percent = Math.max(0, Math.min(100 - z.height_percent, Math.round((origY + dy) * 10) / 10)); el.style.left = z.x_percent + '%'; el.style.top = z.y_percent + '%'; updateProperties(); }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; // Resize handle const handle = document.createElement('div'); handle.style.cssText = 'position:absolute;right:0;bottom:0;width:12px;height:12px;cursor:se-resize;background:var(--accent);border-radius:2px 0 0 0;opacity:0.7'; handle.onmousedown = (e) => { e.preventDefault(); e.stopPropagation(); selectedZone = i; const rect = canvas.getBoundingClientRect(); const onMove = (e2) => { const newW = ((e2.clientX - rect.left) / rect.width * 100) - z.x_percent; const newH = ((e2.clientY - rect.top) / rect.height * 100) - z.y_percent; z.width_percent = Math.max(5, Math.min(100 - z.x_percent, Math.round(newW * 10) / 10)); z.height_percent = Math.max(5, Math.min(100 - z.y_percent, Math.round(newH * 10) / 10)); el.style.width = z.width_percent + '%'; el.style.height = z.height_percent + '%'; updateProperties(); }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); }; el.appendChild(handle); canvas.appendChild(el); }); // Zone list sidebar document.getElementById('zoneList').innerHTML = zones.map((z, i) => `
${z.name}
${Math.round(z.width_percent)}% x ${Math.round(z.height_percent)}% • ${z.zone_type}
`).join(''); document.querySelectorAll('[data-zone-idx]').forEach(el => { el.onclick = () => { selectedZone = parseInt(el.dataset.zoneIdx); renderZones(); updateProperties(); }; }); } function updateProperties() { const panel = document.getElementById('zoneProperties'); if (selectedZone === null || !zones[selectedZone]) { panel.style.display = 'none'; return; } panel.style.display = 'block'; const z = zones[selectedZone]; document.getElementById('propName').value = z.name; document.getElementById('propX').value = z.x_percent; document.getElementById('propY').value = z.y_percent; document.getElementById('propW').value = z.width_percent; document.getElementById('propH').value = z.height_percent; document.getElementById('propType').value = z.zone_type; } // Property input handlers ['propName', 'propX', 'propY', 'propW', 'propH', 'propType'].forEach(id => { document.getElementById(id).oninput = () => { if (selectedZone === null) return; const z = zones[selectedZone]; z.name = document.getElementById('propName').value; z.x_percent = parseFloat(document.getElementById('propX').value) || 0; z.y_percent = parseFloat(document.getElementById('propY').value) || 0; z.width_percent = parseFloat(document.getElementById('propW').value) || 10; z.height_percent = parseFloat(document.getElementById('propH').value) || 10; z.zone_type = document.getElementById('propType').value; renderZones(); }; }); document.getElementById('addZoneBtn').onclick = () => { zones.push({ id: null, name: `Zone ${zones.length + 1}`, x_percent: 10, y_percent: 10, width_percent: 30, height_percent: 30, z_index: 0, zone_type: 'content', fit_mode: 'cover', background_color: '#000000', sort_order: zones.length }); selectedZone = zones.length - 1; renderZones(); updateProperties(); }; document.getElementById('deleteZoneBtn').onclick = () => { if (selectedZone === null) return; zones.splice(selectedZone, 1); selectedZone = null; renderZones(); updateProperties(); }; document.getElementById('saveLayoutBtn').onclick = async () => { try { // Delete existing zones and recreate for (const z of layout.zones || []) { await API(`/layouts/${layoutId}/zones/${z.id}`, { method: 'DELETE' }); } for (const z of zones) { await API(`/layouts/${layoutId}/zones`, { method: 'POST', body: JSON.stringify(z) }); } showToast('Layout saved', 'success'); layout = await API(`/layouts/${layoutId}`); zones = layout.zones; } catch (err) { showToast(err.message, 'error'); } }; renderZones(); } export function cleanup() {}