Ubuntu commited on
Commit
1671832
·
1 Parent(s): 0d6e94b

Fixed more bugs ; qol changes

Browse files
templates/_revision_notes_preact.html CHANGED
@@ -81,6 +81,27 @@
81
  .crm-tool-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
82
  .crm-tool-btn { flex-direction: column; gap: 4px; padding: 10px; height: 60px; font-size: 0.75rem; }
83
  .crm-tool-btn i { font-size: 1.2rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  /* Canvas Viewport */
86
  .crm-viewport {
@@ -90,10 +111,10 @@
90
  width: 100%; height: 100%;
91
  background-image: linear-gradient(45deg, #1a1a1a 25%, transparent 25%), linear-gradient(-45deg, #1a1a1a 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1a1a1a 75%), linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
92
  background-size: 20px 20px; background-color: #111;
93
- position: relative;
94
  }
95
  .crm-canvas-container canvas {
96
- box-shadow: 0 20px 50px rgba(0,0,0,0.5); touch-action: none; display: block;
97
  }
98
  .crm-canvas-container.tool-pen canvas, .crm-canvas-container.tool-marker canvas { cursor: crosshair; }
99
  .crm-canvas-container.tool-eraser canvas { cursor: cell; }
@@ -137,6 +158,8 @@
137
  import htm from 'https://esm.sh/htm@3.1.1';
138
 
139
  const html = htm.bind(h);
 
 
140
 
141
  // === Core Math Utilities ===
142
  function distSqToSegment(p, v, w) {
@@ -290,6 +313,9 @@
290
  const [uploadedImages, setUploadedImages] = useState([]);
291
  const [history, setHistory] = useState([]);
292
  const [historyIndex, setHistoryIndex] = useState(-1);
 
 
 
293
 
294
  // Core Engine Refs
295
  const bgImageRef = useRef(null);
@@ -303,6 +329,29 @@
303
  const activePointerId = useRef(null);
304
  const dragState = useRef({ type: 'none', id: null });
305
  const isEraserActive = useRef(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  // --- Core Rendering Engine ---
308
  const renderCanvas = useCallback(() => {
@@ -313,7 +362,7 @@
313
  // 1. Base Layer
314
  ctx.fillStyle = '#ffffff';
315
  ctx.fillRect(0, 0, canvas.width, canvas.height);
316
- if (bgImageRef.current) ctx.drawImage(bgImageRef.current, 0, 0);
317
 
318
  // 2. Images Layer (MUST be below strokes)
319
  imagesRef.current.forEach(img => {
@@ -427,8 +476,25 @@
427
  const container = containerRef.current;
428
  if (!canvas || !container) return;
429
 
430
- canvas.width = container.clientWidth;
431
- canvas.height = container.clientHeight;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
 
433
  const fetchNote = async () => {
434
  try {
@@ -442,26 +508,18 @@
442
  })).filter(p => p.data));
443
  }
444
  if (data.success && data.image_data) {
445
- const img = new Image();
446
- img.crossOrigin = "anonymous";
447
- img.onload = () => {
448
- bgImageRef.current = img;
449
- renderCanvas();
450
- saveState();
451
- };
452
- img.src = data.image_data;
453
- } else saveState();
454
- } else saveState();
455
- } catch (e) { saveState(); }
456
  };
457
  fetchNote();
458
 
459
  const handleResize = () => {
460
- if (container.clientWidth !== canvas.width) {
461
- canvas.width = container.clientWidth;
462
- canvas.height = container.clientHeight;
463
- renderCanvas();
464
- }
465
  };
466
 
467
  const handleKeyDown = (e) => {
@@ -508,8 +566,17 @@
508
  // --- Interaction Handlers ---
509
  const handlePointerDown = useCallback((e) => {
510
  e.preventDefault();
 
 
 
 
 
 
 
 
511
  if (activePointerId.current !== null) return;
512
  activePointerId.current = e.pointerId;
 
513
 
514
  const rect = visibleCanvasRef.current.getBoundingClientRect();
515
  const x = e.clientX - rect.left;
@@ -551,10 +618,13 @@
551
  isEraserActive.current = true;
552
  executeStrokeEraser(x, y);
553
  } else {
554
- currentPathRef.current = { id: Date.now(), tool: tool, color: color, size: size, points: [{x, y}] };
 
 
 
555
  renderCanvas(); // Synchronous for immediate dot placement
556
  }
557
- }, [tool, color, size, renderCanvas, executeStrokeEraser]);
558
 
559
  const handlePointerMove = useCallback((e) => {
560
  if (activePointerId.current !== e.pointerId) return;
@@ -597,6 +667,10 @@
597
 
598
  const handlePointerUp = useCallback((e) => {
599
  if (activePointerId.current !== e.pointerId) return;
 
 
 
 
600
 
601
  if (currentPathRef.current) {
602
  pathsRef.current.push(currentPathRef.current);
@@ -625,6 +699,7 @@
625
  }, [renderCanvas, saveState]);
626
 
627
  const handleSave = useCallback(async () => {
 
628
  selectedFloatingIdRef.current = null;
629
  renderCanvas(); // Strip selection UI
630
 
@@ -632,6 +707,7 @@
632
  const imageData = canvas.toDataURL('image/png');
633
 
634
  try {
 
635
  const response = await fetch('/save_note_json', {
636
  method: 'POST',
637
  headers: { 'Content-Type': 'application/json' },
@@ -641,15 +717,17 @@
641
  const result = await response.json();
642
  if (result.success) {
643
  if (window.showStatus) window.showStatus('Notes saved!', 'success');
 
644
  onClose();
645
- setTimeout(() => location.reload(), 300);
646
  } else {
647
  if (window.showStatus) window.showStatus('Error: ' + result.error, 'danger');
648
  }
649
  } catch (e) {
650
  if (window.showStatus) window.showStatus(e.message, 'danger');
 
 
651
  }
652
- }, [imageId, sessionId, onClose, renderCanvas]);
653
 
654
  const handleImageToCanvas = useCallback((imgDataUrl) => {
655
  const img = new Image();
@@ -685,13 +763,16 @@
685
  <div class="crm-modal-overlay">
686
  <header class="crm-header">
687
  <div style="font-weight: 700; display: flex; align-items: center; gap: 10px;">
688
- <i class="bi bi-palette" style="color: var(--primary);"></i> ColorRM Notes Pro
689
  </div>
690
  <div style="display: flex; gap: 8px;">
691
- <button class="crm-btn crm-btn-primary" onClick=${handleSave}>
692
- <i class="bi bi-check2"></i> Save Changes
 
 
 
693
  </button>
694
- <button class="crm-btn" onClick=${onClose}>
695
  <i class="bi bi-x-lg"></i>
696
  </button>
697
  </div>
@@ -704,12 +785,19 @@
704
  <h4>Tools</h4>
705
  <div class="crm-tool-row">
706
  <${ToolButton} icon="bi bi-pen" label="Pen" active=${tool === 'pen'} onClick=${() => setTool('pen')} />
707
- <${ToolButton} icon="bi bi-highlighter" label="Marker" active=${tool === 'marker'} onClick=${() => setTool('marker')} />
708
- <${ToolButton} icon="bi bi-eraser" label="Stroke Eraser" active=${tool === 'eraser'} onClick=${() => setTool('eraser')} title="Deletes entire lines on click/touch" />
709
- <${ToolButton} icon="bi bi-arrows-move" label="Move Tool" active=${tool === 'move'} onClick=${() => setTool('move')} title="Select, move, and resize images" />
710
  </div>
711
 
712
- <div style="margin-top: 15px;">
 
 
 
 
 
 
 
713
  <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
714
  <span style="font-size: 0.8rem; color: var(--text-muted);">Brush Size</span>
715
  <span style="font-size: 0.8rem; color: var(--text-main); font-family: monospace;">${size}px</span>
@@ -720,20 +808,33 @@
720
  onInput=${(e) => setSize(parseInt(e.target.value))}
721
  disabled=${tool === 'move'}
722
  />
 
 
 
 
 
 
 
 
 
 
 
723
  </div>
724
  </div>
725
 
726
  <div class="crm-control-section" style="opacity: ${tool === 'move' ? 0.3 : 1}; pointer-events: ${tool === 'move' ? 'none' : 'auto'}">
727
- <h4>Color Selection</h4>
 
 
 
 
 
728
  <${IroColorPicker} color=${color} onChange=${setColor} />
729
  </div>
730
 
731
  <div class="crm-control-section">
732
  <h4>Actions</h4>
733
  <div style="display: flex; flex-direction: column; gap: 8px;">
734
- <button class="crm-btn" onClick=${() => restoreState(historyIndex - 1)} disabled=${historyIndex <= 0} style="justify-content: flex-start;">
735
- <i class="bi bi-arrow-counterclockwise"></i> Undo
736
- </button>
737
  <button class="crm-btn crm-btn-danger" onClick=${handleClear} style="justify-content: flex-start;">
738
  <i class="bi bi-trash"></i> Clear Canvas
739
  </button>
@@ -750,6 +851,7 @@
750
  onPointerMove=${handlePointerMove}
751
  onPointerUp=${handlePointerUp}
752
  onPointerLeave=${handlePointerUp}
 
753
  ></canvas>
754
  </div>
755
  </main>
@@ -787,6 +889,38 @@
787
  render(null, document.getElementById('notes-modal-root'));
788
  };
789
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
790
  window.deleteNote = async function(imageId) {
791
  if (!confirm('Delete this revision note?')) return;
792
  try {
 
81
  .crm-tool-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
82
  .crm-tool-btn { flex-direction: column; gap: 4px; padding: 10px; height: 60px; font-size: 0.75rem; }
83
  .crm-tool-btn i { font-size: 1.2rem; }
84
+ .crm-swatch-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; margin-bottom: 14px; }
85
+ .crm-swatch {
86
+ height: 30px; border-radius: 6px; border: 2px solid rgba(255,255,255,0.18); cursor: pointer;
87
+ box-shadow: inset 0 0 0 1px rgba(0,0,0,0.25);
88
+ }
89
+ .crm-swatch.active { border-color: #fff; box-shadow: 0 0 0 2px var(--primary); }
90
+ .crm-size-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 12px; }
91
+ .crm-size-btn { height: 34px; padding: 0; }
92
+ .crm-size-dot { display: block; border-radius: 999px; background: currentColor; }
93
+ .crm-input-row {
94
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
95
+ margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border);
96
+ font-size: 0.8rem; color: var(--text-muted);
97
+ }
98
+ .crm-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
99
+ .crm-switch input { accent-color: var(--primary); }
100
+ .crm-badge {
101
+ display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px;
102
+ border-radius: 999px; background: rgba(16, 185, 129, 0.12); color: var(--success);
103
+ font-size: 0.72rem; font-weight: 600;
104
+ }
105
 
106
  /* Canvas Viewport */
107
  .crm-viewport {
 
111
  width: 100%; height: 100%;
112
  background-image: linear-gradient(45deg, #1a1a1a 25%, transparent 25%), linear-gradient(-45deg, #1a1a1a 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #1a1a1a 75%), linear-gradient(-45deg, transparent 75%, #1a1a1a 75%);
113
  background-size: 20px 20px; background-color: #111;
114
+ position: relative; display: flex; align-items: center; justify-content: center; padding: 20px;
115
  }
116
  .crm-canvas-container canvas {
117
+ box-shadow: 0 20px 50px rgba(0,0,0,0.5); touch-action: none; display: block; max-width: 100%; max-height: 100%; background: white;
118
  }
119
  .crm-canvas-container.tool-pen canvas, .crm-canvas-container.tool-marker canvas { cursor: crosshair; }
120
  .crm-canvas-container.tool-eraser canvas { cursor: cell; }
 
158
  import htm from 'https://esm.sh/htm@3.1.1';
159
 
160
  const html = htm.bind(h);
161
+ const QUICK_COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#06b6d4', '#3b82f6', '#111827'];
162
+ const QUICK_SIZES = [2, 4, 8, 14];
163
 
164
  // === Core Math Utilities ===
165
  function distSqToSegment(p, v, w) {
 
313
  const [uploadedImages, setUploadedImages] = useState([]);
314
  const [history, setHistory] = useState([]);
315
  const [historyIndex, setHistoryIndex] = useState(-1);
316
+ const [isSaving, setIsSaving] = useState(false);
317
+ const [stylusDetected, setStylusDetected] = useState(false);
318
+ const [allowTouchDrawing, setAllowTouchDrawing] = useState(false);
319
 
320
  // Core Engine Refs
321
  const bgImageRef = useRef(null);
 
329
  const activePointerId = useRef(null);
330
  const dragState = useRef({ type: 'none', id: null });
331
  const isEraserActive = useRef(false);
332
+ const stylusDetectedRef = useRef(false);
333
+
334
+ const resizeCanvasToContainer = useCallback(() => {
335
+ const canvas = visibleCanvasRef.current;
336
+ const container = containerRef.current;
337
+ if (!canvas || !container) return;
338
+
339
+ const maxW = Math.max(320, container.clientWidth - 40);
340
+ const maxH = Math.max(320, container.clientHeight - 40);
341
+ const source = bgImageRef.current;
342
+ const aspect = source ? source.width / source.height : 4 / 3;
343
+ let width = maxW;
344
+ let height = width / aspect;
345
+ if (height > maxH) {
346
+ height = maxH;
347
+ width = height * aspect;
348
+ }
349
+
350
+ canvas.width = Math.round(width);
351
+ canvas.height = Math.round(height);
352
+ canvas.style.width = `${Math.round(width)}px`;
353
+ canvas.style.height = `${Math.round(height)}px`;
354
+ }, []);
355
 
356
  // --- Core Rendering Engine ---
357
  const renderCanvas = useCallback(() => {
 
362
  // 1. Base Layer
363
  ctx.fillStyle = '#ffffff';
364
  ctx.fillRect(0, 0, canvas.width, canvas.height);
365
+ if (bgImageRef.current) ctx.drawImage(bgImageRef.current, 0, 0, canvas.width, canvas.height);
366
 
367
  // 2. Images Layer (MUST be below strokes)
368
  imagesRef.current.forEach(img => {
 
476
  const container = containerRef.current;
477
  if (!canvas || !container) return;
478
 
479
+ resizeCanvasToContainer();
480
+
481
+ const loadBackground = (src, saveInitialState = true) => {
482
+ const img = new Image();
483
+ img.crossOrigin = "anonymous";
484
+ img.onload = () => {
485
+ bgImageRef.current = img;
486
+ resizeCanvasToContainer();
487
+ renderCanvas();
488
+ if (saveInitialState) saveState();
489
+ };
490
+ img.src = src;
491
+ };
492
+ const loadBlankCanvas = () => {
493
+ bgImageRef.current = null;
494
+ resizeCanvasToContainer();
495
+ renderCanvas();
496
+ saveState();
497
+ };
498
 
499
  const fetchNote = async () => {
500
  try {
 
508
  })).filter(p => p.data));
509
  }
510
  if (data.success && data.image_data) {
511
+ loadBackground(data.image_data);
512
+ } else loadBlankCanvas();
513
+ } else loadBlankCanvas();
514
+ } catch (e) {
515
+ loadBlankCanvas();
516
+ }
 
 
 
 
 
517
  };
518
  fetchNote();
519
 
520
  const handleResize = () => {
521
+ resizeCanvasToContainer();
522
+ renderCanvas();
 
 
 
523
  };
524
 
525
  const handleKeyDown = (e) => {
 
566
  // --- Interaction Handlers ---
567
  const handlePointerDown = useCallback((e) => {
568
  e.preventDefault();
569
+ if (e.button && e.button !== 0) return;
570
+ if (e.pointerType === 'pen' && !stylusDetectedRef.current) {
571
+ stylusDetectedRef.current = true;
572
+ setStylusDetected(true);
573
+ }
574
+ if (tool !== 'move' && e.pointerType === 'touch' && stylusDetectedRef.current && !allowTouchDrawing) {
575
+ return;
576
+ }
577
  if (activePointerId.current !== null) return;
578
  activePointerId.current = e.pointerId;
579
+ visibleCanvasRef.current?.setPointerCapture?.(e.pointerId);
580
 
581
  const rect = visibleCanvasRef.current.getBoundingClientRect();
582
  const x = e.clientX - rect.left;
 
618
  isEraserActive.current = true;
619
  executeStrokeEraser(x, y);
620
  } else {
621
+ const pressureScale = e.pointerType === 'pen' && e.pressure
622
+ ? Math.max(0.55, Math.min(1.5, e.pressure * 1.35))
623
+ : 1;
624
+ currentPathRef.current = { id: Date.now(), tool: tool, color: color, size: size * pressureScale, points: [{x, y}] };
625
  renderCanvas(); // Synchronous for immediate dot placement
626
  }
627
+ }, [tool, color, size, renderCanvas, executeStrokeEraser, allowTouchDrawing]);
628
 
629
  const handlePointerMove = useCallback((e) => {
630
  if (activePointerId.current !== e.pointerId) return;
 
667
 
668
  const handlePointerUp = useCallback((e) => {
669
  if (activePointerId.current !== e.pointerId) return;
670
+ const canvas = visibleCanvasRef.current;
671
+ if (canvas?.hasPointerCapture?.(e.pointerId)) {
672
+ canvas.releasePointerCapture(e.pointerId);
673
+ }
674
 
675
  if (currentPathRef.current) {
676
  pathsRef.current.push(currentPathRef.current);
 
699
  }, [renderCanvas, saveState]);
700
 
701
  const handleSave = useCallback(async () => {
702
+ if (isSaving) return;
703
  selectedFloatingIdRef.current = null;
704
  renderCanvas(); // Strip selection UI
705
 
 
707
  const imageData = canvas.toDataURL('image/png');
708
 
709
  try {
710
+ setIsSaving(true);
711
  const response = await fetch('/save_note_json', {
712
  method: 'POST',
713
  headers: { 'Content-Type': 'application/json' },
 
717
  const result = await response.json();
718
  if (result.success) {
719
  if (window.showStatus) window.showStatus('Notes saved!', 'success');
720
+ if (window.markRevisionNoteSaved) window.markRevisionNoteSaved(imageId);
721
  onClose();
 
722
  } else {
723
  if (window.showStatus) window.showStatus('Error: ' + result.error, 'danger');
724
  }
725
  } catch (e) {
726
  if (window.showStatus) window.showStatus(e.message, 'danger');
727
+ } finally {
728
+ setIsSaving(false);
729
  }
730
+ }, [imageId, sessionId, onClose, renderCanvas, isSaving]);
731
 
732
  const handleImageToCanvas = useCallback((imgDataUrl) => {
733
  const img = new Image();
 
763
  <div class="crm-modal-overlay">
764
  <header class="crm-header">
765
  <div style="font-weight: 700; display: flex; align-items: center; gap: 10px;">
766
+ <i class="bi bi-pencil-square" style="color: var(--primary);"></i> Revision Notes
767
  </div>
768
  <div style="display: flex; gap: 8px;">
769
+ <button class="crm-btn" onClick=${() => restoreState(historyIndex - 1)} disabled=${historyIndex <= 0 || isSaving} title="Undo">
770
+ <i class="bi bi-arrow-counterclockwise"></i>
771
+ </button>
772
+ <button class="crm-btn crm-btn-primary" onClick=${handleSave} disabled=${isSaving}>
773
+ <i class="bi ${isSaving ? 'bi-hourglass-split' : 'bi-check2'}"></i> ${isSaving ? 'Saving...' : 'Save'}
774
  </button>
775
+ <button class="crm-btn" onClick=${onClose} disabled=${isSaving} title="Close">
776
  <i class="bi bi-x-lg"></i>
777
  </button>
778
  </div>
 
785
  <h4>Tools</h4>
786
  <div class="crm-tool-row">
787
  <${ToolButton} icon="bi bi-pen" label="Pen" active=${tool === 'pen'} onClick=${() => setTool('pen')} />
788
+ <${ToolButton} icon="bi bi-highlighter" label="Highlight" active=${tool === 'marker'} onClick=${() => setTool('marker')} />
789
+ <${ToolButton} icon="bi bi-eraser" label="Erase" active=${tool === 'eraser'} onClick=${() => setTool('eraser')} title="Deletes entire lines on click/touch" />
790
+ <${ToolButton} icon="bi bi-arrows-move" label="Move" active=${tool === 'move'} onClick=${() => setTool('move')} title="Select, move, and resize images" />
791
  </div>
792
 
793
+ <div style="margin-top: 14px;">
794
+ <div class="crm-size-row">
795
+ ${QUICK_SIZES.map(s => html`
796
+ <button class="crm-btn crm-size-btn ${size === s ? 'active' : ''}" onClick=${() => setSize(s)} disabled=${tool === 'move'} title=${`${s}px`}>
797
+ <span class="crm-size-dot" style=${`width:${Math.min(18, s + 4)}px;height:${Math.min(18, s + 4)}px;`}></span>
798
+ </button>
799
+ `)}
800
+ </div>
801
  <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
802
  <span style="font-size: 0.8rem; color: var(--text-muted);">Brush Size</span>
803
  <span style="font-size: 0.8rem; color: var(--text-main); font-family: monospace;">${size}px</span>
 
808
  onInput=${(e) => setSize(parseInt(e.target.value))}
809
  disabled=${tool === 'move'}
810
  />
811
+ <div class="crm-input-row">
812
+ ${stylusDetected ? html`
813
+ <span class="crm-badge"><i class="bi bi-pen"></i> Stylus</span>
814
+ ` : html`
815
+ <span>Input</span>
816
+ `}
817
+ <label class="crm-switch" title="Allow finger strokes even after stylus detection">
818
+ <input type="checkbox" checked=${allowTouchDrawing} onChange=${(e) => setAllowTouchDrawing(e.currentTarget.checked)} />
819
+ <span>Finger draw</span>
820
+ </label>
821
+ </div>
822
  </div>
823
  </div>
824
 
825
  <div class="crm-control-section" style="opacity: ${tool === 'move' ? 0.3 : 1}; pointer-events: ${tool === 'move' ? 'none' : 'auto'}">
826
+ <h4>Color</h4>
827
+ <div class="crm-swatch-row">
828
+ ${QUICK_COLORS.map(c => html`
829
+ <button class="crm-swatch ${color === c ? 'active' : ''}" style=${`background:${c}`} onClick=${() => setColor(c)} title=${c}></button>
830
+ `)}
831
+ </div>
832
  <${IroColorPicker} color=${color} onChange=${setColor} />
833
  </div>
834
 
835
  <div class="crm-control-section">
836
  <h4>Actions</h4>
837
  <div style="display: flex; flex-direction: column; gap: 8px;">
 
 
 
838
  <button class="crm-btn crm-btn-danger" onClick=${handleClear} style="justify-content: flex-start;">
839
  <i class="bi bi-trash"></i> Clear Canvas
840
  </button>
 
851
  onPointerMove=${handlePointerMove}
852
  onPointerUp=${handlePointerUp}
853
  onPointerLeave=${handlePointerUp}
854
+ onPointerCancel=${handlePointerUp}
855
  ></canvas>
856
  </div>
857
  </main>
 
889
  render(null, document.getElementById('notes-modal-root'));
890
  };
891
 
892
+ window.markRevisionNoteSaved = function(imageId) {
893
+ const fieldset = document.getElementById(`question-fieldset-${imageId}`);
894
+ if (!fieldset) return;
895
+
896
+ const addButton = Array.from(fieldset.querySelectorAll('button[onclick^="openNotesModal"]'))
897
+ .find(button => !button.closest('.note-card'));
898
+ if (!addButton) return;
899
+
900
+ const onclick = addButton.getAttribute('onclick');
901
+ addButton.outerHTML = `
902
+ <div class="note-card">
903
+ <div class="d-flex align-items-center justify-content-center gap-2 py-2 text-success">
904
+ <i class="bi bi-check-circle-fill"></i>
905
+ <span class="small">Notes saved</span>
906
+ </div>
907
+ <div class="note-actions flex-wrap justify-content-center">
908
+ <div class="form-check form-switch include-pdf-toggle">
909
+ <input class="form-check-input" type="checkbox" id="include_note_${imageId}" checked
910
+ onchange="toggleNoteInPdf('${imageId}', this.checked)">
911
+ <label class="form-check-label small" for="include_note_${imageId}">In PDF</label>
912
+ </div>
913
+ <button type="button" class="btn btn-sm btn-outline-info btn-pill" onclick="${onclick}" title="Edit Note">
914
+ <i class="bi bi-pencil"></i>
915
+ </button>
916
+ <button type="button" class="btn btn-sm btn-outline-danger btn-pill" onclick="deleteNote('${imageId}')" title="Delete Note">
917
+ <i class="bi bi-trash"></i>
918
+ </button>
919
+ </div>
920
+ </div>
921
+ `;
922
+ };
923
+
924
  window.deleteNote = async function(imageId) {
925
  if (!confirm('Delete this revision note?')) return;
926
  try {
templates/cropv2.html CHANGED
@@ -2028,6 +2028,8 @@
2028
  const currentLeftPageIndex = CONFIG.leftPageIndex;
2029
  const currentRightPageIndex = CONFIG.rightPageIndex;
2030
  const backgroundRequests = [];
 
 
2031
 
2032
  if (!hasLeftBoxes && !hasRightBoxes) {
2033
  toast('Skipping page(s)...');
@@ -2036,6 +2038,9 @@
2036
  }
2037
 
2038
  try {
 
 
 
2039
  // Process left page
2040
  if (hasLeftBoxes || !CONFIG.twoPageMode) {
2041
  const finalBoxes = boxes.map(b => ({
@@ -2098,20 +2103,34 @@
2098
  }));
2099
  }
2100
 
2101
- navigate(currentImageIndex + 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2102
 
2103
- Promise.all(backgroundRequests)
2104
- .then(() => {
2105
- const savedLabel = CONFIG.twoPageMode && currentRightPageIndex < CONFIG.allPages.length
2106
- ? `Pages ${currentLeftPageIndex + 1}-${currentRightPageIndex + 1} saved`
2107
- : `Page ${currentLeftPageIndex + 1} saved`;
2108
- toast(savedLabel);
2109
- })
2110
- .catch((e) => {
2111
- toast(`Save failed: ${e.message}`);
2112
- });
2113
  } catch (e) {
2114
  toast(`Save failed: ${e.message}`);
 
 
2115
  }
2116
  }
2117
 
 
2028
  const currentLeftPageIndex = CONFIG.leftPageIndex;
2029
  const currentRightPageIndex = CONFIG.rightPageIndex;
2030
  const backgroundRequests = [];
2031
+ const processBtn = document.getElementById('processBtn');
2032
+ const isLastPage = currentImageIndex + 1 >= CONFIG.totalPages;
2033
 
2034
  if (!hasLeftBoxes && !hasRightBoxes) {
2035
  toast('Skipping page(s)...');
 
2038
  }
2039
 
2040
  try {
2041
+ if (processBtn) processBtn.disabled = true;
2042
+ toast('Saving page(s)...');
2043
+
2044
  // Process left page
2045
  if (hasLeftBoxes || !CONFIG.twoPageMode) {
2046
  const finalBoxes = boxes.map(b => ({
 
2103
  }));
2104
  }
2105
 
2106
+ if (!isLastPage) {
2107
+ navigate(currentImageIndex + 1);
2108
+ Promise.all(backgroundRequests)
2109
+ .then(() => {
2110
+ const savedLabel = CONFIG.twoPageMode && currentRightPageIndex < CONFIG.allPages.length
2111
+ ? `Pages ${currentLeftPageIndex + 1}-${currentRightPageIndex + 1} saved`
2112
+ : `Page ${currentLeftPageIndex + 1} saved`;
2113
+ toast(savedLabel);
2114
+ })
2115
+ .catch((e) => {
2116
+ toast(`Save failed: ${e.message}`);
2117
+ })
2118
+ .finally(() => {
2119
+ if (processBtn) processBtn.disabled = false;
2120
+ });
2121
+ return;
2122
+ }
2123
 
2124
+ await Promise.all(backgroundRequests);
2125
+ const savedLabel = CONFIG.twoPageMode && currentRightPageIndex < CONFIG.allPages.length
2126
+ ? `Pages ${currentLeftPageIndex + 1}-${currentRightPageIndex + 1} saved`
2127
+ : `Page ${currentLeftPageIndex + 1} saved`;
2128
+ toast(savedLabel);
2129
+ navigate(currentImageIndex + 1);
 
 
 
 
2130
  } catch (e) {
2131
  toast(`Save failed: ${e.message}`);
2132
+ } finally {
2133
+ if (processBtn) processBtn.disabled = false;
2134
  }
2135
  }
2136