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.
- package/docs/plans/2026-02-24-premium-gate-design.md +52 -0
- package/docs/plans/2026-02-24-premium-gate.md +323 -0
- package/lib/pdf-handler.js +33 -0
- package/library.js +17 -6
- package/package.json +1 -1
- package/static/viewer-app.js +194 -39
- package/static/viewer.html +296 -38
package/static/viewer.html
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
3287
|
-
|
|
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
|
|
3292
|
-
|
|
3293
|
-
const
|
|
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
|
|
3306
|
-
document.removeEventListener('mousemove',
|
|
3307
|
-
document.removeEventListener('mouseup',
|
|
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
|
-
|
|
3320
|
-
showTextEditor(
|
|
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',
|
|
3327
|
-
document.addEventListener('mouseup',
|
|
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 =
|
|
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 =
|
|
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
|
|