ntdservices commited on
Commit
708b0c0
·
verified ·
1 Parent(s): 30314cf

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +100 -75
static/index.html CHANGED
@@ -14,10 +14,9 @@
14
  *{box-sizing:border-box}
15
  html,body{height:100%; margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
16
 
17
- /* App container just hosts overlays; stage lives full-screen behind */
18
  #app{position:relative; height:100vh; width:100vw; overflow:hidden}
19
 
20
- /* Stage is full-viewport, underlays everything (so BG truly covers screen) */
21
  #stage{position:fixed; inset:0; width:100vw; height:100vh; overflow:hidden; background:#000; user-select:none; z-index:0}
22
  .bgmedia{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0}
23
  #overlay{position:absolute; inset:0; z-index:2}
@@ -32,21 +31,18 @@
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}
@@ -60,7 +56,7 @@
60
  .badge{display:inline-block; padding:2px 8px; border-radius:999px; font-size:.75rem; background:#1b2b54; color:#cfe0ff}
61
  .hr{height:1px; background:#203059; margin:12px 0}
62
 
63
- /* Panel toggle sits near the left edge whether expanded or collapsed */
64
  #toggle{
65
  position:fixed; top:12px; left:calc(var(--panelW) + 8px);
66
  z-index:6; width:36px; height:36px; border-radius:10px; display:grid; place-items:center;
@@ -71,47 +67,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
- /* 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 */
105
  .popup{
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}
114
-
115
  /* Items list */
116
  .item{display:grid; grid-template-columns:auto 1fr auto; gap:8px; align-items:center; padding:8px; border:1px solid #21345d; border-radius:10px; margin:8px 0; background:#0e1834}
117
  .item .name{font-size:.9rem}
@@ -199,7 +182,8 @@
199
 
200
  <main id="stage" aria-label="Stage">
201
  <img id="bgImg" class="bgmedia" alt="" style="display:none" />
202
- <video id="bgVid" class="bgmedia" autoplay muted loop playsinline style="display:none"></video>
 
203
  <div id="overlay" title="Click to place when in placement mode"></div>
204
  <div id="stageMsg" style="display:none"></div>
205
  </main>
@@ -221,6 +205,8 @@
221
  const stageMsg = $('#stageMsg');
222
  const saveBadge = $('#saveState');
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}
@@ -234,24 +220,15 @@
234
  };
235
  })();
236
 
237
- // Panel toggle
238
- const updateToggleIcon = () => {
239
- toggle.textContent = panel.classList.contains('min') ? '▶' : '◀';
240
- };
241
- toggle.addEventListener('click', () => {
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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
251
 
252
  async function fileToDataURL(file){
253
  if (!file) return '';
254
- const maxInlineMB = 40; // guardrail
255
  if (file.size > maxInlineMB * 1024 * 1024) { alert(`File is larger than ${maxInlineMB} MB. Use a URL instead.`); return ''; }
256
  return new Promise((res,rej)=>{
257
  const r = new FileReader();
@@ -261,7 +238,6 @@
261
  });
262
  }
263
 
264
- // Infer a video MIME type from URL (rough guess)
265
  function guessVideoMime(src){
266
  try{
267
  if (src.startsWith('data:')) {
@@ -272,12 +248,18 @@
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;
283
  const fit = $('#bgFit').value;
@@ -301,17 +283,49 @@
301
  renderBackground(); markDirty();
302
  });
303
 
 
 
 
 
304
  function renderBackground(){
305
- if (!state.background){
306
- bgImg.style.display='none'; bgVid.style.display='none'; return;
307
- }
 
 
 
 
 
 
308
  const {type, src, fit} = state.background;
309
  bgImg.style.objectFit = fit; bgVid.style.objectFit = fit;
310
- if (type === 'video'){ bgVid.src = src; bgVid.style.display='block'; bgImg.style.display='none'; }
311
- else { bgImg.src = src; bgImg.style.display='block'; bgVid.style.display='none'; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
 
314
- // Items
315
  $('#addItem').addEventListener('click', () => addOrPlace(false));
316
  $('#addAndPlace').addEventListener('click', () => addOrPlace(true));
317
 
@@ -321,7 +335,7 @@
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; }
@@ -361,16 +375,15 @@
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
  }
371
 
372
  function adjustPopupPosition(el){
373
- // Keep centered popup fully within viewport by nudging
374
  const rect = el.getBoundingClientRect();
375
  const margin = 8; let dx=0, dy=0;
376
  if (rect.left < margin) dx = margin - rect.left;
@@ -409,33 +422,46 @@
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
 
@@ -540,7 +566,7 @@
540
  URL.revokeObjectURL(url);
541
  });
542
 
543
- // Misc
544
  window.addEventListener('resize', ()=>{
545
  $$('.popup').forEach(el => {
546
  el.style.transform='translate(-50%, -50%)';
@@ -548,12 +574,11 @@
548
  });
549
  });
550
  stage.addEventListener('click', e => {
551
- if (state.placingId) return; // placement handled on overlay
552
  const inPopup = e.target.closest('.popup,.pin');
553
  if (!inPopup){ state.items.forEach(x=>x.open=false); renderItems(); }
554
  });
555
 
556
- // Boot
557
  loadFromServer();
558
  })();
