Spaces:
Sleeping
Sleeping
Update static/index.html
Browse files- static/index.html +160 -129
static/index.html
CHANGED
|
@@ -31,23 +31,23 @@
|
|
| 31 |
transition:transform .28s ease; box-shadow:var(--shadow); z-index:5;
|
| 32 |
transform:translateX(0);
|
| 33 |
}
|
| 34 |
-
#panel.min{
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
}
|
| 45 |
#panel h2{margin:.3rem 0 1rem; font-size:1.05rem; color:#cfe0ff}
|
| 46 |
fieldset{border:1px solid #2a395f; border-radius:12px; padding:12px; margin:10px 0}
|
| 47 |
legend{font-size:.9rem; color:#bcd0ff; padding:0 6px}
|
| 48 |
label{display:block; font-size:.85rem; color:var(--muted); margin:8px 0 4px}
|
| 49 |
-
input[type="text"], input[type="url"], select{width:100%; padding:10px 12px; border-radius:10px; border:1px solid #26365a; background:#0c1430; color
|
| 50 |
-
input[type="
|
| 51 |
.row{display:flex; gap:8px}
|
| 52 |
.row>*{flex:1}
|
| 53 |
button{cursor:pointer; border:1px solid #2a3a63; background:#132042; color:#e8f0ff; padding:10px 12px; border-radius:12px; transition:filter .2s, transform .05s; font-weight:600}
|
|
@@ -71,38 +71,34 @@
|
|
| 71 |
|
| 72 |
#saveState{position:fixed; bottom:12px; left:calc(var(--panelW) + 12px); font-size:.8rem; border-radius:999px; padding:6px 10px; opacity:.9; z-index:4; transition:left .28s ease}
|
| 73 |
#panel.min ~ #stage #saveState, #panel.min ~ #saveState { left:28px; }
|
|
|
|
|
|
|
| 74 |
|
| 75 |
/* Pins (bigger + more transparent) */
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
/*
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
position:absolute;
|
| 101 |
-
inset:-10px; /* +10px all around = easier tapping */
|
| 102 |
-
border-radius:50%;
|
| 103 |
-
/* no background needed; clicks will still land on .pin */
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
.pin:hover{background:rgba(255,255,255,.26)}
|
| 107 |
|
| 108 |
/* Popups: centered exactly on the pin midpoint */
|
|
@@ -110,15 +106,8 @@
|
|
| 110 |
position:absolute; z-index:4;
|
| 111 |
transform: translate(-50%, -50%); /* center on pin */
|
| 112 |
background:rgba(13,20,40,.92); border:1px solid #2a395f; border-radius:16px; box-shadow:var(--shadow);
|
| 113 |
-
padding:0; min-width:
|
| 114 |
}
|
| 115 |
-
/* Make header overlay so media stays geometrically centered */
|
| 116 |
-
.popup header{
|
| 117 |
-
position:absolute; top:6px; right:6px; left:auto;
|
| 118 |
-
display:flex; align-items:center; gap:8px; margin:0; padding:0;
|
| 119 |
-
}
|
| 120 |
-
.popup h4{margin:0; font-size:.95rem; color:#d9e6ff; background:rgba(0,0,0,.35); padding:6px 10px; border-radius:10px}
|
| 121 |
-
.x{background:rgba(0,0,0,.35); border:1px solid #2a395f; color:#cfe0ff; font-size:16px; padding:6px 8px; border-radius:10px}
|
| 122 |
|
| 123 |
.media{display:block; width:100%; height:auto; border-radius:16px; overflow:hidden; background:#000}
|
| 124 |
.media video, .media img{display:block; width:100%; height:auto}
|
|
@@ -128,10 +117,6 @@
|
|
| 128 |
.item .name{font-size:.9rem}
|
| 129 |
.item .tiny{font-size:.75rem; color:#9fb0cf}
|
| 130 |
.item .actions{display:flex; gap:6px}
|
| 131 |
-
|
| 132 |
-
/* Hide the "Saved" badge when the sidebar is minimized */
|
| 133 |
-
#panel.min ~ #saveState { display: none; }
|
| 134 |
-
|
| 135 |
</style>
|
| 136 |
</head>
|
| 137 |
<body>
|
|
@@ -169,11 +154,19 @@
|
|
| 169 |
<fieldset>
|
| 170 |
<div class="row">
|
| 171 |
<label>Type
|
| 172 |
-
<select id="itemType"
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
</div>
|
| 178 |
<label>Title</label>
|
| 179 |
<input type="text" id="itemTitle" placeholder="Short label" />
|
|
@@ -214,6 +207,9 @@
|
|
| 214 |
<button id="saveState" class="btn-ghost">Saved</button>
|
| 215 |
</div>
|
| 216 |
|
|
|
|
|
|
|
|
|
|
| 217 |
<script>
|
| 218 |
(() => {
|
| 219 |
const $ = s => document.querySelector(s);
|
|
@@ -227,7 +223,7 @@
|
|
| 227 |
|
| 228 |
const state = {
|
| 229 |
background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
|
| 230 |
-
items: [], // {id,type,title,src,x,y,
|
| 231 |
placingId: null
|
| 232 |
};
|
| 233 |
const uid = () => Math.random().toString(36).slice(2,9);
|
|
@@ -238,7 +234,6 @@
|
|
| 238 |
};
|
| 239 |
})();
|
| 240 |
|
| 241 |
-
// Panel toggle
|
| 242 |
// Panel toggle
|
| 243 |
const updateToggleIcon = () => {
|
| 244 |
toggle.textContent = panel.classList.contains('min') ? '▶' : '◀';
|
|
@@ -247,12 +242,10 @@
|
|
| 247 |
panel.classList.toggle('min');
|
| 248 |
updateToggleIcon();
|
| 249 |
});
|
| 250 |
-
|
| 251 |
// start minimized on load
|
| 252 |
panel.classList.add('min');
|
| 253 |
updateToggleIcon();
|
| 254 |
|
| 255 |
-
|
| 256 |
// Helpers
|
| 257 |
function escapeHtml(s){return (s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
| 258 |
|
|
@@ -268,6 +261,22 @@
|
|
| 268 |
});
|
| 269 |
}
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
// Background
|
| 272 |
$('#applyBg').addEventListener('click', async () => {
|
| 273 |
const type = $('#bgType').value;
|
|
@@ -311,13 +320,14 @@
|
|
| 311 |
const title = $('#itemTitle').value.trim();
|
| 312 |
const url = $('#itemUrl').value.trim();
|
| 313 |
const file = $('#itemFile').files[0];
|
| 314 |
-
const
|
|
|
|
| 315 |
let src = url;
|
| 316 |
|
| 317 |
if (file){ src = await fileToDataURL(file); if (!src) return; }
|
| 318 |
if (!src){ alert('Provide a media URL or choose a file.'); return; }
|
| 319 |
|
| 320 |
-
const it = { id: uid(), type, title, src, x:50, y:50,
|
| 321 |
state.items.push(it);
|
| 322 |
renderItems(); markDirty();
|
| 323 |
|
|
@@ -351,10 +361,10 @@
|
|
| 351 |
stopPlacing();
|
| 352 |
});
|
| 353 |
|
|
|
|
| 354 |
function togglePopup(id){
|
| 355 |
const it = state.items.find(x=>x.id===id);
|
| 356 |
if (!it) return;
|
| 357 |
-
state.items.forEach(x=>{ if (x.id!==id) x.open=false; });
|
| 358 |
it.open = !it.open;
|
| 359 |
renderItems(); // visual only, don't mark dirty for open state
|
| 360 |
}
|
|
@@ -373,66 +383,66 @@
|
|
| 373 |
function renderItems(){
|
| 374 |
$('#count').textContent = String(state.items.length);
|
| 375 |
overlay.innerHTML = '';
|
|
|
|
| 376 |
for (const it of state.items){
|
|
|
|
| 377 |
const pin = document.createElement('div');
|
| 378 |
-
pin.className = 'pin';
|
| 379 |
-
pin.setAttribute('role', 'button');
|
| 380 |
-
pin.setAttribute('tabindex', '0');
|
| 381 |
-
|
| 382 |
-
pin.style.
|
| 383 |
-
pin.
|
| 384 |
-
pin.
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
}
|
| 437 |
|
| 438 |
// items list
|
|
@@ -442,7 +452,7 @@ if (it.open){
|
|
| 442 |
const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
|
| 443 |
const info = document.createElement('div');
|
| 444 |
info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
|
| 445 |
-
<div class="tiny">${it.
|
| 446 |
const actions = document.createElement('div'); actions.className='actions';
|
| 447 |
|
| 448 |
const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
|
|
@@ -451,13 +461,27 @@ if (it.open){
|
|
| 451 |
const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
|
| 452 |
btnPreview.addEventListener('click', ()=> togglePopup(it.id));
|
| 453 |
|
| 454 |
-
const
|
| 455 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
|
| 457 |
const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
|
| 458 |
btnDel.addEventListener('click', ()=> removeItem(it.id));
|
| 459 |
|
| 460 |
-
actions.append(btnPlace, btnPreview,
|
| 461 |
row.append(kind, info, actions); list.appendChild(row);
|
| 462 |
}
|
| 463 |
}
|
|
@@ -483,7 +507,14 @@ if (it.open){
|
|
| 483 |
if (!res.ok) throw new Error('Load failed');
|
| 484 |
const data = await res.json();
|
| 485 |
state.background = data.background || null;
|
| 486 |
-
state.items = Array.isArray(data.items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 487 |
renderBackground(); renderItems();
|
| 488 |
saveBadge.textContent = 'Loaded'; setTimeout(()=> saveBadge.textContent='Saved', 700);
|
| 489 |
}catch(e){
|
|
|
|
| 31 |
transition:transform .28s ease; box-shadow:var(--shadow); z-index:5;
|
| 32 |
transform:translateX(0);
|
| 33 |
}
|
| 34 |
+
#panel.min{
|
| 35 |
+
/* Fully off-canvas; no sliver */
|
| 36 |
+
transform: translateX(-100%);
|
| 37 |
+
/* If you ever see a 0.5px hairline from subpixel rounding, use:
|
| 38 |
+
transform: translateX(calc(-100% - 1px)); */
|
| 39 |
+
overflow: hidden;
|
| 40 |
+
border-right: none;
|
| 41 |
+
background: transparent; /* kill gradient tint */
|
| 42 |
+
box-shadow: none; /* kill edge glow */
|
| 43 |
+
pointer-events: none; /* panel won’t catch clicks while hidden */
|
| 44 |
+
}
|
| 45 |
#panel h2{margin:.3rem 0 1rem; font-size:1.05rem; color:#cfe0ff}
|
| 46 |
fieldset{border:1px solid #2a395f; border-radius:12px; padding:12px; margin:10px 0}
|
| 47 |
legend{font-size:.9rem; color:#bcd0ff; padding:0 6px}
|
| 48 |
label{display:block; font-size:.85rem; color:var(--muted); margin:8px 0 4px}
|
| 49 |
+
input[type="text"], input[type="url"], select{width:100%; padding:10px 12px; border-radius:10px; border:1px solid #26365a; background:#0c1430; color:#var(--text)}
|
| 50 |
+
input[type="number"]{width:100%; padding:10px 12px; border-radius:10px; border:1px solid #26365a; background:#0c1430; color:#e6edf7}
|
| 51 |
.row{display:flex; gap:8px}
|
| 52 |
.row>*{flex:1}
|
| 53 |
button{cursor:pointer; border:1px solid #2a3a63; background:#132042; color:#e8f0ff; padding:10px 12px; border-radius:12px; transition:filter .2s, transform .05s; font-weight:600}
|
|
|
|
| 71 |
|
| 72 |
#saveState{position:fixed; bottom:12px; left:calc(var(--panelW) + 12px); font-size:.8rem; border-radius:999px; padding:6px 10px; opacity:.9; z-index:4; transition:left .28s ease}
|
| 73 |
#panel.min ~ #stage #saveState, #panel.min ~ #saveState { left:28px; }
|
| 74 |
+
/* Hide the "Saved" badge when the sidebar is minimized */
|
| 75 |
+
#panel.min ~ #saveState { display: none; }
|
| 76 |
|
| 77 |
/* Pins (bigger + more transparent) */
|
| 78 |
+
.pin{
|
| 79 |
+
position:absolute;
|
| 80 |
+
left:0; top:0;
|
| 81 |
+
width:34px; height:34px; /* bigger */
|
| 82 |
+
border-radius:50%;
|
| 83 |
+
border:2px solid rgba(255,255,255,.45);
|
| 84 |
+
background:rgba(255,255,255,.12); /* more transparent */
|
| 85 |
+
box-shadow:0 2px 10px rgba(0,0,0,.45);
|
| 86 |
+
transform:translate(-50%, -50%);
|
| 87 |
+
z-index:3;
|
| 88 |
+
cursor:pointer;
|
| 89 |
+
display:block;
|
| 90 |
+
padding:0;
|
| 91 |
+
line-height:0;
|
| 92 |
+
-webkit-appearance:none;
|
| 93 |
+
appearance:none;
|
| 94 |
+
}
|
| 95 |
+
/* Expand the clickable area beyond the visual circle (keeps look the same) */
|
| 96 |
+
.pin::before{
|
| 97 |
+
content:"";
|
| 98 |
+
position:absolute;
|
| 99 |
+
inset:-10px; /* +10px all around = easier tapping */
|
| 100 |
+
border-radius:50%;
|
| 101 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
.pin:hover{background:rgba(255,255,255,.26)}
|
| 103 |
|
| 104 |
/* Popups: centered exactly on the pin midpoint */
|
|
|
|
| 106 |
position:absolute; z-index:4;
|
| 107 |
transform: translate(-50%, -50%); /* center on pin */
|
| 108 |
background:rgba(13,20,40,.92); border:1px solid #2a395f; border-radius:16px; box-shadow:var(--shadow);
|
| 109 |
+
padding:0; min-width:120px; max-width:none; backdrop-filter: blur(6px);
|
| 110 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
.media{display:block; width:100%; height:auto; border-radius:16px; overflow:hidden; background:#000}
|
| 113 |
.media video, .media img{display:block; width:100%; height:auto}
|
|
|
|
| 117 |
.item .name{font-size:.9rem}
|
| 118 |
.item .tiny{font-size:.75rem; color:#9fb0cf}
|
| 119 |
.item .actions{display:flex; gap:6px}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</style>
|
| 121 |
</head>
|
| 122 |
<body>
|
|
|
|
| 154 |
<fieldset>
|
| 155 |
<div class="row">
|
| 156 |
<label>Type
|
| 157 |
+
<select id="itemType">
|
| 158 |
+
<option value="image">Image</option>
|
| 159 |
+
<option value="video">Video</option>
|
| 160 |
+
</select>
|
| 161 |
</label>
|
| 162 |
+
<label>Popup width</label>
|
| 163 |
+
</div>
|
| 164 |
+
<div class="row">
|
| 165 |
+
<input type="number" id="itemWidth" min="10" max="2000" value="25" />
|
| 166 |
+
<select id="itemWidthUnit">
|
| 167 |
+
<option value="vw" selected>vw (responsive)</option>
|
| 168 |
+
<option value="px">px (exact)</option>
|
| 169 |
+
</select>
|
| 170 |
</div>
|
| 171 |
<label>Title</label>
|
| 172 |
<input type="text" id="itemTitle" placeholder="Short label" />
|
|
|
|
| 207 |
<button id="saveState" class="btn-ghost">Saved</button>
|
| 208 |
</div>
|
| 209 |
|
| 210 |
+
<!-- HLS support for .m3u8 streams -->
|
| 211 |
+
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
|
| 212 |
+
|
| 213 |
<script>
|
| 214 |
(() => {
|
| 215 |
const $ = s => document.querySelector(s);
|
|
|
|
| 223 |
|
| 224 |
const state = {
|
| 225 |
background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
|
| 226 |
+
items: [], // {id,type,title,src,x,y,width,widthUnit,open:false}
|
| 227 |
placingId: null
|
| 228 |
};
|
| 229 |
const uid = () => Math.random().toString(36).slice(2,9);
|
|
|
|
| 234 |
};
|
| 235 |
})();
|
| 236 |
|
|
|
|
| 237 |
// Panel toggle
|
| 238 |
const updateToggleIcon = () => {
|
| 239 |
toggle.textContent = panel.classList.contains('min') ? '▶' : '◀';
|
|
|
|
| 242 |
panel.classList.toggle('min');
|
| 243 |
updateToggleIcon();
|
| 244 |
});
|
|
|
|
| 245 |
// start minimized on load
|
| 246 |
panel.classList.add('min');
|
| 247 |
updateToggleIcon();
|
| 248 |
|
|
|
|
| 249 |
// Helpers
|
| 250 |
function escapeHtml(s){return (s||'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));}
|
| 251 |
|
|
|
|
| 261 |
});
|
| 262 |
}
|
| 263 |
|
| 264 |
+
// Infer a video MIME type from URL (rough guess)
|
| 265 |
+
function guessVideoMime(src){
|
| 266 |
+
try{
|
| 267 |
+
if (src.startsWith('data:')) {
|
| 268 |
+
const m = src.slice(5, src.indexOf(';'));
|
| 269 |
+
if (m.startsWith('video/')) return m;
|
| 270 |
+
}
|
| 271 |
+
const u = new URL(src, window.location.href).pathname.toLowerCase();
|
| 272 |
+
if (u.endsWith('.mp4')) return 'video/mp4';
|
| 273 |
+
if (u.endsWith('.webm')) return 'video/webm';
|
| 274 |
+
if (u.endsWith('.ogv') || u.endsWith('.ogg')) return 'video/ogg';
|
| 275 |
+
if (u.endsWith('.m3u8')) return 'application/vnd.apple.mpegurl';
|
| 276 |
+
}catch(e){}
|
| 277 |
+
return '';
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
// Background
|
| 281 |
$('#applyBg').addEventListener('click', async () => {
|
| 282 |
const type = $('#bgType').value;
|
|
|
|
| 320 |
const title = $('#itemTitle').value.trim();
|
| 321 |
const url = $('#itemUrl').value.trim();
|
| 322 |
const file = $('#itemFile').files[0];
|
| 323 |
+
const width = Number($('#itemWidth').value);
|
| 324 |
+
const widthUnit = $('#itemWidthUnit').value; // 'vw' | 'px'
|
| 325 |
let src = url;
|
| 326 |
|
| 327 |
if (file){ src = await fileToDataURL(file); if (!src) return; }
|
| 328 |
if (!src){ alert('Provide a media URL or choose a file.'); return; }
|
| 329 |
|
| 330 |
+
const it = { id: uid(), type, title, src, x:50, y:50, width, widthUnit, open:false };
|
| 331 |
state.items.push(it);
|
| 332 |
renderItems(); markDirty();
|
| 333 |
|
|
|
|
| 361 |
stopPlacing();
|
| 362 |
});
|
| 363 |
|
| 364 |
+
// Allow multiple popups to stay open; clicking pin toggles only that one
|
| 365 |
function togglePopup(id){
|
| 366 |
const it = state.items.find(x=>x.id===id);
|
| 367 |
if (!it) return;
|
|
|
|
| 368 |
it.open = !it.open;
|
| 369 |
renderItems(); // visual only, don't mark dirty for open state
|
| 370 |
}
|
|
|
|
| 383 |
function renderItems(){
|
| 384 |
$('#count').textContent = String(state.items.length);
|
| 385 |
overlay.innerHTML = '';
|
| 386 |
+
|
| 387 |
for (const it of state.items){
|
| 388 |
+
// PIN
|
| 389 |
const pin = document.createElement('div');
|
| 390 |
+
pin.className = 'pin';
|
| 391 |
+
pin.setAttribute('role', 'button');
|
| 392 |
+
pin.setAttribute('tabindex', '0');
|
| 393 |
+
pin.style.left = it.x + '%';
|
| 394 |
+
pin.style.top = it.y + '%';
|
| 395 |
+
pin.title = it.title || it.type;
|
| 396 |
+
pin.addEventListener('click', (e) => { e.stopPropagation(); togglePopup(it.id); });
|
| 397 |
+
pin.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePopup(it.id); } });
|
| 398 |
+
overlay.appendChild(pin);
|
| 399 |
+
|
| 400 |
+
// POPUP
|
| 401 |
+
if (it.open){
|
| 402 |
+
const pop = document.createElement('div');
|
| 403 |
+
pop.className = 'popup';
|
| 404 |
+
pop.style.left = it.x + '%';
|
| 405 |
+
pop.style.top = it.y + '%';
|
| 406 |
+
pop.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
|
| 407 |
+
|
| 408 |
+
const wrap = document.createElement('div');
|
| 409 |
+
wrap.className = 'media';
|
| 410 |
+
|
| 411 |
+
if (it.type === 'video') {
|
| 412 |
+
const v = document.createElement('video');
|
| 413 |
+
v.controls = true;
|
| 414 |
+
v.preload = 'metadata';
|
| 415 |
+
v.playsInline = true;
|
| 416 |
+
v.style.width = '100%';
|
| 417 |
+
v.style.height = 'auto';
|
| 418 |
+
v.style.aspectRatio = '16/9'; // placeholder to avoid "thin line" before metadata
|
| 419 |
+
// HLS support
|
| 420 |
+
if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
|
| 421 |
+
const hls = new Hls();
|
| 422 |
+
hls.loadSource(it.src);
|
| 423 |
+
hls.attachMedia(v);
|
| 424 |
+
} else {
|
| 425 |
+
const srcEl = document.createElement('source');
|
| 426 |
+
srcEl.src = it.src;
|
| 427 |
+
const mime = guessVideoMime(it.src);
|
| 428 |
+
if (mime) srcEl.type = mime;
|
| 429 |
+
v.appendChild(srcEl);
|
| 430 |
+
}
|
| 431 |
+
v.addEventListener('loadedmetadata', () => {
|
| 432 |
+
if (v.videoWidth && v.videoHeight) v.style.aspectRatio = (v.videoWidth / v.videoHeight).toString();
|
| 433 |
+
});
|
| 434 |
+
wrap.appendChild(v);
|
| 435 |
+
} else {
|
| 436 |
+
const img = document.createElement('img');
|
| 437 |
+
img.src = it.src;
|
| 438 |
+
img.alt = it.title || '';
|
| 439 |
+
wrap.appendChild(img);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
pop.appendChild(wrap);
|
| 443 |
+
overlay.appendChild(pop);
|
| 444 |
+
requestAnimationFrame(() => adjustPopupPosition(pop));
|
| 445 |
+
}
|
|
|
|
|
|
|
| 446 |
}
|
| 447 |
|
| 448 |
// items list
|
|
|
|
| 452 |
const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
|
| 453 |
const info = document.createElement('div');
|
| 454 |
info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
|
| 455 |
+
<div class="tiny">${it.width}${it.widthUnit} • (${it.x.toFixed(1)}%, ${it.y.toFixed(1)}%)</div>`;
|
| 456 |
const actions = document.createElement('div'); actions.className='actions';
|
| 457 |
|
| 458 |
const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
|
|
|
|
| 461 |
const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
|
| 462 |
btnPreview.addEventListener('click', ()=> togglePopup(it.id));
|
| 463 |
|
| 464 |
+
const widthIn = document.createElement('input');
|
| 465 |
+
widthIn.type = 'number'; widthIn.min = '10'; widthIn.max = '2000';
|
| 466 |
+
widthIn.value = String(it.width ?? 25);
|
| 467 |
+
|
| 468 |
+
const unitSel = document.createElement('select');
|
| 469 |
+
unitSel.innerHTML = '<option value="vw">vw</option><option value="px">px</option>';
|
| 470 |
+
unitSel.value = it.widthUnit || 'vw';
|
| 471 |
+
|
| 472 |
+
widthIn.addEventListener('change', () => {
|
| 473 |
+
it.width = Number(widthIn.value) || (it.widthUnit === 'px' ? 320 : 25);
|
| 474 |
+
renderItems(); markDirty();
|
| 475 |
+
});
|
| 476 |
+
unitSel.addEventListener('change', () => {
|
| 477 |
+
it.widthUnit = unitSel.value;
|
| 478 |
+
renderItems(); markDirty();
|
| 479 |
+
});
|
| 480 |
|
| 481 |
const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
|
| 482 |
btnDel.addEventListener('click', ()=> removeItem(it.id));
|
| 483 |
|
| 484 |
+
actions.append(btnPlace, btnPreview, widthIn, unitSel, btnDel);
|
| 485 |
row.append(kind, info, actions); list.appendChild(row);
|
| 486 |
}
|
| 487 |
}
|
|
|
|
| 507 |
if (!res.ok) throw new Error('Load failed');
|
| 508 |
const data = await res.json();
|
| 509 |
state.background = data.background || null;
|
| 510 |
+
state.items = Array.isArray(data.items)
|
| 511 |
+
? data.items.map(x => ({
|
| 512 |
+
open:false,
|
| 513 |
+
...x,
|
| 514 |
+
width: (x.width != null) ? x.width : (x.widthPct != null ? x.widthPct : 25),
|
| 515 |
+
widthUnit: x.widthUnit || (x.widthPct != null ? 'vw' : 'vw')
|
| 516 |
+
}))
|
| 517 |
+
: [];
|
| 518 |
renderBackground(); renderItems();
|
| 519 |
saveBadge.textContent = 'Loaded'; setTimeout(()=> saveBadge.textContent='Saved', 700);
|
| 520 |
}catch(e){
|