Spaces:
Running
Running
Ubuntu commited on
Commit ·
1671832
1
Parent(s): 0d6e94b
Fixed more bugs ; qol changes
Browse files- templates/_revision_notes_preact.html +171 -37
- templates/cropv2.html +30 -11
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 |
-
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 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 |
-
|
| 461 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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-
|
| 689 |
</div>
|
| 690 |
<div style="display: flex; gap: 8px;">
|
| 691 |
-
<button class="crm-btn
|
| 692 |
-
<i class="bi bi-
|
|
|
|
|
|
|
|
|
|
| 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="
|
| 708 |
-
<${ToolButton} icon="bi bi-eraser" label="
|
| 709 |
-
<${ToolButton} icon="bi bi-arrows-move" label="Move
|
| 710 |
</div>
|
| 711 |
|
| 712 |
-
<div style="margin-top:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2102 |
|
| 2103 |
-
Promise.all(backgroundRequests)
|
| 2104 |
-
|
| 2105 |
-
|
| 2106 |
-
|
| 2107 |
-
|
| 2108 |
-
|
| 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 |
|