559
  </script>
 
14
  *{box-sizing:border-box}
15
  html,body{height:100%; margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif}
16
 
 
17
  #app{position:relative; height:100vh; width:100vw; overflow:hidden}
18
 
19
+ /* Stage is full-viewport and underlays everything */
20
  #stage{position:fixed; inset:0; width:100vw; height:100vh; overflow:hidden; background:#000; user-select:none; z-index:0}
21
  .bgmedia{position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0}
22
  #overlay{position:absolute; inset:0; z-index:2}
 
31
  transform:translateX(0);
32
  }
33
  #panel.min{
 
34
  transform: translateX(-100%);
 
 
35
  overflow: hidden;
36
  border-right: none;
37
+ background: transparent;
38
+ box-shadow: none;
39
+ pointer-events: none;
40
  }
41
  #panel h2{margin:.3rem 0 1rem; font-size:1.05rem; color:#cfe0ff}
42
  fieldset{border:1px solid #2a395f; border-radius:12px; padding:12px; margin:10px 0}
43
  legend{font-size:.9rem; color:#bcd0ff; padding:0 6px}
44
  label{display:block; font-size:.85rem; color:var(--muted); margin:8px 0 4px}
45
+ input[type="text"], input[type="url"], select{width:100%; padding:10px 12px; border-radius:10px; border:1px solid #26365a; background:#0c1430; color:var(--text)}
46
  input[type="number"]{width:100%; padding:10px 12px; border-radius:10px; border:1px solid #26365a; background:#0c1430; color:#e6edf7}
47
  .row{display:flex; gap:8px}
48
  .row>*{flex:1}
 
56
  .badge{display:inline-block; padding:2px 8px; border-radius:999px; font-size:.75rem; background:#1b2b54; color:#cfe0ff}
57
  .hr{height:1px; background:#203059; margin:12px 0}
58
 
59
+ /* Panel toggle */
60
  #toggle{
61
  position:fixed; top:12px; left:calc(var(--panelW) + 8px);
62
  z-index:6; width:36px; height:36px; border-radius:10px; display:grid; place-items:center;
 
67
 
68
  #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}
69
  #panel.min ~ #stage #saveState, #panel.min ~ #saveState { left:28px; }
 
70
  #panel.min ~ #saveState { display: none; }
71
 
72
  /* Pins (bigger + more transparent) */
73
  .pin{
74
+ position:absolute; left:0; top:0;
75
+ width:34px; height:34px; border-radius:50%;
 
 
76
  border:2px solid rgba(255,255,255,.45);
77
+ background:rgba(255,255,255,.12);
78
  box-shadow:0 2px 10px rgba(0,0,0,.45);
79
  transform:translate(-50%, -50%);
80
+ z-index:3; cursor:pointer; display:block; padding:0; line-height:0;
81
+ -webkit-appearance:none; appearance:none;
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
+ .pin::before{content:""; position:absolute; inset:-10px; border-radius:50%;}
84
  .pin:hover{background:rgba(255,255,255,.26)}
85
 
86
+ /* Popups centered on pin midpoint */
87
  .popup{
88
  position:absolute; z-index:4;
89
+ transform: translate(-50%, -50%);
90
  background:rgba(13,20,40,.92); border:1px solid #2a395f; border-radius:16px; box-shadow:var(--shadow);
91
  padding:0; min-width:120px; max-width:none; backdrop-filter: blur(6px);
92
  }
 
93
  .media{display:block; width:100%; height:auto; border-radius:16px; overflow:hidden; background:#000}
94
+ /* Reserve space so videos don't appear as a thin line pre-metadata */
95
+ .media[data-kind="video"]{ aspect-ratio:16/9; min-height:100px; }
96
  .media video, .media img{display:block; width:100%; height:auto}
97
+
98
  /* Items list */
99
  .item{display:grid; grid-template-columns:auto 1fr auto; gap:8px; align-items:center; padding:8px; border:1px solid #21345d; border-radius:10px; margin:8px 0; background:#0e1834}
100
  .item .name{font-size:.9rem}
 
182
 
183
  <main id="stage" aria-label="Stage">
184
  <img id="bgImg" class="bgmedia" alt="" style="display:none" />
185
+ <!-- attrs for autoplay/loop/muted already present -->
186
+ <video id="bgVid" class="bgmedia" autoplay loop muted playsinline style="display:none"></video>
187
  <div id="overlay" title="Click to place when in placement mode"></div>
188
  <div id="stageMsg" style="display:none"></div>
189
  </main>
 
205
  const stageMsg = $('#stageMsg');
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'}
212
  items: [], // {id,type,title,src,x,y,width,widthUnit,open:false}
 
220
  };
221
  })();
222
 
223
+ const updateToggleIcon = () => { toggle.textContent = panel.classList.contains('min') ? '▶' : '◀'; };
224
+ toggle.addEventListener('click', () => { panel.classList.toggle('min'); updateToggleIcon(); });
225
+ panel.classList.add('min'); updateToggleIcon();
 
 
 
 
 
 
 
 
226
 
 
227
  function escapeHtml(s){return (s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
228
 
229
  async function fileToDataURL(file){
230
  if (!file) return '';
231
+ const maxInlineMB = 40;
232
  if (file.size > maxInlineMB * 1024 * 1024) { alert(`File is larger than ${maxInlineMB} MB. Use a URL instead.`); return ''; }
233
  return new Promise((res,rej)=>{
234
  const r = new FileReader();
 
238
  });
239
  }
240
 
 
241
  function guessVideoMime(src){
242
  try{
243
  if (src.startsWith('data:')) {
 
248
  if (u.endsWith('.mp4')) return 'video/mp4';
249
  if (u.endsWith('.webm')) return 'video/webm';
250
  if (u.endsWith('.ogv') || u.endsWith('.ogg')) return 'video/ogg';
251
+ if (u.endsWith('.mov')) return 'video/quicktime';
252
  if (u.endsWith('.m3u8')) return 'application/vnd.apple.mpegurl';
253
  }catch(e){}
254
  return '';
255
  }
256
 
257
+ function showStageMessage(msg, ms=2500){
258
+ stageMsg.textContent = msg; stageMsg.style.display='block';
259
+ setTimeout(()=>stageMsg.style.display='none', ms);
260
+ }
261
+
262
+ // ---------- Background ----------
263
  $('#applyBg').addEventListener('click', async () => {
264
  const type = $('#bgType').value;
265
  const fit = $('#bgFit').value;
 
283
  renderBackground(); markDirty();
284
  });
285
 
286
+ function cleanupHlsBg(){
287
+ if (hlsBg){ try{ hlsBg.destroy(); }catch(e){} hlsBg = null; }
288
+ }
289
+
290
  function renderBackground(){
291
+ cleanupHlsBg();
292
+ bgImg.style.display='none'; bgVid.style.display='none';
293
+
294
+ // clear previous <source> tags
295
+ bgVid.removeAttribute('src');
296
+ while (bgVid.firstChild) bgVid.removeChild(bgVid.firstChild);
297
+
298
+ if (!state.background) return;
299
+
300
  const {type, src, fit} = state.background;
301
  bgImg.style.objectFit = fit; bgVid.style.objectFit = fit;
302
+
303
+ if (type === 'video'){
304
+ bgVid.autoplay = true; bgVid.loop = true; bgVid.muted = true; bgVid.playsInline = true;
305
+ bgVid.crossOrigin = 'anonymous';
306
+
307
+ if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(src)) {
308
+ hlsBg = new Hls();
309
+ hlsBg.loadSource(src);
310
+ hlsBg.attachMedia(bgVid);
311
+ hlsBg.on(Hls.Events.MANIFEST_PARSED, () => { const p = bgVid.play(); if (p && p.catch) p.catch(()=>{}); });
312
+ } else {
313
+ const s = document.createElement('source');
314
+ s.src = src; const mime=guessVideoMime(src); if (mime) s.type=mime;
315
+ bgVid.appendChild(s);
316
+ bgVid.load();
317
+ const p = bgVid.play(); if (p && p.catch) p.catch(()=>{});
318
+ }
319
+ bgVid.style.display='block';
320
+ bgVid.addEventListener('error', () => {
321
+ showStageMessage('Background video failed to load. Try MP4 (H.264/AAC), WEBM, or HLS .m3u8.');
322
+ }, {once:true});
323
+ } else {
324
+ bgImg.src = src; bgImg.style.display='block';
325
+ }
326
  }
327
 
328
+ // ---------- Items ----------
329
  $('#addItem').addEventListener('click', () => addOrPlace(false));
330
  $('#addAndPlace').addEventListener('click', () => addOrPlace(true));
331
 
 
335
  const url = $('#itemUrl').value.trim();
336
  const file = $('#itemFile').files[0];
337
  const width = Number($('#itemWidth').value);
338
+ const widthUnit = $('#itemWidthUnit').value;
339
  let src = url;
340
 
341
  if (file){ src = await fileToDataURL(file); if (!src) return; }
 
375
  stopPlacing();
376
  });
377
 
378
+ // Keep multiple popups; toggling pin affects only that item
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){
 
387
  const rect = el.getBoundingClientRect();
388
  const margin = 8; let dx=0, dy=0;
389
  if (rect.left < margin) dx = margin - rect.left;
 
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
+ // No controls; looped; autoplay; muted for autoplay to work; inline for mobile
429
+ v.autoplay = true; v.loop = true; v.muted = true; v.playsInline = true;
430
+ v.crossOrigin = 'anonymous';
431
+ v.style.width = '100%'; v.style.height = 'auto';
432
+
 
 
433
  if (window.Hls && window.Hls.isSupported() && /\.m3u8($|\?)/i.test(it.src)) {
434
  const hls = new Hls();
435
  hls.loadSource(it.src);
436
  hls.attachMedia(v);
437
+ hls.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
+ v.addEventListener('error', () => {
451
+ const note = document.createElement('div');
452
+ note.style.padding = '10px 12px';
453
+ note.style.color = '#ffd2d2';
454
+ note.textContent = 'Video failed to load. Use MP4 (H.264/AAC), WEBM, or HLS (.m3u8).';
455
+ wrap.innerHTML = ''; wrap.appendChild(note);
456
+ }, {once:true});
457
+
458
  wrap.appendChild(v);
459
+ // Clicking video should NOT close; user may tap it; it loops anyway.
460
  } else {
461
  const img = document.createElement('img');
462
+ img.src = it.src; img.alt = it.title || '';
463
+ // Click image to close that popup
464
+ wrap.addEventListener('click', (e) => { e.stopPropagation(); it.open = false; renderItems(); });
465
  wrap.appendChild(img);
466
  }
467
 
 
566
  URL.revokeObjectURL(url);
567
  });
568
 
569
+ // Close all popups when clicking empty stage area
570
  window.addEventListener('resize', ()=>{
571
  $$('.popup').forEach(el => {
572
  el.style.transform='translate(-50%, -50%)';
 
574
  });
575
  });
576
  stage.addEventListener('click', e => {
577
+ if (state.placingId) return;
578
  const inPopup = e.target.closest('.popup,.pin');
579
  if (!inPopup){ state.items.forEach(x=>x.open=false); renderItems(); }
580
  });
581
 
 
582
  loadFromServer();
583
  })();
584
  </script>