nodebb-plugin-pdf-secure 1.2.18 → 1.2.20

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.
@@ -401,6 +401,18 @@
401
401
  border-radius: 4px 0 0 4px;
402
402
  }
403
403
 
404
+ /* Color indicator bar under tool button */
405
+ .toolColorIndicator {
406
+ position: absolute;
407
+ bottom: 2px;
408
+ left: 6px;
409
+ right: calc(20px + 6px);
410
+ height: 3px;
411
+ border-radius: 2px;
412
+ pointer-events: none;
413
+ transition: background 0.15s;
414
+ }
415
+
404
416
  .dropdownArrow {
405
417
  width: 20px;
406
418
  height: 36px;
@@ -1178,12 +1190,17 @@
1178
1190
  z-index: 100;
1179
1191
  padding: 0 8px;
1180
1192
  padding-bottom: var(--safe-area-bottom);
1193
+ /* Flex layout: scrollable tools + fixed fullscreen button */
1194
+ align-items: center;
1195
+ gap: 0;
1181
1196
  }
1182
1197
 
1183
1198
  .bottomToolbarInner {
1184
1199
  display: flex;
1185
1200
  align-items: center;
1186
1201
  gap: 2px;
1202
+ flex: 1;
1203
+ min-width: 0;
1187
1204
  height: var(--bottom-bar-height);
1188
1205
  overflow-x: auto;
1189
1206
  overflow-y: hidden;
@@ -1196,6 +1213,14 @@
1196
1213
  display: none;
1197
1214
  }
1198
1215
 
1216
+ /* Fullscreen button pinned to right side of bottom toolbar */
1217
+ .bottomFullscreenBtn {
1218
+ flex-shrink: 0;
1219
+ margin-left: 4px;
1220
+ border-left: 1px solid var(--border-color);
1221
+ padding-left: 4px;
1222
+ }
1223
+
1199
1224
  /* Dropdown backdrop overlay */
1200
1225
  #dropdownBackdrop {
1201
1226
  display: none;
@@ -1248,7 +1273,7 @@
1248
1273
 
1249
1274
  /* Show bottom toolbar */
1250
1275
  #bottomToolbar {
1251
- display: block;
1276
+ display: flex;
1252
1277
  }
1253
1278
 
1254
1279
  /* Viewer container adjusted for mobile toolbars */
@@ -1358,7 +1383,7 @@
1358
1383
  ========================================== */
1359
1384
  /* Ensure bottom toolbar is visible and viewer container makes room for it */
1360
1385
  body.viewer-fullscreen #bottomToolbar {
1361
- display: block;
1386
+ display: flex;
1362
1387
  }
1363
1388
 
1364
1389
  body.viewer-fullscreen #viewerContainer {
@@ -1430,7 +1455,7 @@
1430
1455
 
1431
1456
  /* Show bottom toolbar */
1432
1457
  #bottomToolbar {
1433
- display: block;
1458
+ display: flex;
1434
1459
  }
1435
1460
 
1436
1461
  /* Viewer container adjusted for both toolbars */
@@ -1547,6 +1572,106 @@
1547
1572
  touch-action: manipulation;
1548
1573
  }
1549
1574
  }
