tester343 commited on
Commit
ff1347c
·
verified ·
1 Parent(s): 41320de

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +100 -49
app_enhanced.py CHANGED
@@ -336,27 +336,30 @@ INDEX_HTML = '''
336
  z-index: 1;
337
  overflow: hidden;
338
  /* UPDATED: THICK WHITE BORDER */
339
- border: 10px solid white;
340
- box-shadow: 0 0 30px rgba(0,0,0,0.5);
 
341
  }
342
 
343
  /* HANDLES FOR DRAGGING */
344
  .handle {
345
  position: absolute;
346
- /* UPDATED: LARGER HANDLES */
347
- width: 24px; height: 24px;
348
  background: #ff4757;
349
  border: 3px solid white;
350
  border-radius: 50%;
351
  cursor: ew-resize;
352
  z-index: 999;
353
  transform: translate(-50%, -50%);
354
- box-shadow: 0 2px 5px rgba(0,0,0,0.3);
 
355
  }
 
356
  .handle.horiz {
357
  cursor: ns-resize;
358
- width: 60px; height: 18px;
359
- border-radius: 9px;
360
  }
361
 
362
  /* PANELS */
@@ -383,10 +386,12 @@ INDEX_HTML = '''
383
  position: absolute; display: flex; justify-content: center; align-items: center;
384
  width: auto; height: auto; min-width: 80px; min-height: 50px; max-width: 250px;
385
  box-sizing: border-box;
 
386
  z-index: 500;
387
  cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
388
  font-size: 13px; text-align: center;
389
  line-height: 1.2; --tail-pos: 50%; padding: 5px;
 
390
  }
391
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
392
 
@@ -516,8 +521,11 @@ INDEX_HTML = '''
516
  let currentSaveCode = null;
517
  let isProcessing = false;
518
  let interval, selectedBubble = null, selectedPanel = null;
519
- let isDragging = false, isResizing = false, isPanning = false;
520
- let startX, startY, initX, initY, panStartX, panStartY, panStartTx, panStartTy;
 
 
 
521
  let activeLayoutHandle = null, activePageId = null;
522
 
523
  const DEFAULT_LAYOUT = {
@@ -632,6 +640,7 @@ INDEX_HTML = '''
632
  div.id = pageId;
633
  div.dataset.layout = JSON.stringify(layout);
634
 
 
635
  for(let i=0; i<4; i++) {
636
  const pDiv = document.createElement('div');
637
  pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
@@ -647,6 +656,7 @@ INDEX_HTML = '''
647
  div.appendChild(pDiv);
648
  }
649
 
 
650
  const handles = [
651
  {id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0},
652
  {id: 'h2-top', t:0, l:0}, {id: 'h2-bot', t:0, l:0},
@@ -715,42 +725,97 @@ INDEX_HTML = '''
715
  setH('h-tier', w/2, ty);
716
  }
717
 
 
 
718
  function startDragHandle(e, pageId, handleRole) {
719
  e.stopPropagation(); e.preventDefault();
720
- activeLayoutHandle = handleRole;
721
- activePageId = pageId;
722
- isDragging = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
723
  }
724
 
 
725
  document.addEventListener('mousemove', (e) => {
726
- if(isDragging && activeLayoutHandle && activePageId) {
727
- const pageEl = document.getElementById(activePageId);
 
 
728
  const rect = pageEl.getBoundingClientRect();
729
  const state = JSON.parse(pageEl.dataset.layout);
730
  const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
731
  const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
 
732
 
733
- if(activeLayoutHandle === 'h1-top') state.r1.topX = x;
734
- else if(activeLayoutHandle === 'h1-bot') state.r1.botX = x;
735
- else if(activeLayoutHandle === 'h2-top') state.r2.topX = x;
736
- else if(activeLayoutHandle === 'h2-bot') state.r2.botX = x;
737
- else if(activeLayoutHandle === 'h-tier') state.tierY = y;
738
 
739
  pageEl.dataset.layout = JSON.stringify(state);
740
- drawLayout(activePageId);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
741
  }
742
- else if(isDragging && selectedBubble) {
743
- selectedBubble.style.left = (initX + e.clientX - startX) + 'px';
744
- selectedBubble.style.top = (initY + e.clientY - startY) + 'px';
745
- }
746
- else if(isResizing && selectedBubble) { resizeBubble(e); }
747
- else if(isPanning && selectedPanel) { panImage(e); }
748
  });
