nodebb-plugin-pdf-secure 1.2.7 → 1.2.8

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.
@@ -3,1050 +3,18 @@
3
3
 
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
7
8
  <title>PDF Viewer</title>
8
9
 
9
10
  <!-- PDF.js -->
10
- <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
11
+ <script defer src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
11
12
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css">
12
- <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.js"></script>
13
+ <script defer src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.js"></script>
13
14
 
14
- <style>
15
- /* Microsoft Edge / Dark Theme */
16
- :root {
17
- --bg-primary: #1f1f1f;
18
- --bg-secondary: #2d2d2d;
19
- --bg-tertiary: #3d3d3d;
20
- --text-primary: #ffffff;
21
- --text-secondary: #a0a0a0;
22
- --accent: #0078d4;
23
- --accent-hover: #1a86d9;
24
- --border-color: #404040;
25
- --toolbar-height: 48px;
26
- --sidebar-width: 200px;
27
- }
15
+ <!-- Viewer styles -->
16
+ <link rel="stylesheet" href="viewer.css">
28
17
 
29
- * {
30
- margin: 0;
31
- padding: 0;
32
- box-sizing: border-box;
33
- }
34
-
35
- html,
36
- body {
37
- height: 100%;
38
- background: var(--bg-primary);
39
- font-family: "Segoe UI", system-ui, sans-serif;
40
- font-size: 14px;
41
- overflow: hidden;
42
- color: var(--text-primary);
43
- /* Security: prevent text selection globally */
44
- -webkit-user-select: none;
45
- -moz-user-select: none;
46
- -ms-user-select: none;
47
- user-select: none;
48
- }
49
-
50
- /* Print Protection - hide everything when printing */
51
- @media print {
52
-
53
- html,
54
- body,
55
- #viewerContainer,
56
- #viewer,
57
- .pdfViewer,
58
- .page {
59
- display: none !important;
60
- visibility: hidden !important;
61
- }
62
-
63
- body::before {
64
- content: 'Bu içeriğin yazdırılması engellenmiştir.' !important;
65
- display: block !important;
66
- font-size: 24px;
67
- padding: 50px;
68
- text-align: center;
69
- color: #666;
70
- }
71
- }
72
-
73
- /* Loading Spinner Animation */
74
- @keyframes spin {
75
- from {
76
- transform: rotate(0deg);
77
- }
78
-
79
- to {
80
- transform: rotate(360deg);
81
- }
82
- }
83
-
84
- .spin {
85
- animation: spin 1s linear infinite;
86
- }
87
-
88
- .dropzone svg.spin {
89
- fill: var(--accent);
90
- }
91
-
92
- /* Toolbar - Edge Style */
93
- #toolbar {
94
- position: fixed;
95
- top: 0;
96
- left: 0;
97
- right: 0;
98
- height: var(--toolbar-height);
99
- background: var(--bg-secondary);
100
- border-bottom: 1px solid var(--border-color);
101
- display: flex;
102
- align-items: center;
103
- padding: 0 12px;
104
- gap: 4px;
105
- z-index: 100;
106
- }
107
-
108
- .toolbarGroup {
109
- display: flex;
110
- align-items: center;
111
- gap: 2px;
112
- }
113
-
114
- .toolbarBtn {
115
- width: 36px;
116
- height: 36px;
117
- border: none;
118
- background: transparent;
119
- color: var(--text-primary);
120
- border-radius: 4px;
121
- cursor: pointer;
122
- display: flex;
123
- align-items: center;
124
- justify-content: center;
125
- transition: background 0.1s;
126
- }
127
-
128
- .toolbarBtn:hover {
129
- background: var(--bg-tertiary);
130
- }
131
-
132
- .toolbarBtn.active {
133
- background: var(--accent);
134
- }
135
-
136
- .toolbarBtn svg {
137
- width: 18px;
138
- height: 18px;
139
- fill: currentColor;
140
- }
141
-
142
- .separator {
143
- width: 1px;
144
- height: 24px;
145
- background: var(--border-color);
146
- margin: 0 8px;
147
- }
148
-
149
- /* Enhanced Tooltips */
150
- .toolbarBtn {
151
- position: relative;
152
- }
153
-
154
- .toolbarBtn::after {
155
- content: attr(data-tooltip);
156
- position: absolute;
157
- top: 100%;
158
- left: 50%;
159
- transform: translateX(-50%);
160
- background: #1a1a1a;
161
- color: #fff;
162
- padding: 6px 10px;
163
- border-radius: 6px;
164
- font-size: 12px;
165
- white-space: nowrap;
166
- opacity: 0;
167
- visibility: hidden;
168
- transition: opacity 0.2s, visibility 0.2s;
169
- z-index: 1000;
170
- margin-top: 8px;
171
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
172
- pointer-events: none;
173
- }
174
-
175
- .toolbarBtn::before {
176
- content: '';
177
- position: absolute;
178
- top: 100%;
179
- left: 50%;
180
- transform: translateX(-50%);
181
- border: 6px solid transparent;
182
- border-bottom-color: #1a1a1a;
183
- opacity: 0;
184
- visibility: hidden;
185
- transition: opacity 0.2s, visibility 0.2s;
186
- z-index: 1001;
187
- margin-top: -4px;
188
- }
189
-
190
- .toolbarBtn:hover::after,
191
- .toolbarBtn:hover::before {
192
- opacity: 1;
193
- visibility: visible;
194
- }
195
-
196
- .toolbarBtn .shortcut {
197
- display: inline;
198
- opacity: 0.6;
199
- margin-left: 8px;
200
- padding: 2px 5px;
201
- background: rgba(255, 255, 255, 0.15);
202
- border-radius: 3px;
203
- font-size: 10px;
204
- }
205
-
206
- /* Context Menu */
207
- .contextMenu {
208
- position: fixed;
209
- background: #2d2d2d;
210
- border: 1px solid var(--border-color);
211
- border-radius: 8px;
212
- padding: 6px 0;
213
- min-width: 180px;
214
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
215
- z-index: 2000;
216
- display: none;
217
- }
218
-
219
- .contextMenu.visible {
220
- display: block;
221
- }
222
-
223
- .contextMenuItem {
224
- padding: 10px 16px;
225
- cursor: pointer;
226
- display: flex;
227
- align-items: center;
228
- gap: 12px;
229
- color: var(--text-primary);
230
- font-size: 13px;
231
- transition: background 0.1s;
232
- }
233
-
234
- .contextMenuItem:hover {
235
- background: var(--bg-tertiary);
236
- }
237
-
238
- .contextMenuItem svg {
239
- width: 16px;
240
- height: 16px;
241
- fill: currentColor;
242
- opacity: 0.8;
243
- }
244
-
245
- .contextMenuItem .shortcutHint {
246
- margin-left: auto;
247
- opacity: 0.5;
248
- font-size: 11px;
249
- }
250
-
251
- .contextMenuDivider {
252
- height: 1px;
253
- background: var(--border-color);
254
- margin: 6px 0;
255
- }
256
-
257
- /* Tool Dropdown Panel - Microsoft Edge Style */
258
- .toolDropdown {
259
- position: absolute;
260
- top: calc(var(--toolbar-height) + 4px);
261
- background: #2d2d2d;
262
- border-radius: 8px;
263
- padding: 16px;
264
- min-width: 240px;
265
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
266
- display: none;
267
- z-index: 200;
268
- }
269
-
270
- .toolDropdown.visible {
271
- display: block;
272
- }
273
-
274
- .dropdownSection {
275
- margin-bottom: 16px;
276
- }
277
-
278
- .dropdownSection:last-child {
279
- margin-bottom: 0;
280
- }
281
-
282
- .dropdownLabel {
283
- font-size: 13px;
284
- font-weight: 600;
285
- color: var(--text-primary);
286
- margin-bottom: 12px;
287
- display: flex;
288
- align-items: center;
289
- gap: 6px;
290
- }
291
-
292
- .dropdownLabel svg {
293
- width: 14px;
294
- height: 14px;
295
- fill: var(--text-secondary);
296
- }
297
-
298
- /* Color Grid */
299
- .colorGrid {
300
- display: grid;
301
- grid-template-columns: repeat(6, 1fr);
302
- gap: 8px;
303
- }
304
-
305
- .colorDot {
306
- width: 28px;
307
- height: 28px;
308
- border-radius: 50%;
309
- border: 2px solid transparent;
310
- cursor: pointer;
311
- transition: transform 0.1s, border-color 0.1s;
312
- }
313
-
314
- .colorDot:hover {
315
- transform: scale(1.1);
316
- }
317
-
318
- .colorDot.active {
319
- border-color: var(--text-primary);
320
- }
321
-
322
- /* Stroke Preview Wave */
323
- .strokePreview {
324
- height: 50px;
325
- background: #1f1f1f;
326
- border-radius: 8px;
327
- display: flex;
328
- align-items: center;
329
- justify-content: center;
330
- margin-bottom: 16px;
331
- overflow: hidden;
332
- }
333
-
334
- .strokePreview svg {
335
- width: 100%;
336
- height: 100%;
337
- }
338
-
339
- /* Thickness Slider */
340
- .thicknessSlider {
341
- width: 100%;
342
- display: flex;
343
- flex-direction: column;
344
- gap: 8px;
345
- }
346
-
347
- .thicknessSlider input[type="range"] {
348
- -webkit-appearance: none;
349
- appearance: none;
350
- width: 100%;
351
- height: 4px;
352
- background: #555;
353
- border-radius: 2px;
354
- outline: none;
355
- }
356
-
357
- .thicknessSlider input[type="range"]::-webkit-slider-thumb {
358
- -webkit-appearance: none;
359
- appearance: none;
360
- width: 16px;
361
- height: 16px;
362
- background: #fff;
363
- border-radius: 50%;
364
- cursor: pointer;
365
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
366
- }
367
-
368
- .thicknessSlider input[type="range"]::-moz-range-thumb {
369
- width: 16px;
370
- height: 16px;
371
- background: #fff;
372
- border-radius: 50%;
373
- cursor: pointer;
374
- border: none;
375
- }
376
-
377
- .thicknessLabels {
378
- display: flex;
379
- justify-content: space-between;
380
- font-size: 12px;
381
- color: var(--text-secondary);
382
- }
383
-
384
- /* Tool button with dropdown arrow */
385
- .toolbarBtnWithDropdown {
386
- position: relative;
387
- display: flex;
388
- align-items: center;
389
- }
390
-
391
- .toolbarBtnWithDropdown .toolbarBtn {
392
- border-radius: 4px 0 0 4px;
393
- }
394
-
395
- .dropdownArrow {
396
- width: 20px;
397
- height: 36px;
398
- border: none;
399
- background: transparent;
400
- color: var(--text-primary);
401
- border-radius: 0 4px 4px 0;
402
- cursor: pointer;
403
- display: flex;
404
- align-items: center;
405
- justify-content: center;
406
- }
407
-
408
- .dropdownArrow:hover {
409
- background: var(--bg-tertiary);
410
- }
411
-
412
- .toolbarBtnWithDropdown.active .toolbarBtn,
413
- .toolbarBtnWithDropdown.active .dropdownArrow {
414
- background: var(--accent);
415
- }
416
-
417
- .dropdownArrow svg {
418
- width: 12px;
419
- height: 12px;
420
- fill: currentColor;
421
- }
422
-
423
- /* Shape Grid */
424
- .shapeGrid {
425
- display: grid;
426
- grid-template-columns: repeat(4, 1fr);
427
- gap: 8px;
428
- }
429
-
430
- .shapeBtn {
431
- width: 48px;
432
- height: 48px;
433
- background: #1f1f1f;
434
- border: 2px solid transparent;
435
- border-radius: 8px;
436
- cursor: pointer;
437
- display: flex;
438
- align-items: center;
439
- justify-content: center;
440
- color: var(--text-primary);
441
- transition: border-color 0.1s, background 0.1s;
442
- }
443
-
444
- .shapeBtn:hover {
445
- background: #3d3d3d;
446
- }
447
-
448
- .shapeBtn.active {
449
- border-color: var(--accent);
450
- background: #3d3d3d;
451
- }
452
-
453
- .shapeBtn svg {
454
- width: 28px;
455
- height: 28px;
456
- }
457
-
458
- /* Page Info */
459
- .pageInfo {
460
- display: flex;
461
- align-items: center;
462
- gap: 8px;
463
- margin-left: auto;
464
- }
465
-
466
- #pageInput {
467
- width: 40px;
468
- height: 28px;
469
- background: var(--bg-tertiary);
470
- border: 1px solid var(--border-color);
471
- border-radius: 4px;
472
- color: var(--text-primary);
473
- text-align: center;
474
- font-size: 13px;
475
- }
476
-
477
- #pageCount {
478
- color: var(--text-secondary);
479
- font-size: 13px;
480
- }
481
-
482
- /* Sidebar - Thumbnails */
483
- #sidebar {
484
- position: fixed;
485
- top: var(--toolbar-height);
486
- left: 0;
487
- bottom: 0;
488
- width: var(--sidebar-width);
489
- background: var(--bg-secondary);
490
- border-right: 1px solid var(--border-color);
491
- overflow-y: auto;
492
- display: none;
493
- z-index: 50;
494
- }
495
-
496
- #sidebar.open {
497
- display: block;
498
- }
499
-
500
- .sidebarHeader {
501
- padding: 12px 16px;
502
- font-size: 13px;
503
- font-weight: 600;
504
- border-bottom: 1px solid var(--border-color);
505
- display: flex;
506
- justify-content: space-between;
507
- align-items: center;
508
- }
509
-
510
- .closeBtn {
511
- background: none;
512
- border: none;
513
- color: var(--text-primary);
514
- cursor: pointer;
515
- font-size: 18px;
516
- }
517
-
518
- #thumbnailContainer {
519
- padding: 12px;
520
- display: flex;
521
- flex-direction: column;
522
- gap: 12px;
523
- }
524
-
525
- .thumbnail {
526
- background: var(--bg-tertiary);
527
- border: 2px solid transparent;
528
- border-radius: 4px;
529
- cursor: pointer;
530
- padding: 4px;
531
- transition: border-color 0.15s;
532
- }
533
-
534
- .thumbnail:hover {
535
- border-color: var(--accent);
536
- }
537
-
538
- .thumbnail.active {
539
- border-color: var(--accent);
540
- }
541
-
542
- .thumbnail canvas {
543
- width: 100%;
544
- display: block;
545
- }
546
-
547
- .thumbnailNum {
548
- text-align: center;
549
- font-size: 11px;
550
- color: var(--text-secondary);
551
- margin-top: 4px;
552
- }
553
-
554
- /* Viewer Container */
555
- #viewerContainer {
556
- position: fixed;
557
- top: var(--toolbar-height);
558
- left: 0;
559
- right: 0;
560
- bottom: 0;
561
- overflow: auto;
562
- background: #525659;
563
- }
564
-
565
- #viewerContainer.withSidebar {
566
- left: var(--sidebar-width);
567
- }
568
-
569
- .pdfViewer .page {
570
- margin: 8px auto;
571
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
572
- position: relative;
573
- transition: filter 0.3s ease;
574
- }
575
-
576
- /* Sepia Reading Mode */
577
- .pdfViewer.sepia .page canvas {
578
- filter: sepia(40%) brightness(0.95) contrast(0.9);
579
- }
580
-
581
- .pdfViewer.sepia .page {
582
- background: #f4ecd8 !important;
583
- }
584
-
585
- #viewerContainer.sepia {
586
- background: #d4c9a8;
587
- }
588
-
589
- /* Annotation Layer */
590
- .annotationLayer {
591
- position: absolute;
592
- top: 0;
593
- left: 0;
594
- right: 0;
595
- bottom: 0;
596
- pointer-events: none;
597
- z-index: 10;
598
- }
599
-
600
- .annotationLayer.active {
601
- pointer-events: auto;
602
- cursor: crosshair;
603
- }
604
-
605
- .annotationLayer path {
606
- fill: none;
607
- stroke-linecap: round;
608
- stroke-linejoin: round;
609
- }
610
-
611
- /* Select tool cursor */
612
- .annotationLayer.select-mode {
613
- cursor: default;
614
- }
615
-
616
- .annotationLayer.select-mode path,
617
- .annotationLayer.select-mode rect,
618
- .annotationLayer.select-mode ellipse,
619
- .annotationLayer.select-mode line,
620
- .annotationLayer.select-mode text {
621
- cursor: grab;
622
- pointer-events: all;
623
- transition: transform 0.1s ease, opacity 0.1s ease;
624
- }
625
-
626
- /* Invisible hit area for easier selection */
627
- .annotationLayer.select-mode path,
628
- .annotationLayer.select-mode line {
629
- stroke-linecap: round;
630
- }
631
-
632
- .annotationLayer.select-mode path:hover,
633
- .annotationLayer.select-mode rect:hover,
634
- .annotationLayer.select-mode ellipse:hover,
635
- .annotationLayer.select-mode line:hover,
636
- .annotationLayer.select-mode text:hover {
637
- opacity: 0.8;
638
- cursor: grab;
639
- }
640
-
641
- /* Selected annotation element - use filter for SVG compatibility */
642
- .annotation-selected {
643
- filter: drop-shadow(0 0 4px #0078d4) drop-shadow(0 0 8px rgba(0, 120, 212, 0.6)) !important;
644
- opacity: 1 !important;
645
- }
646
-
647
- /* Touch feedback */
648
- .annotation-dragging {
649
- opacity: 0.6;
650
- cursor: grabbing !important;
651
- filter: drop-shadow(0 4px 12px rgba(0, 120, 212, 0.5));
652
- }
653
-
654
- /* Tablet/Touch optimizations for select mode */
655
- @media (pointer: coarse),
656
- (max-width: 1024px) {
657
-
658
- .annotationLayer.select-mode path,
659
- .annotationLayer.select-mode rect,
660
- .annotationLayer.select-mode ellipse,
661
- .annotationLayer.select-mode line,
662
- .annotationLayer.select-mode text {
663
- /* Ensure touch-friendly interaction */
664
- cursor: pointer;
665
- }
666
-
667
- /* Bigger toolbar buttons for touch */
668
- .toolbarBtn {
669
- width: 44px;
670
- height: 44px;
671
- min-width: 44px;
672
- }
673
- }
674
-
675
- /* Touch selection ring animation */
676
- @keyframes selectionPulse {
677
- 0% {
678
- outline-color: rgba(0, 120, 212, 1);
679
- }
680
-
681
- 50% {
682
- outline-color: rgba(0, 120, 212, 0.5);
683
- }
684
-
685
- 100% {
686
- outline-color: rgba(0, 120, 212, 1);
687
- }
688
- }
689
-
690
- .annotation-selected.just-selected {
691
- animation: selectionPulse 0.6s ease-out;
692
- }
693
-
694
- /* Move handle for touch devices */
695
- .annotation-move-handle {
696
- position: absolute;
697
- width: 36px;
698
- height: 36px;
699
- background: rgba(0, 120, 212, 0.9);
700
- border-radius: 50%;
701
- display: none;
702
- align-items: center;
703
- justify-content: center;
704
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
705
- cursor: grab;
706
- z-index: 100;
707
- touch-action: none;
708
- }
709
-
710
- .annotation-move-handle svg {
711
- width: 20px;
712
- height: 20px;
713
- fill: white;
714
- }
715
-
716
- @media (pointer: coarse) {
717
- .annotation-move-handle {
718
- display: flex;
719
- }
720
- }
721
-
722
- /* Ghost element while dragging */
723
- .annotation-ghost {
724
- opacity: 0.3;
725
- pointer-events: none;
726
- }
727
-
728
- /* Selection toolbar for touch - action buttons */
729
- .selection-toolbar {
730
- position: fixed;
731
- bottom: 24px;
732
- left: 50%;
733
- transform: translateX(-50%) translateY(100px);
734
- background: linear-gradient(135deg, #363636 0%, #2d2d2d 100%);
735
- border: 1px solid rgba(255, 255, 255, 0.1);
736
- border-radius: 16px;
737
- padding: 12px 16px;
738
- display: flex;
739
- align-items: center;
740
- gap: 12px;
741
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05);
742
- z-index: 2000;
743
- opacity: 0;
744
- transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.2s ease;
745
- pointer-events: none;
746
- }
747
-
748
- .selection-toolbar.visible {
749
- transform: translateX(-50%) translateY(0);
750
- opacity: 1;
751
- pointer-events: auto;
752
- }
753
-
754
- .selection-toolbar::before {
755
- content: 'Seçili Öğe';
756
- position: absolute;
757
- top: -28px;
758
- left: 50%;
759
- transform: translateX(-50%);
760
- font-size: 11px;
761
- color: rgba(255, 255, 255, 0.6);
762
- white-space: nowrap;
763
- text-transform: uppercase;
764
- letter-spacing: 0.5px;
765
- }
766
-
767
- .selection-toolbar button {
768
- width: 52px;
769
- height: 52px;
770
- border: none;
771
- background: rgba(255, 255, 255, 0.08);
772
- color: white;
773
- border-radius: 12px;
774
- cursor: pointer;
775
- display: flex;
776
- flex-direction: column;
777
- align-items: center;
778
- justify-content: center;
779
- gap: 4px;
780
- transition: all 0.15s ease;
781
- position: relative;
782
- }
783
-
784
- .selection-toolbar button:hover {
785
- background: rgba(255, 255, 255, 0.15);
786
- transform: translateY(-2px);
787
- }
788
-
789
- .selection-toolbar button:active {
790
- transform: translateY(0);
791
- background: rgba(255, 255, 255, 0.2);
792
- }
793
-
794
- .selection-toolbar button.delete {
795
- background: rgba(196, 43, 28, 0.8);
796
- }
797
-
798
- .selection-toolbar button.delete:hover {
799
- background: #e03e2f;
800
- transform: translateY(-2px);
801
- }
802
-
803
- .selection-toolbar button svg {
804
- width: 22px;
805
- height: 22px;
806
- fill: currentColor;
807
- }
808
-
809
- .selection-toolbar button span {
810
- font-size: 9px;
811
- opacity: 0.8;
812
- text-transform: uppercase;
813
- letter-spacing: 0.3px;
814
- }
815
-
816
- /* Toast notification for copy/paste */
817
- .toast-notification {
818
- position: fixed;
819
- bottom: 80px;
820
- left: 50%;
821
- transform: translateX(-50%);
822
- background: #323232;
823
- color: white;
824
- padding: 12px 24px;
825
- border-radius: 8px;
826
- font-size: 14px;
827
- z-index: 3000;
828
- animation: toastIn 0.3s ease, toastOut 0.3s ease 1.7s forwards;
829
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
830
- }
831
-
832
- @keyframes toastIn {
833
- from {
834
- opacity: 0;
835
- transform: translateX(-50%) translateY(20px);
836
- }
837
-
838
- to {
839
- opacity: 1;
840
- transform: translateX(-50%) translateY(0);
841
- }
842
- }
843
-
844
- @keyframes toastOut {
845
- from {
846
- opacity: 1;
847
- transform: translateX(-50%) translateY(0);
848
- }
849
-
850
- to {
851
- opacity: 0;
852
- transform: translateX(-50%) translateY(-20px);
853
- }
854
- }
855
-
856
- /* Text Selection Highlight */
857
- .textHighlight {
858
- position: absolute;
859
- pointer-events: none;
860
- border-radius: 2px;
861
- }
862
-
863
- /* Selection Popup Button */
864
- .highlightPopup {
865
- position: absolute;
866
- background: var(--bg-secondary);
867
- border: 1px solid var(--border-color);
868
- border-radius: 8px;
869
- padding: 6px;
870
- display: flex;
871
- gap: 4px;
872
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
873
- z-index: 500;
874
- }
875
-
876
- .highlightPopup button {
877
- width: 28px;
878
- height: 28px;
879
- border: none;
880
- border-radius: 50%;
881
- cursor: pointer;
882
- transition: transform 0.1s;
883
- }
884
-
885
- .highlightPopup button:hover {
886
- transform: scale(1.15);
887
- }
888
-
889
- /* Upload Overlay */
890
- #uploadOverlay {
891
- position: fixed;
892
- top: var(--toolbar-height);
893
- left: 0;
894
- right: 0;
895
- bottom: 0;
896
- background: var(--bg-primary);
897
- display: flex;
898
- align-items: center;
899
- justify-content: center;
900
- z-index: 40;
901
- }
902
-
903
- .dropzone {
904
- width: 400px;
905
- padding: 60px 40px;
906
- background: var(--bg-secondary);
907
- border: 2px dashed var(--border-color);
908
- border-radius: 12px;
909
- text-align: center;
910
- cursor: pointer;
911
- transition: all 0.2s;
912
- }
913
-
914
- .dropzone:hover {
915
- border-color: var(--accent);
916
- background: var(--bg-tertiary);
917
- }
918
-
919
- .dropzone svg {
920
- width: 64px;
921
- height: 64px;
922
- fill: var(--text-secondary);
923
- margin-bottom: 16px;
924
- }
925
-
926
- .dropzone h2 {
927
- font-size: 18px;
928
- font-weight: 500;
929
- margin-bottom: 8px;
930
- }
931
-
932
- .dropzone p {
933
- color: var(--text-secondary);
934
- font-size: 13px;
935
- }
936
-
937
- /* Inline Text Editor */
938
- .textEditorOverlay {
939
- position: fixed;
940
- top: 0;
941
- left: 0;
942
- right: 0;
943
- bottom: 0;
944
- z-index: 1000;
945
- }
946
-
947
- .textEditorBox {
948
- position: absolute;
949
- background: white;
950
- border: 2px solid var(--accent);
951
- border-radius: 4px;
952
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
953
- min-width: 200px;
954
- max-width: 400px;
955
- }
956
-
957
- .textEditorInput {
958
- padding: 12px 16px;
959
- font-size: 14px;
960
- font-family: 'Segoe UI', system-ui, sans-serif;
961
- color: #333;
962
- outline: none;
963
- min-height: 40px;
964
- word-wrap: break-word;
965
- }
966
-
967
- .textEditorInput:empty:before {
968
- content: 'Buraya yazmaya başla...';
969
- color: #999;
970
- }
971
-
972
- .textEditorToolbar {
973
- display: flex;
974
- align-items: center;
975
- gap: 4px;
976
- padding: 8px 12px;
977
- border-top: 1px solid #e0e0e0;
978
- background: #f5f5f5;
979
- }
980
-
981
- .textEditorBtn {
982
- width: 28px;
983
- height: 28px;
984
- border: none;
985
- background: transparent;
986
- border-radius: 4px;
987
- cursor: pointer;
988
- display: flex;
989
- align-items: center;
990
- justify-content: center;
991
- color: #333;
992
- font-size: 12px;
993
- font-weight: 600;
994
- }
995
-
996
- .textEditorBtn:hover {
997
- background: #e0e0e0;
998
- }
999
-
1000
- .textEditorBtn.delete {
1001
- color: #d32f2f;
1002
- margin-left: auto;
1003
- }
1004
-
1005
- .textEditorColorDot {
1006
- width: 20px;
1007
- height: 20px;
1008
- border-radius: 50%;
1009
- border: 2px solid transparent;
1010
- cursor: pointer;
1011
- }
1012
-
1013
- .textEditorColorDot.active {
1014
- border-color: var(--accent);
1015
- }
1016
-
1017
- /* Draggable text annotations */
1018
- .annotationLayer svg text {
1019
- cursor: move;
1020
- user-select: none;
1021
- }
1022
-
1023
- .annotationLayer svg text.dragging {
1024
- opacity: 0.7;
1025
- }
1026
-
1027
- .hidden {
1028
- display: none !important;
1029
- }
1030
-
1031
- /* Scrollbar */
1032
- ::-webkit-scrollbar {
1033
- width: 8px;
1034
- height: 8px;
1035
- }
1036
-
1037
- ::-webkit-scrollbar-track {
1038
- background: var(--bg-secondary);
1039
- }
1040
-
1041
- ::-webkit-scrollbar-thumb {
1042
- background: var(--bg-tertiary);
1043
- border-radius: 4px;
1044
- }
1045
-
1046
- ::-webkit-scrollbar-thumb:hover {
1047
- background: #555;
1048
- }
1049
- </style>
1050
18
  </head>