1575
+
1576
+ /* Premium Lock Overlay */
1577
+ #premiumLockOverlay {
1578
+ display: flex;
1579
+ flex-direction: column;
1580
+ align-items: center;
1581
+ justify-content: center;
1582
+ padding: 60px 20px;
1583
+ text-align: center;
1584
+ background: linear-gradient(180deg, rgba(31,31,31,0.95) 0%, rgba(20,20,20,0.98) 100%);
1585
+ border-top: 2px solid rgba(255, 215, 0, 0.3);
1586
+ min-height: 400px;
1587
+ margin: 0 auto;
1588
+ max-width: 100%;
1589
+ }
1590
+
1591
+ .premium-lock-icon {
1592
+ margin-bottom: 20px;
1593
+ opacity: 0.9;
1594
+ }
1595
+
1596
+ .premium-lock-pages {
1597
+ font-size: 22px;
1598
+ font-weight: 600;
1599
+ color: #ffd700;
1600
+ margin-bottom: 12px;
1601
+ }
1602
+
1603
+ .premium-lock-message {
1604
+ font-size: 16px;
1605
+ color: #a0a0a0;
1606
+ margin-bottom: 28px;
1607
+ max-width: 400px;
1608
+ line-height: 1.5;
1609
+ }
1610
+
1611
+ .premium-lock-button {
1612
+ display: inline-block;
1613
+ padding: 14px 40px;
1614
+ background: linear-gradient(135deg, #ffd700 0%, #ffaa00 100%);
1615
+ color: #1a1a1a;
1616
+ font-size: 16px;
1617
+ font-weight: 700;
1618
+ border-radius: 8px;
1619
+ text-decoration: none;
1620
+ transition: transform 0.2s, box-shadow 0.2s;
1621
+ box-shadow: 0 4px 15px rgba(255, 215, 0, 0.3);
1622
+ }
1623
+
1624
+ .premium-lock-button:hover {
1625
+ transform: translateY(-2px);
1626
+ box-shadow: 0 6px 20px rgba(255, 215, 0, 0.4);
1627
+ }
1628
+
1629
+ .premium-lock-secondary {
1630
+ margin-top: 20px;
1631
+ font-size: 14px;
1632
+ color: #888;
1633
+ font-style: italic;
1634
+ }
1635
+
1636
+ /* Locked Thumbnails */
1637
+ .thumbnail.locked {
1638
+ opacity: 0.4;
1639
+ cursor: not-allowed;
1640
+ position: relative;
1641
+ }
1642
+
1643
+ .thumbnail.locked:hover {
1644
+ border-color: rgba(255, 215, 0, 0.4);
1645
+ }
1646
+
1647
+ .thumbnail-lock {
1648
+ position: absolute;
1649
+ top: 50%;
1650
+ left: 50%;
1651
+ transform: translate(-50%, -50%);
1652
+ font-size: 20px;
1653
+ z-index: 2;
1654
+ }
1655
+
1656
+ @media (max-width: 768px) {
1657
+ #premiumLockOverlay {
1658
+ padding: 40px 16px;
1659
+ min-height: 300px;
1660
+ }
1661
+
1662
+ .premium-lock-pages {
1663
+ font-size: 18px;
1664
+ }
1665
+
1666
+ .premium-lock-message {
1667
+ font-size: 14px;
1668
+ }
1669
+
1670
+ .premium-lock-button {
1671
+ padding: 12px 32px;
1672
+ font-size: 14px;
1673
+ }
1674
+ }
1550
1675
  </style>
1551
1676
  </head>
1552
1677
 
@@ -1573,6 +1698,7 @@
1573
1698
  <path d="M3 21h18v-2H3v2zM5 16h14l-3-10H8l-3 10zM9 8h6l1.5 5h-9L9 8z" opacity="0.7" />
1574
1699
  </svg>
1575
1700
  </button>
1701
+ <div class="toolColorIndicator" id="highlightColorIndicator" style="background:#fff100"></div>
1576
1702
  <button class="dropdownArrow" id="highlightArrow">
1577
1703
  <svg viewBox="0 0 24 24">
1578
1704
  <path d="M7 10l5 5 5-5z" />
@@ -1618,6 +1744,7 @@
1618
1744
  d="M20.71 4.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83zM3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25z" />
1619
1745
  </svg>
1620
1746
  </button>
1747
+ <div class="toolColorIndicator" id="drawColorIndicator" style="background:#e81224"></div>
1621
1748
  <button class="dropdownArrow" id="drawArrow">
1622
1749
  <svg viewBox="0 0 24 24">
1623
1750
  <path d="M7 10l5 5 5-5z" />
@@ -1721,6 +1848,7 @@
1721
1848
  <path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm13 0a5 5 0 110 10 5 5 0 010-10z" />
1722
1849
  </svg>
1723
1850
  </button>
1851
+ <div class="toolColorIndicator" id="shapeColorIndicator" style="background:#e81224"></div>
1724
1852
  <button class="dropdownArrow" id="shapesArrow">
1725
1853
  <svg viewBox="0 0 24 24">
1726
1854
  <path d="M7 10l5 5 5-5z" />
@@ -1863,6 +1991,13 @@
1863
1991
  </svg>
1864
1992
  <span>Okuma Modu</span>
1865
1993
  </button>
1994
+ <div class="overflowDivider"></div>
1995
+ <button class="overflowItem" id="overflowFullscreen">
1996
+ <svg viewBox="0 0 24 24">
1997
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
1998
+ </svg>
1999
+ <span>Tam Ekran</span>
2000
+ </button>
1866
2001
  </div>
1867
2002
  </div>
1868
2003
  </div>
@@ -1873,11 +2008,16 @@
1873
2008
  </div>
1874
2009
  </div>
1875
2010
 