749
 
750
  document.addEventListener('mouseup', () => {
751
- if(activeLayoutHandle) { activeLayoutHandle = null; activePageId = null; saveDraft(true); }
752
- if(isDragging || isResizing || isPanning) { saveDraft(true); }
753
- isDragging = false; isResizing = false; isPanning = false;
 
 
754
  });
755
 
756
  function updateGutter(val) {
@@ -773,13 +838,12 @@ INDEX_HTML = '''
773
  return;
774
  }
775
 
776
- // CONTAIN LOGIC: Ensure WHOLE image fits
777
  const pW = 1000;
778
  const pH = panel.offsetHeight;
779
  const iW = img.naturalWidth || img.width;
780
  const iH = img.naturalHeight || img.height;
781
 
782
- // Math.min makes sure it fits fully (contain)
783
  const scale = Math.min(pW / iW, pH / iH);
784
 
785
  const tx = (pW - iW * scale) / 2;
@@ -802,21 +866,9 @@ INDEX_HTML = '''
802
  img.classList.toggle('pannable', true);
803
  }
804
 
805
- function startPan(e, img) { e.preventDefault(); isPanning = true; selectedPanel = img.closest('.panel'); panStartX = e.clientX; panStartY = e.clientY; panStartTx = parseFloat(img.dataset.translateX || 0); panStartTy = parseFloat(img.dataset.translateY || 0); img.classList.add('panning'); }
806
- function panImage(e) { if(!isPanning || !selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.translateX = panStartTx + (e.clientX - panStartX); img.dataset.translateY = panStartTy + (e.clientY - panStartY); updateImageTransform(img); }
807
  function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
808
  function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
809
 
810
- function selectPanel(el) {
811
- if(selectedPanel) selectedPanel.classList.remove('selected');
812
- if(selectedBubble) { selectedBubble.classList.remove('selected'); selectedBubble = null; }
813
- selectedPanel = el; el.classList.add('selected');
814
- document.getElementById('zoom-slider').disabled = false;
815
- const img = el.querySelector('img');
816
- document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
817
- document.getElementById('bubble-type-select').disabled = true; document.getElementById('font-select').disabled = true;
818
- }
819
-
820
  function createBubbleHTML(data) {
821
  const b = document.createElement('div');
822
  const type = data.type || 'speech';
@@ -830,14 +882,15 @@ INDEX_HTML = '''
830
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
831
  if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
832
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
833
- b.onmousedown = (e) => {
834
- if(e.target.classList.contains('resize-handle')) return;
835
- e.stopPropagation(); selectBubble(b); isDragging = true; startX = e.clientX; startY = e.clientY; initX = b.offsetLeft; initY = b.offsetTop;
836
- };
837
  b.onclick = (e) => { e.stopPropagation(); };
838
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
839
  return b;
840
  }
 
841
  function selectBubble(el) { if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; } selectedBubble = el; el.classList.add('selected'); document.getElementById('bubble-type-select').disabled = false; document.getElementById('font-select').disabled = false; document.getElementById('bubble-text-color').disabled = false; document.getElementById('bubble-fill-color').disabled = false; document.getElementById('bubble-type-select').value = el.dataset.type || 'speech'; }
842
  function addBubble() { if(!selectedPanel) return alert("Select a panel (click an image) to add bubble to that page."); const pageDiv = selectedPanel.closest('.comic-page'); const b = createBubbleHTML({ text: "Text", left: "100px", top: "100px", type: 'speech', classes: "speech-bubble speech tail-bottom" }); pageDiv.appendChild(b); selectBubble(b); saveDraft(true); }
843
  function deleteBubble() { if(!selectedBubble) return alert("Select a bubble"); selectedBubble.remove(); selectedBubble=null; saveDraft(true); }
@@ -847,8 +900,6 @@ INDEX_HTML = '''
847
  function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
848
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
849
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
850
- function startResize(e, dir) { e.preventDefault(); e.stopPropagation(); isResizing = true; resizeHandle = dir; const rect = selectedBubble.getBoundingClientRect(); originalWidth = rect.width; originalHeight = rect.height; originalMouseX = e.clientX; originalMouseY = e.clientY; }
851
- function resizeBubble(e) { if (!isResizing || !selectedBubble) return; const dx = e.clientX - originalMouseX; const dy = e.clientY - originalMouseY; if(resizeHandle.includes('e')) selectedBubble.style.width = (originalWidth + dx)+'px'; if(resizeHandle.includes('s')) selectedBubble.style.height = (originalHeight + dy)+'px'; }
852
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
853
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
854
  async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
 
336
  z-index: 1;
337
  overflow: hidden;
338
  /* UPDATED: THICK WHITE BORDER */
339
+ border: 12px solid white;
340
+ box-shadow: 0 0 40px rgba(0,0,0,0.5);
341
+ box-sizing: border-box;
342
  }
343
 
344
  /* HANDLES FOR DRAGGING */
345
  .handle {
346
  position: absolute;
347
+ /* UPDATED: MUCH LARGER HANDLES (30px) */
348
+ width: 30px; height: 30px;
349
  background: #ff4757;
350
  border: 3px solid white;
351
  border-radius: 50%;
352
  cursor: ew-resize;
353
  z-index: 999;
354
  transform: translate(-50%, -50%);
355
+ box-shadow: 0 4px 8px rgba(0,0,0,0.4);
356
+ transition: transform 0.1s;
357
  }
358
+ .handle:hover { transform: translate(-50%, -50%) scale(1.1); }
359
  .handle.horiz {
360
  cursor: ns-resize;
361
+ width: 80px; height: 20px;
362
+ border-radius: 10px;
363
  }
364
 
365
  /* PANELS */
 
386
  position: absolute; display: flex; justify-content: center; align-items: center;
387
  width: auto; height: auto; min-width: 80px; min-height: 50px; max-width: 250px;
388
  box-sizing: border-box;
389
+ /* UPDATED Z-INDEX TO ENSURE DRAGGABILITY */
390
  z-index: 500;
391
  cursor: move; font-family: 'Comic Neue', cursive; font-weight: bold;
392
  font-size: 13px; text-align: center;
393
  line-height: 1.2; --tail-pos: 50%; padding: 5px;
394
+ user-select: none;
395
  }