1051
19
 
1052
20
  <body>
@@ -1183,6 +151,30 @@
1183
151
  <path d="M7 2l12 11.2-5.8.5 3.3 7.3-2.2 1-3.2-7.4L7 18.5V2z" />
1184
152
  </svg>
1185
153
  </button>
154
+
155
+ <div class="separator"></div>
156
+
157
+ <!-- Undo / Redo / Clear All -->
158
+ <button class="toolbarBtn" id="undoBtn" data-tooltip="Geri Al (Ctrl+Z)" disabled>
159
+ <svg viewBox="0 0 24 24">
160
+ <path
161
+ d="M12.5 8c-2.65 0-5.05 1.04-6.83 2.73L2.5 7.5v9h9l-3.19-3.19c1.29-1.25 3.04-2.02 5-2.02 3.24 0 5.97 2.13 6.89 5.07l2.36-.78C21.19 11.79 17.22 8 12.5 8z" />
162
+ </svg>
163
+ </button>
164
+ <button class="toolbarBtn" id="redoBtn" data-tooltip="Yinele (Ctrl+Y)" disabled>
165
+ <svg viewBox="0 0 24 24">
166
+ <path
167
+ d="M18.4 10.6C16.55 8.99 14.15 8 11.5 8c-4.65 0-8.58 3.03-9.96 7.22L3.9 16c1.05-3.19 4.05-5.5 7.6-5.5 1.95 0 3.73.72 5.12 1.88L13.5 16.5h9v-9l-4.1 3.1z" />
168
+ </svg>
169
+ </button>
170
+ <button class="toolbarBtn" id="clearAllBtn" data-tooltip="Tümünü Temizle">
171
+ <svg viewBox="0 0 24 24">
172
+ <path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
173
+ </svg>
174
+ </button>
175
+
176
+ <div class="separator"></div>
177
+
1186
178
  <button class="toolbarBtn" id="textBtn" data-tooltip="Metin Ekle (T)">