1876
- <!-- Bottom Toolbar (Mobile Only) -->
2011
+ <!-- Bottom Toolbar (Mobile/Tablet Portrait) -->
1877
2012
  <div id="bottomToolbar">
1878
2013
  <div class="bottomToolbarInner" id="bottomToolbarInner">
1879
2014
  <!-- Annotation tool buttons will be moved here on mobile via JS -->
1880
2015
  </div>
2016
+ <button class="toolbarBtn bottomFullscreenBtn" id="bottomFullscreenBtn" title="Tam Ekran">
2017
+ <svg viewBox="0 0 24 24">
2018
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
2019
+ </svg>
2020
+ </button>
1881
2021
  </div>
1882
2022
 
1883
2023
  <!-- Dropdown Backdrop (Mobile) -->
@@ -1914,6 +2054,10 @@
1914
2054
  (function () {
1915
2055
  'use strict';
1916
2056
 
2057
+ // Security: Capture config early and delete from window immediately
2058
+ const _cfg = window.PDF_SECURE_CONFIG ? Object.assign({}, window.PDF_SECURE_CONFIG) : null;
2059
+ delete window.PDF_SECURE_CONFIG;
2060
+
1917
2061
  // ============================================
1918
2062
  // CANVAS EXPORT PROTECTION
1919
2063
  // Block toDataURL/toBlob for PDF render canvas only
@@ -1955,6 +2099,9 @@
1955
2099
  let currentPath = null;
1956
2100
  let currentDrawingPage = null;
1957
2101
 
2102
+ // Premium info (saved before config deletion for UI use)
2103
+ let premiumInfo = null;
2104
+
1958
2105
  // RAF throttle for smooth drawing performance
1959
2106
  let pathSegments = []; // Buffer path segments
1960
2107
  let drawRAF = null; // requestAnimationFrame ID
@@ -2004,8 +2151,7 @@
2004
2151
  firstPageRendered = true;
2005
2152
  // Notify parent that PDF is fully rendered (for queue system)
2006
2153
  if (window.parent && window.parent !== window) {
2007
- const config = window.PDF_SECURE_CONFIG || {};
2008
- window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, window.location.origin);
2154
+ window.parent.postMessage({ type: 'pdf-secure-ready', filename: (_cfg || {}).filename }, window.location.origin);
2009
2155
  console.log('[PDF-Secure] First page rendered, notifying parent');
2010
2156
  }
2011
2157
  }
@@ -2078,14 +2224,90 @@
2078
2224
  return data.buffer;
2079
2225
  }
2080
2226
 
2227
+ function showPremiumLockOverlay(totalPages) {
2228
+ var viewerEl = document.getElementById('viewer');
2229
+ if (!viewerEl) return;
2230
+
2231
+ var overlay = document.createElement('div');
2232
+ overlay.id = 'premiumLockOverlay';
2233
+ overlay.innerHTML = '\
2234
+ <div class="premium-lock-icon">\
2235
+ <svg viewBox="0 0 24 24" width="64" height="64" fill="#ffd700">\
2236
+ <path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1s3.1 1.39 3.1 3.1v2z"/>\
2237
+ </svg>\
2238
+ </div>\
2239
+ <div class="premium-lock-pages">' + (totalPages - 1) + ' sayfa daha kilitli</div>\
2240
+ <div class="premium-lock-message">Bu icerigi goruntulemeye devam etmek icin Premium uyelik gereklidir.</div>\
2241
+ <a href="https://forumtest.ieu.app/premium" target="_blank" class="premium-lock-button">Premium Satin Al</a>\
2242
+ <div class="premium-lock-secondary">Materyal yukleyerek de Premium olabilirsiniz!</div>';
2243
+
2244
+ viewerEl.appendChild(overlay);
2245
+ }
2246
+
2247
+ // ============================================
2248
+ // PREMIUM INTEGRITY: Periodic Check (2s interval)
2249
+ // ============================================
2250
+ function startPeriodicCheck() {
2251
+ setInterval(function () {
2252
+ if (!premiumInfo || premiumInfo.isPremium) return;
2253
+ var pages = document.querySelectorAll('#viewer .page');
2254
+ pages.forEach(function (page, idx) {
2255
+ if (idx > 0 && page.style.display !== 'none') {
2256
+ page.style.display = 'none';
2257
+ }
2258
+ });
2259
+ if (!document.getElementById('premiumLockOverlay')) {
2260
+ showPremiumLockOverlay(premiumInfo.totalPages);
2261
+ }
2262
+ if (pdfViewer && pdfViewer.currentPageNumber > 1) {
2263
+ pdfViewer.currentPageNumber = 1;
2264
+ }
2265
+ }, 2000);
2266
+ }
2267
+
2268
+ // ============================================
2269
+ // PREMIUM INTEGRITY: MutationObserver Anti-Tampering
2270
+ // ============================================
2271
+ function setupAntiTampering() {
2272
+ var viewerEl = document.getElementById('viewer');
2273
+ if (!viewerEl) return;
2274
+
2275
+ new MutationObserver(function (mutations) {
2276
+ for (var i = 0; i < mutations.length; i++) {
2277
+ var removed = mutations[i].removedNodes;
2278
+ for (var j = 0; j < removed.length; j++) {
2279
+ if (removed[j].id === 'premiumLockOverlay') {
2280
+ showPremiumLockOverlay(premiumInfo.totalPages);
2281
+ return;
2282
+ }
2283
+ }
2284
+ }
2285
+ }).observe(viewerEl, { childList: true });
2286
+
2287
+ new MutationObserver(function (mutations) {
2288
+ for (var i = 0; i < mutations.length; i++) {
2289
+ var m = mutations[i];
2290
+ if (m.type === 'attributes' && m.attributeName === 'style') {
2291
+ var target = m.target;
2292
+ if (target.classList && target.classList.contains('page')) {
2293
+ var pageNum = parseInt(target.dataset.pageNumber || '0', 10);
2294
+ if (pageNum > 1 && target.style.display !== 'none') {
2295
+ target.style.display = 'none';
2296
+ }
2297
+ }
2298
+ }
2299
+ }
2300
+ }).observe(viewerEl, { childList: true, subtree: true, attributes: true, attributeFilter: ['style'] });
2301
+ }
2302
+
2081
2303
  // Auto-load PDF if config is present (injected by NodeBB plugin)
