ntdservices commited on
Commit
d323ba5
·
verified ·
1 Parent(s): 8d0720b

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +152 -103
static/index.html CHANGED
@@ -206,6 +206,7 @@
206
  const saveBadge = $('#saveState');
207
 
208
  let hlsBg = null; // background HLS instance
 
209
 
210
  const state = {
211
  background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
@@ -325,6 +326,101 @@
325
  }
326
  }
327
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  // ---------- Items ----------
329
  $('#addItem').addEventListener('click', () => addOrPlace(false));
330
  $('#addAndPlace').addEventListener('click', () => addOrPlace(true));
@@ -351,7 +447,14 @@
351
 
352
  function removeItem(id){
353
  const i = state.items.findIndex(x=>x.id===id);
354
- if (i>=0){ state.items.splice(i,1); renderItems(); markDirty(); }
 
 
 
 
 
 
 
355
  }
356
 
357
  function startPlacing(id){
@@ -370,7 +473,16 @@
370
  if (it){
371
  it.x = Math.max(0, Math.min(100, xPct));
372
  it.y = Math.max(0, Math.min(100, yPct));
373
- renderItems(); markDirty();
 
 
 
 
 
 
 
 
 
374
  }
375
  stopPlacing();
376
  });
@@ -379,8 +491,9 @@
379
  function togglePopup(id){
380
  const it = state.items.find(x=>x.id===id);
381
  if (!it) return;
382
- it.open = !it.open;
383
- renderItems();
 
384
  }
385
 
386
  function adjustPopupPosition(el){
@@ -394,110 +507,34 @@
394
  }
395
 
396
  function renderItems(){
397
- $('#count').textContent = String(state.items.length);
398
  overlay.innerHTML = '';
399
-
400
  for (const it of state.items){
401
- // PIN
402
- const pin = document.createElement('div');
403
- pin.className = 'pin';
404
- pin.setAttribute('role', 'button');
405
- pin.setAttribute('tabindex', '0');
406
- pin.style.left = it.x + '%';
407
- pin.style.top = it.y + '%';
408
- pin.title = it.title || it.type;
409
- pin.addEventListener('click', (e) => { e.stopPropagation(); togglePopup(it.id); });
410
- pin.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePopup(it.id); } });
411
- overlay.appendChild(pin);
412
-
413
- // POPUP
414
- if (it.open){
415
- const pop = document.createElement('div');
416
- pop.className = 'popup';
417
- pop.style.left = it.x + '%';
418
- pop.style.top = it.y + '%';
419
- pop.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
420
-
421
- const wrap = document.createElement('div');
422
- wrap.className = 'media';
423
-
424
- if (it.type === 'video') {
425
- wrap.setAttribute('data-kind','video'); // reserve space
426
-
427
- const v = document.createElement('video');
428
- v.autoplay = true; v.loop = true; v.muted = true; v.playsInline = true;
429
- v.crossOrigin = 'anonymous';
430
- v.style.width = '100%'; v.style.height = 'auto';
431
-
432
- let hlsInst = null;
433
- if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
434
- hlsInst = new Hls();
435
- hlsInst.loadSource(it.src);
436
- hlsInst.attachMedia(v);
437
- hlsInst.on(Hls.Events.MANIFEST_PARSED, () => { v.play().catch(()=>{}); });
438
- } else {
439
- const srcEl = document.createElement('source');
440
- srcEl.src = it.src;
441
- const mime = guessVideoMime(it.src); if (mime) srcEl.type = mime;
442
- v.appendChild(srcEl);
443
- v.load();
444
- v.play().catch(()=>{});
445
- }
446
-
447
- v.addEventListener('loadedmetadata', () => {
448
- if (v.videoWidth && v.videoHeight) wrap.style.aspectRatio = (v.videoWidth / v.videoHeight).toString();
449
- });
450
-
451
- // CLOSE THIS POPUP when user clicks the small video
452
- const closeSelf = (e) => {
453
- e.stopPropagation();
454
- it.open = false;
455
- try { v.pause(); } catch {}
456
- if (hlsInst) { try { hlsInst.destroy(); } catch {} }
457
- renderItems();
458
- };
459
- // Click anywhere on the video area to close
460
- wrap.addEventListener('click', closeSelf);
461
- v.addEventListener('click', closeSelf);
462
-
463
- v.addEventListener('error', () => {
464
- const note = document.createElement('div');
465
- note.style.padding = '10px 12px';
466
- note.style.color = '#ffd2d2';
467
- note.textContent = 'Video failed to load. Use MP4 (H.264/AAC), WEBM, or HLS (.m3u8).';
468
- wrap.innerHTML = ''; wrap.appendChild(note);
469
- }, {once:true});
470
-
471
- wrap.appendChild(v);
472
- } else {
473
- const img = document.createElement('img');
474
- img.src = it.src; img.alt = it.title || '';
475
- // Click image to close that popup
476
- wrap.addEventListener('click', (e) => { e.stopPropagation(); it.open = false; renderItems(); });
477
- wrap.appendChild(img);
478
- }
479
-
480
- pop.appendChild(wrap);
481
- overlay.appendChild(pop);
482
- requestAnimationFrame(() => adjustPopupPosition(pop));
483
- }
484
  }
 
 