1187
179
  <svg viewBox="0 0 24 24">
1188
180
  <path d="M5 4v3h5.5v12h3V7H19V4H5z" />
@@ -1295,6 +287,42 @@
1295
287
  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" />
1296
288
  </svg>
1297
289
  </button>
290
+
291
+ <div class="separator"></div>
292
+
293
+ <!-- Fullscreen Toggle -->
294
+ <button class="toolbarBtn" id="fullscreenBtn" data-tooltip="Tam Ekran (F)">
295
+ <svg viewBox="0 0 24 24" id="fullscreenIcon">
296
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
297
+ </svg>
298
+ </button>
299
+
300
+ <div class="separator overflowSep"></div>
301
+
302
+ <div class="toolbarBtnWithDropdown" id="overflowWrapper">
303
+ <button class="toolbarBtn" id="overflowBtn" title="Daha Fazla">
304
+ <svg viewBox="0 0 24 24">
305
+ <circle cx="12" cy="5" r="2"/>
306
+ <circle cx="12" cy="12" r="2"/>
307
+ <circle cx="12" cy="19" r="2"/>
308
+ </svg>
309
+ </button>
310
+ <div class="toolDropdown" id="overflowDropdown">
311
+ <button class="overflowItem" id="overflowRotateLeft">
312
+ <svg viewBox="0 0 24 24"><path 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"/></svg>
313
+ <span>Sola Döndür</span>
314
+ </button>
315
+ <button class="overflowItem" id="overflowRotateRight">
316
+ <svg viewBox="0 0 24 24"><path 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"/></svg>
317
+ <span>Sağa Döndür</span>
318
+ </button>
319
+ <div class="overflowDivider"></div>
320
+ <button class="overflowItem" id="overflowSepia">
321
+ <svg viewBox="0 0 24 24"><path 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"/></svg>
322
+ <span>Okuma Modu</span>
323
+ </button>
324
+ </div>
325
+ </div>
1298
326
  </div>
1299
327
 
1300
328
  <div class="pageInfo">
@@ -1303,6 +331,16 @@
1303
331
  </div>
1304
332
  </div>
1305
333
 
334
+ <!-- Bottom Toolbar (Mobile Only) -->
335
+ <div id="bottomToolbar">
336
+ <div class="bottomToolbarInner" id="bottomToolbarInner">
337
+ <!-- Annotation tool buttons will be moved here on mobile via JS -->
338
+ </div>
339
+ </div>
340
+
341
+ <!-- Dropdown Backdrop (Mobile) -->
342
+ <div id="dropdownBackdrop"></div>
343
+
1306
344
  <!-- Sidebar - Thumbnails -->
1307
345
  <div id="sidebar">
1308
346
  <div class="sidebarHeader">
@@ -1329,1973 +367,7 @@
1329
367
  <div id="viewer" class="pdfViewer"></div>
1330
368
  </div>
1331
369
 