2082
2304
  async function autoLoadSecurePDF() {
2083
- if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
2305
+ if (!_cfg || !_cfg.filename) {
2084
2306
  console.log('[PDF-Secure] No config found, showing file picker');
2085
2307
  return;
2086
2308
  }
2087
2309
 
2088
- const config = window.PDF_SECURE_CONFIG;
2310
+ const config = _cfg;
2089
2311
  console.log('[PDF-Secure] Auto-loading:', config.filename);
2090
2312
 
2091
2313
  // Show loading state
@@ -2157,8 +2379,8 @@
2157
2379
  pdfBuffer = encodedBuffer;
2158
2380
  }
2159
2381
 
2160
- // Send buffer to parent for caching
2161
- if (window.parent && window.parent !== window) {
2382
+ // Send buffer to parent for caching (premium only - non-premium must not leak decoded buffer)
2383
+ if (_cfg.isPremium !== false && window.parent && window.parent !== window) {
2162
2384
  // Clone buffer for parent (we keep original)
2163
2385
  const bufferCopy = pdfBuffer.slice(0);
2164
2386
  window.parent.postMessage({
@@ -2174,14 +2396,21 @@
2174
2396
  // Step 4: Load into viewer
2175
2397
  await loadPDFFromBuffer(pdfBuffer);
2176
2398
 
2399
+ // Premium Gate: Client-side page restriction for non-premium users
2400
+ if (config.isPremium === false && pdfDoc && pdfDoc.numPages > 1) {
2401
+ premiumInfo = Object.freeze({ isPremium: false, totalPages: pdfDoc.numPages });
2402
+ showPremiumLockOverlay(pdfDoc.numPages);
2403
+ startPeriodicCheck();
2404
+ setupAntiTampering();
2405
+ } else {
2406
+ premiumInfo = Object.freeze({ isPremium: true, totalPages: pdfDoc ? pdfDoc.numPages : 1 });
2407
+ }
2408
+
2177
2409
  // Step 5: Moved to pagerendered event for proper timing
2178
2410
 
2179
2411
  // Step 6: Security - clear references to prevent extraction
2180
2412
  pdfBuffer = null;
2181
2413
 
2182
- // Security: Delete config containing sensitive data (nonce, key)
2183
- delete window.PDF_SECURE_CONFIG;
2184
-
2185
2414
  // Security: Remove PDF.js globals to prevent console manipulation
2186
2415
  delete window.pdfjsLib;
2187
2416
  delete window.pdfjsViewer;
@@ -2205,10 +2434,9 @@
2205
2434
 
2206
2435
  // Notify parent of error (prevents 60s queue hang)
2207
2436
  if (window.parent && window.parent !== window) {
2208
- const config = window.PDF_SECURE_CONFIG || {};
2209
2437
  window.parent.postMessage({
2210
2438
  type: 'pdf-secure-ready',
2211
- filename: config.filename,
2439
+ filename: (_cfg || {}).filename,
2212
2440
  error: err.message
2213
2441
  }, window.location.origin);
2214
2442
  }
@@ -2429,7 +2657,10 @@
2429
2657
  dropdown.addEventListener('touchstart', (e) => {
2430
2658
  const rect = dropdown.getBoundingClientRect();
2431
2659
  const touchY = e.touches[0].clientY;
2432
- if (touchY - rect.top > 40 && dropdown.scrollTop > 0) return;
2660
+ // Only start swipe-to-dismiss from the handle area (top ~40px)
2661
+ // Don't intercept touches on interactive content (color dots, sliders, shape buttons)
2662
+ const isInteractive = e.target.closest('.colorDot, .colorGrid, .thicknessSlider, .shapeBtn, .shapeGrid, input, .strokePreview, .overflowItem');
2663
+ if (touchY - rect.top > 40 || isInteractive) return;
2433
2664
  startY = touchY;
2434
2665
  currentY = startY;
2435
2666
  isDragging = true;
@@ -2487,6 +2718,10 @@
2487
2718
  document.getElementById('sepiaBtn').classList.contains('active'));
2488
2719
  closeAllDropdowns();
2489
2720
  };
2721
+ document.getElementById('overflowFullscreen').onclick = () => {
2722
+ toggleFullscreen();
2723
+ closeAllDropdowns();
2724
+ };
2490
2725
 
2491
2726
  // Close dropdowns when clicking outside
2492
2727
  document.addEventListener('click', (e) => {
@@ -2569,8 +2804,9 @@
2569
2804
  dot.classList.add('active');
2570
2805
  highlightColor = dot.dataset.color;
2571
2806
  if (currentTool === 'highlight') currentColor = highlightColor;
2572
- // Update preview
2807
+ // Update preview and color indicator
2573
2808
  document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
2809
+ document.getElementById('highlightColorIndicator').style.background = highlightColor;
2574
2810
  };
2575
2811
  });
2576
2812
 
@@ -2582,8 +2818,9 @@
2582
2818
  dot.classList.add('active');
2583
2819
  drawColor = dot.dataset.color;
2584
2820
  if (currentTool === 'pen') currentColor = drawColor;
2585
- // Update preview
2821
+ // Update preview and color indicator
2586
2822
  document.getElementById('drawWave').setAttribute('stroke', drawColor);
2823
+ document.getElementById('drawColorIndicator').style.background = drawColor;
2587
2824
  };
2588
2825
  });