396
  .speech-bubble.selected { outline: 2px dashed #4CAF50; z-index: 501; }
397
 
 
521
  let currentSaveCode = null;
522
  let isProcessing = false;
523
  let interval, selectedBubble = null, selectedPanel = null;
524
+
525
+ // DRAG MODES
526
+ let dragMode = null; // 'layout', 'bubble', 'pan', 'resize-bubble'
527
+ let dragData = {}; // Stores offsets and active IDs
528
+
529
  let activeLayoutHandle = null, activePageId = null;
530
 
531
  const DEFAULT_LAYOUT = {
 
640
  div.id = pageId;
641
  div.dataset.layout = JSON.stringify(layout);
642
 
643
+ // Generate 4 Panels
644
  for(let i=0; i<4; i++) {
645
  const pDiv = document.createElement('div');
646
  pDiv.className = 'panel'; pDiv.id = `${pageId}-p${i}`;
 
656
  div.appendChild(pDiv);
657
  }
658
 
659
+ // Generate Handles
660
  const handles = [
661
  {id: 'h1-top', t:0, l:0}, {id: 'h1-bot', t:0, l:0},
662
  {id: 'h2-top', t:0, l:0}, {id: 'h2-bot', t:0, l:0},
 
725
  setH('h-tier', w/2, ty);
726
  }
727
 
728
+ // --- INTERACTION LOGIC (FIXED) ---
729
+
730
  function startDragHandle(e, pageId, handleRole) {
731
  e.stopPropagation(); e.preventDefault();
732
+ dragMode = 'layout';
733
+ dragData = { handle: handleRole, pageId: pageId };
734
+ }
735
+
736
+ function startBubbleDrag(e, bubble) {
737
+ if(e.target.classList.contains('resize-handle')) return;
738
+ e.stopPropagation(); e.preventDefault();
739
+ selectBubble(bubble);
740
+ dragMode = 'bubble';
741
+ dragData = {
742
+ el: bubble,
743
+ startX: e.clientX, startY: e.clientY,
744
+ initX: bubble.offsetLeft, initY: bubble.offsetTop
745
+ };
746
+ }
747
+
748
+ function startResize(e, dir) {
749
+ e.stopPropagation(); e.preventDefault();
750
+ dragMode = 'resize-bubble';
751
+ const rect = selectedBubble.getBoundingClientRect();
752
+ dragData = {
753
+ el: selectedBubble, dir: dir,
754
+ startX: e.clientX, startY: e.clientY,
755
+ startW: rect.width, startH: rect.height
756
+ };
757
+ }
758
+
759
+ function startPan(e, img) {
760
+ e.preventDefault();
761
+ selectPanel(img.closest('.panel'));
762
+ dragMode = 'pan';
763
+ dragData = {
764
+ el: img,
765
+ startX: e.clientX, startY: e.clientY,
766
+ startTx: parseFloat(img.dataset.translateX || 0),
767
+ startTy: parseFloat(img.dataset.translateY || 0)
768
+ };
769
+ img.classList.add('panning');
770
  }
771
 
772
+ // GLOBAL MOUSEMOVE
773
  document.addEventListener('mousemove', (e) => {
774
+ if(!dragMode) return;
775
+
776
+ if(dragMode === 'layout') {
777
+ const pageEl = document.getElementById(dragData.pageId);
778
  const rect = pageEl.getBoundingClientRect();
779
  const state = JSON.parse(pageEl.dataset.layout);
780
  const x = Math.max(0, Math.min(1000, (e.clientX - rect.left) * (1000 / rect.width)));
781
  const y = Math.max(50, Math.min(650, (e.clientY - rect.top) * (700 / rect.height)));
782
+ const role = dragData.handle;
783
 
784
+ if(role === 'h1-top') state.r1.topX = x;
785
+ else if(role === 'h1-bot') state.r1.botX = x;
786
+ else if(role === 'h2-top') state.r2.topX = x;
787
+ else if(role === 'h2-bot') state.r2.botX = x;
788
+ else if(role === 'h-tier') state.tierY = y;
789
 
790
  pageEl.dataset.layout = JSON.stringify(state);
791
+ drawLayout(dragData.pageId);
792
+ }
793
+ else if(dragMode === 'bubble') {
794
+ const d = dragData;
795
+ d.el.style.left = (d.initX + e.clientX - d.startX) + 'px';
796
+ d.el.style.top = (d.initY + e.clientY - d.startY) + 'px';
797
+ }
798
+ else if(dragMode === 'resize-bubble') {
799
+ const d = dragData;
800
+ const dx = e.clientX - d.startX;
801
+ const dy = e.clientY - d.startY;
802
+ if(d.dir.includes('e')) d.el.style.width = (d.startW + dx)+'px';
803
+ if(d.dir.includes('s')) d.el.style.height = (d.startH + dy)+'px';
804
+ }
805
+ else if(dragMode === 'pan') {
806
+ const d = dragData;
807
+ d.el.dataset.translateX = d.startTx + (e.clientX - d.startX);
808
+ d.el.dataset.translateY = d.startTy + (e.clientY - d.startY);
809
+ updateImageTransform(d.el);
810
  }
 
 
 
 
 
 
811
  });
812
 
813
  document.addEventListener('mouseup', () => {
814
+ if(dragMode) {
815
+ if(dragMode === 'pan' && dragData.el) dragData.el.classList.remove('panning');
816
+ dragMode = null;
817
+ saveDraft(true);
818
+ }
819
  });
820
 
821
  function updateGutter(val) {
 
838
  return;
839
  }
840
 
 
841
  const pW = 1000;
842
  const pH = panel.offsetHeight;
843
  const iW = img.naturalWidth || img.width;
844
  const iH = img.naturalHeight || img.height;
845
 
846
+ // CONTAIN LOGIC: Ensure WHOLE image fits
847
  const scale = Math.min(pW / iW, pH / iH);
848
 
849
  const tx = (pW - iW * scale) / 2;
 
866
  img.classList.toggle('pannable', true);
867
  }
868
 
 
 
869
  function handleZoom(el) { if(!selectedPanel) return; const img = selectedPanel.querySelector('img'); img.dataset.zoom = el.value; updateImageTransform(img); }
870
  function resetPanelTransform() { if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); fitImageToPanel(img, selectedPanel, null); saveDraft(true); }
