Spaces:
Running
Running
t commited on
Commit ·
9ac61d7
1
Parent(s): 92ef226
updated cropv2
Browse files- templates/cropv2.html +696 -183
templates/cropv2.html
CHANGED
|
@@ -13,7 +13,7 @@
|
|
| 13 |
--slider-height: 90px;
|
| 14 |
--bg-dark: #121212;
|
| 15 |
--surface-color: #1e1e1e;
|
| 16 |
-
--lens-size: 140px;
|
| 17 |
}
|
| 18 |
|
| 19 |
html, body {
|
|
@@ -29,6 +29,28 @@
|
|
| 29 |
flex-direction: column;
|
| 30 |
}
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
/* --- 1. HEADER --- */
|
| 33 |
.app-header {
|
| 34 |
height: var(--header-height);
|
|
@@ -88,9 +110,13 @@
|
|
| 88 |
overflow: hidden;
|
| 89 |
}
|
| 90 |
|
| 91 |
-
/* Crosshair inside magnifier */
|
| 92 |
#magnifier::after {
|
| 93 |
-
content: '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
border-left: 2px solid rgba(255,50,50,0.8);
|
| 95 |
border-top: 2px solid rgba(255,50,50,0.8);
|
| 96 |
transform: translate(-50%, -50%);
|
|
@@ -98,35 +124,191 @@
|
|
| 98 |
|
| 99 |
/* --- 3. THUMBNAILS --- */
|
| 100 |
.thumbnail-bar {
|
| 101 |
-
height: var(--slider-height);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
-
.thumb-item { height: 60px; min-width: 45px; border-radius: 6px; overflow: hidden; position: relative; border: 2px solid transparent; opacity: 0.6; background: #000; flex-shrink: 0; }
|
| 104 |
-
.thumb-item.active { border-color: #0d6efd; opacity: 1; transform: scale(1.05); }
|
| 105 |
-
.thumb-item img { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.3s ease-in; }
|
| 106 |
.thumb-item img.loaded { opacity: 1; }
|
| 107 |
-
.thumb-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
/* --- FLOATING UI --- */
|
| 110 |
-
#box-toolbar {
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
#box-toolbar button.delete-btn { color: #dc3545; }
|
| 114 |
|
| 115 |
-
|
| 116 |
-
.fab-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
.filters-panel.show { display: block; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
.form-range { height: 4px; }
|
| 121 |
|
| 122 |
-
#loader-overlay {
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
</style>
|
| 126 |
</head>
|
| 127 |
<body>
|
| 128 |
{% include '_navbar.html' %}
|
| 129 |
|
|
|
|
|
|
|
|
|
|
| 130 |
<header class="app-header">
|
| 131 |
<h1 class="header-title"><i class="bi bi-bounding-box me-2"></i>Page {{ image_index + 1 }} / {{ total_pages }}</h1>
|
| 132 |
<div class="header-actions">
|
|
@@ -151,39 +333,67 @@
|
|
| 151 |
</div>
|
| 152 |
</div>
|
| 153 |
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
<
|
| 164 |
-
<
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
</div>
|
| 171 |
-
|
| 172 |
-
</div>
|
| 173 |
-
<button class="fab-btn" id="dataToggle" title="Data Entry"><i class="bi bi-pencil-square"></i></button>
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
<div class="mb-3">
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
</div>
|
| 180 |
-
<button class="fab-btn" id="filterToggle"><i class="bi bi-sliders"></i></button>
|
| 181 |
</div>
|
| 182 |
</div>
|
| 183 |
|
| 184 |
<div class="thumbnail-bar">
|
| 185 |
{% for page in all_pages %}
|
| 186 |
-
<div class="thumb-item {% if page.image_index == image_index %}active{% endif %}"
|
|
|
|
| 187 |
<img data-src="/image/upload/{{ page.filename }}"
|
| 188 |
alt="Page {{ page.image_index + 1 }}"
|
| 189 |
data-session="{{ session_id }}"
|
|
@@ -208,54 +418,155 @@
|
|
| 208 |
};
|
| 209 |
const storageKey = CONFIG.userId ? `cropState_${CONFIG.userId}_${CONFIG.sessionId}_${CONFIG.imageIndex}` : `cropState_${CONFIG.sessionId}_${CONFIG.imageIndex}`;
|
| 210 |
|
| 211 |
-
// ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
const ThumbCache = {
|
| 213 |
-
DB_NAME: 'PDF_Crop_Thumbs',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
async open() {
|
| 215 |
return new Promise((resolve, reject) => {
|
| 216 |
const req = indexedDB.open(this.DB_NAME, 1);
|
| 217 |
-
req.onupgradeneeded = (e) => {
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
});
|
| 220 |
},
|
|
|
|
| 221 |
async cleanup() {
|
| 222 |
const db = await this.open();
|
| 223 |
return new Promise((resolve) => {
|
| 224 |
-
const tx = db.transaction(this.STORE_NAME, 'readwrite');
|
|
|
|
|
|
|
| 225 |
const req = store.openCursor();
|
| 226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
req.onerror = () => resolve();
|
| 228 |
});
|
| 229 |
},
|
| 230 |
-
|
|
|
|
| 231 |
try {
|
| 232 |
-
const db = await this.open();
|
|
|
|
| 233 |
const req = txRead.objectStore(this.STORE_NAME).get(url);
|
|
|
|
| 234 |
req.onsuccess = async (e) => {
|
| 235 |
const record = e.target.result;
|
| 236 |
-
if (record) {
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
| 238 |
try {
|
| 239 |
-
const response = await fetch(url);
|
| 240 |
-
const
|
| 241 |
-
|
| 242 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
}
|
| 244 |
};
|
| 245 |
-
} catch (err) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
},
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
};
|
| 253 |
|
| 254 |
// --- APP LOGIC ---
|
| 255 |
const els = {
|
| 256 |
-
image: document.getElementById('main-image'),
|
| 257 |
-
|
| 258 |
-
|
|
|
|
|
|
|
|
|
|
| 259 |
magnifier: document.getElementById('magnifier')
|
| 260 |
};
|
| 261 |
|
|
@@ -263,35 +574,49 @@
|
|
| 263 |
let selectedBoxIndex = -1;
|
| 264 |
let isDrawing = false;
|
| 265 |
let startX, startY;
|
| 266 |
-
let dragTarget = null;
|
| 267 |
let startPositions = {};
|
| 268 |
let stitchBuffer = JSON.parse(localStorage.getItem('gemini_stitch_buffer') || 'null');
|
| 269 |
|
| 270 |
// ZOOM STATE
|
| 271 |
-
const LENS_SIZE_PX = 140;
|
| 272 |
const ZOOM_LEVEL = 2.5;
|
| 273 |
let isMagnifying = false;
|
| 274 |
-
let magnifierPos = { x: 0, y: 0 };
|
| 275 |
|
| 276 |
function init() {
|
| 277 |
-
ThumbCache.
|
| 278 |
-
if (els.image.complete && els.image.naturalWidth > 0) fitImage();
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
function fitImage() {
|
| 285 |
if (!els.image.naturalWidth) return;
|
| 286 |
const rect = els.imagePane.getBoundingClientRect();
|
| 287 |
const padding = 10;
|
| 288 |
-
const availableW = rect.width - padding;
|
|
|
|
| 289 |
const scale = Math.min(availableW / els.image.naturalWidth, availableH / els.image.naturalHeight);
|
| 290 |
|
| 291 |
-
const finalW = Math.floor(els.image.naturalWidth * scale);
|
| 292 |
-
|
| 293 |
-
els.
|
| 294 |
-
els.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
if (CONFIG.enableMagnifier) {
|
| 297 |
els.magnifier.style.backgroundImage = `url('${els.image.src}')`;
|
|
@@ -305,7 +630,6 @@
|
|
| 305 |
function drawBoxes() {
|
| 306 |
els.ctx.clearRect(0, 0, els.canvas.width, els.canvas.height);
|
| 307 |
|
| 308 |
-
// 1. Draw Boxes
|
| 309 |
boxes.forEach((box, index) => {
|
| 310 |
const isSelected = index === selectedBoxIndex;
|
| 311 |
const isStitched = box.remote_stitch_source != null;
|
|
@@ -316,45 +640,53 @@
|
|
| 316 |
els.ctx.fillStyle = isSelected ? 'rgba(255, 77, 77, 0.15)' : (isStitched ? 'rgba(13, 202, 240, 0.2)' : 'rgba(255, 193, 7, 0.1)');
|
| 317 |
|
| 318 |
els.ctx.beginPath();
|
| 319 |
-
els.ctx.moveTo(p(box.tl).x, p(box.tl).y);
|
| 320 |
-
els.ctx.lineTo(p(box.
|
| 321 |
-
els.ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
|
| 323 |
if (isSelected) {
|
| 324 |
els.ctx.fillStyle = 'white';
|
| 325 |
-
['tl','tr','bl','br'].forEach(k => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}
|
| 327 |
|
| 328 |
-
const cx = (p(box.tl).x + p(box.br).x)
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
els.ctx.fillText(index + 1, cx - 6, cy + 8);
|
| 331 |
-
if(isStitched) {
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
| 333 |
});
|
| 334 |
|
| 335 |
-
// 2. Draw Magnifier Source Region Indicator
|
| 336 |
if (isMagnifying && CONFIG.enableMagnifier) {
|
| 337 |
const px = magnifierPos.x * els.canvas.width;
|
| 338 |
const py = magnifierPos.y * els.canvas.height;
|
| 339 |
-
|
| 340 |
-
// Calculate radius on canvas that matches the lens view
|
| 341 |
-
// Lens is 140px on screen. Zoom is 2.5x.
|
| 342 |
-
// So the source circle diameter is 140 / 2.5 = 56px. Radius = 28px.
|
| 343 |
const sourceRadius = (LENS_SIZE_PX / ZOOM_LEVEL) / 2;
|
| 344 |
|
| 345 |
els.ctx.beginPath();
|
| 346 |
els.ctx.arc(px, py, sourceRadius, 0, Math.PI * 2);
|
| 347 |
-
|
| 348 |
-
// Style: White stroke + Black Shadow for visibility on all backgrounds
|
| 349 |
els.ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
| 350 |
els.ctx.lineWidth = 1.5;
|
| 351 |
els.ctx.shadowColor = 'rgba(0,0,0,1)';
|
| 352 |
els.ctx.shadowBlur = 4;
|
| 353 |
-
els.ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
|
| 354 |
-
|
| 355 |
els.ctx.stroke();
|
| 356 |
els.ctx.fill();
|
| 357 |
-
els.ctx.shadowBlur = 0;
|
| 358 |
}
|
| 359 |
}
|
| 360 |
|
|
@@ -367,18 +699,18 @@
|
|
| 367 |
|
| 368 |
if (active) {
|
| 369 |
const lens = els.magnifier;
|
| 370 |
-
const w = els.canvas.width;
|
| 371 |
-
const
|
|
|
|
|
|
|
| 372 |
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
if (top < 0) top = py + 50;
|
| 376 |
|
| 377 |
lens.style.display = 'block';
|
| 378 |
-
lens.style.left = (px - LENS_SIZE_PX/2) + 'px';
|
| 379 |
lens.style.top = top + 'px';
|
| 380 |
|
| 381 |
-
// Zoom bg
|
| 382 |
lens.style.backgroundSize = `${w * ZOOM_LEVEL}px ${h * ZOOM_LEVEL}px`;
|
| 383 |
const bgX = (px * ZOOM_LEVEL) - (LENS_SIZE_PX / 2);
|
| 384 |
const bgY = (py * ZOOM_LEVEL) - (LENS_SIZE_PX / 2);
|
|
@@ -399,57 +731,81 @@
|
|
| 399 |
}
|
| 400 |
|
| 401 |
function hitTest(x, y) {
|
| 402 |
-
const pad = 30 / els.canvas.width;
|
| 403 |
for (let i = boxes.length - 1; i >= 0; i--) {
|
| 404 |
const b = boxes[i];
|
| 405 |
-
for(let k of ['tl','tr','bl','br']) {
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
}
|
| 410 |
return null;
|
| 411 |
}
|
| 412 |
|
| 413 |
function onDown(e) {
|
| 414 |
-
if(e.target.closest('#box-toolbar')) return;
|
| 415 |
e.preventDefault();
|
| 416 |
-
const {x, y} = getPos(e);
|
| 417 |
|
| 418 |
-
updateMagnifierState(x, y, true);
|
| 419 |
|
| 420 |
const hit = hitTest(x, y);
|
| 421 |
if (hit) {
|
| 422 |
-
dragTarget = hit;
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
} else {
|
| 425 |
-
selectedBoxIndex = -1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
}
|
| 427 |
drawBoxes();
|
| 428 |
}
|
| 429 |
|
| 430 |
function onMove(e) {
|
| 431 |
-
if(isDrawing || dragTarget) {
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
const b = boxes[dragTarget.index];
|
| 439 |
-
if (dragTarget.type === 'corner') {
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 443 |
drawBoxes();
|
| 444 |
const sx = startX * els.canvas.width, sy = startY * els.canvas.height;
|
| 445 |
const w = (x - startX) * els.canvas.width, h = (y - startY) * els.canvas.height;
|
| 446 |
-
els.ctx.strokeStyle = 'rgba(255, 77, 77, 0.5)';
|
| 447 |
-
|
|
|
|
| 448 |
}
|
| 449 |
}
|
| 450 |
|
| 451 |
function onUp(e) {
|
| 452 |
-
updateMagnifierState(0, 0, false);
|
| 453 |
|
| 454 |
if (isDrawing) {
|
| 455 |
const rect = els.canvas.getBoundingClientRect();
|
|
@@ -469,29 +825,83 @@
|
|
| 469 |
selectedBoxIndex = boxes.length - 1;
|
| 470 |
}
|
| 471 |
}
|
| 472 |
-
isDrawing = false;
|
| 473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
}
|
| 475 |
|
| 476 |
// --- HELPERS ---
|
| 477 |
function setupListeners() {
|
| 478 |
els.canvas.addEventListener('mousedown', onDown);
|
| 479 |
-
els.canvas.addEventListener('touchstart', onDown, {passive:false});
|
| 480 |
document.addEventListener('mousemove', onMove);
|
| 481 |
-
document.addEventListener('touchmove', onMove, {passive:false});
|
| 482 |
document.addEventListener('mouseup', onUp);
|
| 483 |
document.addEventListener('touchend', onUp);
|
| 484 |
|
| 485 |
-
document.getElementById('backBtn').onclick = () =>
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
document.getElementById('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
document.getElementById('stitch-btn').onclick = handleStitch;
|
| 491 |
document.getElementById('processBtn').onclick = processPage;
|
|
|
|
| 492 |
// Data Entry
|
| 493 |
document.getElementById('dataToggle').onclick = () => {
|
| 494 |
const panel = document.getElementById('dataPanel');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
if (!panel.classList.contains('show') && selectedBoxIndex === -1 && boxes.length > 0) {
|
| 496 |
selectedBoxIndex = 0;
|
| 497 |
updateToolbar();
|
|
@@ -521,7 +931,6 @@
|
|
| 521 |
if (!e.shiftKey) return;
|
| 522 |
const key = e.key.toLowerCase();
|
| 523 |
|
| 524 |
-
// Navigation
|
| 525 |
if (e.key === 'ArrowRight') {
|
| 526 |
e.preventDefault();
|
| 527 |
document.getElementById('processBtn').click();
|
|
@@ -533,7 +942,6 @@
|
|
| 533 |
return;
|
| 534 |
}
|
| 535 |
|
| 536 |
-
// Box Navigation
|
| 537 |
if (key === 'q') {
|
| 538 |
e.preventDefault();
|
| 539 |
if (boxes.length > 0) {
|
|
@@ -544,7 +952,6 @@
|
|
| 544 |
return;
|
| 545 |
}
|
| 546 |
|
| 547 |
-
// Box Actions (Require Selection)
|
| 548 |
if (selectedBoxIndex === -1) return;
|
| 549 |
|
| 550 |
if (key === 'm') {
|
|
@@ -573,10 +980,28 @@
|
|
| 573 |
|
| 574 |
// Filters
|
| 575 |
document.getElementById('filterToggle').onclick = () => {
|
| 576 |
-
document.getElementById('filtersPanel')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 577 |
};
|
| 578 |
|
| 579 |
-
['brightness','contrast','gamma'].forEach(id =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
}
|
| 581 |
|
| 582 |
function updateDataPanel() {
|
|
@@ -585,13 +1010,13 @@
|
|
| 585 |
const form = document.getElementById('data-form');
|
| 586 |
|
| 587 |
if (!b) {
|
| 588 |
-
if(msg) msg.style.display = 'block';
|
| 589 |
-
if(form) form.style.display = 'none';
|
| 590 |
return;
|
| 591 |
}
|
| 592 |
|
| 593 |
-
if(msg) msg.style.display = 'none';
|
| 594 |
-
if(form) form.style.display = 'block';
|
| 595 |
|
| 596 |
document.getElementById('box-q-num').value = b.question_number || '';
|
| 597 |
document.getElementById('box-status').value = b.status || 'unattempted';
|
|
@@ -601,14 +1026,20 @@
|
|
| 601 |
|
| 602 |
function updateToolbar() {
|
| 603 |
updateDataPanel();
|
| 604 |
-
if (selectedBoxIndex === -1) {
|
|
|
|
|
|
|
|
|
|
| 605 |
const b = boxes[selectedBoxIndex];
|
| 606 |
const p = (pt) => ({ x: pt.x * els.canvas.width, y: pt.y * els.canvas.height });
|
| 607 |
const maxX = Math.max(p(b.tr).x, p(b.br).x);
|
| 608 |
const minY = Math.min(p(b.tl).y, p(b.tr).y);
|
| 609 |
-
let left = maxX - 180;
|
|
|
|
| 610 |
let top = minY + 10;
|
| 611 |
-
els.toolbar.style.left = `${left}px`;
|
|
|
|
|
|
|
| 612 |
}
|
| 613 |
|
| 614 |
function updateStitchButton() {
|
|
@@ -616,69 +1047,151 @@
|
|
| 616 |
const icon = btn.querySelector('i');
|
| 617 |
const isBuffer = stitchBuffer && stitchBuffer.session_id === CONFIG.sessionId;
|
| 618 |
const isStitched = selectedBoxIndex > -1 && boxes[selectedBoxIndex]?.remote_stitch_source;
|
| 619 |
-
if (isBuffer) {
|
| 620 |
-
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
}
|
| 623 |
|
| 624 |
function handleStitch(e) {
|
| 625 |
-
e.stopPropagation();
|
|
|
|
| 626 |
const b = boxes[selectedBoxIndex];
|
| 627 |
-
if(stitchBuffer && stitchBuffer.session_id === CONFIG.sessionId) {
|
| 628 |
-
b.remote_stitch_source = {
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
} else {
|
| 633 |
-
const minX=Math.min(b.tl.x,b.bl.x), minY=Math.min(b.tl.y,
|
|
|
|
| 634 |
const cleanBox = { ...b, x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
| 635 |
-
stitchBuffer = {
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
}
|
| 638 |
-
saveBoxes();
|
|
|
|
|
|
|
| 639 |
}
|
| 640 |
|
| 641 |
function toast(msg) {
|
| 642 |
-
const t = document.createElement('div');
|
|
|
|
| 643 |
t.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div></div>`;
|
| 644 |
-
document.getElementById('toastContainer').appendChild(t);
|
|
|
|
| 645 |
}
|
| 646 |
|
| 647 |
async function processPage() {
|
| 648 |
-
if(!boxes.length && !confirm("Skip page?")) return;
|
| 649 |
-
|
|
|
|
|
|
|
|
|
|
| 650 |
const finalBoxes = boxes.map(b => ({
|
| 651 |
-
...b,
|
|
|
|
|
|
|
| 652 |
w: Math.max(b.tr.x, b.br.x) - Math.min(b.tl.x, b.bl.x),
|
| 653 |
h: Math.max(b.bl.y, b.br.y) - Math.min(b.tl.y, b.tr.y)
|
| 654 |
}));
|
| 655 |
-
|
| 656 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
try {
|
| 658 |
-
const res = await fetch('/process_crop_v2', {
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
}
|
| 665 |
|
| 666 |
-
function saveBoxes() {
|
|
|
|
|
|
|
|
|
|
| 667 |
function loadBoxes() {
|
| 668 |
try {
|
| 669 |
const s = localStorage.getItem(storageKey);
|
| 670 |
-
if
|
| 671 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
}
|
| 673 |
|
| 674 |
function updateFilters() {
|
| 675 |
-
const b = document.getElementById('brightness').value
|
| 676 |
-
|
| 677 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
els.magnifier.style.filter = els.image.style.filter;
|
| 679 |
-
localStorage.setItem('pdfFilters', JSON.stringify({b,c,g}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
}
|
| 681 |
-
function loadSettings() { const s = JSON.parse(localStorage.getItem('pdfFilters') || '{}'); if(s.b) document.getElementById('brightness').value=s.b; if(s.c) document.getElementById('contrast').value=s.c; if(s.g) document.getElementById('gamma').value=s.g; updateFilters(); }
|
| 682 |
|
| 683 |
init();
|
| 684 |
</script>
|
|
|
|
| 13 |
--slider-height: 90px;
|
| 14 |
--bg-dark: #121212;
|
| 15 |
--surface-color: #1e1e1e;
|
| 16 |
+
--lens-size: 140px;
|
| 17 |
}
|
| 18 |
|
| 19 |
html, body {
|
|
|
|
| 29 |
flex-direction: column;
|
| 30 |
}
|
| 31 |
|
| 32 |
+
/* --- PROGRESS BAR --- */
|
| 33 |
+
#progress-bar {
|
| 34 |
+
position: fixed;
|
| 35 |
+
top: 0;
|
| 36 |
+
left: 0;
|
| 37 |
+
right: 0;
|
| 38 |
+
height: 3px;
|
| 39 |
+
background: linear-gradient(90deg, #0d6efd, #0dcaf0);
|
| 40 |
+
transform: scaleX(0);
|
| 41 |
+
transform-origin: left;
|
| 42 |
+
transition: transform 0.3s ease;
|
| 43 |
+
z-index: 10000;
|
| 44 |
+
box-shadow: 0 0 10px rgba(13, 110, 253, 0.5);
|
| 45 |
+
}
|
| 46 |
+
#progress-bar.active {
|
| 47 |
+
animation: progress-pulse 2s ease-in-out infinite;
|
| 48 |
+
}
|
| 49 |
+
@keyframes progress-pulse {
|
| 50 |
+
0%, 100% { opacity: 1; }
|
| 51 |
+
50% { opacity: 0.7; }
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
/* --- 1. HEADER --- */
|
| 55 |
.app-header {
|
| 56 |
height: var(--header-height);
|
|
|
|
| 110 |
overflow: hidden;
|
| 111 |
}
|
| 112 |
|
|
|
|
| 113 |
#magnifier::after {
|
| 114 |
+
content: '';
|
| 115 |
+
position: absolute;
|
| 116 |
+
top: 50%;
|
| 117 |
+
left: 50%;
|
| 118 |
+
width: 12px;
|
| 119 |
+
height: 12px;
|
| 120 |
border-left: 2px solid rgba(255,50,50,0.8);
|
| 121 |
border-top: 2px solid rgba(255,50,50,0.8);
|
| 122 |
transform: translate(-50%, -50%);
|
|
|
|
| 124 |
|
| 125 |
/* --- 3. THUMBNAILS --- */
|
| 126 |
.thumbnail-bar {
|
| 127 |
+
height: var(--slider-height);
|
| 128 |
+
flex-shrink: 0;
|
| 129 |
+
background: #212529;
|
| 130 |
+
border-top: 1px solid #495057;
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
padding: 0 10px;
|
| 134 |
+
padding-bottom: env(safe-area-inset-bottom);
|
| 135 |
+
gap: 10px;
|
| 136 |
+
overflow-x: auto;
|
| 137 |
+
z-index: 40;
|
| 138 |
+
}
|
| 139 |
+
.thumb-item {
|
| 140 |
+
height: 60px;
|
| 141 |
+
min-width: 45px;
|
| 142 |
+
border-radius: 6px;
|
| 143 |
+
overflow: hidden;
|
| 144 |
+
position: relative;
|
| 145 |
+
border: 2px solid transparent;
|
| 146 |
+
opacity: 0.6;
|
| 147 |
+
background: #000;
|
| 148 |
+
flex-shrink: 0;
|
| 149 |
+
cursor: pointer;
|
| 150 |
+
transition: all 0.2s ease;
|
| 151 |
+
}
|
| 152 |
+
.thumb-item.active {
|
| 153 |
+
border-color: #0d6efd;
|
| 154 |
+
opacity: 1;
|
| 155 |
+
transform: scale(1.05);
|
| 156 |
+
}
|
| 157 |
+
.thumb-item img {
|
| 158 |
+
width: 100%;
|
| 159 |
+
height: 100%;
|
| 160 |
+
object-fit: cover;
|
| 161 |
+
opacity: 0;
|
| 162 |
+
transition: opacity 0.3s ease-in;
|
| 163 |
}
|
|
|
|
|
|
|
|
|
|
| 164 |
.thumb-item img.loaded { opacity: 1; }
|
| 165 |
+
.thumb-item .thumb-loader {
|
| 166 |
+
position: absolute;
|
| 167 |
+
top: 50%;
|
| 168 |
+
left: 50%;
|
| 169 |
+
transform: translate(-50%, -50%);
|
| 170 |
+
width: 20px;
|
| 171 |
+
height: 20px;
|
| 172 |
+
border: 2px solid #495057;
|
| 173 |
+
border-top-color: #0d6efd;
|
| 174 |
+
border-radius: 50%;
|
| 175 |
+
animation: spin 0.8s linear infinite;
|
| 176 |
+
}
|
| 177 |
+
@keyframes spin {
|
| 178 |
+
to { transform: translate(-50%, -50%) rotate(360deg); }
|
| 179 |
+
}
|
| 180 |
+
.thumb-number {
|
| 181 |
+
position: absolute;
|
| 182 |
+
bottom: 0;
|
| 183 |
+
right: 0;
|
| 184 |
+
background: rgba(0,0,0,0.7);
|
| 185 |
+
color: #fff;
|
| 186 |
+
font-size: 10px;
|
| 187 |
+
padding: 2px 4px;
|
| 188 |
+
}
|
| 189 |
|
| 190 |
/* --- FLOATING UI --- */
|
| 191 |
+
#box-toolbar {
|
| 192 |
+
position: absolute;
|
| 193 |
+
background: rgba(33, 37, 41, 0.95);
|
| 194 |
+
border: 1px solid #6c757d;
|
| 195 |
+
border-radius: 50px;
|
| 196 |
+
padding: 8px 16px;
|
| 197 |
+
display: none;
|
| 198 |
+
gap: 16px;
|
| 199 |
+
backdrop-filter: blur(5px);
|
| 200 |
+
z-index: 100;
|
| 201 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
| 202 |
+
}
|
| 203 |
+
#box-toolbar button {
|
| 204 |
+
background: transparent;
|
| 205 |
+
border: none;
|
| 206 |
+
color: #e9ecef;
|
| 207 |
+
width: 32px;
|
| 208 |
+
height: 32px;
|
| 209 |
+
font-size: 1.4rem;
|
| 210 |
+
padding: 0;
|
| 211 |
+
display: flex;
|
| 212 |
+
align-items: center;
|
| 213 |
+
justify-content: center;
|
| 214 |
+
}
|
| 215 |
+
#box-toolbar button:active {
|
| 216 |
+
transform: scale(0.9);
|
| 217 |
+
color: #fff;
|
| 218 |
+
}
|
| 219 |
#box-toolbar button.delete-btn { color: #dc3545; }
|
| 220 |
|
| 221 |
+
/* --- FAB CONTAINER - Improved Spacing --- */
|
| 222 |
+
.fab-container {
|
| 223 |
+
position: absolute;
|
| 224 |
+
bottom: 20px;
|
| 225 |
+
right: 20px;
|
| 226 |
+
z-index: 45;
|
| 227 |
+
display: flex;
|
| 228 |
+
flex-direction: column;
|
| 229 |
+
gap: 16px;
|
| 230 |
+
}
|
| 231 |
+
.fab-btn {
|
| 232 |
+
width: 56px;
|
| 233 |
+
height: 56px;
|
| 234 |
+
border-radius: 50%;
|
| 235 |
+
background: #212529;
|
| 236 |
+
border: 1px solid #495057;
|
| 237 |
+
color: white;
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
justify-content: center;
|
| 241 |
+
font-size: 1.3rem;
|
| 242 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
| 243 |
+
transition: transform 0.2s, background 0.2s;
|
| 244 |
+
cursor: pointer;
|
| 245 |
+
}
|
| 246 |
+
.fab-btn:hover {
|
| 247 |
+
transform: scale(1.05);
|
| 248 |
+
background: #2c3034;
|
| 249 |
+
}
|
| 250 |
+
.fab-btn:active {
|
| 251 |
+
transform: scale(0.95);
|
| 252 |
+
}
|
| 253 |
|
| 254 |
+
/* --- PANELS - Better Positioning --- */
|
| 255 |
+
.filters-panel {
|
| 256 |
+
position: absolute;
|
| 257 |
+
right: 0;
|
| 258 |
+
background: #212529;
|
| 259 |
+
border-radius: 12px;
|
| 260 |
+
padding: 16px;
|
| 261 |
+
width: 220px;
|
| 262 |
+
box-shadow: 0 5px 20px rgba(0,0,0,0.6);
|
| 263 |
+
display: none;
|
| 264 |
+
border: 1px solid #495057;
|
| 265 |
+
}
|
| 266 |
.filters-panel.show { display: block; }
|
| 267 |
+
|
| 268 |
+
/* Data Panel - Higher up to avoid brightness panel */
|
| 269 |
+
#dataPanel {
|
| 270 |
+
bottom: 150px;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* Brightness Panel - Lower */
|
| 274 |
+
#filtersPanel {
|
| 275 |
+
bottom: 80px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
.form-range { height: 4px; }
|
| 279 |
|
| 280 |
+
#loader-overlay {
|
| 281 |
+
position: fixed;
|
| 282 |
+
inset: 0;
|
| 283 |
+
background: rgba(0,0,0,0.7);
|
| 284 |
+
display: none;
|
| 285 |
+
align-items: center;
|
| 286 |
+
justify-content: center;
|
| 287 |
+
z-index: 9999;
|
| 288 |
+
}
|
| 289 |
+
.toast-container {
|
| 290 |
+
position: fixed;
|
| 291 |
+
top: 80px;
|
| 292 |
+
left: 50%;
|
| 293 |
+
transform: translateX(-50%);
|
| 294 |
+
z-index: 2000;
|
| 295 |
+
width: auto;
|
| 296 |
+
pointer-events: none;
|
| 297 |
+
}
|
| 298 |
+
.toast {
|
| 299 |
+
background: #333;
|
| 300 |
+
color: white;
|
| 301 |
+
border: 1px solid #555;
|
| 302 |
+
pointer-events: auto;
|
| 303 |
+
}
|
| 304 |
</style>
|
| 305 |
</head>
|
| 306 |
<body>
|
| 307 |
{% include '_navbar.html' %}
|
| 308 |
|
| 309 |
+
<!-- Progress Bar -->
|
| 310 |
+
<div id="progress-bar"></div>
|
| 311 |
+
|
| 312 |
<header class="app-header">
|
| 313 |
<h1 class="header-title"><i class="bi bi-bounding-box me-2"></i>Page {{ image_index + 1 }} / {{ total_pages }}</h1>
|
| 314 |
<div class="header-actions">
|
|
|
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
|
| 336 |
+
<!-- Floating Actions -->
|
| 337 |
+
<div class="fab-container">
|
| 338 |
+
<!-- Data Panel - Higher up -->
|
| 339 |
+
<div class="filters-panel" id="dataPanel">
|
| 340 |
+
<div id="no-selection-msg" class="text-center text-muted small py-3">Select a box to edit</div>
|
| 341 |
+
<div id="data-form" style="display: none;">
|
| 342 |
+
<div class="mb-2">
|
| 343 |
+
<label class="small text-secondary">Q. No</label>
|
| 344 |
+
<input type="text" class="form-control form-control-sm bg-dark text-white border-secondary" id="box-q-num">
|
| 345 |
+
</div>
|
| 346 |
+
<div class="mb-2">
|
| 347 |
+
<label class="small text-secondary">Status</label>
|
| 348 |
+
<select class="form-select form-select-sm bg-dark text-white border-secondary" id="box-status">
|
| 349 |
+
<option value="unattempted">Unattempted</option>
|
| 350 |
+
<option value="correct">Correct</option>
|
| 351 |
+
<option value="wrong">Wrong</option>
|
| 352 |
+
</select>
|
| 353 |
+
</div>
|
| 354 |
+
<div class="row g-2 mb-0">
|
| 355 |
+
<div class="col-6">
|
| 356 |
+
<label class="small text-secondary">Marked</label>
|
| 357 |
+
<input type="text" class="form-control form-control-sm bg-dark text-white border-secondary" id="box-marked">
|
| 358 |
+
</div>
|
| 359 |
+
<div class="col-6">
|
| 360 |
+
<label class="small text-secondary">Actual</label>
|
| 361 |
+
<input type="text" class="form-control form-control-sm bg-dark text-white border-secondary" id="box-actual">
|
| 362 |
+
</div>
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
</div>
|
| 366 |
+
<button class="fab-btn" id="dataToggle" title="Data Entry"><i class="bi bi-pencil-square"></i></button>
|
|
|
|
|
|
|
| 367 |
|
| 368 |
+
<!-- Brightness Panel - Lower -->
|
| 369 |
+
<div class="filters-panel" id="filtersPanel">
|
| 370 |
+
<div class="mb-3">
|
| 371 |
+
<label class="small text-secondary d-flex justify-content-between">
|
| 372 |
+
Brightness <span id="val-b" class="text-white">0</span>
|
| 373 |
+
</label>
|
| 374 |
+
<input type="range" class="form-range" id="brightness" min="-100" max="100" value="0" step="5">
|
| 375 |
+
</div>
|
| 376 |
+
<div class="mb-3">
|
| 377 |
+
<label class="small text-secondary d-flex justify-content-between">
|
| 378 |
+
Contrast <span id="val-c" class="text-white">1.0</span>
|
| 379 |
+
</label>
|
| 380 |
+
<input type="range" class="form-range" id="contrast" min="0.5" max="2.5" step="0.05" value="1.0">
|
| 381 |
+
</div>
|
| 382 |
+
<div class="mb-0">
|
| 383 |
+
<label class="small text-secondary d-flex justify-content-between">
|
| 384 |
+
Gamma <span id="val-g" class="text-white">1.0</span>
|
| 385 |
+
</label>
|
| 386 |
+
<input type="range" class="form-range" id="gamma" min="0.2" max="2.2" step="0.1" value="1.0">
|
| 387 |
+
</div>
|
| 388 |
</div>
|
| 389 |
+
<button class="fab-btn" id="filterToggle" title="Brightness & Filters"><i class="bi bi-sliders"></i></button>
|
| 390 |
</div>
|
| 391 |
</div>
|
| 392 |
|
| 393 |
<div class="thumbnail-bar">
|
| 394 |
{% for page in all_pages %}
|
| 395 |
+
<div class="thumb-item {% if page.image_index == image_index %}active{% endif %}" data-page-index="{{ page.image_index }}">
|
| 396 |
+
<div class="thumb-loader"></div>
|
| 397 |
<img data-src="/image/upload/{{ page.filename }}"
|
| 398 |
alt="Page {{ page.image_index + 1 }}"
|
| 399 |
data-session="{{ session_id }}"
|
|
|
|
| 418 |
};
|
| 419 |
const storageKey = CONFIG.userId ? `cropState_${CONFIG.userId}_${CONFIG.sessionId}_${CONFIG.imageIndex}` : `cropState_${CONFIG.sessionId}_${CONFIG.imageIndex}`;
|
| 420 |
|
| 421 |
+
// --- PROGRESS BAR UTILITY ---
|
| 422 |
+
const ProgressBar = {
|
| 423 |
+
el: document.getElementById('progress-bar'),
|
| 424 |
+
show(progress = 0) {
|
| 425 |
+
this.el.style.transform = `scaleX(${progress})`;
|
| 426 |
+
this.el.classList.add('active');
|
| 427 |
+
},
|
| 428 |
+
update(progress) {
|
| 429 |
+
this.el.style.transform = `scaleX(${Math.min(1, Math.max(0, progress))})`;
|
| 430 |
+
},
|
| 431 |
+
hide() {
|
| 432 |
+
this.el.classList.remove('active');
|
| 433 |
+
setTimeout(() => this.el.style.transform = 'scaleX(0)', 300);
|
| 434 |
+
}
|
| 435 |
+
};
|
| 436 |
+
|
| 437 |
+
// --- INDEXED DB CACHE WITH LAZY LOADING ---
|
| 438 |
const ThumbCache = {
|
| 439 |
+
DB_NAME: 'PDF_Crop_Thumbs',
|
| 440 |
+
STORE_NAME: 'images',
|
| 441 |
+
EXPIRY_MS: 2 * 24 * 60 * 60 * 1000,
|
| 442 |
+
observer: null,
|
| 443 |
+
|
| 444 |
async open() {
|
| 445 |
return new Promise((resolve, reject) => {
|
| 446 |
const req = indexedDB.open(this.DB_NAME, 1);
|
| 447 |
+
req.onupgradeneeded = (e) => {
|
| 448 |
+
const db = e.target.result;
|
| 449 |
+
if (!db.objectStoreNames.contains(this.STORE_NAME)) {
|
| 450 |
+
db.createObjectStore(this.STORE_NAME, { keyPath: 'url' });
|
| 451 |
+
}
|
| 452 |
+
};
|
| 453 |
+
req.onsuccess = (e) => resolve(e.target.result);
|
| 454 |
+
req.onerror = (e) => reject(e);
|
| 455 |
});
|
| 456 |
},
|
| 457 |
+
|
| 458 |
async cleanup() {
|
| 459 |
const db = await this.open();
|
| 460 |
return new Promise((resolve) => {
|
| 461 |
+
const tx = db.transaction(this.STORE_NAME, 'readwrite');
|
| 462 |
+
const store = tx.objectStore(this.STORE_NAME);
|
| 463 |
+
const now = Date.now();
|
| 464 |
const req = store.openCursor();
|
| 465 |
+
|
| 466 |
+
req.onsuccess = (e) => {
|
| 467 |
+
const cursor = e.target.result;
|
| 468 |
+
if (cursor) {
|
| 469 |
+
if ((now - cursor.value.timestamp) > this.EXPIRY_MS) {
|
| 470 |
+
cursor.delete();
|
| 471 |
+
}
|
| 472 |
+
cursor.continue();
|
| 473 |
+
} else {
|
| 474 |
+
resolve();
|
| 475 |
+
}
|
| 476 |
+
};
|
| 477 |
req.onerror = () => resolve();
|
| 478 |
});
|
| 479 |
},
|
| 480 |
+
|
| 481 |
+
async loadImage(url, imgElement, loaderElement) {
|
| 482 |
try {
|
| 483 |
+
const db = await this.open();
|
| 484 |
+
const txRead = db.transaction(this.STORE_NAME, 'readonly');
|
| 485 |
const req = txRead.objectStore(this.STORE_NAME).get(url);
|
| 486 |
+
|
| 487 |
req.onsuccess = async (e) => {
|
| 488 |
const record = e.target.result;
|
| 489 |
+
if (record) {
|
| 490 |
+
imgElement.src = URL.createObjectURL(record.blob);
|
| 491 |
+
imgElement.classList.add('loaded');
|
| 492 |
+
if (loaderElement) loaderElement.remove();
|
| 493 |
+
} else {
|
| 494 |
try {
|
| 495 |
+
const response = await fetch(url);
|
| 496 |
+
const blob = await response.blob();
|
| 497 |
+
const txWrite = db.transaction(this.STORE_NAME, 'readwrite');
|
| 498 |
+
txWrite.objectStore(this.STORE_NAME).put({
|
| 499 |
+
url: url,
|
| 500 |
+
blob: blob,
|
| 501 |
+
timestamp: Date.now(),
|
| 502 |
+
sessionId: CONFIG.sessionId
|
| 503 |
+
});
|
| 504 |
+
imgElement.src = URL.createObjectURL(blob);
|
| 505 |
+
imgElement.classList.add('loaded');
|
| 506 |
+
if (loaderElement) loaderElement.remove();
|
| 507 |
+
} catch (err) {
|
| 508 |
+
imgElement.src = url;
|
| 509 |
+
imgElement.classList.add('loaded');
|
| 510 |
+
if (loaderElement) loaderElement.remove();
|
| 511 |
+
}
|
| 512 |
}
|
| 513 |
};
|
| 514 |
+
} catch (err) {
|
| 515 |
+
imgElement.src = url;
|
| 516 |
+
imgElement.classList.add('loaded');
|
| 517 |
+
if (loaderElement) loaderElement.remove();
|
| 518 |
+
}
|
| 519 |
},
|
| 520 |
+
|
| 521 |
+
initLazyLoading() {
|
| 522 |
+
this.cleanup();
|
| 523 |
+
|
| 524 |
+
const options = {
|
| 525 |
+
root: document.querySelector('.thumbnail-bar'),
|
| 526 |
+
rootMargin: '50px',
|
| 527 |
+
threshold: 0.01
|
| 528 |
+
};
|
| 529 |
+
|
| 530 |
+
this.observer = new IntersectionObserver((entries) => {
|
| 531 |
+
entries.forEach(entry => {
|
| 532 |
+
if (entry.isIntersecting) {
|
| 533 |
+
const img = entry.target.querySelector('.thumb-img');
|
| 534 |
+
const loader = entry.target.querySelector('.thumb-loader');
|
| 535 |
+
const url = img.getAttribute('data-src');
|
| 536 |
+
|
| 537 |
+
if (url && !img.classList.contains('loaded')) {
|
| 538 |
+
this.loadImage(url, img, loader);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
this.observer.unobserve(entry.target);
|
| 542 |
+
}
|
| 543 |
+
});
|
| 544 |
+
}, options);
|
| 545 |
+
|
| 546 |
+
// Load active thumbnail immediately
|
| 547 |
+
const activeThumb = document.querySelector('.thumb-item.active');
|
| 548 |
+
if (activeThumb) {
|
| 549 |
+
const img = activeThumb.querySelector('.thumb-img');
|
| 550 |
+
const loader = activeThumb.querySelector('.thumb-loader');
|
| 551 |
+
const url = img.getAttribute('data-src');
|
| 552 |
+
if (url) this.loadImage(url, img, loader);
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
// Observe all other thumbnails
|
| 556 |
+
document.querySelectorAll('.thumb-item:not(.active)').forEach(thumb => {
|
| 557 |
+
this.observer.observe(thumb);
|
| 558 |
+
});
|
| 559 |
}
|
| 560 |
};
|
| 561 |
|
| 562 |
// --- APP LOGIC ---
|
| 563 |
const els = {
|
| 564 |
+
image: document.getElementById('main-image'),
|
| 565 |
+
imagePane: document.getElementById('imagePane'),
|
| 566 |
+
cropArea: document.getElementById('crop-area'),
|
| 567 |
+
canvas: document.getElementById('draw-canvas'),
|
| 568 |
+
ctx: document.getElementById('draw-canvas').getContext('2d'),
|
| 569 |
+
toolbar: document.getElementById('box-toolbar'),
|
| 570 |
magnifier: document.getElementById('magnifier')
|
| 571 |
};
|
| 572 |
|
|
|
|
| 574 |
let selectedBoxIndex = -1;
|
| 575 |
let isDrawing = false;
|
| 576 |
let startX, startY;
|
| 577 |
+
let dragTarget = null;
|
| 578 |
let startPositions = {};
|
| 579 |
let stitchBuffer = JSON.parse(localStorage.getItem('gemini_stitch_buffer') || 'null');
|
| 580 |
|
| 581 |
// ZOOM STATE
|
| 582 |
+
const LENS_SIZE_PX = 140;
|
| 583 |
const ZOOM_LEVEL = 2.5;
|
| 584 |
let isMagnifying = false;
|
| 585 |
+
let magnifierPos = { x: 0, y: 0 };
|
| 586 |
|
| 587 |
function init() {
|
| 588 |
+
ThumbCache.initLazyLoading();
|
| 589 |
+
if (els.image.complete && els.image.naturalWidth > 0) fitImage();
|
| 590 |
+
else els.image.onload = fitImage;
|
| 591 |
+
|
| 592 |
+
const ro = new ResizeObserver(() => requestAnimationFrame(fitImage));
|
| 593 |
+
ro.observe(els.imagePane);
|
| 594 |
+
|
| 595 |
+
loadSettings();
|
| 596 |
+
loadBoxes();
|
| 597 |
+
setupListeners();
|
| 598 |
+
updateStitchButton();
|
| 599 |
+
|
| 600 |
+
const active = document.querySelector('.thumb-item.active');
|
| 601 |
+
if (active) active.scrollIntoView({ inline: 'center' });
|
| 602 |
}
|
| 603 |
|
| 604 |
function fitImage() {
|
| 605 |
if (!els.image.naturalWidth) return;
|
| 606 |
const rect = els.imagePane.getBoundingClientRect();
|
| 607 |
const padding = 10;
|
| 608 |
+
const availableW = rect.width - padding;
|
| 609 |
+
const availableH = rect.height - padding;
|
| 610 |
const scale = Math.min(availableW / els.image.naturalWidth, availableH / els.image.naturalHeight);
|
| 611 |
|
| 612 |
+
const finalW = Math.floor(els.image.naturalWidth * scale);
|
| 613 |
+
const finalH = Math.floor(els.image.naturalHeight * scale);
|
| 614 |
+
els.cropArea.style.width = `${finalW}px`;
|
| 615 |
+
els.cropArea.style.height = `${finalH}px`;
|
| 616 |
+
els.image.style.width = `${finalW}px`;
|
| 617 |
+
els.image.style.height = `${finalH}px`;
|
| 618 |
+
els.canvas.width = finalW;
|
| 619 |
+
els.canvas.height = finalH;
|
| 620 |
|
| 621 |
if (CONFIG.enableMagnifier) {
|
| 622 |
els.magnifier.style.backgroundImage = `url('${els.image.src}')`;
|
|
|
|
| 630 |
function drawBoxes() {
|
| 631 |
els.ctx.clearRect(0, 0, els.canvas.width, els.canvas.height);
|
| 632 |
|
|
|
|
| 633 |
boxes.forEach((box, index) => {
|
| 634 |
const isSelected = index === selectedBoxIndex;
|
| 635 |
const isStitched = box.remote_stitch_source != null;
|
|
|
|
| 640 |
els.ctx.fillStyle = isSelected ? 'rgba(255, 77, 77, 0.15)' : (isStitched ? 'rgba(13, 202, 240, 0.2)' : 'rgba(255, 193, 7, 0.1)');
|
| 641 |
|
| 642 |
els.ctx.beginPath();
|
| 643 |
+
els.ctx.moveTo(p(box.tl).x, p(box.tl).y);
|
| 644 |
+
els.ctx.lineTo(p(box.tr).x, p(box.tr).y);
|
| 645 |
+
els.ctx.lineTo(p(box.br).x, p(box.br).y);
|
| 646 |
+
els.ctx.lineTo(p(box.bl).x, p(box.bl).y);
|
| 647 |
+
els.ctx.closePath();
|
| 648 |
+
els.ctx.stroke();
|
| 649 |
+
els.ctx.fill();
|
| 650 |
|
| 651 |
if (isSelected) {
|
| 652 |
els.ctx.fillStyle = 'white';
|
| 653 |
+
['tl', 'tr', 'bl', 'br'].forEach(k => {
|
| 654 |
+
els.ctx.beginPath();
|
| 655 |
+
els.ctx.arc(p(box[k]).x, p(box[k]).y, 8, 0, Math.PI * 2);
|
| 656 |
+
els.ctx.fill();
|
| 657 |
+
els.ctx.stroke();
|
| 658 |
+
});
|
| 659 |
}
|
| 660 |
|
| 661 |
+
const cx = (p(box.tl).x + p(box.br).x) / 2;
|
| 662 |
+
const cy = (p(box.tl).y + p(box.br).y) / 2;
|
| 663 |
+
els.ctx.font = "bold 24px system-ui";
|
| 664 |
+
els.ctx.fillStyle = "white";
|
| 665 |
+
els.ctx.shadowColor = "rgba(0,0,0,0.8)";
|
| 666 |
+
els.ctx.shadowBlur = 6;
|
| 667 |
els.ctx.fillText(index + 1, cx - 6, cy + 8);
|
| 668 |
+
if (isStitched) {
|
| 669 |
+
els.ctx.font = "20px system-ui";
|
| 670 |
+
els.ctx.fillText("🔗", p(box.tr).x - 28, p(box.tr).y + 24);
|
| 671 |
+
}
|
| 672 |
+
els.ctx.shadowBlur = 0;
|
| 673 |
});
|
| 674 |
|
|
|
|
| 675 |
if (isMagnifying && CONFIG.enableMagnifier) {
|
| 676 |
const px = magnifierPos.x * els.canvas.width;
|
| 677 |
const py = magnifierPos.y * els.canvas.height;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
const sourceRadius = (LENS_SIZE_PX / ZOOM_LEVEL) / 2;
|
| 679 |
|
| 680 |
els.ctx.beginPath();
|
| 681 |
els.ctx.arc(px, py, sourceRadius, 0, Math.PI * 2);
|
|
|
|
|
|
|
| 682 |
els.ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
| 683 |
els.ctx.lineWidth = 1.5;
|
| 684 |
els.ctx.shadowColor = 'rgba(0,0,0,1)';
|
| 685 |
els.ctx.shadowBlur = 4;
|
| 686 |
+
els.ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
|
|
|
|
| 687 |
els.ctx.stroke();
|
| 688 |
els.ctx.fill();
|
| 689 |
+
els.ctx.shadowBlur = 0;
|
| 690 |
}
|
| 691 |
}
|
| 692 |
|
|
|
|
| 699 |
|
| 700 |
if (active) {
|
| 701 |
const lens = els.magnifier;
|
| 702 |
+
const w = els.canvas.width;
|
| 703 |
+
const h = els.canvas.height;
|
| 704 |
+
const px = x * w;
|
| 705 |
+
const py = y * h;
|
| 706 |
|
| 707 |
+
let top = py - LENS_SIZE_PX - 50;
|
| 708 |
+
if (top < 0) top = py + 50;
|
|
|
|
| 709 |
|
| 710 |
lens.style.display = 'block';
|
| 711 |
+
lens.style.left = (px - LENS_SIZE_PX / 2) + 'px';
|
| 712 |
lens.style.top = top + 'px';
|
| 713 |
|
|
|
|
| 714 |
lens.style.backgroundSize = `${w * ZOOM_LEVEL}px ${h * ZOOM_LEVEL}px`;
|
| 715 |
const bgX = (px * ZOOM_LEVEL) - (LENS_SIZE_PX / 2);
|
| 716 |
const bgY = (py * ZOOM_LEVEL) - (LENS_SIZE_PX / 2);
|
|
|
|
| 731 |
}
|
| 732 |
|
| 733 |
function hitTest(x, y) {
|
| 734 |
+
const pad = 30 / els.canvas.width;
|
| 735 |
for (let i = boxes.length - 1; i >= 0; i--) {
|
| 736 |
const b = boxes[i];
|
| 737 |
+
for (let k of ['tl', 'tr', 'bl', 'br']) {
|
| 738 |
+
if (Math.hypot(b[k].x - x, b[k].y - y) < pad) {
|
| 739 |
+
return { type: 'corner', index: i, corner: k };
|
| 740 |
+
}
|
| 741 |
+
}
|
| 742 |
+
const mx = Math.min(b.tl.x, b.br.x), Mx = Math.max(b.tl.x, b.br.x);
|
| 743 |
+
const my = Math.min(b.tl.y, b.br.y), My = Math.max(b.tl.y, b.br.y);
|
| 744 |
+
if (x > mx && x < Mx && y > my && y < My) {
|
| 745 |
+
return { type: 'body', index: i };
|
| 746 |
+
}
|
| 747 |
}
|
| 748 |
return null;
|
| 749 |
}
|
| 750 |
|
| 751 |
function onDown(e) {
|
| 752 |
+
if (e.target.closest('#box-toolbar')) return;
|
| 753 |
e.preventDefault();
|
| 754 |
+
const { x, y } = getPos(e);
|
| 755 |
|
| 756 |
+
updateMagnifierState(x, y, true);
|
| 757 |
|
| 758 |
const hit = hitTest(x, y);
|
| 759 |
if (hit) {
|
| 760 |
+
dragTarget = hit;
|
| 761 |
+
selectedBoxIndex = hit.index;
|
| 762 |
+
startPositions = JSON.parse(JSON.stringify(boxes[hit.index]));
|
| 763 |
+
startX = x;
|
| 764 |
+
startY = y;
|
| 765 |
+
updateToolbar();
|
| 766 |
+
updateStitchButton();
|
| 767 |
} else {
|
| 768 |
+
selectedBoxIndex = -1;
|
| 769 |
+
els.toolbar.style.display = 'none';
|
| 770 |
+
isDrawing = true;
|
| 771 |
+
startX = x;
|
| 772 |
+
startY = y;
|
| 773 |
}
|
| 774 |
drawBoxes();
|
| 775 |
}
|
| 776 |
|
| 777 |
function onMove(e) {
|
| 778 |
+
if (isDrawing || dragTarget) {
|
| 779 |
+
e.preventDefault();
|
| 780 |
+
const { x, y } = getPos(e);
|
| 781 |
+
updateMagnifierState(x, y, true);
|
| 782 |
+
|
| 783 |
+
const dx = x - startX, dy = y - startY;
|
| 784 |
+
if (dragTarget) {
|
| 785 |
const b = boxes[dragTarget.index];
|
| 786 |
+
if (dragTarget.type === 'corner') {
|
| 787 |
+
b[dragTarget.corner].x = x;
|
| 788 |
+
b[dragTarget.corner].y = y;
|
| 789 |
+
} else {
|
| 790 |
+
['tl', 'tr', 'bl', 'br'].forEach(k => {
|
| 791 |
+
b[k].x = startPositions[k].x + dx;
|
| 792 |
+
b[k].y = startPositions[k].y + dy;
|
| 793 |
+
});
|
| 794 |
+
}
|
| 795 |
+
drawBoxes();
|
| 796 |
+
updateToolbar();
|
| 797 |
+
} else if (isDrawing) {
|
| 798 |
drawBoxes();
|
| 799 |
const sx = startX * els.canvas.width, sy = startY * els.canvas.height;
|
| 800 |
const w = (x - startX) * els.canvas.width, h = (y - startY) * els.canvas.height;
|
| 801 |
+
els.ctx.strokeStyle = 'rgba(255, 77, 77, 0.5)';
|
| 802 |
+
els.ctx.strokeRect(sx, sy, w, h);
|
| 803 |
+
}
|
| 804 |
}
|
| 805 |
}
|
| 806 |
|
| 807 |
function onUp(e) {
|
| 808 |
+
updateMagnifierState(0, 0, false);
|
| 809 |
|
| 810 |
if (isDrawing) {
|
| 811 |
const rect = els.canvas.getBoundingClientRect();
|
|
|
|
| 825 |
selectedBoxIndex = boxes.length - 1;
|
| 826 |
}
|
| 827 |
}
|
| 828 |
+
isDrawing = false;
|
| 829 |
+
dragTarget = null;
|
| 830 |
+
saveBoxes();
|
| 831 |
+
drawBoxes();
|
| 832 |
+
updateToolbar();
|
| 833 |
+
updateStitchButton();
|
| 834 |
}
|
| 835 |
|
| 836 |
// --- HELPERS ---
|
| 837 |
function setupListeners() {
|
| 838 |
els.canvas.addEventListener('mousedown', onDown);
|
| 839 |
+
els.canvas.addEventListener('touchstart', onDown, { passive: false });
|
| 840 |
document.addEventListener('mousemove', onMove);
|
| 841 |
+
document.addEventListener('touchmove', onMove, { passive: false });
|
| 842 |
document.addEventListener('mouseup', onUp);
|
| 843 |
document.addEventListener('touchend', onUp);
|
| 844 |
|
| 845 |
+
document.getElementById('backBtn').onclick = () => {
|
| 846 |
+
location.href = CONFIG.imageIndex > 0 ? `/cropv2/${CONFIG.sessionId}/${CONFIG.imageIndex - 1}` : `/v2`;
|
| 847 |
+
};
|
| 848 |
+
|
| 849 |
+
document.getElementById('clearBtn').onclick = () => {
|
| 850 |
+
if (confirm("Clear all boxes?")) {
|
| 851 |
+
boxes = [];
|
| 852 |
+
selectedBoxIndex = -1;
|
| 853 |
+
saveBoxes();
|
| 854 |
+
drawBoxes();
|
| 855 |
+
els.toolbar.style.display = 'none';
|
| 856 |
+
}
|
| 857 |
+
};
|
| 858 |
+
|
| 859 |
+
document.getElementById('delete-btn').onclick = (e) => {
|
| 860 |
+
e.stopPropagation();
|
| 861 |
+
boxes.splice(selectedBoxIndex, 1);
|
| 862 |
+
selectedBoxIndex = -1;
|
| 863 |
+
els.toolbar.style.display = 'none';
|
| 864 |
+
saveBoxes();
|
| 865 |
+
drawBoxes();
|
| 866 |
+
};
|
| 867 |
+
|
| 868 |
+
document.getElementById('move-up-btn').onclick = (e) => {
|
| 869 |
+
e.stopPropagation();
|
| 870 |
+
if (selectedBoxIndex < boxes.length - 1) {
|
| 871 |
+
const b = boxes.splice(selectedBoxIndex, 1)[0];
|
| 872 |
+
boxes.splice(selectedBoxIndex + 1, 0, b);
|
| 873 |
+
selectedBoxIndex++;
|
| 874 |
+
saveBoxes();
|
| 875 |
+
drawBoxes();
|
| 876 |
+
updateToolbar();
|
| 877 |
+
}
|
| 878 |
+
};
|
| 879 |
+
|
| 880 |
+
document.getElementById('move-down-btn').onclick = (e) => {
|
| 881 |
+
e.stopPropagation();
|
| 882 |
+
if (selectedBoxIndex > 0) {
|
| 883 |
+
const b = boxes.splice(selectedBoxIndex, 1)[0];
|
| 884 |
+
boxes.splice(selectedBoxIndex - 1, 0, b);
|
| 885 |
+
selectedBoxIndex--;
|
| 886 |
+
saveBoxes();
|
| 887 |
+
drawBoxes();
|
| 888 |
+
updateToolbar();
|
| 889 |
+
}
|
| 890 |
+
};
|
| 891 |
+
|
| 892 |
document.getElementById('stitch-btn').onclick = handleStitch;
|
| 893 |
document.getElementById('processBtn').onclick = processPage;
|
| 894 |
+
|
| 895 |
// Data Entry
|
| 896 |
document.getElementById('dataToggle').onclick = () => {
|
| 897 |
const panel = document.getElementById('dataPanel');
|
| 898 |
+
const filterPanel = document.getElementById('filtersPanel');
|
| 899 |
+
|
| 900 |
+
// Close filter panel if open
|
| 901 |
+
if (filterPanel.classList.contains('show')) {
|
| 902 |
+
filterPanel.classList.remove('show');
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
if (!panel.classList.contains('show') && selectedBoxIndex === -1 && boxes.length > 0) {
|
| 906 |
selectedBoxIndex = 0;
|
| 907 |
updateToolbar();
|
|
|
|
| 931 |
if (!e.shiftKey) return;
|
| 932 |
const key = e.key.toLowerCase();
|
| 933 |
|
|
|
|
| 934 |
if (e.key === 'ArrowRight') {
|
| 935 |
e.preventDefault();
|
| 936 |
document.getElementById('processBtn').click();
|
|
|
|
| 942 |
return;
|
| 943 |
}
|
| 944 |
|
|
|
|
| 945 |
if (key === 'q') {
|
| 946 |
e.preventDefault();
|
| 947 |
if (boxes.length > 0) {
|
|
|
|
| 952 |
return;
|
| 953 |
}
|
| 954 |
|
|
|
|
| 955 |
if (selectedBoxIndex === -1) return;
|
| 956 |
|
| 957 |
if (key === 'm') {
|
|
|
|
| 980 |
|
| 981 |
// Filters
|
| 982 |
document.getElementById('filterToggle').onclick = () => {
|
| 983 |
+
const panel = document.getElementById('filtersPanel');
|
| 984 |
+
const dataPanel = document.getElementById('dataPanel');
|
| 985 |
+
|
| 986 |
+
// Close data panel if open
|
| 987 |
+
if (dataPanel.classList.contains('show')) {
|
| 988 |
+
dataPanel.classList.remove('show');
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
panel.classList.toggle('show');
|
| 992 |
};
|
| 993 |
|
| 994 |
+
['brightness', 'contrast', 'gamma'].forEach(id => {
|
| 995 |
+
document.getElementById(id).addEventListener('input', updateFilters);
|
| 996 |
+
});
|
| 997 |
+
|
| 998 |
+
// Thumbnail Click Navigation
|
| 999 |
+
document.querySelectorAll('.thumb-item').forEach(thumb => {
|
| 1000 |
+
thumb.addEventListener('click', () => {
|
| 1001 |
+
const pageIndex = parseInt(thumb.getAttribute('data-page-index'));
|
| 1002 |
+
location.href = `/cropv2/${CONFIG.sessionId}/${pageIndex}`;
|
| 1003 |
+
});
|
| 1004 |
+
});
|
| 1005 |
}
|
| 1006 |
|
| 1007 |
function updateDataPanel() {
|
|
|
|
| 1010 |
const form = document.getElementById('data-form');
|
| 1011 |
|
| 1012 |
if (!b) {
|
| 1013 |
+
if (msg) msg.style.display = 'block';
|
| 1014 |
+
if (form) form.style.display = 'none';
|
| 1015 |
return;
|
| 1016 |
}
|
| 1017 |
|
| 1018 |
+
if (msg) msg.style.display = 'none';
|
| 1019 |
+
if (form) form.style.display = 'block';
|
| 1020 |
|
| 1021 |
document.getElementById('box-q-num').value = b.question_number || '';
|
| 1022 |
document.getElementById('box-status').value = b.status || 'unattempted';
|
|
|
|
| 1026 |
|
| 1027 |
function updateToolbar() {
|
| 1028 |
updateDataPanel();
|
| 1029 |
+
if (selectedBoxIndex === -1) {
|
| 1030 |
+
els.toolbar.style.display = 'none';
|
| 1031 |
+
return;
|
| 1032 |
+
}
|
| 1033 |
const b = boxes[selectedBoxIndex];
|
| 1034 |
const p = (pt) => ({ x: pt.x * els.canvas.width, y: pt.y * els.canvas.height });
|
| 1035 |
const maxX = Math.max(p(b.tr).x, p(b.br).x);
|
| 1036 |
const minY = Math.min(p(b.tl).y, p(b.tr).y);
|
| 1037 |
+
let left = maxX - 180;
|
| 1038 |
+
if (left < 0) left = 0;
|
| 1039 |
let top = minY + 10;
|
| 1040 |
+
els.toolbar.style.left = `${left}px`;
|
| 1041 |
+
els.toolbar.style.top = `${top}px`;
|
| 1042 |
+
els.toolbar.style.display = 'flex';
|
| 1043 |
}
|
| 1044 |
|
| 1045 |
function updateStitchButton() {
|
|
|
|
| 1047 |
const icon = btn.querySelector('i');
|
| 1048 |
const isBuffer = stitchBuffer && stitchBuffer.session_id === CONFIG.sessionId;
|
| 1049 |
const isStitched = selectedBoxIndex > -1 && boxes[selectedBoxIndex]?.remote_stitch_source;
|
| 1050 |
+
if (isBuffer) {
|
| 1051 |
+
icon.className = 'bi bi-link-45deg';
|
| 1052 |
+
btn.style.color = '#0dcaf0';
|
| 1053 |
+
} else if (isStitched) {
|
| 1054 |
+
icon.className = 'bi bi-x-lg';
|
| 1055 |
+
btn.style.color = '#dc3545';
|
| 1056 |
+
} else {
|
| 1057 |
+
icon.className = 'bi bi-scissors';
|
| 1058 |
+
btn.style.color = '#e9ecef';
|
| 1059 |
+
}
|
| 1060 |
}
|
| 1061 |
|
| 1062 |
function handleStitch(e) {
|
| 1063 |
+
e.stopPropagation();
|
| 1064 |
+
if (selectedBoxIndex === -1) return;
|
| 1065 |
const b = boxes[selectedBoxIndex];
|
| 1066 |
+
if (stitchBuffer && stitchBuffer.session_id === CONFIG.sessionId) {
|
| 1067 |
+
b.remote_stitch_source = {
|
| 1068 |
+
page_index: stitchBuffer.page_index,
|
| 1069 |
+
box: stitchBuffer.box
|
| 1070 |
+
};
|
| 1071 |
+
stitchBuffer = null;
|
| 1072 |
+
localStorage.removeItem('gemini_stitch_buffer');
|
| 1073 |
+
toast('Boxes Linked!');
|
| 1074 |
+
} else if (b.remote_stitch_source) {
|
| 1075 |
+
b.remote_stitch_source = null;
|
| 1076 |
+
toast('Link Removed');
|
| 1077 |
} else {
|
| 1078 |
+
const minX = Math.min(b.tl.x, b.bl.x), minY = Math.min(b.tl.y, b.tr.y);
|
| 1079 |
+
const maxX = Math.max(b.tr.x, b.br.x), maxY = Math.max(b.bl.y, b.br.y);
|
| 1080 |
const cleanBox = { ...b, x: minX, y: minY, w: maxX - minX, h: maxY - minY };
|
| 1081 |
+
stitchBuffer = {
|
| 1082 |
+
session_id: CONFIG.sessionId,
|
| 1083 |
+
page_index: CONFIG.imageIndex,
|
| 1084 |
+
box: cleanBox
|
| 1085 |
+
};
|
| 1086 |
+
localStorage.setItem('gemini_stitch_buffer', JSON.stringify(stitchBuffer));
|
| 1087 |
+
toast('Copied!');
|
| 1088 |
}
|
| 1089 |
+
saveBoxes();
|
| 1090 |
+
updateStitchButton();
|
| 1091 |
+
drawBoxes();
|
| 1092 |
}
|
| 1093 |
|
| 1094 |
function toast(msg) {
|
| 1095 |
+
const t = document.createElement('div');
|
| 1096 |
+
t.className = 'toast align-items-center show fade p-2 rounded-3';
|
| 1097 |
t.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div></div>`;
|
| 1098 |
+
document.getElementById('toastContainer').appendChild(t);
|
| 1099 |
+
setTimeout(() => t.remove(), 2500);
|
| 1100 |
}
|
| 1101 |
|
| 1102 |
async function processPage() {
|
| 1103 |
+
if (!boxes.length && !confirm("Skip this page?")) return;
|
| 1104 |
+
|
| 1105 |
+
ProgressBar.show(0.3);
|
| 1106 |
+
document.getElementById('loader-overlay').style.display = 'flex';
|
| 1107 |
+
|
| 1108 |
const finalBoxes = boxes.map(b => ({
|
| 1109 |
+
...b,
|
| 1110 |
+
x: Math.min(b.tl.x, b.bl.x),
|
| 1111 |
+
y: Math.min(b.tl.y, b.tr.y),
|
| 1112 |
w: Math.max(b.tr.x, b.br.x) - Math.min(b.tl.x, b.bl.x),
|
| 1113 |
h: Math.max(b.bl.y, b.br.y) - Math.min(b.tl.y, b.tr.y)
|
| 1114 |
}));
|
| 1115 |
+
|
| 1116 |
+
const cv = document.createElement('canvas');
|
| 1117 |
+
cv.width = els.image.naturalWidth;
|
| 1118 |
+
cv.height = els.image.naturalHeight;
|
| 1119 |
+
const c = cv.getContext('2d');
|
| 1120 |
+
c.filter = els.image.style.filter;
|
| 1121 |
+
c.drawImage(els.image, 0, 0);
|
| 1122 |
+
|
| 1123 |
+
ProgressBar.update(0.5);
|
| 1124 |
+
|
| 1125 |
try {
|
| 1126 |
+
const res = await fetch('/process_crop_v2', {
|
| 1127 |
+
method: 'POST',
|
| 1128 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1129 |
+
body: JSON.stringify({
|
| 1130 |
+
session_id: CONFIG.sessionId,
|
| 1131 |
+
image_index: CONFIG.imageIndex,
|
| 1132 |
+
boxes: finalBoxes,
|
| 1133 |
+
imageData: cv.toDataURL('image/jpeg', 0.85)
|
| 1134 |
+
})
|
| 1135 |
+
});
|
| 1136 |
+
|
| 1137 |
+
if (!res.ok) throw new Error(await res.text());
|
| 1138 |
+
|
| 1139 |
+
ProgressBar.update(1);
|
| 1140 |
+
setTimeout(() => {
|
| 1141 |
+
const next = CONFIG.imageIndex + 1;
|
| 1142 |
+
location.href = next < CONFIG.totalPages
|
| 1143 |
+
? `/cropv2/${CONFIG.sessionId}/${next}`
|
| 1144 |
+
: `/question_entry_v2/${CONFIG.sessionId}`;
|
| 1145 |
+
}, 200);
|
| 1146 |
+
} catch (e) {
|
| 1147 |
+
alert(e.message);
|
| 1148 |
+
document.getElementById('loader-overlay').style.display = 'none';
|
| 1149 |
+
ProgressBar.hide();
|
| 1150 |
+
}
|
| 1151 |
}
|
| 1152 |
|
| 1153 |
+
function saveBoxes() {
|
| 1154 |
+
localStorage.setItem(storageKey, JSON.stringify(boxes));
|
| 1155 |
+
}
|
| 1156 |
+
|
| 1157 |
function loadBoxes() {
|
| 1158 |
try {
|
| 1159 |
const s = localStorage.getItem(storageKey);
|
| 1160 |
+
if (s) {
|
| 1161 |
+
boxes = JSON.parse(s).map(b => b.tl ? b : {
|
| 1162 |
+
id: b.id || Date.now(),
|
| 1163 |
+
tl: { x: b.x, y: b.y },
|
| 1164 |
+
tr: { x: b.x + b.w, y: b.y },
|
| 1165 |
+
bl: { x: b.x, y: b.y + b.h },
|
| 1166 |
+
br: { x: b.x + b.w, y: b.y + b.h },
|
| 1167 |
+
remote_stitch_source: b.remote_stitch_source
|
| 1168 |
+
});
|
| 1169 |
+
drawBoxes();
|
| 1170 |
+
}
|
| 1171 |
+
} catch (e) {}
|
| 1172 |
}
|
| 1173 |
|
| 1174 |
function updateFilters() {
|
| 1175 |
+
const b = document.getElementById('brightness').value;
|
| 1176 |
+
const c = document.getElementById('contrast').value;
|
| 1177 |
+
const g = document.getElementById('gamma').value;
|
| 1178 |
+
|
| 1179 |
+
document.getElementById('val-b').innerText = b;
|
| 1180 |
+
document.getElementById('val-c').innerText = c;
|
| 1181 |
+
document.getElementById('val-g').innerText = g;
|
| 1182 |
+
|
| 1183 |
+
els.image.style.filter = `brightness(${100 + parseFloat(b)}%) contrast(${c})`;
|
| 1184 |
els.magnifier.style.filter = els.image.style.filter;
|
| 1185 |
+
localStorage.setItem('pdfFilters', JSON.stringify({ b, c, g }));
|
| 1186 |
+
}
|
| 1187 |
+
|
| 1188 |
+
function loadSettings() {
|
| 1189 |
+
const s = JSON.parse(localStorage.getItem('pdfFilters') || '{}');
|
| 1190 |
+
if (s.b) document.getElementById('brightness').value = s.b;
|
| 1191 |
+
if (s.c) document.getElementById('contrast').value = s.c;
|
| 1192 |
+
if (s.g) document.getElementById('gamma').value = s.g;
|
| 1193 |
+
updateFilters();
|
| 1194 |
}
|
|
|
|
| 1195 |
|
| 1196 |
init();
|
| 1197 |
</script>
|