2589
2826
 
@@ -2621,6 +2858,8 @@
2621
2858
  dot.classList.add('active');
2622
2859
  shapeColor = dot.dataset.color;
2623
2860
  if (currentTool === 'shape') currentColor = shapeColor;
2861
+ // Update color indicator
2862
+ document.getElementById('shapeColorIndicator').style.background = shapeColor;
2624
2863
  };
2625
2864
  });
2626
2865
 
@@ -3005,7 +3244,8 @@
3005
3244
  // Text tool - create/edit/drag text
3006
3245
  if (currentTool === 'text') {
3007
3246
  // Check if clicked on existing text element
3008
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
3247
+ // Use coords (touch-safe) instead of e.clientX which is undefined on TouchEvent
3248
+ const elementsUnderClick = document.elementsFromPoint(coords.clientX, coords.clientY);
3009
3249
  const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
3010
3250
 
3011
3251
  if (existingText) {
@@ -3013,7 +3253,7 @@
3013
3253
  startTextDrag(e, existingText, svg, scaleX, pageNum);
3014
3254
  } else {
3015
3255
  // Create new text
3016
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
3256
+ showTextEditor(coords.clientX, coords.clientY, svg, x, y, scaleX, pageNum);
3017
3257
  }
3018
3258
  return;
3019
3259
  }
@@ -3283,14 +3523,18 @@
3283
3523
  textEl.classList.add('dragging');
3284
3524
  hasDragged = false;
3285
3525
 
3286
- dragStartX = e.clientX;
3287
- dragStartY = e.clientY;
3526
+ // Touch-safe coordinate extraction
3527
+ const startCoords = getEventCoords(e);
3528
+ dragStartX = startCoords.clientX;
3529
+ dragStartY = startCoords.clientY;
3288
3530
  textOriginalX = parseFloat(textEl.getAttribute('x'));