485
 
486
- // items list
 
487
  const list = $('#items'); list.innerHTML='';
488
  for (const it of state.items){
489
  const row = document.createElement('div'); row.className='item';
490
  const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
491
  const info = document.createElement('div');
 
 
492
  info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
493
- <div class="tiny">${it.width}${it.widthUnit} • (${it.x.toFixed(1)}%, ${it.y.toFixed(1)}%)</div>`;
494
  const actions = document.createElement('div'); actions.className='actions';
 
495
 
496
  const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
497
  btnPlace.addEventListener('click', ()=> startPlacing(it.id));
498
 
499
  const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
500
- btnPreview.addEventListener('click', ()=> togglePopup(it.id));
501
 
502
  const widthIn = document.createElement('input');
503
  widthIn.type = 'number'; widthIn.min = '10'; widthIn.max = '2000';
@@ -509,11 +546,21 @@ if (it.type === 'video') {
509
 
510
  widthIn.addEventListener('change', () => {
511
  it.width = Number(widthIn.value) || (it.widthUnit === 'px' ? 320 : 25);
512
- renderItems(); markDirty();
 
 
 
 
 
513
  });
514
  unitSel.addEventListener('change', () => {
515
  it.widthUnit = unitSel.value;
516
- renderItems(); markDirty();
 
 
 
 
 
517
  });
518
 
519
  const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
@@ -563,8 +610,13 @@ if (it.type === 'video') {
563
  // Clear / Download
564
  $('#clearAll').addEventListener('click', async () => {
565
  if (!confirm('Clear background and all items?')) return;
 
 
566
  state.background = null; state.items = [];
567
- renderBackground(); renderItems(); markDirty();
 
 
 
568
  });
569
 
570
  $('#downloadJson').addEventListener('click', () => {
@@ -578,20 +630,17 @@ if (it.type === 'video') {
578
  URL.revokeObjectURL(url);
579
  });
580
 
581
- // Close all popups when clicking empty stage area
582
  window.addEventListener('resize', ()=>{
583
  $$('.popup').forEach(el => {
584
  el.style.transform='translate(-50%, -50%)';
585
  requestAnimationFrame(()=>adjustPopupPosition(el));
586
  });
587
  });
588
- // Do not close popups when clicking outside.
589
- // (Placement mode still works via #overlay click handler.)
590
- stage.addEventListener('click', e => {
591
- if (state.placingId) return; // placement handled on overlay
592
- // intentionally do nothing so outside clicks don't close anything
593
- });
594
-
595
 
596
  loadFromServer();
597
  })();
 
206
  const saveBadge = $('#saveState');
207
 
208
  let hlsBg = null; // background HLS instance
209
+ const hlsMap = new Map(); // per-popup HLS instances
210
 
211
  const state = {
212
  background: null, // {type:'image'|'video', src:'data/http(s) URL', fit:'cover'|'contain'}
 
326
  }
327
  }
328
 
329
+ // ---------- Incremental DOM helpers for items ----------
330
+ const getPinEl = (id) => overlay.querySelector(`.pin[data-id="${id}"]`);
331
+ const getPopupEl = (id) => overlay.querySelector(`.popup[data-id="${id}"]`);
332
+
333
+ function createPin(it){
334
+ const pin = document.createElement('div');
335
+ pin.className = 'pin';
336
+ pin.dataset.id = it.id;
337
+ pin.setAttribute('role', 'button');
338
+ pin.setAttribute('tabindex', '0');
339
+ pin.style.left = it.x + '%';
340
+ pin.style.top = it.y + '%';
341
+ pin.title = it.title || it.type;
342
+ pin.addEventListener('click', (e) => { e.stopPropagation(); togglePopup(it.id); });
343
+ pin.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); togglePopup(it.id); } });
344
+ overlay.appendChild(pin);
345
+ }
346
+
347
+ function createPopup(it){
348
+ if (getPopupEl(it.id)) return; // already open
349
+ const pop = document.createElement('div');
350
+ pop.className = 'popup';
351
+ pop.dataset.id = it.id;
352
+ pop.style.left = it.x + '%';
353
+ pop.style.top = it.y + '%';
354
+ pop.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
355
+
356
+ const wrap = document.createElement('div');
357
+ wrap.className = 'media';
358
+
359
+ if (it.type === 'video') {
360
+ wrap.setAttribute('data-kind','video'); // reserve space via CSS
361
+ const v = document.createElement('video');
362
+ v.autoplay = true; v.loop = true; v.muted = true; v.playsInline = true;
363
+ v.crossOrigin = 'anonymous';
364
+ v.style.width = '100%'; v.style.height = 'auto';
365
+
366
+ let hlsInst = null;
367
+ if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
368
+ hlsInst = new Hls();
369
+ hlsInst.loadSource(it.src);
370
+ hlsInst.attachMedia(v);
371
+ hlsInst.on(Hls.Events.MANIFEST_PARSED, () => { v.play().catch(()=>{}); });
372
+ hlsMap.set(it.id, hlsInst);
373
+ } else {
374
+ const srcEl = document.createElement('source');
375
+ srcEl.src = it.src;
376
+ const mime = guessVideoMime(it.src); if (mime) srcEl.type = mime;
377
+ v.appendChild(srcEl);
378
+ v.load();
379
+ v.play().catch(()=>{});
380
+ }
381
+
382
+ v.addEventListener('loadedmetadata', () => {
383
+ if (v.videoWidth && v.videoHeight) wrap.style.aspectRatio = (v.videoWidth / v.videoHeight).toString();
384
+ });
385
+
386
+ // Close THIS popup on click (don’t touch others)
387
+ const closeSelf = (e) => { e.stopPropagation(); removePopup(it.id); it.open = false; };
388
+ wrap.addEventListener('click', closeSelf);
389
+ v.addEventListener('click', closeSelf);
390
+
391
+ v.addEventListener('error', () => {
392
+ const note = document.createElement('div');
393
+ note.style.padding = '10px 12px';
394
+ note.style.color = '#ffd2d2';
395
+ note.textContent = 'Video failed to load. Use MP4 (H.264/AAC), WEBM, or HLS (.m3u8).';
396
+ wrap.innerHTML = ''; wrap.appendChild(note);
397
+ }, {once:true});
398
+
399
+ wrap.appendChild(v);
400
+ } else {
401
+ const img = document.createElement('img');
402
+ img.src = it.src; img.alt = it.title || '';
403
+ // Close THIS popup on click
404
+ wrap.addEventListener('click', (e) => { e.stopPropagation(); removePopup(it.id); it.open = false; });
405
+ wrap.appendChild(img);
406
+ }
407
+
408
+ pop.appendChild(wrap);
409
+ overlay.appendChild(pop);
410
+ requestAnimationFrame(() => adjustPopupPosition(pop));
411
+ }
412
+
413
+ function removePopup(id){
414
+ const el = getPopupEl(id);
415
+ if (!el) return;
416
+ // stop video & destroy HLS if present
417
+ const v = el.querySelector('video');
418
+ if (v){ try{ v.pause(); }catch{} }
419
+ const hlsInst = hlsMap.get(id);
420
+ if (hlsInst){ try{ hlsInst.destroy(); }catch{} hlsMap.delete(id); }
421
+ el.remove();
422
+ }
423
+
424
  // ---------- Items ----------
425
  $('#addItem').addEventListener('click', () => addOrPlace(false));
426
  $('#addAndPlace').addEventListener('click', () => addOrPlace(true));
 
447
 
448
  function removeItem(id){
449
  const i = state.items.findIndex(x=>x.id===id);
450
+ if (i>=0){
451
+ const it = state.items[i];
452
+ removePopup(id);
453
+ const pin = getPinEl(id); if (pin) pin.remove();
454
+ state.items.splice(i,1);
455
+ renderItemsList(); // refresh list & count only
456
+ markDirty();
457
+ }
458
  }
459
 
460
  function startPlacing(id){
 
473
  if (it){
474
  it.x = Math.max(0, Math.min(100, xPct));
475
  it.y = Math.max(0, Math.min(100, yPct));
476
+ // update only that pin/popup
477
+ const pinEl = getPinEl(it.id);
478
+ if (pinEl){ pinEl.style.left = it.x + '%'; pinEl.style.top = it.y + '%'; }
479
+ const popEl = getPopupEl(it.id);
480
+ if (popEl){
481
+ popEl.style.left = it.x + '%';
482
+ popEl.style.top = it.y + '%';
483
+ requestAnimationFrame(()=>adjustPopupPosition(popEl));
484
+ }
485
+ markDirty();
486
  }
487
  stopPlacing();
488
  });
 
491
  function togglePopup(id){
492
  const it = state.items.find(x=>x.id===id);
493
  if (!it) return;
494
+ if (it.open){ removePopup(id); it.open = false; }
495
+ else { it.open = true; createPopup(it); }
496
+ // no global re-render here, so other videos keep playing
497
  }
498
 
499
  function adjustPopupPosition(el){
 
507
  }
508
 
509
  function renderItems(){
510
+ // Initial (or full) rebuild of pins/popups + list
511
  overlay.innerHTML = '';
 
512
  for (const it of state.items){
513
+ createPin(it);
514
+ if (it.open) createPopup(it);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  }
516
+ renderItemsList();
517
+ }
518
 
519
+ function renderItemsList(){
520
+ $('#count').textContent = String(state.items.length);
521
  const list = $('#items'); list.innerHTML='';
522
  for (const it of state.items){
523
  const row = document.createElement('div'); row.className='item';
524
  const kind = document.createElement('div'); kind.className='badge'; kind.textContent = it.type.toUpperCase();
525
  const info = document.createElement('div');
526
+ const updateInfoTiny = () => info.querySelector('.tiny').textContent =
527
+ `${it.width}${it.widthUnit} • (${it.x.toFixed(1)}%, ${it.y.toFixed(1)}%)`;
528
  info.innerHTML = `<div class="name">${escapeHtml(it.title||'')}</div>
529
+ <div class="tiny"></div>`;
530
  const actions = document.createElement('div'); actions.className='actions';
531
+ updateInfoTiny();
532
 
533
  const btnPlace = document.createElement('button'); btnPlace.textContent='Place';
534
  btnPlace.addEventListener('click', ()=> startPlacing(it.id));
535
 
536
  const btnPreview = document.createElement('button'); btnPreview.textContent = it.open ? 'Hide' : 'Preview';
537
+ btnPreview.addEventListener('click', ()=> { togglePopup(it.id); btnPreview.textContent = it.open ? 'Hide' : 'Preview'; });
538
 
539
  const widthIn = document.createElement('input');
540
  widthIn.type = 'number'; widthIn.min = '10'; widthIn.max = '2000';
 
546
 
547
  widthIn.addEventListener('change', () => {
548
  it.width = Number(widthIn.value) || (it.widthUnit === 'px' ? 320 : 25);
549
+ const popEl = getPopupEl(it.id);
550
+ if (popEl){
551
+ popEl.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
552
+ requestAnimationFrame(()=>adjustPopupPosition(popEl));
553
+ }
554
+ updateInfoTiny(); markDirty();
555
  });
556
  unitSel.addEventListener('change', () => {
557
  it.widthUnit = unitSel.value;
558
+ const popEl = getPopupEl(it.id);
559
+ if (popEl){
560
+ popEl.style.width = (it.widthUnit === 'px' ? it.width + 'px' : it.width + 'vw');
561
+ requestAnimationFrame(()=>adjustPopupPosition(popEl));
562
+ }
563
+ updateInfoTiny(); markDirty();
564
  });
565
 
566
  const btnDel = document.createElement('button'); btnDel.textContent='Delete'; btnDel.className='btn-danger';
 
610
  // Clear / Download
611
  $('#clearAll').addEventListener('click', async () => {
612
  if (!confirm('Clear background and all items?')) return;
613
+ // close all popups, clean HLS
614
+ state.items.forEach(it => removePopup(it.id));
615
  state.background = null; state.items = [];
616
+ overlay.innerHTML = '';
617
+ renderItemsList();
618
+ renderBackground();
619
+ markDirty();
620
  });
621
 
622
  $('#downloadJson').addEventListener('click', () => {
 
630
  URL.revokeObjectURL(url);
631
  });
632
 
633
+ // Outside click: do nothing (don’t close popups)
634
  window.addEventListener('resize', ()=>{
635
  $$('.popup').forEach(el => {
636
  el.style.transform='translate(-50%, -50%)';
637
  requestAnimationFrame(()=>adjustPopupPosition(el));
638
  });
639
  });
640
+ stage.addEventListener('click', e => {
641
+ if (state.placingId) return; // placement handled on overlay
642
+ // intentionally no-op so outside clicks don't close anything
643
+ });
 
 
 
644
 
645
  loadFromServer();
646
  })();