871
 
 
 
 
 
 
 
 
 
 
 
872
  function createBubbleHTML(data) {
873
  const b = document.createElement('div');
874
  const type = data.type || 'speech';
 
882
  const textSpan = document.createElement('span'); textSpan.className = 'bubble-text'; textSpan.textContent = data.text || ''; b.appendChild(textSpan);
883
  if(type === 'thought') { for(let i=1; i<=2; i++){ const d = document.createElement('div'); d.className = `thought-dot thought-dot-${i}`; b.appendChild(d); } }
884
  ['nw', 'ne', 'sw', 'se'].forEach(dir => { const handle = document.createElement('div'); handle.className = `resize-handle ${dir}`; handle.onmousedown = (e) => startResize(e, dir); b.appendChild(handle); });
885
+
886
+ // BUBBLE DRAG HANDLER
887
+ b.onmousedown = (e) => startBubbleDrag(e, b);
888
+
889
  b.onclick = (e) => { e.stopPropagation(); };
890
  b.ondblclick = (e) => { e.stopPropagation(); editBubbleText(b); };
891
  return b;
892
  }
893
+
894
  function selectBubble(el) { if(selectedBubble) selectedBubble.classList.remove('selected'); if(selectedPanel) { selectedPanel.classList.remove('selected'); selectedPanel = null; } selectedBubble = el; el.classList.add('selected'); document.getElementById('bubble-type-select').disabled = false; document.getElementById('font-select').disabled = false; document.getElementById('bubble-text-color').disabled = false; document.getElementById('bubble-fill-color').disabled = false; document.getElementById('bubble-type-select').value = el.dataset.type || 'speech'; }