1332
- <script>
1333
- // IIFE to prevent global access to pdfDoc, pdfViewer
1334
- (function () {
1335
- 'use strict';
1336
-
1337
- // ============================================
1338
- // CANVAS EXPORT PROTECTION
1339
- // Block toDataURL/toBlob for PDF render canvas only
1340
- // Allows: thumbnails, annotations, other canvases
1341
- // ============================================
1342
- const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
1343
- const originalToBlob = HTMLCanvasElement.prototype.toBlob;
1344
-
1345
- HTMLCanvasElement.prototype.toDataURL = function () {
1346
- // Block only main PDF page canvases (inside .page elements in #viewerContainer)
1347
- if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1348
- console.warn('[Security] Canvas toDataURL blocked for PDF page');
1349
- return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; // 1x1 transparent
1350
- }
1351
- return originalToDataURL.apply(this, arguments);
1352
- };
1353
-
1354
- HTMLCanvasElement.prototype.toBlob = function (callback) {
1355
- // Block only main PDF page canvases
1356
- if (this.closest && this.closest('.page') && this.closest('#viewerContainer')) {
1357
- console.warn('[Security] Canvas toBlob blocked for PDF page');
1358
- // Return empty blob
1359
- if (callback) callback(new Blob([], { type: 'image/png' }));
1360
- return;
1361
- }
1362
- return originalToBlob.apply(this, arguments);
1363
- };
1364
-
1365
- pdfjsLib.GlobalWorkerOptions.workerSrc = '';
1366
-
1367
- // State - now private, not accessible from console
1368
- let pdfDoc = null;
1369
- let pdfViewer = null;
1370
- let annotationMode = false;
1371
- let currentTool = null; // null, 'pen', 'highlight', 'eraser'
1372
- let currentColor = '#e81224';
1373
- let currentWidth = 2;
1374
- let isDrawing = false;
1375
- let currentPath = null;
1376
- let currentDrawingPage = null;
1377
-
1378
- // Annotation persistence - stores SVG innerHTML per page
1379
- const annotationsStore = new Map();
1380
-
1381
- // Store base dimensions (scale=1.0) for each page - ensures consistent coordinates
1382
- const pageBaseDimensions = new Map();
1383
-
1384
- // Current SVG reference for drawing
1385
- let currentSvg = null;
1386
-
1387
- // Elements
1388
- const container = document.getElementById('viewerContainer');
1389
- const uploadOverlay = document.getElementById('uploadOverlay');
1390
- const fileInput = document.getElementById('fileInput');
1391
- const sidebar = document.getElementById('sidebar');
1392
- const thumbnailContainer = document.getElementById('thumbnailContainer');
1393
-
1394
- // Initialize PDFViewer
1395
- const eventBus = new pdfjsViewer.EventBus();
1396
- const linkService = new pdfjsViewer.PDFLinkService({ eventBus });
1397
-
1398
- pdfViewer = new pdfjsViewer.PDFViewer({
1399
- container: container,
1400
- eventBus: eventBus,
1401
- linkService: linkService,
1402
- removePageBorders: true,
1403
- textLayerMode: 2
1404
- });
1405
- linkService.setViewer(pdfViewer);
1406
-
1407
- // Track first page render for queue system
1408
- let firstPageRendered = false;
1409
- eventBus.on('pagerendered', function (evt) {
1410
- if (!firstPageRendered && evt.pageNumber === 1) {
1411
- firstPageRendered = true;
1412
- // Notify parent that PDF is fully rendered (for queue system)
1413
- if (window.parent && window.parent !== window) {
1414
- const config = window.PDF_SECURE_CONFIG || {};
1415
- window.parent.postMessage({ type: 'pdf-secure-ready', filename: config.filename }, '*');
1416
- console.log('[PDF-Secure] First page rendered, notifying parent');
1417
- }
1418
- }
1419
- });
1420
-
1421
- // File Handling
1422
- document.getElementById('dropzone').onclick = () => fileInput.click();
1423
-
1424
- fileInput.onchange = async (e) => {
1425
- const file = e.target.files[0];
1426
- if (file) await loadPDF(file);
1427
- };
1428
-
1429
- uploadOverlay.ondragover = (e) => e.preventDefault();
1430
- uploadOverlay.ondrop = async (e) => {
1431
- e.preventDefault();
1432
- const file = e.dataTransfer.files[0];
1433
- if (file?.type === 'application/pdf') await loadPDF(file);
1434
- };
1435
-
1436
- async function loadPDF(file) {
1437
- uploadOverlay.classList.add('hidden');
1438
-
1439
- const data = await file.arrayBuffer();
1440
- pdfDoc = await pdfjsLib.getDocument({ data }).promise;
1441
-
1442
- pdfViewer.setDocument(pdfDoc);
1443
- linkService.setDocument(pdfDoc);
1444
-
1445
- ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1446
- document.getElementById(id).disabled = false;
1447
- });
1448
-
1449
- // Thumbnails will be generated on-demand when sidebar opens
1450
- }
1451
-
1452
- // Load PDF from ArrayBuffer (for secure nonce-based loading)
1453
- async function loadPDFFromBuffer(arrayBuffer) {
1454
- uploadOverlay.classList.add('hidden');
1455
-
1456
- pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
1457
-
1458
- pdfViewer.setDocument(pdfDoc);
1459
- linkService.setDocument(pdfDoc);
1460
-
1461
- ['zoomIn', 'zoomOut', 'pageInput', 'rotateLeft', 'rotateRight'].forEach(id => {
1462
- document.getElementById(id).disabled = false;
1463
- });
1464
-
1465
- // Thumbnails will be generated on-demand when sidebar opens
1466
- }
1467
-
1468
- // Partial XOR decoder - must match backend encoding
1469
- function partialXorDecode(encodedData, keyBase64) {
1470
- const key = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
1471
- const data = new Uint8Array(encodedData);
1472
- const keyLen = key.length;
1473
-
1474
- // Decrypt first 10KB fully
1475
- const fullDecryptLen = Math.min(10240, data.length);
1476
- for (let i = 0; i < fullDecryptLen; i++) {
1477
- data[i] = data[i] ^ key[i % keyLen];
1478
- }
1479
-
1480
- // Decrypt every 50th byte after that
1481
- for (let i = fullDecryptLen; i < data.length; i += 50) {
1482
- data[i] = data[i] ^ key[i % keyLen];
1483
- }
1484
-
1485
- return data.buffer;
1486
- }
1487
-
1488
- // Auto-load PDF if config is present (injected by NodeBB plugin)
1489
- async function autoLoadSecurePDF() {
1490
- if (!window.PDF_SECURE_CONFIG || !window.PDF_SECURE_CONFIG.filename) {
1491
- console.log('[PDF-Secure] No config found, showing file picker');
1492
- return;
1493
- }
1494
-
1495
- const config = window.PDF_SECURE_CONFIG;
1496
- console.log('[PDF-Secure] Auto-loading:', config.filename);
1497
-
1498
- // Show loading state
1499
- const dropzone = document.getElementById('dropzone');
1500
- if (dropzone) {
1501
- dropzone.innerHTML = `
1502
- <svg viewBox="0 0 24 24" class="spin">
1503
- <path d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8z" />
1504
- </svg>
1505
- <h2>PDF Yükleniyor...</h2>
1506
- <p>${config.filename}</p>
1507
- `;
1508
- }
1509
-
1510
- try {
1511
- // Nonce and key are embedded in HTML config (not fetched from API)
1512
- // This improves security - key is ONLY in HTML source, not in any network response
1513
- const nonce = config.nonce;
1514
- const xorKey = config.dk;
1515
-
1516
- // Fetch encrypted PDF binary
1517
- const pdfUrl = config.relativePath + '/api/v3/plugins/pdf-secure/pdf-data?nonce=' + encodeURIComponent(nonce);
1518
- const pdfRes = await fetch(pdfUrl, { credentials: 'same-origin' });
1519
-
1520
- if (!pdfRes.ok) {
1521
- throw new Error('PDF yüklenemedi (' + pdfRes.status + ')');
1522
- }
1523
-
1524
- const encodedBuffer = await pdfRes.arrayBuffer();
1525
-
1526
- console.log('[PDF-Secure] Encrypted data received:', encodedBuffer.byteLength, 'bytes');
1527
-
1528
- // Step 3: Decode XOR encrypted data
1529
- let pdfBuffer;
1530
- if (xorKey) {
1531
- console.log('[PDF-Secure] Decoding XOR encrypted data...');
1532
- pdfBuffer = partialXorDecode(encodedBuffer, xorKey);
1533
- } else {
1534
- // Fallback for backward compatibility
1535
- pdfBuffer = encodedBuffer;
1536
- }
1537
-
1538
- console.log('[PDF-Secure] PDF decoded successfully');
1539
-
1540
- // Step 4: Load into viewer
1541
- await loadPDFFromBuffer(pdfBuffer);
1542
-
1543
- // Step 5: Moved to pagerendered event for proper timing
1544
-
1545
- // Step 6: Security - clear references to prevent extraction
1546
- pdfBuffer = null;
1547
-
1548
- // Security: Delete config containing sensitive data (nonce, key)
1549
- delete window.PDF_SECURE_CONFIG;
1550
-
1551
- // Security: Remove PDF.js globals to prevent console manipulation
1552
- delete window.pdfjsLib;
1553
- delete window.pdfjsViewer;
1554
-
1555
- // Security: Block dangerous PDF.js methods
1556
- if (pdfDoc) {
1557
- pdfDoc.getData = function () {
1558
- console.warn('[Security] getData() is blocked');
1559
- return Promise.reject(new Error('Access denied'));
1560
- };
1561
- pdfDoc.saveDocument = function () {
1562
- console.warn('[Security] saveDocument() is blocked');
1563
- return Promise.reject(new Error('Access denied'));
1564
- };
1565
- }
1566
-
1567
- console.log('[PDF-Secure] PDF fully loaded and ready');
1568
-
1569
- } catch (err) {
1570
- console.error('[PDF-Secure] Auto-load error:', err);
1571
-
1572
- // Notify parent of error (prevents 60s queue hang)
1573
- if (window.parent && window.parent !== window) {
1574
- const config = window.PDF_SECURE_CONFIG || {};
1575
- window.parent.postMessage({
1576
- type: 'pdf-secure-ready',
1577
- filename: config.filename,
1578
- error: err.message
1579
- }, '*');
1580
- }
1581
-
1582
- if (dropzone) {
1583
- dropzone.innerHTML = `
1584
- <svg viewBox="0 0 24 24" style="fill: #e81224;">
1585
- <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
1586
- </svg>
1587
- <h2>Hata</h2>
1588
- <p>${err.message}</p>
1589
- `;
1590
- }
1591
- }
1592
- }
1593
-
1594
- // Run auto-load on page ready
1595
- autoLoadSecurePDF();
1596
-
1597
- // Generate Thumbnails (deferred - only when sidebar opens)
1598
- let thumbnailsGenerated = false;
1599
- async function generateThumbnails() {
1600
- if (thumbnailsGenerated) return;
1601
- thumbnailsGenerated = true;
1602
- thumbnailContainer.innerHTML = '';
1603
-
1604
- for (let i = 1; i <= pdfDoc.numPages; i++) {
1605
- const page = await pdfDoc.getPage(i);
1606
- const viewport = page.getViewport({ scale: 0.2 });
1607
-
1608
- const canvas = document.createElement('canvas');
1609
- canvas.width = viewport.width;
1610
- canvas.height = viewport.height;
1611
-
1612
- await page.render({
1613
- canvasContext: canvas.getContext('2d'),
1614
- viewport: viewport
1615
- }).promise;
1616
-
1617
- const thumb = document.createElement('div');
1618
- thumb.className = 'thumbnail' + (i === 1 ? ' active' : '');
1619
- thumb.dataset.page = i;
1620
- thumb.innerHTML = `<div class="thumbnailNum">${i}</div>`;
1621
- thumb.insertBefore(canvas, thumb.firstChild);
1622
-
1623
- thumb.onclick = () => {
1624
- pdfViewer.currentPageNumber = i;
1625
- document.querySelectorAll('.thumbnail').forEach(t => t.classList.remove('active'));
1626
- thumb.classList.add('active');
1627
- };
1628
-
1629
- thumbnailContainer.appendChild(thumb);
1630
- }
1631
- }
1632
-
1633
- // Events
1634
- eventBus.on('pagesinit', () => {
1635
- pdfViewer.currentScaleValue = 'page-width';
1636
- document.getElementById('pageCount').textContent = `/ ${pdfViewer.pagesCount}`;
1637
- });
1638
-
1639
- eventBus.on('pagechanging', (evt) => {
1640
- document.getElementById('pageInput').value = evt.pageNumber;
1641
- // Update active thumbnail
1642
- document.querySelectorAll('.thumbnail').forEach(t => {
1643
- t.classList.toggle('active', parseInt(t.dataset.page) === evt.pageNumber);
1644
- });
1645
- });
1646
-
1647
- eventBus.on('pagerendered', (evt) => {
1648
- if (annotationMode) injectAnnotationLayer(evt.pageNumber);
1649
- });
1650
-
1651
- // Page Navigation
1652
- document.getElementById('pageInput').onchange = (e) => {
1653
- const num = parseInt(e.target.value);
1654
- if (num >= 1 && num <= pdfViewer.pagesCount) {
1655
- pdfViewer.currentPageNumber = num;
1656
- }
1657
- };
1658
-
1659
- // Zoom
1660
- document.getElementById('zoomIn').onclick = () => pdfViewer.currentScale += 0.25;
1661
- document.getElementById('zoomOut').onclick = () => pdfViewer.currentScale -= 0.25;
1662
-
1663
- // Sidebar toggle (deferred thumbnail generation)
1664
- const sidebarEl = document.getElementById('sidebar');
1665
- const sidebarBtnEl = document.getElementById('sidebarBtn');
1666
- const closeSidebarBtn = document.getElementById('closeSidebar');
1667
-
1668
- sidebarBtnEl.onclick = () => {
1669
- const isOpening = !sidebarEl.classList.contains('open');
1670
- sidebarEl.classList.toggle('open');
1671
- sidebarBtnEl.classList.toggle('active');
1672
-
1673
- // Generate thumbnails on first open (deferred loading)
1674
- if (isOpening && pdfDoc) {
1675
- generateThumbnails();
1676
- }
1677
- };
1678
-
1679
- closeSidebarBtn.onclick = () => {
1680
- sidebarEl.classList.remove('open');
1681
- sidebarBtnEl.classList.remove('active');
1682
- };
1683
-
1684
- // Sepia Reading Mode
1685
- let sepiaMode = false;
1686
- document.getElementById('sepiaBtn').onclick = () => {
1687
- sepiaMode = !sepiaMode;
1688
- document.getElementById('viewer').classList.toggle('sepia', sepiaMode);
1689
- container.classList.toggle('sepia', sepiaMode);
1690
- document.getElementById('sepiaBtn').classList.toggle('active', sepiaMode);
1691
- };
1692
-
1693
- // Page Rotation
1694
- const pageRotations = new Map(); // Store rotation per page
1695
-
1696
- function rotatePage(delta) {
1697
- const pageNum = pdfViewer.currentPageNumber;
1698
- const currentRotation = pageRotations.get(pageNum) || 0;
1699
- const newRotation = (currentRotation + delta + 360) % 360;
1700
- pageRotations.set(pageNum, newRotation);
1701
-
1702
- // Apply rotation only to the canvas (not the whole page div)
1703
- const pageView = pdfViewer.getPageView(pageNum - 1);
1704
- if (pageView?.div) {
1705
- const canvas = pageView.div.querySelector('canvas');
1706
- const textLayer = pageView.div.querySelector('.textLayer');
1707
- const annotationLayer = pageView.div.querySelector('.annotationLayer');
1708
-
1709
- if (canvas) {
1710
- canvas.style.transform = `rotate(${newRotation}deg)`;
1711
- canvas.style.transformOrigin = 'center center';
1712
- }
1713
- if (textLayer) {
1714
- textLayer.style.transform = `rotate(${newRotation}deg)`;
1715
- textLayer.style.transformOrigin = 'center center';
1716
- }
1717
- if (annotationLayer) {
1718
- annotationLayer.style.transform = `rotate(${newRotation}deg)`;
1719
- annotationLayer.style.transformOrigin = 'center center';
1720
- }
1721
-
1722
- // Also rotate text highlight container
1723
- const textHighlightContainer = pageView.div.querySelector('.textHighlightContainer');
1724
- if (textHighlightContainer) {
1725
- textHighlightContainer.style.transform = `rotate(${newRotation}deg)`;
1726
- textHighlightContainer.style.transformOrigin = 'center center';
1727
- }
1728
- }
1729
- }
1730
-
1731
- document.getElementById('rotateLeft').onclick = () => rotatePage(-90);
1732
- document.getElementById('rotateRight').onclick = () => rotatePage(90);
1733
-
1734
- // Sidebar Toggle
1735
- document.getElementById('sidebarBtn').onclick = () => {
1736
- sidebar.classList.toggle('open');
1737
- container.classList.toggle('withSidebar', sidebar.classList.contains('open'));
1738
- document.getElementById('sidebarBtn').classList.toggle('active', sidebar.classList.contains('open'));
1739
- };
1740
-
1741
- document.getElementById('closeSidebar').onclick = () => {
1742
- sidebar.classList.remove('open');
1743
- container.classList.remove('withSidebar');
1744
- document.getElementById('sidebarBtn').classList.remove('active');
1745
- };
1746
-
1747
-
1748
- // Tool settings - separate for each tool
1749
- let highlightColor = '#fff100';
1750
- let highlightWidth = 4;
1751
- let drawColor = '#e81224';
1752
- let drawWidth = 2;
1753
- let shapeColor = '#e81224';
1754
- let shapeWidth = 2;
1755
- let currentShape = 'rectangle'; // rectangle, circle, line, arrow
1756
-
1757
- // Dropdown Panel Logic
1758
- const highlightDropdown = document.getElementById('highlightDropdown');
1759
- const drawDropdown = document.getElementById('drawDropdown');
1760
- const shapesDropdown = document.getElementById('shapesDropdown');
1761
- const highlightWrapper = document.getElementById('highlightWrapper');
1762
- const drawWrapper = document.getElementById('drawWrapper');
1763
- const shapesWrapper = document.getElementById('shapesWrapper');
1764
-
1765
- function closeAllDropdowns() {
1766
- highlightDropdown.classList.remove('visible');
1767
- drawDropdown.classList.remove('visible');
1768
- shapesDropdown.classList.remove('visible');
1769
- }
1770
-
1771
- function toggleDropdown(dropdown, e) {
1772
- e.stopPropagation();
1773
- const isVisible = dropdown.classList.contains('visible');
1774
- closeAllDropdowns();
1775
- if (!isVisible) {
1776
- dropdown.classList.add('visible');
1777
- }
1778
- }
1779
-
1780
- // Arrow buttons toggle dropdowns
1781
- document.getElementById('highlightArrow').onclick = (e) => toggleDropdown(highlightDropdown, e);
1782
- document.getElementById('drawArrow').onclick = (e) => toggleDropdown(drawDropdown, e);
1783
- document.getElementById('shapesArrow').onclick = (e) => toggleDropdown(shapesDropdown, e);
1784
-
1785
- // Close dropdowns when clicking outside
1786
- document.addEventListener('click', (e) => {
1787
- if (!e.target.closest('.toolDropdown') && !e.target.closest('.dropdownArrow')) {
1788
- closeAllDropdowns();
1789
- }
1790
- });
1791
-
1792
- // Prevent dropdown from closing when clicking inside
1793
- highlightDropdown.onclick = (e) => e.stopPropagation();
1794
- drawDropdown.onclick = (e) => e.stopPropagation();
1795
- shapesDropdown.onclick = (e) => e.stopPropagation();
1796
-
1797
- // Drawing Tools - Toggle Behavior
1798
- async function setTool(tool) {
1799
- // If same tool clicked again, deactivate
1800
- if (currentTool === tool) {
1801
- currentTool = null;
1802
- annotationMode = false;
1803
- document.querySelectorAll('.annotationLayer').forEach(el => el.classList.remove('active'));
1804
- } else {
1805
- currentTool = tool;
1806
- annotationMode = true;
1807
-
1808
- // Set color and width based on tool
1809
- if (tool === 'highlight') {
1810
- currentColor = highlightColor;
1811
- currentWidth = highlightWidth;
1812
- } else if (tool === 'pen') {
1813
- currentColor = drawColor;
1814
- currentWidth = drawWidth;
1815
- } else if (tool === 'shape') {
1816
- currentColor = shapeColor;
1817
- currentWidth = shapeWidth;
1818
- }
1819
-
1820
- // BUGFIX: Save current annotation state BEFORE re-injecting layers
1821
- // This prevents deleted content from being restored when switching tools
1822
- for (let i = 0; i < pdfViewer.pagesCount; i++) {
1823
- const pageView = pdfViewer.getPageView(i);
1824
- const svg = pageView?.div?.querySelector('.annotationLayer');
1825
- if (svg) {
1826
- const pageNum = i + 1;
1827
- if (svg.innerHTML.trim()) {
1828
- annotationsStore.set(pageNum, svg.innerHTML);
1829
- } else {
1830
- // Clear from store if empty (all content deleted)
1831
- annotationsStore.delete(pageNum);
1832
- }
1833
- }
1834
- }
1835
-
1836
- // Inject annotation layers (await all)
1837
- const promises = [];
1838
- for (let i = 0; i < pdfViewer.pagesCount; i++) {
1839
- const pageView = pdfViewer.getPageView(i);
1840
- if (pageView?.div) {
1841
- promises.push(injectAnnotationLayer(i + 1));
1842
- }
1843
- }
1844
- await Promise.all(promises);
1845
- }
1846
-
1847
- // Update button states
1848
- highlightWrapper.classList.toggle('active', currentTool === 'highlight');
1849
- drawWrapper.classList.toggle('active', currentTool === 'pen');
1850
- shapesWrapper.classList.toggle('active', currentTool === 'shape');
1851
- document.getElementById('eraserBtn').classList.toggle('active', currentTool === 'eraser');
1852
- document.getElementById('textBtn').classList.toggle('active', currentTool === 'text');
1853
- document.getElementById('selectBtn').classList.toggle('active', currentTool === 'select');
1854
-
1855
- // Toggle select-mode class on annotation layers
1856
- document.querySelectorAll('.annotationLayer').forEach(layer => {
1857
- layer.classList.toggle('select-mode', currentTool === 'select');
1858
- });
1859
-
1860
- // Clear selection when switching tools
1861
- if (currentTool !== 'select') {
1862
- clearAnnotationSelection();
1863
- }
1864
- }
1865
-
1866
- document.getElementById('drawBtn').onclick = () => setTool('pen');
1867
- document.getElementById('highlightBtn').onclick = () => setTool('highlight');
1868
- document.getElementById('shapesBtn').onclick = () => setTool('shape');
1869
- document.getElementById('eraserBtn').onclick = () => setTool('eraser');
1870
- document.getElementById('textBtn').onclick = () => setTool('text');
1871
- document.getElementById('selectBtn').onclick = () => setTool('select');
1872
-
1873
- // Highlighter Colors
1874
- document.querySelectorAll('#highlightColors .colorDot').forEach(dot => {
1875
- dot.onclick = (e) => {
1876
- e.stopPropagation();
1877
- document.querySelectorAll('#highlightColors .colorDot').forEach(d => d.classList.remove('active'));
1878
- dot.classList.add('active');
1879
- highlightColor = dot.dataset.color;
1880
- if (currentTool === 'highlight') currentColor = highlightColor;
1881
- // Update preview
1882
- document.getElementById('highlightWave').setAttribute('stroke', highlightColor);
1883
- };
1884
- });
1885
-
1886
- // Pen Colors
1887
- document.querySelectorAll('#drawColors .colorDot').forEach(dot => {
1888
- dot.onclick = (e) => {
1889
- e.stopPropagation();
1890
- document.querySelectorAll('#drawColors .colorDot').forEach(d => d.classList.remove('active'));
1891
- dot.classList.add('active');
1892
- drawColor = dot.dataset.color;
1893
- if (currentTool === 'pen') currentColor = drawColor;
1894
- // Update preview
1895
- document.getElementById('drawWave').setAttribute('stroke', drawColor);
1896
- };
1897
- });
1898
-
1899
- // Highlighter Thickness Slider
1900
- document.getElementById('highlightThickness').oninput = (e) => {
1901
- highlightWidth = parseInt(e.target.value);
1902
- if (currentTool === 'highlight') currentWidth = highlightWidth;
1903
- // Update preview - highlighter uses width * 2 for display
1904
- document.getElementById('highlightWave').setAttribute('stroke-width', highlightWidth * 2);
1905
- };
1906
-
1907
- // Pen Thickness Slider
1908
- document.getElementById('drawThickness').oninput = (e) => {
1909
- drawWidth = parseInt(e.target.value);
1910
- if (currentTool === 'pen') currentWidth = drawWidth;
1911
- // Update preview
1912
- document.getElementById('drawWave').setAttribute('stroke-width', drawWidth);
1913
- };
1914
-
1915
- // Shape Selection
1916
- document.querySelectorAll('.shapeBtn').forEach(btn => {
1917
- btn.onclick = (e) => {
1918
- e.stopPropagation();
1919
- document.querySelectorAll('.shapeBtn').forEach(b => b.classList.remove('active'));
1920
- btn.classList.add('active');
1921
- currentShape = btn.dataset.shape;
1922
- };
1923
- });
1924
-
1925
- // Shape Colors
1926
- document.querySelectorAll('#shapeColors .colorDot').forEach(dot => {
1927
- dot.onclick = (e) => {
1928
- e.stopPropagation();
1929
- document.querySelectorAll('#shapeColors .colorDot').forEach(d => d.classList.remove('active'));
1930
- dot.classList.add('active');
1931
- shapeColor = dot.dataset.color;
1932
- if (currentTool === 'shape') currentColor = shapeColor;
1933
- };
1934
- });
1935
-
1936
- // Shape Thickness Slider
1937
- document.getElementById('shapeThickness').oninput = (e) => {
1938
- shapeWidth = parseInt(e.target.value);
1939
- if (currentTool === 'shape') currentWidth = shapeWidth;
1940
- };
1941
-
1942
- // Annotation Layer with Persistence
1943
- async function injectAnnotationLayer(pageNum) {
1944
- const pageView = pdfViewer.getPageView(pageNum - 1);
1945
- if (!pageView?.div) return;
1946
-
1947
- // Remove old SVG if exists (may have stale reference)
1948
- const oldSvg = pageView.div.querySelector('.annotationLayer');
1949
- if (oldSvg) oldSvg.remove();
1950
-
1951
- // Get or calculate base dimensions (scale=1.0) - FIXED reference
1952
- let baseDims = pageBaseDimensions.get(pageNum);
1953
- if (!baseDims) {
1954
- const page = await pdfDoc.getPage(pageNum);
1955
- const baseViewport = page.getViewport({ scale: 1.0 });
1956
- baseDims = { width: baseViewport.width, height: baseViewport.height };
1957
- pageBaseDimensions.set(pageNum, baseDims);
1958
- }
1959
-
1960
- // Create fresh SVG with FIXED viewBox (always scale=1.0 dimensions)
1961
- const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
1962
- svg.setAttribute('class', 'annotationLayer');
1963
- svg.setAttribute('viewBox', `0 0 ${baseDims.width} ${baseDims.height}`);
1964
- svg.setAttribute('preserveAspectRatio', 'none');
1965
- svg.style.width = '100%';
1966
- svg.style.height = '100%';
1967
- svg.dataset.page = pageNum;
1968
- svg.dataset.viewboxWidth = baseDims.width;
1969
- svg.dataset.viewboxHeight = baseDims.height;
1970
- svg.dataset.viewboxHeight = baseDims.height;
1971
- pageView.div.appendChild(svg);
1972
-
1973
-
1974
-
1975
- // Restore saved annotations for this page
1976
- if (annotationsStore.has(pageNum)) {
1977
- svg.innerHTML = annotationsStore.get(pageNum);
1978
- }
1979
-
1980
- svg.addEventListener('mousedown', (e) => startDraw(e, pageNum));
1981
- svg.addEventListener('mousemove', draw);
1982
- svg.addEventListener('mouseup', () => stopDraw(pageNum));
1983
- svg.addEventListener('mouseleave', () => stopDraw(pageNum));
1984
-
1985
- // Touch support for tablets
1986
- svg.addEventListener('touchstart', (e) => {
1987
- // Prevent default to avoid scroll while drawing/selecting
1988
- if (currentTool) e.preventDefault();
1989
- startDraw(e, pageNum);
1990
- }, { passive: false });
1991
- svg.addEventListener('touchmove', (e) => {
1992
- if (currentTool) e.preventDefault();
1993
- draw(e);
1994
- }, { passive: false });
1995
- svg.addEventListener('touchend', () => stopDraw(pageNum));
1996
- svg.addEventListener('touchcancel', () => stopDraw(pageNum));
1997
-
1998
- svg.classList.toggle('active', annotationMode);
1999
- }
2000
-
2001
- // Save annotations for a page
2002
- function saveAnnotations(pageNum) {
2003
- const pageView = pdfViewer.getPageView(pageNum - 1);
2004
- const svg = pageView?.div?.querySelector('.annotationLayer');
2005
- if (svg && svg.innerHTML.trim()) {
2006
- annotationsStore.set(pageNum, svg.innerHTML);
2007
- }
2008
- }
2009
-
2010
- function startDraw(e, pageNum) {
2011
- if (!annotationMode || !currentTool) return;
2012
-
2013
- e.preventDefault(); // Prevent text selection
2014
-
2015
- const svg = e.currentTarget;
2016
- if (!svg || !svg.dataset.viewboxWidth) return; // Defensive check
2017
-
2018
- // Handle select tool separately
2019
- if (currentTool === 'select') {
2020
- if (handleSelectMouseDown(e, svg, pageNum)) {
2021
- return; // Select tool handled the event
2022
- }
2023
- }
2024
-
2025
- isDrawing = true;
2026
- currentDrawingPage = pageNum;
2027
- currentSvg = svg; // Store reference
2028
-
2029
- const rect = svg.getBoundingClientRect();
2030
-
2031
- // Convert screen coords to viewBox coords
2032
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2033
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2034
- const scaleX = viewBoxWidth / rect.width;
2035
- const scaleY = viewBoxHeight / rect.height;
2036
-
2037
- // Get coordinates from mouse or touch event
2038
- const coords = getEventCoords(e);
2039
- const x = (coords.clientX - rect.left) * scaleX;
2040
- const y = (coords.clientY - rect.top) * scaleY;
2041
-
2042
- if (currentTool === 'eraser') {
2043
- eraseAt(svg, x, y, scaleX);
2044
- saveAnnotations(pageNum);
2045
- return;
2046
- }
2047
-
2048
- // Text tool - create/edit/drag text
2049
- if (currentTool === 'text') {
2050
- // Check if clicked on existing text element
2051
- const elementsUnderClick = document.elementsFromPoint(e.clientX, e.clientY);
2052
- const existingText = elementsUnderClick.find(el => el.tagName === 'text' && el.closest('.annotationLayer'));
2053
-
2054
- if (existingText) {
2055
- // Start dragging (double-click will edit via separate handler)
2056
- startTextDrag(e, existingText, svg, scaleX, scaleY, pageNum);
2057
- } else {
2058
- // Create new text
2059
- showTextEditor(e.clientX, e.clientY, svg, x, y, scaleX, pageNum);
2060
- }
2061
- return;
2062
- }
2063
-
2064
- // Shape tool - create shapes
2065
- if (currentTool === 'shape') {
2066
- isDrawing = true;
2067
- // Store start position for shape drawing
2068
- svg.dataset.shapeStartX = x;
2069
- svg.dataset.shapeStartY = y;
2070
- svg.dataset.shapeScaleX = scaleX;
2071
- svg.dataset.shapeScaleY = scaleY;
2072
-
2073
- let shapeEl;
2074
- if (currentShape === 'rectangle') {
2075
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
2076
- shapeEl.setAttribute('x', x);
2077
- shapeEl.setAttribute('y', y);
2078
- shapeEl.setAttribute('width', 0);
2079
- shapeEl.setAttribute('height', 0);
2080
- } else if (currentShape === 'circle') {
2081
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'ellipse');
2082
- shapeEl.setAttribute('cx', x);
2083
- shapeEl.setAttribute('cy', y);
2084
- shapeEl.setAttribute('rx', 0);
2085
- shapeEl.setAttribute('ry', 0);
2086
- } else if (currentShape === 'line' || currentShape === 'arrow') {
2087
- shapeEl = document.createElementNS('http://www.w3.org/2000/svg', 'line');
2088
- shapeEl.setAttribute('x1', x);
2089
- shapeEl.setAttribute('y1', y);
2090
- shapeEl.setAttribute('x2', x);
2091
- shapeEl.setAttribute('y2', y);
2092
- }
2093
-
2094
- shapeEl.setAttribute('stroke', currentColor);
2095
- shapeEl.setAttribute('stroke-width', currentWidth * scaleX);
2096
- shapeEl.setAttribute('fill', 'none');
2097
- shapeEl.classList.add('current-shape');
2098
- svg.appendChild(shapeEl);
2099
- return;
2100
- }
2101
-
2102
- currentPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2103
- currentPath.setAttribute('stroke', currentColor);
2104
- currentPath.setAttribute('fill', 'none');
2105
-
2106
- if (currentTool === 'highlight') {
2107
- // Highlighter uses stroke size * 5 for thicker strokes
2108
- currentPath.setAttribute('stroke-width', String(currentWidth * 5 * scaleX));
2109
- currentPath.setAttribute('stroke-opacity', '0.35');
2110
- } else {
2111
- currentPath.setAttribute('stroke-width', String(currentWidth * scaleX));
2112
- currentPath.setAttribute('stroke-opacity', '1');
2113
- }
2114
-
2115
- currentPath.setAttribute('d', `M${x.toFixed(2)},${y.toFixed(2)}`);
2116
- svg.appendChild(currentPath);
2117
- }
2118
-
2119
- function draw(e) {
2120
- if (!isDrawing || !currentSvg) return;
2121
-
2122
- e.preventDefault(); // Prevent text selection
2123
-
2124
- const svg = currentSvg; // Use stored reference
2125
- if (!svg || !svg.dataset.viewboxWidth) return;
2126
-
2127
- const rect = svg.getBoundingClientRect();
2128
-
2129
- // Convert screen coords to viewBox coords
2130
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2131
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2132
- const scaleX = viewBoxWidth / rect.width;
2133
- const scaleY = viewBoxHeight / rect.height;
2134
-
2135
- // Get coordinates from mouse or touch event
2136
- const coords = getEventCoords(e);
2137
- const x = (coords.clientX - rect.left) * scaleX;
2138
- const y = (coords.clientY - rect.top) * scaleY;
2139
-
2140
- if (currentTool === 'eraser') {
2141
- eraseAt(svg, x, y, scaleX);
2142
- return;
2143
- }
2144
-
2145
- // Shape tool - update shape size
2146
- if (currentTool === 'shape') {
2147
- const shapeEl = svg.querySelector('.current-shape');
2148
- if (!shapeEl) return;
2149
-
2150
- const startX = parseFloat(svg.dataset.shapeStartX);
2151
- const startY = parseFloat(svg.dataset.shapeStartY);
2152
-
2153
- if (currentShape === 'rectangle') {
2154
- const width = Math.abs(x - startX);
2155
- const height = Math.abs(y - startY);
2156
- shapeEl.setAttribute('x', Math.min(x, startX));
2157
- shapeEl.setAttribute('y', Math.min(y, startY));
2158
- shapeEl.setAttribute('width', width);
2159
- shapeEl.setAttribute('height', height);
2160
- } else if (currentShape === 'circle') {
2161
- const rx = Math.abs(x - startX) / 2;
2162
- const ry = Math.abs(y - startY) / 2;
2163
- shapeEl.setAttribute('cx', (startX + x) / 2);
2164
- shapeEl.setAttribute('cy', (startY + y) / 2);
2165
- shapeEl.setAttribute('rx', rx);
2166
- shapeEl.setAttribute('ry', ry);
2167
- } else if (currentShape === 'line' || currentShape === 'arrow' || currentShape === 'callout') {
2168
- shapeEl.setAttribute('x2', x);
2169
- shapeEl.setAttribute('y2', y);
2170
- }
2171
- return;
2172
- }
2173
-
2174
- if (currentPath) {
2175
- currentPath.setAttribute('d', currentPath.getAttribute('d') + ` L${x.toFixed(2)},${y.toFixed(2)}`);
2176
- }
2177
- }
2178
-
2179
- function stopDraw(pageNum) {
2180
- // Handle arrow marker
2181
- if (currentTool === 'shape' && currentShape === 'arrow' && currentSvg) {
2182
- const shapeEl = currentSvg.querySelector('.current-shape');
2183
- if (shapeEl && shapeEl.tagName === 'line') {
2184
- // Create arrow head as a group
2185
- const x1 = parseFloat(shapeEl.getAttribute('x1'));
2186
- const y1 = parseFloat(shapeEl.getAttribute('y1'));
2187
- const x2 = parseFloat(shapeEl.getAttribute('x2'));
2188
- const y2 = parseFloat(shapeEl.getAttribute('y2'));
2189
-
2190
- // Calculate arrow head
2191
- const angle = Math.atan2(y2 - y1, x2 - x1);
2192
- const headLength = 15 * parseFloat(currentSvg.dataset.shapeScaleX || 1);
2193
-
2194
- const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2195
- const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
2196
- const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
2197
- const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
2198
- const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
2199
-
2200
- arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
2201
- arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
2202
- arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
2203
- arrowHead.setAttribute('fill', 'none');
2204
- currentSvg.appendChild(arrowHead);
2205
- }
2206
- }
2207
-
2208
- // Handle callout - arrow with text at the start, pointing to end
2209
- // UX: Click where you want text box, drag to point at something
2210
- if (currentTool === 'shape' && currentShape === 'callout' && currentSvg) {
2211
- const shapeEl = currentSvg.querySelector('.current-shape');
2212
- if (shapeEl && shapeEl.tagName === 'line') {
2213
- const x1 = parseFloat(shapeEl.getAttribute('x1')); // Start - where text box goes
2214
- const y1 = parseFloat(shapeEl.getAttribute('y1'));
2215
- const x2 = parseFloat(shapeEl.getAttribute('x2')); // End - where arrow points
2216
- const y2 = parseFloat(shapeEl.getAttribute('y2'));
2217
-
2218
- // Only create callout if line has been drawn (not just a click)
2219
- if (Math.abs(x2 - x1) > 5 || Math.abs(y2 - y1) > 5) {
2220
- const scaleX = parseFloat(currentSvg.dataset.shapeScaleX || 1);
2221
-
2222
- // Arrow head points TO the end (x2,y2) - where user wants to point at
2223
- const angle = Math.atan2(y2 - y1, x2 - x1);
2224
- const headLength = 12 * scaleX;
2225
-
2226
- const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'path');
2227
- const p1x = x2 - headLength * Math.cos(angle - Math.PI / 6);
2228
- const p1y = y2 - headLength * Math.sin(angle - Math.PI / 6);
2229
- const p2x = x2 - headLength * Math.cos(angle + Math.PI / 6);
2230
- const p2y = y2 - headLength * Math.sin(angle + Math.PI / 6);
2231
-
2232
- arrowHead.setAttribute('d', `M${x2},${y2} L${p1x},${p1y} M${x2},${y2} L${p2x},${p2y}`);
2233
- arrowHead.setAttribute('stroke', shapeEl.getAttribute('stroke'));
2234
- arrowHead.setAttribute('stroke-width', shapeEl.getAttribute('stroke-width'));
2235
- arrowHead.setAttribute('fill', 'none');
2236
- arrowHead.classList.add('callout-arrow');
2237
- currentSvg.appendChild(arrowHead);
2238
-
2239
- // Store references for text editor
2240
- const svg = currentSvg;
2241
- const currentPageNum = currentDrawingPage;
2242
- const arrowColor = shapeEl.getAttribute('stroke');
2243
-
2244
- // Calculate screen position for text editor at START of arrow (x1,y1)
2245
- // This is where the user clicked first - where they want the text
2246
- const rect = svg.getBoundingClientRect();
2247
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2248
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2249
- const screenX = rect.left + (x1 / viewBoxWidth) * rect.width;
2250
- const screenY = rect.top + (y1 / viewBoxHeight) * rect.height;
2251
-
2252
- // Remove the current-shape class before showing editor
2253
- shapeEl.classList.remove('current-shape');
2254
-
2255
- // Save first, then open text editor
2256
- saveAnnotations(currentPageNum);
2257
-
2258
- // Open text editor at the START of the arrow (where user clicked)
2259
- setTimeout(() => {
2260
- showTextEditor(screenX, screenY, svg, x1, y1, scaleX, currentPageNum, null, arrowColor);
2261
- }, 50);
2262
-
2263
- // Reset state
2264
- isDrawing = false;
2265
- currentPath = null;
2266
- currentSvg = null;
2267
- currentDrawingPage = null;
2268
- return; // Exit early, text editor will handle the rest
2269
- }
2270
- }
2271
- }
2272
-
2273
- // Remove the current-shape class
2274
- if (currentSvg) {
2275
- const shapeEl = currentSvg.querySelector('.current-shape');
2276
- if (shapeEl) shapeEl.classList.remove('current-shape');
2277
- }
2278
-
2279
- if (isDrawing && currentDrawingPage) {
2280
- saveAnnotations(currentDrawingPage);
2281
- }
2282
- isDrawing = false;
2283
- currentPath = null;
2284
- currentSvg = null;
2285
- currentDrawingPage = null;
2286
- }
2287
-
2288
- // Text Drag-and-Drop
2289
- let draggedText = null;
2290
- let dragStartX = 0;
2291
- let dragStartY = 0;
2292
- let textOriginalX = 0;
2293
- let textOriginalY = 0;
2294
- let hasDragged = false;
2295
-
2296
- function startTextDrag(e, textEl, svg, scaleX, scaleY, pageNum) {
2297
- e.preventDefault();
2298
- e.stopPropagation();
2299
-
2300
- draggedText = textEl;
2301
- textEl.classList.add('dragging');
2302
- hasDragged = false;
2303
-
2304
- const rect = svg.getBoundingClientRect();
2305
- dragStartX = e.clientX;
2306
- dragStartY = e.clientY;
2307
- textOriginalX = parseFloat(textEl.getAttribute('x'));
2308
- textOriginalY = parseFloat(textEl.getAttribute('y'));
2309
-
2310
- function onMouseMove(ev) {
2311
- const dx = (ev.clientX - dragStartX) * scaleX;
2312
- const dy = (ev.clientY - dragStartY) * scaleY;
2313
-
2314
- if (Math.abs(dx) > 2 || Math.abs(dy) > 2) {
2315
- hasDragged = true;
2316
- }
2317
-
2318
- textEl.setAttribute('x', (textOriginalX + dx).toFixed(2));
2319
- textEl.setAttribute('y', (textOriginalY + dy).toFixed(2));
2320
- }
2321
-
2322
- function onMouseUp(ev) {
2323
- document.removeEventListener('mousemove', onMouseMove);
2324
- document.removeEventListener('mouseup', onMouseUp);
2325
- textEl.classList.remove('dragging');
2326
-
2327
- if (hasDragged) {
2328
- // Moved - save position
2329
- saveAnnotations(pageNum);
2330
- } else {
2331
- // Not moved - short click = edit
2332
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2333
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2334
- const svgX = parseFloat(textEl.getAttribute('x'));
2335
- const svgY = parseFloat(textEl.getAttribute('y'));
2336
- // Note: showTextEditor needs scaleX for font scaling logic, which we still have from arguments
2337
- showTextEditor(ev.clientX, ev.clientY, svg, svgX, svgY, scaleX, pageNum, textEl);
2338
- }
2339
-
2340
- draggedText = null;
2341
- }
2342
-
2343
- document.addEventListener('mousemove', onMouseMove);
2344
- document.addEventListener('mouseup', onMouseUp);
2345
- }
2346
-
2347
- // Inline Text Editor
2348
- let textFontSize = 14;
2349
-
2350
- function showTextEditor(screenX, screenY, svg, svgX, svgY, scale, pageNum, existingTextEl = null, overrideColor = null) {
2351
- // Remove existing editor if any
2352
- const existingOverlay = document.querySelector('.textEditorOverlay');
2353
- if (existingOverlay) existingOverlay.remove();
2354
-
2355
- // Use override color (for callout) or current color
2356
- const textColor = overrideColor || currentColor;
2357
-
2358
- // If editing existing text, get its properties
2359
- let editingText = null;
2360
- if (existingTextEl && typeof existingTextEl === 'object' && existingTextEl.textContent !== undefined) {
2361
- editingText = existingTextEl.textContent;
2362
- textFontSize = parseFloat(existingTextEl.getAttribute('font-size')) / scale || 14;
2363
- }
2364
-
2365
- // Create overlay
2366
- const overlay = document.createElement('div');
2367
- overlay.className = 'textEditorOverlay';
2368
-
2369
- // Create editor box
2370
- const box = document.createElement('div');
2371
- box.className = 'textEditorBox';
2372
- box.style.left = screenX + 'px';
2373
- box.style.top = screenY + 'px';
2374
-
2375
- // Input area
2376
- const input = document.createElement('div');
2377
- input.className = 'textEditorInput';
2378
- input.contentEditable = true;
2379
- input.style.color = textColor;
2380
- input.style.fontSize = textFontSize + 'px';
2381
- if (editingText) {
2382
- input.textContent = editingText;
2383
- }
2384
-
2385
- // Toolbar
2386
- const toolbar = document.createElement('div');
2387
- toolbar.className = 'textEditorToolbar';
2388
-
2389
- // Color indicator
2390
- const colorDot = document.createElement('div');
2391
- colorDot.className = 'textEditorColorDot active';
2392
- colorDot.style.background = textColor;
2393
-
2394
- // Font size decrease
2395
- const decreaseBtn = document.createElement('button');
2396
- decreaseBtn.className = 'textEditorBtn';
2397
- decreaseBtn.innerHTML = 'A<sup>-</sup>';
2398
- decreaseBtn.onclick = (e) => {
2399
- e.stopPropagation();
2400
- if (textFontSize > 10) {
2401
- textFontSize -= 2;
2402
- input.style.fontSize = textFontSize + 'px';
2403
- }
2404
- };
2405
-
2406
- // Font size increase
2407
- const increaseBtn = document.createElement('button');
2408
- increaseBtn.className = 'textEditorBtn';
2409
- increaseBtn.innerHTML = 'A<sup>+</sup>';
2410
- increaseBtn.onclick = (e) => {
2411
- e.stopPropagation();
2412
- if (textFontSize < 32) {
2413
- textFontSize += 2;
2414
- input.style.fontSize = textFontSize + 'px';
2415
- }
2416
- };
2417
-
2418
- // Delete button - also deletes existing element if editing
2419
- const deleteBtn = document.createElement('button');
2420
- deleteBtn.className = 'textEditorBtn delete';
2421
- deleteBtn.innerHTML = '🗑️';
2422
- deleteBtn.onclick = (e) => {
2423
- e.stopPropagation();
2424
- if (existingTextEl) {
2425
- existingTextEl.remove();
2426
- saveAnnotations(pageNum);
2427
- }
2428
- overlay.remove();
2429
- };
2430
-
2431
- toolbar.appendChild(colorDot);
2432
- toolbar.appendChild(decreaseBtn);
2433
- toolbar.appendChild(increaseBtn);
2434
- toolbar.appendChild(deleteBtn);
2435
-
2436
- box.appendChild(input);
2437
- box.appendChild(toolbar);
2438
- overlay.appendChild(box);
2439
- document.body.appendChild(overlay);
2440
-
2441
- // Focus input and select all if editing
2442
- setTimeout(() => {
2443
- input.focus();
2444
- if (editingText) {
2445
- const range = document.createRange();
2446
- range.selectNodeContents(input);
2447
- const sel = window.getSelection();
2448
- sel.removeAllRanges();
2449
- sel.addRange(range);
2450
- }
2451
- }, 50);
2452
-
2453
- // Confirm on click outside or Enter
2454
- function confirmText() {
2455
- const text = input.textContent.trim();
2456
- if (text) {
2457
- if (existingTextEl) {
2458
- // Update existing text element
2459
- existingTextEl.textContent = text;
2460
- existingTextEl.setAttribute('fill', textColor);
2461
- existingTextEl.setAttribute('font-size', String(textFontSize * scale));
2462
- } else {
2463
- // Create new text element
2464
- const textEl = document.createElementNS('http://www.w3.org/2000/svg', 'text');
2465
- textEl.setAttribute('x', svgX.toFixed(2));
2466
- textEl.setAttribute('y', svgY.toFixed(2));
2467
- textEl.setAttribute('fill', textColor);
2468
- textEl.setAttribute('font-size', String(textFontSize * scale));
2469
- textEl.setAttribute('font-family', 'Segoe UI, Arial, sans-serif');
2470
- textEl.textContent = text;
2471
- svg.appendChild(textEl);
2472
- }
2473
- saveAnnotations(pageNum);
2474
- } else if (existingTextEl) {
2475
- // Empty text = delete existing
2476
- existingTextEl.remove();
2477
- saveAnnotations(pageNum);
2478
- }
2479
- overlay.remove();
2480
- }
2481
-
2482
- overlay.addEventListener('click', (e) => {
2483
- if (e.target === overlay) confirmText();
2484
- });
2485
-
2486
- input.addEventListener('keydown', (e) => {
2487
- if (e.key === 'Enter' && !e.shiftKey) {
2488
- e.preventDefault();
2489
- confirmText();
2490
- }
2491
- if (e.key === 'Escape') {
2492
- overlay.remove();
2493
- }
2494
- });
2495
- }
2496
-
2497
- function eraseAt(svg, x, y, scale = 1) {
2498
- const hitRadius = 15 * scale; // Scale hit radius with viewBox
2499
- // Erase paths, text, and shape elements (rect, ellipse, line)
2500
- svg.querySelectorAll('path, text, rect, ellipse, line').forEach(el => {
2501
- const bbox = el.getBBox();
2502
- if (x >= bbox.x - hitRadius && x <= bbox.x + bbox.width + hitRadius &&
2503
- y >= bbox.y - hitRadius && y <= bbox.y + bbox.height + hitRadius) {
2504
- el.remove();
2505
- }
2506
- });
2507
-
2508
- // Also erase text highlights (in separate container)
2509
- const pageDiv = svg.closest('.page');
2510
- if (pageDiv) {
2511
- const highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2512
- if (highlightContainer) {
2513
- const pageRect = pageDiv.getBoundingClientRect();
2514
- const svgRect = svg.getBoundingClientRect();
2515
- // Convert viewBox coords to screen coords, then to percentages
2516
- const screenX = (x / scale) + svgRect.left - pageRect.left;
2517
- const screenY = (y / scale) + svgRect.top - pageRect.top;
2518
- const screenXPercent = (screenX / pageRect.width) * 100;
2519
- const screenYPercent = (screenY / pageRect.height) * 100;
2520
-
2521
- highlightContainer.querySelectorAll('.textHighlight').forEach(el => {
2522
- const left = parseFloat(el.style.left); // Already in %
2523
- const top = parseFloat(el.style.top);
2524
- const width = parseFloat(el.style.width);
2525
- const height = parseFloat(el.style.height);
2526
-
2527
- if (screenXPercent >= left - 2 && screenXPercent <= left + width + 2 &&
2528
- screenYPercent >= top - 2 && screenYPercent <= top + height + 2) {
2529
- el.remove();
2530
- // Save changes
2531
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2532
- saveTextHighlights(pageNum, pageDiv);
2533
- }
2534
- });
2535
- }
2536
- }
2537
- }
2538
-
2539
- // ==========================================
2540
- // TEXT SELECTION HIGHLIGHTING (Adobe/Edge style)
2541
- // ==========================================
2542
- let highlightPopup = null;
2543
-
2544
- function removeHighlightPopup() {
2545
- if (highlightPopup) {
2546
- highlightPopup.remove();
2547
- highlightPopup = null;
2548
- }
2549
- }
2550
-
2551
- function getSelectionRects() {
2552
- const selection = window.getSelection();
2553
- if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
2554
-
2555
- const range = selection.getRangeAt(0);
2556
- const rects = range.getClientRects();
2557
- if (rects.length === 0) return null;
2558
-
2559
- // Find which page the selection is in
2560
- const startNode = range.startContainer.parentElement;
2561
- const textLayer = startNode?.closest('.textLayer');
2562
- if (!textLayer) return null;
2563
-
2564
- const pageDiv = textLayer.closest('.page');
2565
- if (!pageDiv) return null;
2566
-
2567
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2568
- const pageRect = pageDiv.getBoundingClientRect();
2569
-
2570
- // Convert rects to page-relative coordinates
2571
- const relativeRects = [];
2572
- for (let i = 0; i < rects.length; i++) {
2573
- const rect = rects[i];
2574
- relativeRects.push({
2575
- x: rect.left - pageRect.left,
2576
- y: rect.top - pageRect.top,
2577
- width: rect.width,
2578
- height: rect.height
2579
- });
2580
- }
2581
-
2582
- return { pageNum, pageDiv, relativeRects, lastRect: rects[rects.length - 1] };
2583
- }
2584
-
2585
- function createTextHighlights(pageDiv, rects, color) {
2586
- // Find or create highlight container
2587
- let highlightContainer = pageDiv.querySelector('.textHighlightContainer');
2588
- if (!highlightContainer) {
2589
- highlightContainer = document.createElement('div');
2590
- highlightContainer.className = 'textHighlightContainer';
2591
- highlightContainer.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2592
- pageDiv.insertBefore(highlightContainer, pageDiv.firstChild);
2593
- }
2594
-
2595
- // Get page dimensions for percentage calculation
2596
- const pageRect = pageDiv.getBoundingClientRect();
2597
- const pageWidth = pageRect.width;
2598
- const pageHeight = pageRect.height;
2599
-
2600
- // Add highlight rectangles with percentage positioning
2601
- rects.forEach(rect => {
2602
- const div = document.createElement('div');
2603
- div.className = 'textHighlight';
2604
-
2605
- // Convert to percentages for zoom-independent positioning
2606
- const leftPercent = (rect.x / pageWidth) * 100;
2607
- const topPercent = (rect.y / pageHeight) * 100;
2608
- const widthPercent = (rect.width / pageWidth) * 100;
2609
- const heightPercent = (rect.height / pageHeight) * 100;
2610
-
2611
- div.style.cssText = `
2612
- left: ${leftPercent}%;
2613
- top: ${topPercent}%;
2614
- width: ${widthPercent}%;
2615
- height: ${heightPercent}%;
2616
- background: ${color};
2617
- opacity: 0.35;
2618
- `;
2619
- highlightContainer.appendChild(div);
2620
- });
2621
-
2622
- // Save to annotations store
2623
- const pageNum = parseInt(pageDiv.dataset.pageNumber);
2624
- saveTextHighlights(pageNum, pageDiv);
2625
- }
2626
-
2627
- function saveTextHighlights(pageNum, pageDiv) {
2628
- const container = pageDiv.querySelector('.textHighlightContainer');
2629
- if (container) {
2630
- const key = `textHighlight_${pageNum}`;
2631
- localStorage.setItem(key, container.innerHTML);
2632
- }
2633
- }
2634
-
2635
- function loadTextHighlights(pageNum, pageDiv) {
2636
- const key = `textHighlight_${pageNum}`;
2637
- const saved = localStorage.getItem(key);
2638
- if (saved) {
2639
- let container = pageDiv.querySelector('.textHighlightContainer');
2640
- if (!container) {
2641
- container = document.createElement('div');
2642
- container.className = 'textHighlightContainer';
2643
- container.style.cssText = 'position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5;';
2644
- pageDiv.insertBefore(container, pageDiv.firstChild);
2645
- }
2646
- container.innerHTML = saved;
2647
- }
2648
- }
2649
-
2650
- function showHighlightPopup(x, y, pageDiv, rects) {
2651
- removeHighlightPopup();
2652
-
2653
- highlightPopup = document.createElement('div');
2654
- highlightPopup.className = 'highlightPopup';
2655
- highlightPopup.style.left = x + 'px';
2656
- highlightPopup.style.top = (y + 10) + 'px';
2657
-
2658
- const colors = ['#fff100', '#16c60c', '#00b7c3', '#0078d4', '#886ce4', '#e81224'];
2659
- colors.forEach(color => {
2660
- const btn = document.createElement('button');
2661
- btn.style.background = color;
2662
- btn.title = 'Vurgula';
2663
- btn.onclick = (e) => {
2664
- e.stopPropagation();
2665
- createTextHighlights(pageDiv, rects, color);
2666
- window.getSelection().removeAllRanges();
2667
- removeHighlightPopup();
2668
- };
2669
- highlightPopup.appendChild(btn);
2670
- });
2671
-
2672
- document.body.appendChild(highlightPopup);
2673
- }
2674
-
2675
- // Listen for text selection
2676
- document.addEventListener('mouseup', (e) => {
2677
- // Small delay to let selection finalize
2678
- setTimeout(() => {
2679
- const selData = getSelectionRects();
2680
- if (selData && selData.relativeRects.length > 0) {
2681
- const lastRect = selData.lastRect;
2682
- showHighlightPopup(lastRect.right, lastRect.bottom, selData.pageDiv, selData.relativeRects);
2683
- } else {
2684
- removeHighlightPopup();
2685
- }
2686
- }, 10);
2687
- });
2688
-
2689
- // Remove popup on click elsewhere
2690
- document.addEventListener('mousedown', (e) => {
2691
- if (highlightPopup && !highlightPopup.contains(e.target)) {
2692
- removeHighlightPopup();
2693
- }
2694
- });
2695
-
2696
- // Load text highlights when pages render
2697
- eventBus.on('pagerendered', (evt) => {
2698
- const pageDiv = pdfViewer.getPageView(evt.pageNumber - 1)?.div;
2699
- if (pageDiv) {
2700
- loadTextHighlights(evt.pageNumber, pageDiv);
2701
- }
2702
- });
2703
-
2704
- // ==========================================
2705
- // SELECT/MOVE TOOL (Fixed + Touch Support)
2706
- // ==========================================
2707
- let selectedAnnotation = null;
2708
- let selectedSvg = null;
2709
- let selectedPageNum = null;
2710
- let copiedAnnotation = null;
2711
- let copiedPageNum = null;
2712
- let isDraggingAnnotation = false;
2713
- let annotationDragStartX = 0;
2714
- let annotationDragStartY = 0;
2715
-
2716
- // Create selection toolbar for touch devices
2717
- const selectionToolbar = document.createElement('div');
2718
- selectionToolbar.className = 'selection-toolbar';
2719
- selectionToolbar.innerHTML = `
2720
- <button data-action="copy" title="Kopyala (Ctrl+C)">
2721
- <svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
2722
- <span>Kopyala</span>
2723
- </button>
2724
- <button data-action="duplicate" title="Çoğalt">
2725
- <svg viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4v2z"/></svg>
2726
- <span>Çoğalt</span>
2727
- </button>
2728
- <button data-action="delete" class="delete" title="Sil (Del)">
2729
- <svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
2730
- <span>Sil</span>
2731
- </button>
2732
- `;
2733
- document.body.appendChild(selectionToolbar);
2734
-
2735
- // Selection toolbar event handlers
2736
- selectionToolbar.addEventListener('click', (e) => {
2737
- const btn = e.target.closest('button');
2738
- if (!btn) return;
2739
-
2740
- const action = btn.dataset.action;
2741
- if (action === 'copy') {
2742
- copySelectedAnnotation();
2743
- showToast('Kopyalandı!');
2744
- } else if (action === 'duplicate') {
2745
- copySelectedAnnotation();
2746
- pasteAnnotation();
2747
- showToast('Çoğaltıldı!');
2748
- } else if (action === 'delete') {
2749
- deleteSelectedAnnotation();
2750
- showToast('Silindi!');
2751
- }
2752
- });
2753
-
2754
- function showToast(message) {
2755
- const existingToast = document.querySelector('.toast-notification');
2756
- if (existingToast) existingToast.remove();
2757
-
2758
- const toast = document.createElement('div');
2759
- toast.className = 'toast-notification';
2760
- toast.textContent = message;
2761
- document.body.appendChild(toast);
2762
- setTimeout(() => toast.remove(), 2000);
2763
- }
2764
-
2765
- function updateSelectionToolbar() {
2766
- if (selectedAnnotation && currentTool === 'select') {
2767
- selectionToolbar.classList.add('visible');
2768
- } else {
2769
- selectionToolbar.classList.remove('visible');
2770
- }
2771
- }
2772
-
2773
- function clearAnnotationSelection() {
2774
- if (selectedAnnotation) {
2775
- selectedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2776
- }
2777
- selectedAnnotation = null;
2778
- selectedSvg = null;
2779
- selectedPageNum = null;
2780
- isDraggingAnnotation = false;
2781
- updateSelectionToolbar();
2782
- }
2783
-
2784
- function selectAnnotation(element, svg, pageNum) {
2785
- clearAnnotationSelection();
2786
- selectedAnnotation = element;
2787
- selectedSvg = svg;
2788
- selectedPageNum = pageNum;
2789
- element.classList.add('annotation-selected', 'just-selected');
2790
-
2791
- // Remove pulse animation after it completes
2792
- setTimeout(() => {
2793
- element.classList.remove('just-selected');
2794
- }, 600);
2795
-
2796
- updateSelectionToolbar();
2797
- }
2798
-
2799
- function deleteSelectedAnnotation() {
2800
- if (selectedAnnotation && selectedSvg) {
2801
- selectedAnnotation.remove();
2802
- saveAnnotations(selectedPageNum);
2803
- clearAnnotationSelection();
2804
- }
2805
- }
2806
-
2807
- function copySelectedAnnotation() {
2808
- if (selectedAnnotation) {
2809
- copiedAnnotation = selectedAnnotation.cloneNode(true);
2810
- copiedAnnotation.classList.remove('annotation-selected', 'annotation-dragging', 'just-selected');
2811
- copiedPageNum = selectedPageNum;
2812
- }
2813
- }
2814
-
2815
- function pasteAnnotation() {
2816
- if (!copiedAnnotation || !pdfViewer) return;
2817
-
2818
- // Paste to current page
2819
- const currentPage = pdfViewer.currentPageNumber;
2820
- const pageView = pdfViewer.getPageView(currentPage - 1);
2821
- const svg = pageView?.div?.querySelector('.annotationLayer');
2822
-
2823
- if (svg) {
2824
- const cloned = copiedAnnotation.cloneNode(true);
2825
- const offset = 30; // Offset amount for pasted elements
2826
-
2827
- // Offset pasted element slightly
2828
- if (cloned.tagName === 'path') {
2829
- // For paths, add/update transform translate
2830
- const currentTransform = cloned.getAttribute('transform') || '';
2831
- const match = currentTransform.match(/translate\(([^,]+),([^)]+)\)/);
2832
- let tx = offset, ty = offset;
2833
- if (match) {
2834
- tx = parseFloat(match[1]) + offset;
2835
- ty = parseFloat(match[2]) + offset;
2836
- }
2837
- cloned.setAttribute('transform', `translate(${tx}, ${ty})`);
2838
- } else if (cloned.tagName === 'rect') {
2839
- cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2840
- cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2841
- } else if (cloned.tagName === 'ellipse') {
2842
- cloned.setAttribute('cx', parseFloat(cloned.getAttribute('cx')) + offset);
2843
- cloned.setAttribute('cy', parseFloat(cloned.getAttribute('cy')) + offset);
2844
- } else if (cloned.tagName === 'line') {
2845
- cloned.setAttribute('x1', parseFloat(cloned.getAttribute('x1')) + offset);
2846
- cloned.setAttribute('y1', parseFloat(cloned.getAttribute('y1')) + offset);
2847
- cloned.setAttribute('x2', parseFloat(cloned.getAttribute('x2')) + offset);
2848
- cloned.setAttribute('y2', parseFloat(cloned.getAttribute('y2')) + offset);
2849
- } else if (cloned.tagName === 'text') {
2850
- cloned.setAttribute('x', parseFloat(cloned.getAttribute('x')) + offset);
2851
- cloned.setAttribute('y', parseFloat(cloned.getAttribute('y')) + offset);
2852
- }
2853
-
2854
- svg.appendChild(cloned);
2855
- saveAnnotations(currentPage);
2856
- selectAnnotation(cloned, svg, currentPage);
2857
- }
2858
- }
2859
-
2860
- // Get coordinates from mouse or touch event
2861
- function getEventCoords(e) {
2862
- if (e.touches && e.touches.length > 0) {
2863
- return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY };
2864
- }
2865
- if (e.changedTouches && e.changedTouches.length > 0) {
2866
- return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY };
2867
- }
2868
- return { clientX: e.clientX, clientY: e.clientY };
2869
- }
2870
-
2871
- // Handle select tool events (both mouse and touch)
2872
- function handleSelectPointerDown(e, svg, pageNum) {
2873
- if (currentTool !== 'select') return false;
2874
-
2875
- const coords = getEventCoords(e);
2876
- const target = e.target;
2877
-
2878
- if (target === svg || target.tagName === 'svg') {
2879
- // Clicked on empty area - deselect
2880
- clearAnnotationSelection();
2881
- return true;
2882
- }
2883
-
2884
- // Check if clicked on an annotation element
2885
- if (target.closest('.annotationLayer') && target !== svg) {
2886
- e.preventDefault();
2887
- e.stopPropagation();
2888
-
2889
- selectAnnotation(target, svg, pageNum);
2890
-
2891
- // Start drag
2892
- const rect = svg.getBoundingClientRect();
2893
- const viewBoxWidth = parseFloat(svg.dataset.viewboxWidth);
2894
- const viewBoxHeight = parseFloat(svg.dataset.viewboxHeight);
2895
- const scaleX = viewBoxWidth / rect.width;
2896
- const scaleY = viewBoxHeight / rect.height;
2897
-
2898
- isDraggingAnnotation = true;
2899
- annotationDragStartX = coords.clientX;
2900
- annotationDragStartY = coords.clientY;
2901
-
2902
- target.classList.add('annotation-dragging');
2903
-
2904
- function onMove(ev) {
2905
- if (!isDraggingAnnotation) return;
2906
- ev.preventDefault();
2907
-
2908
- const moveCoords = getEventCoords(ev);
2909
- const dx = (moveCoords.clientX - annotationDragStartX) * scaleX;
2910
- const dy = (moveCoords.clientY - annotationDragStartY) * scaleY;
2911
-
2912
- // Move the element
2913
- moveAnnotation(target, dx, dy);
2914
-
2915
- // Update start position for next move (CRITICAL FIX)
2916
- annotationDragStartX = moveCoords.clientX;
2917
- annotationDragStartY = moveCoords.clientY;
2918
- }
2919
-
2920
- function onEnd(ev) {
2921
- document.removeEventListener('mousemove', onMove);
2922
- document.removeEventListener('mouseup', onEnd);
2923
- document.removeEventListener('touchmove', onMove);
2924
- document.removeEventListener('touchend', onEnd);
2925
- document.removeEventListener('touchcancel', onEnd);
2926
-
2927
- target.classList.remove('annotation-dragging');
2928
- isDraggingAnnotation = false;
2929
- saveAnnotations(pageNum);
2930
- }
2931
-
2932
- document.addEventListener('mousemove', onMove, { passive: false });
2933
- document.addEventListener('mouseup', onEnd);
2934
- document.addEventListener('touchmove', onMove, { passive: false });
2935
- document.addEventListener('touchend', onEnd);
2936
- document.addEventListener('touchcancel', onEnd);
2937
-
2938
- return true;
2939
- }
2940
-
2941
- return false;
2942
- }
2943
-
2944
- // moveAnnotation - applies delta movement to an annotation element
2945
- function moveAnnotation(element, dx, dy) {
2946
- if (element.tagName === 'path') {
2947
- // Transform path using translate
2948
- const currentTransform = element.getAttribute('transform') || '';
2949
- const match = currentTransform.match(/translate\(([^,]+),\s*([^)]+)\)/);
2950
- let tx = 0, ty = 0;
2951
- if (match) {
2952
- tx = parseFloat(match[1]);
2953
- ty = parseFloat(match[2]);
2954
- }
2955
- element.setAttribute('transform', `translate(${tx + dx}, ${ty + dy})`);
2956
- } else if (element.tagName === 'rect') {
2957
- element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2958
- element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2959
- } else if (element.tagName === 'ellipse') {
2960
- element.setAttribute('cx', parseFloat(element.getAttribute('cx')) + dx);
2961
- element.setAttribute('cy', parseFloat(element.getAttribute('cy')) + dy);
2962
- } else if (element.tagName === 'line') {
2963
- element.setAttribute('x1', parseFloat(element.getAttribute('x1')) + dx);
2964
- element.setAttribute('y1', parseFloat(element.getAttribute('y1')) + dy);
2965
- element.setAttribute('x2', parseFloat(element.getAttribute('x2')) + dx);
2966
- element.setAttribute('y2', parseFloat(element.getAttribute('y2')) + dy);
2967
- } else if (element.tagName === 'text') {
2968
- element.setAttribute('x', parseFloat(element.getAttribute('x')) + dx);
2969
- element.setAttribute('y', parseFloat(element.getAttribute('y')) + dy);
2970
- }
2971
- }
2972
-
2973
- // Legacy function for backwards compatibility (used elsewhere)
2974
- function handleSelectMouseDown(e, svg, pageNum) {
2975
- return handleSelectPointerDown(e, svg, pageNum);
2976
- }
2977
-
2978
- // ==========================================
2979
- // KEYBOARD SHORTCUTS
2980
- // ==========================================
2981
- document.addEventListener('keydown', (e) => {
2982
- // Ignore if typing in input
2983
- if (e.target.tagName === 'INPUT' || e.target.contentEditable === 'true') return;
2984
-
2985
- const key = e.key.toLowerCase();
2986
-
2987
- // Tool shortcuts
2988
- if (key === 'h') { setTool('highlight'); e.preventDefault(); }
2989
- if (key === 'p') { setTool('pen'); e.preventDefault(); }
2990
- if (key === 'e') { setTool('eraser'); e.preventDefault(); }
2991
- if (key === 't') { setTool('text'); e.preventDefault(); }
2992
- if (key === 'r') { setTool('shape'); e.preventDefault(); }
2993
- if (key === 'v') { setTool('select'); e.preventDefault(); }
2994
-
2995
- // Delete selected annotation
2996
- if ((key === 'delete' || key === 'backspace') && selectedAnnotation) {
2997
- deleteSelectedAnnotation();
2998
- e.preventDefault();
2999
- }
3000
-
3001
- // Copy/Paste annotations
3002
- if ((e.ctrlKey || e.metaKey) && key === 'c' && selectedAnnotation) {
3003
- copySelectedAnnotation();
3004
- e.preventDefault();
3005
- }
3006
- if ((e.ctrlKey || e.metaKey) && key === 'v' && copiedAnnotation) {
3007
- pasteAnnotation();
3008
- e.preventDefault();
3009
- }
3010
-
3011
- // Navigation
3012
- if (key === 's') {
3013
- document.getElementById('sidebarBtn').click();
3014
- e.preventDefault();
3015
- }
3016
-
3017
- // Arrow key navigation
3018
- if (key === 'arrowleft' || key === 'arrowup') {
3019
- if (pdfViewer && pdfViewer.currentPageNumber > 1) {
3020
- pdfViewer.currentPageNumber--;
3021
- }
3022
- e.preventDefault();
3023
- }
3024
- if (key === 'arrowright' || key === 'arrowdown') {
3025
- if (pdfViewer && pdfViewer.currentPageNumber < pdfViewer.pagesCount) {
3026
- pdfViewer.currentPageNumber++;
3027
- }
3028
- e.preventDefault();
3029
- }
3030
-
3031
- // Home/End
3032
- if (key === 'home') {
3033
- if (pdfViewer) pdfViewer.currentPageNumber = 1;
3034
- e.preventDefault();
3035
- }
3036
- if (key === 'end') {
3037
- if (pdfViewer) pdfViewer.currentPageNumber = pdfViewer.pagesCount;
3038
- e.preventDefault();
3039
- }
3040
-
3041
- // Zoom shortcuts - prevent browser zoom
3042
- if ((e.ctrlKey || e.metaKey) && (key === '=' || key === '+' || e.code === 'Equal')) {
3043
- e.preventDefault();
3044
- e.stopPropagation();
3045
- pdfViewer.currentScale += 0.25;
3046
- return;
3047
- }
3048
- if ((e.ctrlKey || e.metaKey) && (key === '-' || e.code === 'Minus')) {
3049
- e.preventDefault();
3050
- e.stopPropagation();
3051
- pdfViewer.currentScale -= 0.25;
3052
- return;
3053
- }
3054
- if ((e.ctrlKey || e.metaKey) && (key === '0' || e.code === 'Digit0')) {
3055
- e.preventDefault();
3056
- e.stopPropagation();
3057
- pdfViewer.currentScaleValue = 'page-width';
3058
- return;
3059
- }
3060
-
3061
- // Escape to deselect tool
3062
- if (key === 'escape') {
3063
- if (currentTool) {
3064
- setTool(currentTool); // Toggle off
3065
- }
3066
- closeAllDropdowns();
3067
- }
3068
-
3069
- // Sepia mode
3070
- if (key === 'm') {
3071
- document.getElementById('sepiaBtn').click();
3072
- e.preventDefault();
3073
- }
3074
- });
3075
-
3076
- // ==========================================
3077
- // CONTEXT MENU (Right-click)
3078
- // ==========================================
3079
- const contextMenu = document.createElement('div');
3080
- contextMenu.className = 'contextMenu';
3081
- contextMenu.innerHTML = `
3082
- <div class="contextMenuItem" data-action="highlight">
3083
- <svg viewBox="0 0 24 24"><path d="M3 21h18v-2H3v2zM5 16h14l-3-10H8l-3 10z"/></svg>
3084
- Vurgula
3085
- <span class="shortcutHint">H</span>
3086
- </div>
3087
- <div class="contextMenuItem" data-action="pen">
3088
- <svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.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.83z"/></svg>
3089
- Kalem
3090
- <span class="shortcutHint">P</span>
3091
- </div>
3092
- <div class="contextMenuItem" data-action="text">
3093
- <svg viewBox="0 0 24 24"><path d="M5 4v3h5.5v12h3V7H19V4H5z"/></svg>
3094
- Metin Ekle
3095
- <span class="shortcutHint">T</span>
3096
- </div>
3097
- <div class="contextMenuDivider"></div>
3098
- <div class="contextMenuItem" data-action="zoomIn">
3099
- <svg viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
3100
- Yakınlaştır
3101
- <span class="shortcutHint">Ctrl++</span>
3102
- </div>
3103
- <div class="contextMenuItem" data-action="zoomOut">
3104
- <svg viewBox="0 0 24 24"><path d="M19 13H5v-2h14v2z"/></svg>
3105
- Uzaklaştır
3106
- <span class="shortcutHint">Ctrl+-</span>
3107
- </div>
3108
- <div class="contextMenuDivider"></div>
3109
- <div class="contextMenuItem" data-action="sepia">
3110
- <svg viewBox="0 0 24 24"><path 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 5z"/></svg>
3111
- Okuma Modu
3112
- <span class="shortcutHint">M</span>
3113
- </div>
3114
- `;
3115
- document.body.appendChild(contextMenu);
3116
-
3117
- // Show context menu on right-click in viewer
3118
- container.addEventListener('contextmenu', (e) => {
3119
- e.preventDefault();
3120
- contextMenu.style.left = e.clientX + 'px';
3121
- contextMenu.style.top = e.clientY + 'px';
3122
- contextMenu.classList.add('visible');
3123
- });
3124
-
3125
- // Hide context menu on click
3126
- document.addEventListener('click', () => {
3127
- contextMenu.classList.remove('visible');
3128
- });
3129
-
3130
- // Context menu actions
3131
- contextMenu.addEventListener('click', (e) => {
3132
- const item = e.target.closest('.contextMenuItem');
3133
- if (!item) return;
3134
-
3135
- const action = item.dataset.action;
3136
- switch (action) {
3137
- case 'highlight': setTool('highlight'); break;
3138
- case 'pen': setTool('pen'); break;
3139
- case 'text': setTool('text'); break;
3140
- case 'zoomIn': pdfViewer.currentScale += 0.25; break;
3141
- case 'zoomOut': pdfViewer.currentScale -= 0.25; break;
3142
- case 'sepia': document.getElementById('sepiaBtn').click(); break;
3143
- }
3144
- contextMenu.classList.remove('visible');
3145
- });
3146
-
3147
- // ==========================================
3148
- // ERGONOMIC FEATURES
3149
- // ==========================================
3150
-
3151
- // Double-click on page for fullscreen
3152
- let lastClickTime = 0;
3153
- container.addEventListener('click', (e) => {
3154
- const now = Date.now();
3155
- if (now - lastClickTime < 300) {
3156
- // Double click detected
3157
- if (document.fullscreenElement) {
3158
- document.exitFullscreen();
3159
- } else {
3160
- container.requestFullscreen().catch(() => { });
3161
- }
3162
- }
3163
- lastClickTime = now;
3164
- });
3165
-
3166
- // Mouse wheel zoom with Ctrl
3167
- container.addEventListener('wheel', (e) => {
3168
- if (e.ctrlKey) {
3169
- e.preventDefault();
3170
- if (e.deltaY < 0) {
3171
- pdfViewer.currentScale += 0.1;
3172
- } else {
3173
- pdfViewer.currentScale -= 0.1;
3174
- }
3175
- }
3176
- }, { passive: false });
3177
-
3178
- console.log('PDF Viewer Ready');
3179
- console.log('Keyboard Shortcuts: H=Highlight, P=Pen, E=Eraser, T=Text, R=Shapes, S=Sidebar, M=ReadingMode, Arrows=Navigate');
3180
-
3181
- // ==========================================
3182
- // SECURITY FEATURES
3183
- // ==========================================
3184
-
3185
- (function initSecurityFeatures() {
3186
- console.log('[Security] Initializing protection features...');
3187
-
3188
- // 1. Block dangerous keyboard shortcuts
3189
- document.addEventListener('keydown', function (e) {
3190
- // Ctrl+S (Save)
3191
- if (e.ctrlKey && e.key === 's') {
3192
- e.preventDefault();
3193
- console.log('[Security] Ctrl+S blocked');
3194
- return false;
3195
- }
3196
- // Ctrl+P (Print)
3197
- if (e.ctrlKey && e.key === 'p') {
3198
- e.preventDefault();
3199
- console.log('[Security] Ctrl+P blocked');
3200
- return false;
3201
- }
3202
- // Ctrl+Shift+S (Save As)
3203
- if (e.ctrlKey && e.shiftKey && e.key === 'S') {
3204
- e.preventDefault();
3205
- return false;
3206
- }
3207
- // F12 (DevTools)
3208
- if (e.key === 'F12') {
3209
- e.preventDefault();
3210
- console.log('[Security] F12 blocked');
3211
- return false;
3212
- }
3213
- // Ctrl+Shift+I (DevTools)
3214
- if (e.ctrlKey && e.shiftKey && e.key === 'I') {
3215
- e.preventDefault();
3216
- return false;
3217
- }
3218
- // Ctrl+Shift+J (Console)
3219
- if (e.ctrlKey && e.shiftKey && e.key === 'J') {
3220
- e.preventDefault();
3221
- return false;
3222
- }
3223
- // Ctrl+U (View Source)
3224
- if (e.ctrlKey && e.key === 'u') {
3225
- e.preventDefault();
3226
- return false;
3227
- }
3228
- // Ctrl+Shift+C (Inspect Element)
3229
- if (e.ctrlKey && e.shiftKey && e.key === 'C') {
3230
- e.preventDefault();
3231
- return false;
3232
- }
3233
- }, true);
3234
-
3235
- // 2. Block context menu (right-click) - EVERYWHERE
3236
- document.addEventListener('contextmenu', function (e) {
3237
- e.preventDefault();
3238
- e.stopPropagation();
3239
- return false;
3240
- }, true);
3241
-
3242
- // 3. Block copy/cut/paste
3243
- document.addEventListener('copy', function (e) {
3244
- e.preventDefault();
3245
- console.log('[Security] Copy blocked');
3246
- return false;
3247
- }, true);
3248
-
3249
- document.addEventListener('cut', function (e) {
3250
- e.preventDefault();
3251
- return false;
3252
- }, true);
3253
-
3254
- // 4. Block drag events (prevent dragging content out)
3255
- document.addEventListener('dragstart', function (e) {
3256
- e.preventDefault();
3257
- return false;
3258
- }, true);
3259
-
3260
- // 5. Block Print via window.print override
3261
- window.print = function () {
3262
- console.log('[Security] Print function blocked');
3263
- alert('Yazdırma bu belgede engellenmiştir.');
3264
- return false;
3265
- };
3266
-
3267
- // 6. Disable beforeprint event
3268
- window.addEventListener('beforeprint', function (e) {
3269
- e.preventDefault();
3270
- document.body.style.display = 'none';
3271
- });
3272
-
3273
- window.addEventListener('afterprint', function () {
3274
- document.body.style.display = '';
3275
- });
3276
-
3277
- // 7. Block screenshot keyboard shortcuts
3278
- document.addEventListener('keyup', function (e) {
3279
- // PrintScreen key
3280
- if (e.key === 'PrintScreen') {
3281
- navigator.clipboard.writeText('');
3282
- console.log('[Security] PrintScreen clipboard cleared');
3283
- }
3284
- }, true);
3285
-
3286
- // 8. Visibility change detection (tab switching for screenshots)
3287
- document.addEventListener('visibilitychange', function () {
3288
- if (document.hidden) {
3289
- console.log('[Security] Tab hidden');
3290
- }
3291
- });
3292
-
3293
- console.log('[Security] All protection features initialized');
3294
- })();
3295
-
3296
- // End of main IIFE - pdfDoc, pdfViewer not accessible from console
3297
- })();
3298
- </script>
370
+ <script defer src="viewer-app.js"></script>
3299
371
  </body>
3300
372
 
3301
- </html>
373
+ </html>