nodebb-plugin-pdf-secure 1.2.10 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/library.js +12 -1
- package/package.json +1 -1
- package/static/lib/main.js +29 -4
- package/static/viewer.html +163 -84
package/library.js
CHANGED
|
@@ -36,8 +36,19 @@ plugin.init = async (params) => {
|
|
|
36
36
|
|
|
37
37
|
// PDF direct access blocker middleware
|
|
38
38
|
// Intercepts requests to uploaded PDF files and returns 403
|
|
39
|
-
|
|
39
|
+
// Admin and Global Moderators can bypass this restriction
|
|
40
|
+
router.get('/assets/uploads/files/:filename', async (req, res, next) => {
|
|
40
41
|
if (req.params.filename && req.params.filename.toLowerCase().endsWith('.pdf')) {
|
|
42
|
+
// Admin ve Global Mod'lar direkt erişebilsin
|
|
43
|
+
if (req.uid) {
|
|
44
|
+
const [isAdmin, isGlobalMod] = await Promise.all([
|
|
45
|
+
groups.isMember(req.uid, 'administrators'),
|
|
46
|
+
groups.isMember(req.uid, 'Global Moderators'),
|
|
47
|
+
]);
|
|
48
|
+
if (isAdmin || isGlobalMod) {
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
return res.status(403).json({ error: 'Direct PDF access is not allowed. Use the secure viewer.' });
|
|
42
53
|
}
|
|
43
54
|
next();
|
package/package.json
CHANGED
package/static/lib/main.js
CHANGED
|
@@ -215,6 +215,8 @@
|
|
|
215
215
|
targetElement.replaceWith(container);
|
|
216
216
|
|
|
217
217
|
// LAZY LOADING with Intersection Observer + Queue
|
|
218
|
+
// Smart loading: only loads PDFs that are actually visible
|
|
219
|
+
var queueEntry = null; // Track if this PDF is in queue
|
|
218
220
|
var observer = new IntersectionObserver(function (entries) {
|
|
219
221
|
entries.forEach(function (entry) {
|
|
220
222
|
if (entry.isIntersecting) {
|
|
@@ -229,13 +231,36 @@
|
|
|
229
231
|
svgEl.innerHTML = '<path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z"/>';
|
|
230
232
|
}
|
|
231
233
|
|
|
232
|
-
// Add to queue
|
|
233
|
-
|
|
234
|
-
|
|
234
|
+
// Add to queue (if not already)
|
|
235
|
+
if (!queueEntry) {
|
|
236
|
+
queueEntry = { wrapper: iframeWrapper, filename, placeholder: loadingPlaceholder };
|
|
237
|
+
loadQueue.push(queueEntry);
|
|
238
|
+
processQueue();
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
// LEFT viewport - remove from queue if waiting
|
|
242
|
+
if (queueEntry && loadQueue.includes(queueEntry)) {
|
|
243
|
+
var idx = loadQueue.indexOf(queueEntry);
|
|
244
|
+
if (idx > -1) {
|
|
245
|
+
loadQueue.splice(idx, 1);
|
|
246
|
+
console.log('[PDF-Secure] Queue: Removed (left viewport) -', filename);
|
|
247
|
+
|
|
248
|
+
// Reset placeholder to waiting state
|
|
249
|
+
var textEl = loadingPlaceholder.querySelector('.pdf-loading-text');
|
|
250
|
+
if (textEl) textEl.textContent = 'Sırada bekliyor...';
|
|
251
|
+
var svgEl = loadingPlaceholder.querySelector('svg');
|
|
252
|
+
if (svgEl) {
|
|
253
|
+
svgEl.style.fill = '#555';
|
|
254
|
+
svgEl.style.animation = 'none';
|
|
255
|
+
svgEl.innerHTML = '<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
queueEntry = null;
|
|
259
|
+
}
|
|
235
260
|
}
|
|
236
261
|
});
|
|
237
262
|
}, {
|
|
238
|
-
rootMargin: '
|
|
263
|
+
rootMargin: '0px', // Only trigger when actually visible
|
|
239
264
|
threshold: 0
|
|
240
265
|
});
|
|
241
266
|
|
package/static/viewer.html
CHANGED
|
@@ -442,22 +442,46 @@
|
|
|
442
442
|
font-size: 14px;
|
|
443
443
|
white-space: nowrap;
|
|
444
444
|
}
|
|
445
|
-
|
|
446
|
-
.overflowItem
|
|
447
|
-
|
|
448
|
-
|
|
445
|
+
|
|
446
|
+
.overflowItem:hover {
|
|
447
|
+
background: var(--bg-tertiary);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.overflowItem svg {
|
|
451
|
+
width: 20px;
|
|
452
|
+
height: 20px;
|
|
453
|
+
fill: currentColor;
|
|
454
|
+
flex-shrink: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.overflowItem.active {
|
|
458
|
+
color: var(--accent);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.overflowDivider {
|
|
462
|
+
height: 1px;
|
|
463
|
+
background: var(--border-color);
|
|
464
|
+
margin: 6px 0;
|
|
465
|
+
}
|
|
449
466
|
|
|
450
467
|
/* Overflow: visible on all screens, originals hidden */
|
|
451
|
-
#overflowWrapper {
|
|
452
|
-
|
|
468
|
+
#overflowWrapper {
|
|
469
|
+
display: flex;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.overflowSep {
|
|
473
|
+
display: block;
|
|
474
|
+
}
|
|
453
475
|
|
|
454
476
|
/* Hide rotate, sepia and their separators (children 3-8 of view group) */
|
|
455
|
-
.toolbarGroup:nth-child(5)
|
|
456
|
-
.toolbarGroup:nth-child(5)
|
|
457
|
-
.toolbarGroup:nth-child(5)
|
|
458
|
-
.toolbarGroup:nth-child(5)
|
|
459
|
-
.toolbarGroup:nth-child(5)
|
|
460
|
-
.toolbarGroup:nth-child(5)
|
|
477
|
+
.toolbarGroup:nth-child(5)> :nth-child(3),
|
|
478
|
+
.toolbarGroup:nth-child(5)> :nth-child(4),
|
|
479
|
+
.toolbarGroup:nth-child(5)> :nth-child(5),
|
|
480
|
+
.toolbarGroup:nth-child(5)> :nth-child(6),
|
|
481
|
+
.toolbarGroup:nth-child(5)> :nth-child(7),
|
|
482
|
+
.toolbarGroup:nth-child(5)> :nth-child(8) {
|
|
483
|
+
display: none !important;
|
|
484
|
+
}
|
|
461
485
|
|
|
462
486
|
/* Shape Grid */
|
|
463
487
|
.shapeGrid {
|
|
@@ -1785,23 +1809,32 @@
|
|
|
1785
1809
|
<div class="toolbarBtnWithDropdown" id="overflowWrapper">
|
|
1786
1810
|
<button class="toolbarBtn" id="overflowBtn" title="Daha Fazla">
|
|
1787
1811
|
<svg viewBox="0 0 24 24">
|
|
1788
|
-
<circle cx="12" cy="5" r="2"/>
|
|
1789
|
-
<circle cx="12" cy="12" r="2"/>
|
|
1790
|
-
<circle cx="12" cy="19" r="2"/>
|
|
1812
|
+
<circle cx="12" cy="5" r="2" />
|
|
1813
|
+
<circle cx="12" cy="12" r="2" />
|
|
1814
|
+
<circle cx="12" cy="19" r="2" />
|
|
1791
1815
|
</svg>
|
|
1792
1816
|
</button>
|
|
1793
1817
|
<div class="toolDropdown" id="overflowDropdown">
|
|
1794
1818
|
<button class="overflowItem" id="overflowRotateLeft">
|
|
1795
|
-
<svg viewBox="0 0 24 24"
|
|
1819
|
+
<svg viewBox="0 0 24 24">
|
|
1820
|
+
<path
|
|
1821
|
+
d="M7.11 8.53L5.7 7.11C4.8 8.27 4.24 9.61 4.07 11h2.02c.14-.87.49-1.72 1.02-2.47zM6.09 13H4.07c.17 1.39.72 2.73 1.62 3.89l1.41-1.42c-.52-.75-.87-1.59-1.01-2.47zm1.01 5.32c1.16.9 2.51 1.44 3.9 1.61V17.9c-.87-.15-1.71-.49-2.46-1.03L7.1 18.32zM13 4.07V1L8.45 5.55 13 10V6.09c2.84.48 5 2.94 5 5.91s-2.16 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93s-3.05-7.44-7-7.93z" />
|
|
1822
|
+
</svg>
|
|
1796
1823
|
<span>Sola Döndür</span>
|
|
1797
1824
|
</button>
|
|
1798
1825
|
<button class="overflowItem" id="overflowRotateRight">
|
|
1799
|
-
<svg viewBox="0 0 24 24"
|
|
1826
|
+
<svg viewBox="0 0 24 24">
|
|
1827
|
+
<path
|
|
1828
|
+
d="M15.55 5.55L11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10l4.55-4.45zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47h2.02zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03zm3.89-2.42l1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48z" />
|
|
1829
|
+
</svg>
|
|
1800
1830
|
<span>Sağa Döndür</span>
|
|
1801
1831
|
</button>
|
|
1802
1832
|
<div class="overflowDivider"></div>
|
|
1803
1833
|
<button class="overflowItem" id="overflowSepia">
|
|
1804
|
-
<svg viewBox="0 0 24 24"
|
|
1834
|
+
<svg viewBox="0 0 24 24">
|
|
1835
|
+
<path
|
|
1836
|
+
d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
|
|
1837
|
+
</svg>
|
|
1805
1838
|
<span>Okuma Modu</span>
|
|
1806
1839
|
</button>
|
|
1807
1840
|
</div>
|
|
@@ -1896,6 +1929,10 @@
|
|
|
1896
1929
|
let currentPath = null;
|
|
1897
1930
|
let currentDrawingPage = null;
|
|
1898
1931
|
|
|
1932
|
+
// RAF throttle for smooth drawing performance
|
|
1933
|
+
let pathSegments = []; // Buffer path segments
|
|
1934
|
+
let drawRAF = null; // requestAnimationFrame ID
|
|
1935
|
+
|
|
1899
1936
|
// Annotation persistence - stores SVG innerHTML per page
|
|
1900
1937
|
const annotationsStore = new Map();
|
|
1901
1938
|
const annotationRotations = new Map(); // tracks rotation when annotations were saved
|
|
@@ -2406,34 +2443,11 @@
|
|
|
2406
2443
|
currentWidth = shapeWidth;
|
|
2407
2444
|
}
|
|
2408
2445
|
|
|
2409
|
-
//
|
|
2410
|
-
// This
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
const svg = pageView?.div?.querySelector('.annotationLayer');
|
|
2415
|
-
if (svg) {
|
|
2416
|
-
const pageNum = i + 1;
|
|
2417
|
-
const cleanHTML = getCleanSvgInnerHTML(svg);
|
|
2418
|
-
if (cleanHTML) {
|
|
2419
|
-
annotationsStore.set(pageNum, cleanHTML);
|
|
2420
|
-
annotationRotations.set(pageNum, pdfViewer.pagesRotation || 0);
|
|
2421
|
-
} else {
|
|
2422
|
-
annotationsStore.delete(pageNum);
|
|
2423
|
-
annotationRotations.delete(pageNum);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
// Inject annotation layers (await all)
|
|
2429
|
-
const promises = [];
|
|
2430
|
-
for (let i = 0; i < pdfViewer.pagesCount; i++) {
|
|
2431
|
-
const pageView = pdfViewer.getPageView(i);
|
|
2432
|
-
if (pageView?.div) {
|
|
2433
|
-
promises.push(injectAnnotationLayer(i + 1));
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
await Promise.all(promises);
|
|
2446
|
+
// Performance: Just toggle active class instead of re-injecting layers
|
|
2447
|
+
// This avoids expensive DOM recreation on every tool change
|
|
2448
|
+
document.querySelectorAll('.annotationLayer').forEach(layer => {
|
|
2449
|
+
layer.classList.toggle('active', annotationMode);
|
|
2450
|
+
});
|
|
2437
2451
|
}
|
|
2438
2452
|
|
|
2439
2453
|
// Update button states
|
|
@@ -2602,18 +2616,8 @@
|
|
|
2602
2616
|
annotationsStore.set(pageNum, svg.innerHTML);
|
|
2603
2617
|
annotationRotations.set(pageNum, curRot);
|
|
2604
2618
|
|
|
2605
|
-
//
|
|
2606
|
-
|
|
2607
|
-
const entries = stackMap.get(pageNum);
|
|
2608
|
-
if (!entries) return;
|
|
2609
|
-
for (let i = 0; i < entries.length; i++) {
|
|
2610
|
-
if (entries[i].trim()) {
|
|
2611
|
-
entries[i] = `<g transform="${transform}">${entries[i]}</g>`;
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
};
|
|
2615
|
-
wrapStackEntries(undoStacks);
|
|
2616
|
-
wrapStackEntries(redoStacks);
|
|
2619
|
+
// Note: No need to wrap stack entries anymore
|
|
2620
|
+
// Rotation is now stored per-entry, transforms applied on restore
|
|
2617
2621
|
}
|
|
2618
2622
|
}
|
|
2619
2623
|
|
|
@@ -2645,13 +2649,16 @@
|
|
|
2645
2649
|
|
|
2646
2650
|
// Strip transient classes, styles, and elements from SVG before saving
|
|
2647
2651
|
function getCleanSvgInnerHTML(svg) {
|
|
2652
|
+
// Performance: Work on a cloned node to avoid modifying live DOM
|
|
2653
|
+
const clone = svg.cloneNode(true);
|
|
2654
|
+
|
|
2648
2655
|
// Remove marquee rect if present
|
|
2649
|
-
const marquee =
|
|
2656
|
+
const marquee = clone.querySelector('.marquee-rect');
|
|
2650
2657
|
if (marquee) marquee.remove();
|
|
2651
2658
|
|
|
2652
2659
|
// Strip transient classes and inline styles from annotation elements
|
|
2653
2660
|
const transientClasses = ['annotation-selected', 'annotation-multi-selected', 'annotation-dragging', 'just-selected'];
|
|
2654
|
-
|
|
2661
|
+
clone.querySelectorAll('path, rect, ellipse, line, text').forEach(el => {
|
|
2655
2662
|
transientClasses.forEach(cls => el.classList.remove(cls));
|
|
2656
2663
|
// Remove inline cursor style added by multi-drag
|
|
2657
2664
|
if (el.style.cursor) el.style.cursor = '';
|
|
@@ -2661,7 +2668,50 @@
|
|
|
2661
2668
|
if (el.getAttribute('class') === '') el.removeAttribute('class');
|
|
2662
2669
|
});
|
|
2663
2670
|
|
|
2664
|
-
return
|
|
2671
|
+
return clone.innerHTML.trim();
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// Helper: Apply rotation transform from savedRot to curRot
|
|
2675
|
+
// Uses clone-based flatten approach - updates each element's transform individually
|
|
2676
|
+
// This prevents nested <g> accumulation entirely
|
|
2677
|
+
function applyRotationTransform(html, savedRot, curRot, pageNum) {
|
|
2678
|
+
if (!html || !html.trim()) return html;
|
|
2679
|
+
|
|
2680
|
+
const delta = (curRot - savedRot + 360) % 360;
|
|
2681
|
+
if (delta === 0) return html;
|
|
2682
|
+
|
|
2683
|
+
// Calculate transform based on page dimensions
|
|
2684
|
+
const baseDims = pageBaseDimensions.get(pageNum);
|
|
2685
|
+
if (!baseDims) return html; // Fallback if no dims available
|
|
2686
|
+
|
|
2687
|
+
const W = baseDims.width, H = baseDims.height;
|
|
2688
|
+
|
|
2689
|
+
// Old viewBox dimensions (at saved rotation)
|
|
2690
|
+
let oW, oH;
|
|
2691
|
+
if (savedRot === 90 || savedRot === 270) { oW = H; oH = W; }
|
|
2692
|
+
else { oW = W; oH = H; }
|
|
2693
|
+
|
|
2694
|
+
let rotationTransform;
|
|
2695
|
+
if (delta === 90) rotationTransform = `translate(${oH},0) rotate(90)`;
|
|
2696
|
+
else if (delta === 180) rotationTransform = `translate(${oW},${oH}) rotate(180)`;
|
|
2697
|
+
else if (delta === 270) rotationTransform = `translate(0,${oW}) rotate(270)`;
|
|
2698
|
+
else return html;
|
|
2699
|
+
|
|
2700
|
+
// Clone-based flatten: Apply transform to each top-level element individually
|
|
2701
|
+
const tempContainer = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
2702
|
+
tempContainer.innerHTML = html;
|
|
2703
|
+
|
|
2704
|
+
// Process each top-level child element
|
|
2705
|
+
Array.from(tempContainer.children).forEach(child => {
|
|
2706
|
+
const existingTransform = child.getAttribute('transform') || '';
|
|
2707
|
+
// Prepend rotation transform (rotation first, then existing)
|
|
2708
|
+
const newTransform = existingTransform
|
|
2709
|
+
? `${rotationTransform} ${existingTransform}`
|
|
2710
|
+
: rotationTransform;
|
|
2711
|
+
child.setAttribute('transform', newTransform);
|
|
2712
|
+
});
|
|
2713
|
+
|
|
2714
|
+
return tempContainer.innerHTML;
|
|
2665
2715
|
}
|
|
2666
2716
|
|
|
2667
2717
|
// Save annotations for a page (with undo history)
|
|
@@ -2678,7 +2728,8 @@
|
|
|
2678
2728
|
if (previousState !== newState) {
|
|
2679
2729
|
if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
|
|
2680
2730
|
const stack = undoStacks.get(pageNum);
|
|
2681
|
-
|
|
2731
|
+
// Store as {html, rotation} object to avoid nested <g> wrap accumulation
|
|
2732
|
+
stack.push({ html: previousState, rotation: annotationRotations.get(pageNum) || 0 });
|
|
2682
2733
|
if (stack.length > MAX_HISTORY) stack.shift();
|
|
2683
2734
|
|
|
2684
2735
|
// Clear redo stack on new action
|
|
@@ -2715,20 +2766,24 @@
|
|
|
2715
2766
|
const svg = pageView?.div?.querySelector('.annotationLayer');
|
|
2716
2767
|
if (!svg) return;
|
|
2717
2768
|
|
|
2718
|
-
// Save current state to redo stack
|
|
2769
|
+
// Save current state to redo stack with rotation
|
|
2719
2770
|
if (!redoStacks.has(pageNum)) redoStacks.set(pageNum, []);
|
|
2720
2771
|
const redoStack = redoStacks.get(pageNum);
|
|
2721
|
-
redoStack.push(getCleanSvgInnerHTML(svg));
|
|
2772
|
+
redoStack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
|
|
2722
2773
|
if (redoStack.length > MAX_HISTORY) redoStack.shift();
|
|
2723
2774
|
|
|
2724
|
-
// Restore previous state
|
|
2725
|
-
const
|
|
2726
|
-
|
|
2775
|
+
// Restore previous state with rotation transform if needed
|
|
2776
|
+
const entry = stack.pop();
|
|
2777
|
+
const previousHtml = typeof entry === 'object' ? entry.html : entry;
|
|
2778
|
+
const savedRot = typeof entry === 'object' ? entry.rotation : (annotationRotations.get(pageNum) || 0);
|
|
2779
|
+
const curRot = pdfViewer.pagesRotation || 0;
|
|
2780
|
+
|
|
2781
|
+
svg.innerHTML = applyRotationTransform(previousHtml, savedRot, curRot, pageNum);
|
|
2727
2782
|
|
|
2728
2783
|
// Update store
|
|
2729
|
-
if (
|
|
2730
|
-
annotationsStore.set(pageNum,
|
|
2731
|
-
annotationRotations.set(pageNum,
|
|
2784
|
+
if (previousHtml.trim()) {
|
|
2785
|
+
annotationsStore.set(pageNum, svg.innerHTML);
|
|
2786
|
+
annotationRotations.set(pageNum, curRot);
|
|
2732
2787
|
} else {
|
|
2733
2788
|
annotationsStore.delete(pageNum);
|
|
2734
2789
|
annotationRotations.delete(pageNum);
|
|
@@ -2747,20 +2802,24 @@
|
|
|
2747
2802
|
const svg = pageView?.div?.querySelector('.annotationLayer');
|
|
2748
2803
|
if (!svg) return;
|
|
2749
2804
|
|
|
2750
|
-
// Save current state to undo stack
|
|
2805
|
+
// Save current state to undo stack with rotation
|
|
2751
2806
|
if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
|
|
2752
2807
|
const undoStack = undoStacks.get(pageNum);
|
|
2753
|
-
undoStack.push(getCleanSvgInnerHTML(svg));
|
|
2808
|
+
undoStack.push({ html: getCleanSvgInnerHTML(svg), rotation: pdfViewer.pagesRotation || 0 });
|
|
2754
2809
|
if (undoStack.length > MAX_HISTORY) undoStack.shift();
|
|
2755
2810
|
|
|
2756
|
-
// Restore redo state
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2811
|
+
// Restore redo state with rotation transform if needed
|
|
2812
|
+
const entry = stack.pop();
|
|
2813
|
+
const redoHtml = typeof entry === 'object' ? entry.html : entry;
|
|
2814
|
+
const savedRot = typeof entry === 'object' ? entry.rotation : (annotationRotations.get(pageNum) || 0);
|
|
2815
|
+
const curRot = pdfViewer.pagesRotation || 0;
|
|
2816
|
+
|
|
2817
|
+
svg.innerHTML = applyRotationTransform(redoHtml, savedRot, curRot, pageNum);
|
|
2759
2818
|
|
|
2760
2819
|
// Update store
|
|
2761
|
-
if (
|
|
2762
|
-
annotationsStore.set(pageNum,
|
|
2763
|
-
annotationRotations.set(pageNum,
|
|
2820
|
+
if (redoHtml.trim()) {
|
|
2821
|
+
annotationsStore.set(pageNum, svg.innerHTML);
|
|
2822
|
+
annotationRotations.set(pageNum, curRot);
|
|
2764
2823
|
} else {
|
|
2765
2824
|
annotationsStore.delete(pageNum);
|
|
2766
2825
|
annotationRotations.delete(pageNum);
|
|
@@ -2776,10 +2835,10 @@
|
|
|
2776
2835
|
const svg = pageView?.div?.querySelector('.annotationLayer');
|
|
2777
2836
|
if (!svg || !svg.innerHTML.trim()) return;
|
|
2778
2837
|
|
|
2779
|
-
// Save current state to undo stack
|
|
2838
|
+
// Save current state to undo stack with rotation
|
|
2780
2839
|
if (!undoStacks.has(pageNum)) undoStacks.set(pageNum, []);
|
|
2781
2840
|
const stack = undoStacks.get(pageNum);
|
|
2782
|
-
stack.push(svg.innerHTML);
|
|
2841
|
+
stack.push({ html: svg.innerHTML, rotation: pdfViewer.pagesRotation || 0 });
|
|
2783
2842
|
if (stack.length > MAX_HISTORY) stack.shift();
|
|
2784
2843
|
|
|
2785
2844
|
// Clear redo stack
|
|
@@ -2823,7 +2882,7 @@
|
|
|
2823
2882
|
|
|
2824
2883
|
if (currentTool === 'eraser') {
|
|
2825
2884
|
eraseAt(svg, x, y, scaleX);
|
|
2826
|
-
|
|
2885
|
+
// Performance: Don't save here - isDrawing is true, stopDraw will save on mouseup
|
|
2827
2886
|
return;
|
|
2828
2887
|
}
|
|
2829
2888
|
|
|
@@ -2924,8 +2983,7 @@
|
|
|
2924
2983
|
|
|
2925
2984
|
if (currentTool === 'eraser') {
|
|
2926
2985
|
eraseAt(svg, x, y, scaleX);
|
|
2927
|
-
//
|
|
2928
|
-
if (currentDrawingPage) saveAnnotations(currentDrawingPage);
|
|
2986
|
+
// Performance: Don't save on every mousemove - let mouseup handle it
|
|
2929
2987
|
return;
|
|
2930
2988
|
}
|
|
2931
2989
|
|
|
@@ -2958,12 +3016,33 @@
|
|
|
2958
3016
|
return;
|
|
2959
3017
|
}
|
|
2960
3018
|
|
|
3019
|
+
// RAF throttle: buffer segments and flush in animation frame
|
|
2961
3020
|
if (currentPath) {
|
|
2962
|
-
|
|
3021
|
+
pathSegments.push(`L${x.toFixed(2)},${y.toFixed(2)}`);
|
|
3022
|
+
|
|
3023
|
+
if (!drawRAF) {
|
|
3024
|
+
drawRAF = requestAnimationFrame(() => {
|
|
3025
|
+
if (currentPath && pathSegments.length > 0) {
|
|
3026
|
+
currentPath.setAttribute('d', currentPath.getAttribute('d') + ' ' + pathSegments.join(' '));
|
|
3027
|
+
pathSegments = [];
|
|
3028
|
+
}
|
|
3029
|
+
drawRAF = null;
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
2963
3032
|
}
|
|
2964
3033
|
}
|
|
2965
3034
|
|
|
2966
3035
|
function stopDraw(pageNum) {
|
|
3036
|
+
// Flush any pending path segments before stopping
|
|
3037
|
+
if (drawRAF) {
|
|
3038
|
+
cancelAnimationFrame(drawRAF);
|
|
3039
|
+
drawRAF = null;
|
|
3040
|
+
}
|
|
3041
|
+
if (currentPath && pathSegments.length > 0) {
|
|
3042
|
+
currentPath.setAttribute('d', currentPath.getAttribute('d') + ' ' + pathSegments.join(' '));
|
|
3043
|
+
pathSegments = [];
|
|
3044
|
+
}
|
|
3045
|
+
|
|
2967
3046
|
// Handle arrow marker
|
|
2968
3047
|
if (currentTool === 'shape' && currentShape === 'arrow' && currentSvg) {
|
|
2969
3048
|
const shapeEl = currentSvg.querySelector('.current-shape');
|