895
  function addBubble() { if(!selectedPanel) return alert("Select a panel (click an image) to add bubble to that page."); const pageDiv = selectedPanel.closest('.comic-page'); const b = createBubbleHTML({ text: "Text", left: "100px", top: "100px", type: 'speech', classes: "speech-bubble speech tail-bottom" }); pageDiv.appendChild(b); selectBubble(b); saveDraft(true); }
896
  function deleteBubble() { if(!selectedBubble) return alert("Select a bubble"); selectedBubble.remove(); selectedBubble=null; saveDraft(true); }
 
900
  function slideTail(v) { if(selectedBubble) { selectedBubble.style.setProperty('--tail-pos', v+'%'); saveDraft(true); } }
901
  document.getElementById('bubble-text-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveDraft(true); } });
902
  document.getElementById('bubble-fill-color').addEventListener('change', (e) => { if(selectedBubble) { selectedBubble.style.setProperty('--bubble-fill-color', e.target.value); saveDraft(true); } });
 
 
903
  function replacePanelImage() { if(!selectedPanel) return alert("Select a panel"); const inp = document.getElementById('image-uploader'); inp.onchange = async (e) => { const fd = new FormData(); fd.append('image', e.target.files[0]); const img = selectedPanel.querySelector('img'); const r = await fetch(`/replace_panel?sid=${sid}`, {method:'POST', body:fd}); const d = await r.json(); if(d.success) { img.src = `/frames/${d.new_filename}?sid=${sid}`; saveDraft(true); } inp.value = ''; }; inp.click(); }
904
  async function adjustFrame(dir) { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/regenerate_frame?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, direction:dir}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }
905
  async function gotoTimestamp() { if(isProcessing) return; if(!selectedPanel) return alert("Select a panel"); let v = document.getElementById('timestamp-input').value.trim(); if(!v) return; if(v.includes(':')) { let p = v.split(':'); v = parseInt(p[0]) * 60 + parseFloat(p[1]); } else { v = parseFloat(v); } if(isNaN(v)) return alert("Invalid time"); const img = selectedPanel.querySelector('img'); let fname = img.src.split('/').pop().split('?')[0]; setProcessing(true); img.style.opacity = '0.5'; try { const r = await fetch(`/goto_timestamp?sid=${sid}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({filename:fname, timestamp:v}) }); const d = await r.json(); if(d.success) { img.src = `/frames/${fname}?sid=${sid}&t=${Date.now()}`; document.getElementById('timestamp-input').value = ''; } else { alert('Error: ' + d.message); } } catch(e) { console.error(e); } img.style.opacity = '1'; setProcessing(false); saveDraft(true); }