3289
3531
  textOriginalY = parseFloat(textEl.getAttribute('y'));
3290
3532
 
3291
- function onMouseMove(ev) {
3292
- const dxScreen = ev.clientX - dragStartX;
3293
- const dyScreen = ev.clientY - dragStartY;
3533
+ function onMove(ev) {
3534
+ ev.preventDefault();
3535
+ const moveCoords = getEventCoords(ev);
3536
+ const dxScreen = moveCoords.clientX - dragStartX;
3537
+ const dyScreen = moveCoords.clientY - dragStartY;
3294
3538
  // Convert screen delta to viewBox delta (rotation-aware)
3295
3539
  const vbDelta = screenDeltaToViewBox(svg, dxScreen, dyScreen);
3296
3540
 
@@ -3302,29 +3546,31 @@
3302
3546
  textEl.setAttribute('y', (textOriginalY + vbDelta.dy).toFixed(2));
3303
3547
  }
3304
3548
 
3305
- function onMouseUp(ev) {
3306
- document.removeEventListener('mousemove', onMouseMove);
3307
- document.removeEventListener('mouseup', onMouseUp);
3549
+ function onEnd(ev) {
3550
+ document.removeEventListener('mousemove', onMove);
3551
+ document.removeEventListener('mouseup', onEnd);
3552
+ document.removeEventListener('touchmove', onMove);
3553
+ document.removeEventListener('touchend', onEnd);
3308
3554
  textEl.classList.remove('dragging');
3309
3555
 
3310
3556
  if (hasDragged) {
3311
3557
  // Moved - save position
3312
3558
  saveAnnotations(pageNum);
3313
3559
  } else {
3314
- // Not moved - short click = edit
3315
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
3316
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
3560
+ // Not moved - short click/tap = edit
3317
3561
  const svgX = parseFloat(textEl.getAttribute('x'));
3318
3562
  const svgY = parseFloat(textEl.getAttribute('y'));
3319
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
3320
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
3563
+ const endCoords = getEventCoords(ev);
3564
+ showTextEditor(endCoords.clientX, endCoords.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
3321
3565
  }
3322
3566
 
3323
3567
  draggedText = null;
3324
3568
  }
3325
3569
 
3326
- document.addEventListener('mousemove', onMouseMove);
3327
- document.addEventListener('mouseup', onMouseUp);
3570
+ document.addEventListener('mousemove', onMove);
3571
+ document.addEventListener('mouseup', onEnd);
3572
+ document.addEventListener('touchmove', onMove, { passive: false });
3573
+ document.addEventListener('touchend', onEnd);
3328
3574
  }
3329
3575
 
3330
3576
  // Inline Text Editor
@@ -4464,12 +4710,23 @@
4464
4710
  function updateFullscreenIcon() {
4465
4711
  const icon = document.getElementById('fullscreenIcon');
4466
4712
  const btn = document.getElementById('fullscreenBtn');
4713
+ const bottomBtn = document.getElementById('bottomFullscreenBtn');
4714
+ const exitPath = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4715
+ const enterPath = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
4467
4716
  if (isFullscreen) {
4468
- icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
4717
+ icon.innerHTML = exitPath;
4469
4718
  btn.classList.add('active');
4719
+ if (bottomBtn) {
4720
+ bottomBtn.querySelector('svg').innerHTML = exitPath;
4721
+ bottomBtn.classList.add('active');
4722
+ }
4470
4723
  } else {
4471
- icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
4724
+ icon.innerHTML = enterPath;
4472
4725
  btn.classList.remove('active');
4726
+ if (bottomBtn) {
4727
+ bottomBtn.querySelector('svg').innerHTML = enterPath;
4728
+ bottomBtn.classList.remove('active');
4729
+ }
4473
4730
  }
4474
4731
  }
4475
4732
 
@@ -4506,8 +4763,9 @@
4506
4763
  applyFullscreenTouchRestrictions();
4507
4764
  });
4508
4765
 
4509
- // Fullscreen button click
4766
+ // Fullscreen button click (top toolbar + bottom toolbar)
4510
4767
  document.getElementById('fullscreenBtn').onclick = () => toggleFullscreen();
4768
+ document.getElementById('bottomFullscreenBtn').onclick = () => toggleFullscreen();
4511
4769
 
4512
4770
 
4513
4771