floatnote 1.0.0

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.
Files changed (69) hide show
  1. package/.beads/config.json +6 -0
  2. package/.beads/issues/floatnote-1.md +21 -0
  3. package/.beads/issues/floatnote-10.md +28 -0
  4. package/.beads/issues/floatnote-11.md +36 -0
  5. package/.beads/issues/floatnote-12.md +25 -0
  6. package/.beads/issues/floatnote-13.md +37 -0
  7. package/.beads/issues/floatnote-14.md +22 -0
  8. package/.beads/issues/floatnote-15.md +22 -0
  9. package/.beads/issues/floatnote-16.md +20 -0
  10. package/.beads/issues/floatnote-17.md +20 -0
  11. package/.beads/issues/floatnote-18.md +21 -0
  12. package/.beads/issues/floatnote-19.md +19 -0
  13. package/.beads/issues/floatnote-2.md +32 -0
  14. package/.beads/issues/floatnote-20.md +22 -0
  15. package/.beads/issues/floatnote-3.md +50 -0
  16. package/.beads/issues/floatnote-4.md +31 -0
  17. package/.beads/issues/floatnote-5.md +28 -0
  18. package/.beads/issues/floatnote-6.md +30 -0
  19. package/.beads/issues/floatnote-7.md +38 -0
  20. package/.beads/issues/floatnote-8.md +29 -0
  21. package/.beads/issues/floatnote-9.md +32 -0
  22. package/CLAUDE.md +61 -0
  23. package/README.md +95 -0
  24. package/bin/floatnote.js +218 -0
  25. package/coverage/base.css +224 -0
  26. package/coverage/bin/floatnote.js.html +739 -0
  27. package/coverage/bin/index.html +116 -0
  28. package/coverage/block-navigation.js +87 -0
  29. package/coverage/favicon.png +0 -0
  30. package/coverage/index.html +131 -0
  31. package/coverage/lcov-report/base.css +224 -0
  32. package/coverage/lcov-report/bin/floatnote.js.html +739 -0
  33. package/coverage/lcov-report/bin/index.html +116 -0
  34. package/coverage/lcov-report/block-navigation.js +87 -0
  35. package/coverage/lcov-report/favicon.png +0 -0
  36. package/coverage/lcov-report/index.html +131 -0
  37. package/coverage/lcov-report/prettify.css +1 -0
  38. package/coverage/lcov-report/prettify.js +2 -0
  39. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  40. package/coverage/lcov-report/sorter.js +210 -0
  41. package/coverage/lcov-report/src/index.html +146 -0
  42. package/coverage/lcov-report/src/main.js.html +1483 -0
  43. package/coverage/lcov-report/src/preload.js.html +361 -0
  44. package/coverage/lcov-report/src/renderer.js.html +8767 -0
  45. package/coverage/lcov.info +3273 -0
  46. package/coverage/prettify.css +1 -0
  47. package/coverage/prettify.js +2 -0
  48. package/coverage/sort-arrow-sprite.png +0 -0
  49. package/coverage/sorter.js +210 -0
  50. package/coverage/src/index.html +146 -0
  51. package/coverage/src/main.js.html +1483 -0
  52. package/coverage/src/preload.js.html +361 -0
  53. package/coverage/src/renderer.js.html +8767 -0
  54. package/jest.config.js +48 -0
  55. package/package.json +59 -0
  56. package/src/icon-template.png +0 -0
  57. package/src/index.html +296 -0
  58. package/src/main.js +494 -0
  59. package/src/preload.js +96 -0
  60. package/src/renderer.js +3203 -0
  61. package/src/styles.css +1448 -0
  62. package/tests/cli/floatnote.test.js +167 -0
  63. package/tests/main/main.test.js +287 -0
  64. package/tests/mocks/electron.js +126 -0
  65. package/tests/mocks/fs.js +17 -0
  66. package/tests/preload/preload.test.js +218 -0
  67. package/tests/renderer/history.test.js +234 -0
  68. package/tests/renderer/notes.test.js +262 -0
  69. package/tests/renderer/settings.test.js +178 -0
@@ -0,0 +1,3203 @@
1
+ // Glassboard Renderer Process
2
+ // Handles drawing canvas, text overlays, and UI interactions
3
+
4
+ class Glassboard {
5
+ constructor() {
6
+ this.app = document.getElementById('app');
7
+ this.canvas = document.getElementById('draw-canvas');
8
+ this.ctx = this.canvas.getContext('2d');
9
+ this.textContainer = document.getElementById('text-container');
10
+
11
+ // Multi-note system
12
+ this.notes = []; // Array of notes
13
+ this.currentNoteIndex = 0;
14
+
15
+ this.isDrawing = false;
16
+ this.isTextMode = false;
17
+ this.isSelectMode = false;
18
+ this.currentColor = '#ffffff';
19
+ this.currentStrokeWidth = 4;
20
+ this.currentLine = null;
21
+ this.selectedTextId = null;
22
+
23
+ // Object grouping state
24
+ this.currentObjectId = null;
25
+ this.lastStrokeTime = 0;
26
+ this.objectGroupTimeout = 500; // ms - strokes within this time are grouped
27
+
28
+ // Selection state for drawing objects
29
+ this.selectedObjectId = null;
30
+ this.isDraggingObject = false;
31
+ this.dragStartPoint = null;
32
+
33
+ // Zoom state
34
+ this.zoomLevel = 1;
35
+ this.minZoom = 0.25;
36
+ this.maxZoom = 4;
37
+
38
+ // Pan state
39
+ this.panX = 0;
40
+ this.panY = 0;
41
+
42
+ // Rotation state
43
+ this.rotation = 0;
44
+
45
+ // Settings
46
+ this.settings = {
47
+ pinchZoom: true,
48
+ pan: true,
49
+ rotate: true,
50
+ showZoomControls: true,
51
+ openWithCleanSlate: false,
52
+ activeBgMode: 'transparent',
53
+ inactiveBgMode: 'transparent',
54
+ activeOpacity: 100,
55
+ inactiveOpacity: 50,
56
+ autoSaveToFolder: false
57
+ };
58
+
59
+ // Selection rectangle state
60
+ this.isSelecting = false;
61
+ this.selectionStart = null;
62
+ this.selectionRect = document.getElementById('selection-rect');
63
+
64
+ // Multi-selection state
65
+ this.multiSelectedObjects = [];
66
+
67
+ // Clipboard state (internal)
68
+ this.clipboard = null; // { type: 'object' | 'text', data: ... }
69
+
70
+ // System clipboard state
71
+ this.systemClipboard = null; // Content from system clipboard
72
+ this.pasteOverlayVisible = false;
73
+
74
+ // Undo/redo history
75
+ this.history = [];
76
+ this.historyIndex = -1;
77
+ this.maxHistorySize = 50;
78
+ this.isUndoRedoAction = false;
79
+
80
+ // Auto-save debounce
81
+ this.saveTimeout = null;
82
+
83
+ // Select all state
84
+ this.allSelected = false;
85
+ this.selectedObjects = [];
86
+
87
+ // Double command key tracking
88
+ this.lastRightCommandTime = 0;
89
+
90
+ this.init();
91
+ }
92
+
93
+ init() {
94
+ this.setupCanvas();
95
+ this.setupToolbar();
96
+ this.setupDrawing();
97
+ this.setupTextMode();
98
+ this.setupKeyboardShortcuts();
99
+ this.setupFocusHandling();
100
+ this.setupResize();
101
+ this.setupZoom();
102
+ this.setupContextMenu();
103
+ this.setupClipboardPaste();
104
+ this.setupSettings();
105
+ this.setupGestures();
106
+ this.setupFileDrop();
107
+
108
+ // Initialize with empty note if needed
109
+ if (this.notes.length === 0) {
110
+ this.notes.push(this.createEmptyNote());
111
+ }
112
+
113
+ // Load saved data
114
+ this.loadSavedData();
115
+
116
+ // Setup left edge resize
117
+ this.setupLeftResize();
118
+
119
+ // Setup pagination controls
120
+ this.setupPagination();
121
+
122
+ // Setup window toggle handler for clean slate
123
+ this.setupWindowToggleHandler();
124
+ }
125
+
126
+ // Note management - getters for current note's data
127
+ get lines() {
128
+ return this.notes[this.currentNoteIndex]?.lines || [];
129
+ }
130
+ set lines(value) {
131
+ if (this.notes[this.currentNoteIndex]) {
132
+ this.notes[this.currentNoteIndex].lines = value;
133
+ }
134
+ }
135
+
136
+ get textItems() {
137
+ return this.notes[this.currentNoteIndex]?.textItems || [];
138
+ }
139
+ set textItems(value) {
140
+ if (this.notes[this.currentNoteIndex]) {
141
+ this.notes[this.currentNoteIndex].textItems = value;
142
+ }
143
+ }
144
+
145
+ get images() {
146
+ return this.notes[this.currentNoteIndex]?.images || [];
147
+ }
148
+ set images(value) {
149
+ if (this.notes[this.currentNoteIndex]) {
150
+ this.notes[this.currentNoteIndex].images = value;
151
+ }
152
+ }
153
+
154
+ get attachments() {
155
+ return this.notes[this.currentNoteIndex]?.attachments || [];
156
+ }
157
+ set attachments(value) {
158
+ if (this.notes[this.currentNoteIndex]) {
159
+ this.notes[this.currentNoteIndex].attachments = value;
160
+ }
161
+ }
162
+
163
+ createEmptyNote() {
164
+ // Get viewport dimensions for origin calculation
165
+ const rect = this.canvas?.getBoundingClientRect() || { width: 800, height: 600 };
166
+ const zoom = this.zoomLevel || 1;
167
+ return {
168
+ id: Date.now().toString(),
169
+ lines: [],
170
+ textItems: [],
171
+ images: [],
172
+ attachments: [],
173
+ // Store the origin point in content-space (center of viewport when note was created)
174
+ originX: (rect.width / 2) / zoom,
175
+ originY: (rect.height / 2) / zoom,
176
+ createdAt: Date.now(),
177
+ lastModified: Date.now()
178
+ };
179
+ }
180
+
181
+ // Navigate to previous note
182
+ previousNote() {
183
+ if (this.currentNoteIndex > 0) {
184
+ this.saveCurrentNoteState();
185
+ this.currentNoteIndex--;
186
+ this.loadCurrentNote();
187
+ this.updateNoteIndicator();
188
+ }
189
+ }
190
+
191
+ // Navigate to next note or create new one
192
+ nextNote() {
193
+ this.saveCurrentNoteState();
194
+
195
+ if (this.currentNoteIndex < this.notes.length - 1) {
196
+ // Go to existing next note
197
+ this.currentNoteIndex++;
198
+ } else {
199
+ // Create new note
200
+ this.notes.push(this.createEmptyNote());
201
+ this.currentNoteIndex = this.notes.length - 1;
202
+ }
203
+
204
+ this.loadCurrentNote();
205
+ this.updateNoteIndicator();
206
+ }
207
+
208
+ // Save current note state before switching
209
+ saveCurrentNoteState() {
210
+ if (this.notes[this.currentNoteIndex]) {
211
+ this.notes[this.currentNoteIndex].lastModified = Date.now();
212
+ }
213
+ this.autoSave();
214
+ }
215
+
216
+ // Load and display current note
217
+ loadCurrentNote() {
218
+ // Clear current display
219
+ this.clearDisplay();
220
+
221
+ const note = this.notes[this.currentNoteIndex];
222
+ if (!note) return;
223
+
224
+ // Restore text items
225
+ note.textItems.forEach(item => {
226
+ this.restoreTextItem(item);
227
+ });
228
+
229
+ // Restore images
230
+ note.images.forEach(img => {
231
+ this.restoreImage(img);
232
+ });
233
+
234
+ // Redraw canvas with lines
235
+ this.redraw();
236
+
237
+ // Reset history for this note
238
+ this.history = [];
239
+ this.historyIndex = -1;
240
+ this.saveState();
241
+ }
242
+
243
+ // Clear the display without affecting note data
244
+ clearDisplay() {
245
+ this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
246
+ this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
247
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
248
+ this.selectedTextId = null;
249
+ this.selectedObjectId = null;
250
+ this.allSelected = false;
251
+ }
252
+
253
+ // Update note indicator in UI
254
+ updateNoteIndicator() {
255
+ // Update pagination counter
256
+ this.updatePaginationCounter();
257
+
258
+ // Show temporary indicator
259
+ let indicator = document.getElementById('note-indicator');
260
+ if (!indicator) {
261
+ indicator = document.createElement('div');
262
+ indicator.id = 'note-indicator';
263
+ this.app.appendChild(indicator);
264
+ }
265
+ indicator.textContent = `${this.currentNoteIndex + 1} / ${this.notes.length}`;
266
+ indicator.classList.add('visible');
267
+
268
+ // Hide after 1.5 seconds
269
+ clearTimeout(this.noteIndicatorTimeout);
270
+ this.noteIndicatorTimeout = setTimeout(() => {
271
+ indicator.classList.remove('visible');
272
+ }, 1500);
273
+ }
274
+
275
+ setupCanvas() {
276
+ const resize = () => {
277
+ const rect = this.canvas.parentElement.getBoundingClientRect();
278
+ const dpr = window.devicePixelRatio || 1;
279
+ this.canvas.width = rect.width * dpr;
280
+ this.canvas.height = rect.height * dpr;
281
+ this.canvas.style.width = rect.width + 'px';
282
+ this.canvas.style.height = rect.height + 'px';
283
+ this.ctx.scale(dpr, dpr);
284
+ this.redraw();
285
+ };
286
+
287
+ resize();
288
+ window.addEventListener('resize', resize);
289
+ }
290
+
291
+ setupToolbar() {
292
+ // Mode toggle
293
+ const selectModeBtn = document.getElementById('select-mode');
294
+ const drawModeBtn = document.getElementById('draw-mode');
295
+ const textModeBtn = document.getElementById('text-mode');
296
+
297
+ const setMode = (mode) => {
298
+ this.isSelectMode = mode === 'select';
299
+ this.isTextMode = mode === 'text';
300
+
301
+ // Update button states
302
+ selectModeBtn.classList.toggle('active', mode === 'select');
303
+ drawModeBtn.classList.toggle('active', mode === 'draw');
304
+ textModeBtn.classList.toggle('active', mode === 'text');
305
+
306
+ // Update canvas/text container interactions
307
+ if (mode === 'select') {
308
+ this.canvas.style.pointerEvents = 'auto';
309
+ this.canvas.style.cursor = 'default';
310
+ this.textContainer.style.pointerEvents = 'auto';
311
+ this.textContainer.style.cursor = 'default';
312
+ } else if (mode === 'draw') {
313
+ this.canvas.style.pointerEvents = 'auto';
314
+ this.canvas.style.cursor = 'crosshair';
315
+ this.textContainer.style.pointerEvents = 'none';
316
+ this.textContainer.style.cursor = 'default';
317
+ } else if (mode === 'text') {
318
+ this.canvas.style.pointerEvents = 'none';
319
+ this.textContainer.style.pointerEvents = 'auto';
320
+ this.textContainer.style.cursor = 'crosshair';
321
+ }
322
+ };
323
+
324
+ // Store setMode for keyboard shortcuts
325
+ this.setMode = setMode;
326
+
327
+ selectModeBtn.addEventListener('click', () => setMode('select'));
328
+ drawModeBtn.addEventListener('click', () => setMode('draw'));
329
+ textModeBtn.addEventListener('click', () => setMode('text'));
330
+
331
+ // Color picker dropdown
332
+ const colorBtns = document.querySelectorAll('.color-grid .color-btn');
333
+ const currentColorSwatch = document.querySelector('.current-color');
334
+ colorBtns.forEach(btn => {
335
+ btn.addEventListener('click', () => {
336
+ colorBtns.forEach(b => b.classList.remove('active'));
337
+ btn.classList.add('active');
338
+ this.currentColor = btn.dataset.color;
339
+ if (currentColorSwatch) {
340
+ currentColorSwatch.style.background = btn.dataset.color;
341
+ }
342
+ // Update selected object's color
343
+ if (this.selectedObjectId) {
344
+ this.changeObjectColor(this.selectedObjectId, btn.dataset.color);
345
+ }
346
+ // Update selected text item's color
347
+ if (this.selectedTextId) {
348
+ this.changeTextColor(this.selectedTextId, btn.dataset.color);
349
+ }
350
+ });
351
+ });
352
+
353
+ // Stroke width dropdown
354
+ const strokeOptions = document.querySelectorAll('.stroke-option');
355
+ const currentStroke = document.querySelector('.current-stroke');
356
+ strokeOptions.forEach(btn => {
357
+ btn.addEventListener('click', () => {
358
+ strokeOptions.forEach(b => b.classList.remove('active'));
359
+ btn.classList.add('active');
360
+ this.currentStrokeWidth = parseInt(btn.dataset.width);
361
+ if (currentStroke) {
362
+ const size = Math.min(16, 4 + parseInt(btn.dataset.width));
363
+ currentStroke.style.width = size + 'px';
364
+ currentStroke.style.height = size + 'px';
365
+ }
366
+ });
367
+ });
368
+
369
+ // Clear button
370
+ document.getElementById('clear-btn').addEventListener('click', () => {
371
+ this.clear();
372
+ });
373
+
374
+ // Background mode toggle
375
+ this.setupBackgroundToggle();
376
+
377
+ // Pin toggle
378
+ this.setupPinToggle();
379
+
380
+ // Size toggle
381
+ this.setupSizeToggle();
382
+
383
+ // Create zoom controls
384
+ this.createZoomControls();
385
+ }
386
+
387
+ setupPinToggle() {
388
+ const pinCheckbox = document.getElementById('pin-checkbox');
389
+ if (pinCheckbox) {
390
+ pinCheckbox.addEventListener('change', () => {
391
+ window.glassboard.setPinned(pinCheckbox.checked);
392
+ this.animatePinIcon();
393
+ });
394
+ }
395
+ }
396
+
397
+ togglePin() {
398
+ const pinCheckbox = document.getElementById('pin-checkbox');
399
+ if (pinCheckbox) {
400
+ pinCheckbox.checked = !pinCheckbox.checked;
401
+ window.glassboard.setPinned(pinCheckbox.checked);
402
+ this.animatePinIcon();
403
+ }
404
+ }
405
+
406
+ animatePinIcon() {
407
+ const pinToggle = document.querySelector('.pin-toggle');
408
+ if (pinToggle) {
409
+ pinToggle.classList.add('animate');
410
+ setTimeout(() => {
411
+ pinToggle.classList.remove('animate');
412
+ }, 400);
413
+ }
414
+ }
415
+
416
+ setupSizeToggle() {
417
+ const sizeItems = document.querySelectorAll('.dropdown-item[data-size]');
418
+ sizeItems.forEach(item => {
419
+ item.addEventListener('click', () => {
420
+ sizeItems.forEach(i => i.classList.remove('active'));
421
+ item.classList.add('active');
422
+ window.glassboard.setWindowSize(item.dataset.size);
423
+ });
424
+ });
425
+ }
426
+
427
+ updateSizeDropdown(size) {
428
+ const sizeItems = document.querySelectorAll('.dropdown-item[data-size]');
429
+ sizeItems.forEach(item => {
430
+ item.classList.toggle('active', item.dataset.size === size);
431
+ });
432
+ }
433
+
434
+ createZoomControls() {
435
+ const zoomControls = document.createElement('div');
436
+ zoomControls.id = 'zoom-controls';
437
+ zoomControls.innerHTML = `
438
+ <button class="zoom-btn" id="zoom-out" title="Zoom out (Cmd+-)">−</button>
439
+ <span id="zoom-indicator">100%</span>
440
+ <button class="zoom-btn" id="zoom-in" title="Zoom in (Cmd++)">+</button>
441
+ <button class="zoom-btn" id="zoom-reset" title="Reset view (Cmd+0)">⟲</button>
442
+ `;
443
+ this.app.appendChild(zoomControls);
444
+
445
+ document.getElementById('zoom-out').addEventListener('click', () => this.zoomOut());
446
+ document.getElementById('zoom-in').addEventListener('click', () => this.zoomIn());
447
+ document.getElementById('zoom-reset').addEventListener('click', () => this.resetTransform());
448
+ }
449
+
450
+ setupBackgroundToggle() {
451
+ const dropdownItems = document.querySelectorAll('.dropdown-item[data-bg]');
452
+
453
+ // Set initial background mode
454
+ this.app.classList.add('bg-transparent');
455
+ this.currentBgMode = 'transparent';
456
+
457
+ dropdownItems.forEach(item => {
458
+ item.addEventListener('click', () => {
459
+ // Update active state
460
+ dropdownItems.forEach(i => i.classList.remove('active'));
461
+ item.classList.add('active');
462
+
463
+ // Update background class
464
+ const bgMode = item.dataset.bg;
465
+ this.currentBgMode = bgMode;
466
+ this.app.classList.remove('bg-transparent', 'bg-blur', 'bg-dark');
467
+ this.app.classList.add(`bg-${bgMode}`);
468
+
469
+ // Tell main process to update vibrancy (for blur effect)
470
+ window.glassboard.setBackgroundMode(bgMode);
471
+ });
472
+ });
473
+ }
474
+
475
+ setupDrawing() {
476
+ const getPoint = (e) => {
477
+ const rect = this.canvas.getBoundingClientRect();
478
+ let x, y;
479
+ if (e.touches) {
480
+ x = e.touches[0].clientX - rect.left;
481
+ y = e.touches[0].clientY - rect.top;
482
+ } else {
483
+ x = e.clientX - rect.left;
484
+ y = e.clientY - rect.top;
485
+ }
486
+ // Clamp coordinates to canvas bounds and adjust for zoom
487
+ x = Math.max(0, Math.min(x, rect.width)) / this.zoomLevel;
488
+ y = Math.max(0, Math.min(y, rect.height)) / this.zoomLevel;
489
+ return { x, y };
490
+ };
491
+
492
+ const startDrawing = (e) => {
493
+ if (this.isTextMode) return;
494
+
495
+ const point = getPoint(e);
496
+
497
+ // Check if clicking on an existing stroke (for selection)
498
+ const clickedObjectId = this.findObjectAtPoint(point);
499
+ if (clickedObjectId) {
500
+ // Select the object
501
+ this.selectObject(clickedObjectId);
502
+ // Start dragging
503
+ this.isDraggingObject = true;
504
+ this.dragStartPoint = point;
505
+ return;
506
+ }
507
+
508
+ // Deselect if clicking empty space
509
+ this.deselectObject();
510
+ this.clearMultiSelection();
511
+
512
+ // In select mode, start drag-box selection
513
+ if (this.isSelectMode) {
514
+ this.isSelecting = true;
515
+ this.selectionStart = point;
516
+ this.selectionRect.style.left = point.x + 'px';
517
+ this.selectionRect.style.top = point.y + 'px';
518
+ this.selectionRect.style.width = '0px';
519
+ this.selectionRect.style.height = '0px';
520
+ this.selectionRect.classList.add('active');
521
+ return;
522
+ }
523
+
524
+ // Start new drawing (only in draw mode)
525
+ this.isDrawing = true;
526
+ this.app.classList.add('drawing');
527
+
528
+ // Determine object ID based on timing
529
+ const now = Date.now();
530
+ if (now - this.lastStrokeTime > this.objectGroupTimeout) {
531
+ this.currentObjectId = now.toString();
532
+ }
533
+ this.lastStrokeTime = now;
534
+
535
+ this.currentLine = {
536
+ points: [point],
537
+ color: this.currentColor,
538
+ width: this.currentStrokeWidth,
539
+ objectId: this.currentObjectId
540
+ };
541
+ };
542
+
543
+ const draw = (e) => {
544
+ if (this.isTextMode) return;
545
+ e.preventDefault();
546
+
547
+ const point = getPoint(e);
548
+
549
+ // Handle drag-box selection
550
+ if (this.isSelecting && this.selectionStart) {
551
+ const x = Math.min(point.x, this.selectionStart.x);
552
+ const y = Math.min(point.y, this.selectionStart.y);
553
+ const width = Math.abs(point.x - this.selectionStart.x);
554
+ const height = Math.abs(point.y - this.selectionStart.y);
555
+ this.selectionRect.style.left = x + 'px';
556
+ this.selectionRect.style.top = y + 'px';
557
+ this.selectionRect.style.width = width + 'px';
558
+ this.selectionRect.style.height = height + 'px';
559
+ return;
560
+ }
561
+
562
+ // Handle object dragging
563
+ if (this.isDraggingObject && this.selectedObjectId) {
564
+ const dx = point.x - this.dragStartPoint.x;
565
+ const dy = point.y - this.dragStartPoint.y;
566
+ this.moveObject(this.selectedObjectId, dx, dy);
567
+ this.dragStartPoint = point;
568
+ return;
569
+ }
570
+
571
+ if (!this.isDrawing) return;
572
+ this.currentLine.points.push(point);
573
+ this.redraw();
574
+ };
575
+
576
+ const stopDrawing = (e) => {
577
+ // Handle drag-box selection completion
578
+ if (this.isSelecting) {
579
+ this.isSelecting = false;
580
+ this.selectionRect.classList.remove('active');
581
+
582
+ if (this.selectionStart) {
583
+ const point = e ? getPoint(e) : this.selectionStart;
584
+ const rect = {
585
+ x: Math.min(point.x, this.selectionStart.x),
586
+ y: Math.min(point.y, this.selectionStart.y),
587
+ width: Math.abs(point.x - this.selectionStart.x),
588
+ height: Math.abs(point.y - this.selectionStart.y)
589
+ };
590
+ this.selectObjectsInRect(rect);
591
+ }
592
+ this.selectionStart = null;
593
+ return;
594
+ }
595
+
596
+ if (this.isDraggingObject) {
597
+ this.isDraggingObject = false;
598
+ this.dragStartPoint = null;
599
+ this.saveState(); // Save after dragging object
600
+ return;
601
+ }
602
+
603
+ if (!this.isDrawing) return;
604
+ this.isDrawing = false;
605
+ this.app.classList.remove('drawing');
606
+ if (this.currentLine && this.currentLine.points.length > 1) {
607
+ this.lines.push(this.currentLine);
608
+ this.saveState(); // Save after drawing
609
+ }
610
+ this.currentLine = null;
611
+ };
612
+
613
+ // Mouse events - use document for move/up to continue drawing over other elements
614
+ this.canvas.addEventListener('mousedown', startDrawing);
615
+ document.addEventListener('mousemove', draw);
616
+ document.addEventListener('mouseup', stopDrawing);
617
+
618
+ // Touch events - use document for move/end to continue drawing over other elements
619
+ this.canvas.addEventListener('touchstart', startDrawing, { passive: false });
620
+ document.addEventListener('touchmove', draw, { passive: false });
621
+ document.addEventListener('touchend', stopDrawing);
622
+ }
623
+
624
+ // Find which object (if any) is at the given point
625
+ findObjectAtPoint(point) {
626
+ const hitRadius = 10; // pixels tolerance for clicking
627
+ for (let i = this.lines.length - 1; i >= 0; i--) {
628
+ const line = this.lines[i];
629
+ for (const p of line.points) {
630
+ const dist = Math.sqrt((p.x - point.x) ** 2 + (p.y - point.y) ** 2);
631
+ if (dist < hitRadius + line.width / 2) {
632
+ return line.objectId;
633
+ }
634
+ }
635
+ }
636
+ return null;
637
+ }
638
+
639
+ // Select all strokes belonging to an object
640
+ selectObject(objectId) {
641
+ this.selectedObjectId = objectId;
642
+ this.redraw();
643
+ }
644
+
645
+ // Deselect current object
646
+ deselectObject() {
647
+ this.selectedObjectId = null;
648
+ this.redraw();
649
+ }
650
+
651
+ // Move all strokes of an object by dx, dy
652
+ moveObject(objectId, dx, dy) {
653
+ this.lines.forEach(line => {
654
+ if (line.objectId === objectId) {
655
+ line.points.forEach(p => {
656
+ p.x += dx;
657
+ p.y += dy;
658
+ });
659
+ }
660
+ });
661
+ this.redraw();
662
+ }
663
+
664
+ // Delete all strokes of an object
665
+ deleteObject(objectId) {
666
+ this.lines = this.lines.filter(line => line.objectId !== objectId);
667
+ if (this.selectedObjectId === objectId) {
668
+ this.selectedObjectId = null;
669
+ }
670
+ this.redraw();
671
+ this.saveState();
672
+ }
673
+
674
+ // Select all objects within a rectangle
675
+ selectObjectsInRect(rect) {
676
+ const selectedIds = new Set();
677
+
678
+ // Find all objects with points inside the rectangle
679
+ this.lines.forEach(line => {
680
+ for (const p of line.points) {
681
+ if (p.x >= rect.x && p.x <= rect.x + rect.width &&
682
+ p.y >= rect.y && p.y <= rect.y + rect.height) {
683
+ selectedIds.add(line.objectId);
684
+ break;
685
+ }
686
+ }
687
+ });
688
+
689
+ this.multiSelectedObjects = Array.from(selectedIds);
690
+
691
+ // If only one object, use single selection
692
+ if (this.multiSelectedObjects.length === 1) {
693
+ this.selectObject(this.multiSelectedObjects[0]);
694
+ this.multiSelectedObjects = [];
695
+ } else if (this.multiSelectedObjects.length > 1) {
696
+ this.redraw();
697
+ }
698
+ }
699
+
700
+ // Clear multi-selection
701
+ clearMultiSelection() {
702
+ this.multiSelectedObjects = [];
703
+ this.redraw();
704
+ }
705
+
706
+ // Check if object is in multi-selection
707
+ isObjectMultiSelected(objectId) {
708
+ return this.multiSelectedObjects.includes(objectId);
709
+ }
710
+
711
+ // Setup pagination controls
712
+ setupPagination() {
713
+ const prevBtn = document.getElementById('prev-note');
714
+ const nextBtn = document.getElementById('next-note');
715
+ const newNoteBtn = document.getElementById('new-note');
716
+ const counter = document.getElementById('note-counter');
717
+
718
+ if (prevBtn) {
719
+ prevBtn.addEventListener('click', () => this.previousNote());
720
+ }
721
+ if (nextBtn) {
722
+ nextBtn.addEventListener('click', () => this.nextNote());
723
+ }
724
+ if (newNoteBtn) {
725
+ newNoteBtn.addEventListener('click', () => this.createNewNoteAndSwitch());
726
+ }
727
+
728
+ // Update counter display
729
+ this.updatePaginationCounter();
730
+ }
731
+
732
+ // Create a new note and switch to it
733
+ createNewNoteAndSwitch() {
734
+ this.saveCurrentNoteState();
735
+ this.notes.push(this.createEmptyNote());
736
+ this.currentNoteIndex = this.notes.length - 1;
737
+ this.loadCurrentNote();
738
+ this.updateNoteIndicator();
739
+ this.updatePaginationCounter();
740
+ }
741
+
742
+ // Update pagination counter display
743
+ updatePaginationCounter() {
744
+ const counter = document.getElementById('note-counter');
745
+ if (counter) {
746
+ counter.textContent = `${this.currentNoteIndex + 1}/${this.notes.length}`;
747
+ }
748
+
749
+ // Update button states
750
+ const prevBtn = document.getElementById('prev-note');
751
+ const nextBtn = document.getElementById('next-note');
752
+ if (prevBtn) {
753
+ prevBtn.disabled = this.currentNoteIndex === 0;
754
+ }
755
+ if (nextBtn) {
756
+ nextBtn.disabled = false; // Always enabled (creates new note)
757
+ }
758
+ }
759
+
760
+ // Handle window toggle for clean slate mode
761
+ setupWindowToggleHandler() {
762
+ if (window.glassboard?.onWindowToggledOpen) {
763
+ window.glassboard.onWindowToggledOpen(() => {
764
+ this.handleWindowToggledOpen();
765
+ });
766
+ }
767
+ }
768
+
769
+ handleWindowToggledOpen() {
770
+ // Only activate clean slate behavior if setting is enabled
771
+ if (!this.settings.openWithCleanSlate) {
772
+ return;
773
+ }
774
+
775
+ // Create a new blank note
776
+ this.notes.push(this.createEmptyNote());
777
+ this.currentNoteIndex = this.notes.length - 1;
778
+ this.updatePaginationCounter();
779
+ this.redraw();
780
+
781
+ // Switch to text mode
782
+ this.setMode('text');
783
+
784
+ // Create a text item at top-left (under window controls)
785
+ // Position: x=20 (left margin), y=50 (below toolbar)
786
+ const x = 20;
787
+ const y = 50;
788
+ this.createTextItem(x, y);
789
+
790
+ // Save state
791
+ this.saveState();
792
+ }
793
+
794
+ // Change color of all strokes in an object
795
+ changeObjectColor(objectId, color) {
796
+ this.lines.forEach(line => {
797
+ if (line.objectId === objectId) {
798
+ line.color = color;
799
+ }
800
+ });
801
+ this.redraw();
802
+ }
803
+
804
+ // Change color of a text item
805
+ changeTextColor(textId, color) {
806
+ const textItem = this.textItems.find(t => t.id === textId);
807
+ if (textItem) {
808
+ textItem.color = color;
809
+ }
810
+ const element = this.textContainer.querySelector(`[data-id="${textId}"]`);
811
+ if (element) {
812
+ element.style.color = color;
813
+ const editor = element.querySelector('.text-input');
814
+ if (editor) {
815
+ editor.style.color = color;
816
+ }
817
+ }
818
+ }
819
+
820
+ // Copy selected object to clipboard
821
+ copyObject(objectId) {
822
+ const objectLines = this.lines.filter(line => line.objectId === objectId);
823
+ if (objectLines.length === 0) return;
824
+
825
+ // Deep copy the lines
826
+ const copiedLines = objectLines.map(line => ({
827
+ points: line.points.map(p => ({ x: p.x, y: p.y })),
828
+ color: line.color,
829
+ width: line.width
830
+ }));
831
+
832
+ this.clipboard = {
833
+ type: 'object',
834
+ data: copiedLines
835
+ };
836
+ }
837
+
838
+ // Copy selected text item to clipboard
839
+ copyTextItem(textId) {
840
+ const textItem = this.textItems.find(t => t.id === textId);
841
+ if (!textItem) return;
842
+
843
+ const element = this.textContainer.querySelector(`[data-id="${textId}"]`);
844
+ const editor = element?.querySelector('.text-input');
845
+
846
+ this.clipboard = {
847
+ type: 'text',
848
+ data: {
849
+ content: editor?.innerHTML || textItem.content || '',
850
+ color: textItem.color
851
+ }
852
+ };
853
+ }
854
+
855
+ // Paste from clipboard
856
+ paste() {
857
+ if (!this.clipboard) return;
858
+
859
+ const pasteOffset = 20; // Offset for pasted items
860
+
861
+ if (this.clipboard.type === 'object') {
862
+ // Create new object ID for pasted object
863
+ const newObjectId = Date.now().toString();
864
+
865
+ // Calculate bounding box to find offset
866
+ let minX = Infinity, minY = Infinity;
867
+ this.clipboard.data.forEach(line => {
868
+ line.points.forEach(p => {
869
+ minX = Math.min(minX, p.x);
870
+ minY = Math.min(minY, p.y);
871
+ });
872
+ });
873
+
874
+ // Add lines with new objectId and offset
875
+ this.clipboard.data.forEach(line => {
876
+ this.lines.push({
877
+ points: line.points.map(p => ({
878
+ x: p.x + pasteOffset,
879
+ y: p.y + pasteOffset
880
+ })),
881
+ color: line.color,
882
+ width: line.width,
883
+ objectId: newObjectId
884
+ });
885
+ });
886
+
887
+ // Select the pasted object
888
+ this.selectedObjectId = newObjectId;
889
+ this.redraw();
890
+
891
+ } else if (this.clipboard.type === 'text') {
892
+ // Get center of viewport for paste location
893
+ const rect = this.textContainer.getBoundingClientRect();
894
+ const x = rect.width / 2 / this.zoomLevel;
895
+ const y = rect.height / 2 / this.zoomLevel;
896
+
897
+ // Create new text item
898
+ const id = Date.now().toString();
899
+ const item = document.createElement('div');
900
+ item.className = 'text-item selected';
901
+ item.dataset.id = id;
902
+ item.style.left = (x + pasteOffset) + 'px';
903
+ item.style.top = (y + pasteOffset) + 'px';
904
+ item.style.color = this.clipboard.data.color;
905
+ // Apply counter-scale so text doesn't zoom
906
+ item.style.transform = `scale(${1 / this.zoomLevel})`;
907
+ item.style.transformOrigin = 'top left';
908
+
909
+ // Add drag handle
910
+ const dragHandle = document.createElement('div');
911
+ dragHandle.className = 'text-drag-handle';
912
+ dragHandle.innerHTML = `
913
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
914
+ <path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
915
+ </svg>
916
+ `;
917
+ dragHandle.title = 'Drag to move';
918
+
919
+ // Add format bar
920
+ const formatBar = document.createElement('div');
921
+ formatBar.className = 'text-format-bar';
922
+ formatBar.innerHTML = `
923
+ <button class="format-btn" data-format="bold" title="Bold">B</button>
924
+ <button class="format-btn" data-format="italic" title="Italic">I</button>
925
+ <button class="format-btn" data-format="underline" title="Underline">U</button>
926
+ <button class="format-btn delete-btn" title="Delete">✕</button>
927
+ `;
928
+
929
+ // Create contentEditable div
930
+ const editor = document.createElement('div');
931
+ editor.className = 'text-input';
932
+ editor.contentEditable = 'true';
933
+ // Support both old 'text' and new 'content' formats
934
+ editor.innerHTML = this.clipboard.data.content || this.clipboard.data.text || '';
935
+ editor.style.color = this.clipboard.data.color;
936
+
937
+ // Add resize handle
938
+ const resizeHandle = document.createElement('div');
939
+ resizeHandle.className = 'text-resize-handle';
940
+
941
+ item.appendChild(dragHandle);
942
+ item.appendChild(formatBar);
943
+ item.appendChild(editor);
944
+ item.appendChild(resizeHandle);
945
+ this.textContainer.appendChild(item);
946
+
947
+ this.textItems.push({
948
+ id,
949
+ x: x + pasteOffset,
950
+ y: y + pasteOffset,
951
+ content: this.clipboard.data.content || this.clipboard.data.text || '',
952
+ color: this.clipboard.data.color
953
+ });
954
+
955
+ // Setup event handlers for the new text item
956
+ this.setupPastedTextItem(item, id, editor, formatBar);
957
+
958
+ // Select the pasted item
959
+ this.deselectAllText();
960
+ this.selectedTextId = id;
961
+ editor.focus();
962
+ }
963
+ }
964
+
965
+ // Setup event handlers for a pasted/restored text item
966
+ setupPastedTextItem(item, id, editor, formatBar) {
967
+ // Handle input changes
968
+ editor.addEventListener('input', () => {
969
+ const textItem = this.textItems.find(t => t.id === id);
970
+ if (textItem) {
971
+ textItem.content = editor.innerHTML;
972
+ }
973
+ });
974
+
975
+ // Handle delete on empty backspace and rich text shortcuts
976
+ editor.addEventListener('keydown', (e) => {
977
+ if (e.key === 'Backspace' && editor.textContent.trim() === '') {
978
+ e.preventDefault();
979
+ this.deleteTextItem(id);
980
+ }
981
+ // Rich text keyboard shortcuts
982
+ if (e.metaKey && !e.shiftKey) {
983
+ if (e.key === 'b') {
984
+ e.preventDefault();
985
+ document.execCommand('bold', false, null);
986
+ this.updateFormatButtonStates(formatBar);
987
+ } else if (e.key === 'i') {
988
+ e.preventDefault();
989
+ document.execCommand('italic', false, null);
990
+ this.updateFormatButtonStates(formatBar);
991
+ } else if (e.key === 'u') {
992
+ e.preventDefault();
993
+ document.execCommand('underline', false, null);
994
+ this.updateFormatButtonStates(formatBar);
995
+ }
996
+ }
997
+ });
998
+
999
+ // Format button handlers - use execCommand for selection-based formatting
1000
+ formatBar.querySelectorAll('.format-btn[data-format]').forEach(btn => {
1001
+ btn.addEventListener('mousedown', (e) => {
1002
+ e.preventDefault();
1003
+ const format = btn.dataset.format;
1004
+ document.execCommand(format, false, null);
1005
+ this.updateFormatButtonStates(formatBar);
1006
+ });
1007
+ });
1008
+
1009
+ // Delete button handler
1010
+ formatBar.querySelector('.delete-btn').addEventListener('mousedown', (e) => {
1011
+ e.preventDefault();
1012
+ e.stopPropagation();
1013
+ this.deleteTextItem(id);
1014
+ });
1015
+
1016
+ // Delete empty text item when clicking away
1017
+ editor.addEventListener('blur', () => {
1018
+ setTimeout(() => {
1019
+ if (editor.textContent.trim() === '') {
1020
+ this.deleteTextItem(id);
1021
+ }
1022
+ }, 100);
1023
+ });
1024
+
1025
+ // Update format button states when selection changes
1026
+ editor.addEventListener('mouseup', () => this.updateFormatButtonStates(formatBar));
1027
+ editor.addEventListener('keyup', () => this.updateFormatButtonStates(formatBar));
1028
+
1029
+ // Add resize handle if not already present
1030
+ let resizeHandle = item.querySelector('.text-resize-handle');
1031
+ if (!resizeHandle) {
1032
+ resizeHandle = document.createElement('div');
1033
+ resizeHandle.className = 'text-resize-handle';
1034
+ item.appendChild(resizeHandle);
1035
+ }
1036
+
1037
+ // Setup drag and resize
1038
+ this.setupTextItemDrag(item, id);
1039
+ this.setupTextResize(item, id, resizeHandle);
1040
+
1041
+ // Select on click
1042
+ item.addEventListener('click', (e) => {
1043
+ e.stopPropagation();
1044
+ this.selectTextItem(id);
1045
+ });
1046
+ }
1047
+
1048
+ setupTextMode() {
1049
+ this.textContainer.addEventListener('click', (e) => {
1050
+ if (!this.isTextMode) return;
1051
+ if (e.target !== this.textContainer) return;
1052
+
1053
+ const rect = this.textContainer.getBoundingClientRect();
1054
+ const x = e.clientX - rect.left;
1055
+ const y = e.clientY - rect.top;
1056
+
1057
+ this.createTextItem(x, y);
1058
+ });
1059
+
1060
+ // Deselect on click outside (only in draw mode)
1061
+ document.addEventListener('click', (e) => {
1062
+ if (this.isTextMode) return;
1063
+ if (!e.target.closest('.text-item') && !e.target.closest('#toolbar')) {
1064
+ this.deselectAllText();
1065
+ }
1066
+ });
1067
+ }
1068
+
1069
+ createTextItem(x, y) {
1070
+ const id = Date.now().toString();
1071
+ const item = document.createElement('div');
1072
+ item.className = 'text-item selected';
1073
+ item.dataset.id = id;
1074
+ item.style.left = x + 'px';
1075
+ item.style.top = y + 'px';
1076
+ item.style.color = this.currentColor;
1077
+ // Apply counter-scale so text doesn't zoom
1078
+ item.style.transform = `scale(${1 / this.zoomLevel})`;
1079
+ item.style.transformOrigin = 'top left';
1080
+
1081
+ // Add drag handle with move icon
1082
+ const dragHandle = document.createElement('div');
1083
+ dragHandle.className = 'text-drag-handle';
1084
+ dragHandle.innerHTML = `
1085
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
1086
+ <path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
1087
+ </svg>
1088
+ `;
1089
+ dragHandle.title = 'Drag to move';
1090
+
1091
+ // Inline formatting toolbar
1092
+ const formatBar = document.createElement('div');
1093
+ formatBar.className = 'text-format-bar';
1094
+ formatBar.innerHTML = `
1095
+ <button class="format-btn" data-format="bold" title="Bold">B</button>
1096
+ <button class="format-btn" data-format="italic" title="Italic">I</button>
1097
+ <button class="format-btn" data-format="underline" title="Underline">U</button>
1098
+ <button class="format-btn delete-btn" title="Delete">✕</button>
1099
+ `;
1100
+
1101
+ // Use contentEditable div for rich text support
1102
+ const editor = document.createElement('div');
1103
+ editor.className = 'text-input';
1104
+ editor.contentEditable = 'true';
1105
+ editor.dataset.placeholder = 'Type here...';
1106
+ editor.style.color = this.currentColor;
1107
+
1108
+ // Add resize handle
1109
+ const resizeHandle = document.createElement('div');
1110
+ resizeHandle.className = 'text-resize-handle';
1111
+
1112
+ item.appendChild(dragHandle);
1113
+ item.appendChild(formatBar);
1114
+ item.appendChild(editor);
1115
+ item.appendChild(resizeHandle);
1116
+ this.textContainer.appendChild(item);
1117
+
1118
+ this.textItems.push({ id, x, y, content: '', color: this.currentColor });
1119
+ this.selectedTextId = id;
1120
+
1121
+ // Focus editor
1122
+ editor.focus();
1123
+
1124
+ // Handle input changes
1125
+ editor.addEventListener('input', () => {
1126
+ const textItem = this.textItems.find(t => t.id === id);
1127
+ if (textItem) {
1128
+ textItem.content = editor.innerHTML;
1129
+ }
1130
+ });
1131
+
1132
+ // Handle delete on empty backspace
1133
+ editor.addEventListener('keydown', (e) => {
1134
+ if (e.key === 'Backspace' && editor.textContent.trim() === '') {
1135
+ e.preventDefault();
1136
+ this.deleteTextItem(id);
1137
+ }
1138
+ // Rich text keyboard shortcuts
1139
+ if (e.metaKey && !e.shiftKey) {
1140
+ if (e.key === 'b') {
1141
+ e.preventDefault();
1142
+ document.execCommand('bold', false, null);
1143
+ this.updateFormatButtonStates(formatBar);
1144
+ } else if (e.key === 'i') {
1145
+ e.preventDefault();
1146
+ document.execCommand('italic', false, null);
1147
+ this.updateFormatButtonStates(formatBar);
1148
+ } else if (e.key === 'u') {
1149
+ e.preventDefault();
1150
+ document.execCommand('underline', false, null);
1151
+ this.updateFormatButtonStates(formatBar);
1152
+ }
1153
+ }
1154
+ });
1155
+
1156
+ // Format button handlers - use execCommand for selection-based formatting
1157
+ formatBar.querySelectorAll('.format-btn[data-format]').forEach(btn => {
1158
+ btn.addEventListener('mousedown', (e) => {
1159
+ e.preventDefault(); // Prevent focus loss
1160
+ const format = btn.dataset.format;
1161
+ document.execCommand(format, false, null);
1162
+ this.updateFormatButtonStates(formatBar);
1163
+ });
1164
+ });
1165
+
1166
+ // Delete button handler
1167
+ formatBar.querySelector('.delete-btn').addEventListener('mousedown', (e) => {
1168
+ e.preventDefault();
1169
+ e.stopPropagation();
1170
+ this.deleteTextItem(id);
1171
+ });
1172
+
1173
+ // Delete empty text item when clicking away
1174
+ editor.addEventListener('blur', () => {
1175
+ setTimeout(() => {
1176
+ if (editor.textContent.trim() === '') {
1177
+ this.deleteTextItem(id);
1178
+ }
1179
+ }, 100); // Small delay to allow format button clicks
1180
+ });
1181
+
1182
+ // Update format button states when selection changes
1183
+ editor.addEventListener('mouseup', () => this.updateFormatButtonStates(formatBar));
1184
+ editor.addEventListener('keyup', () => this.updateFormatButtonStates(formatBar));
1185
+
1186
+ // Setup drag and resize
1187
+ this.setupTextItemDrag(item, id);
1188
+ this.setupTextResize(item, id, resizeHandle);
1189
+
1190
+ // Select on click
1191
+ item.addEventListener('click', (e) => {
1192
+ e.stopPropagation();
1193
+ this.selectTextItem(id);
1194
+ });
1195
+ }
1196
+
1197
+ updateFormatButtonStates(formatBar) {
1198
+ // Update format button active states based on current selection
1199
+ const boldBtn = formatBar.querySelector('[data-format="bold"]');
1200
+ const italicBtn = formatBar.querySelector('[data-format="italic"]');
1201
+ const underlineBtn = formatBar.querySelector('[data-format="underline"]');
1202
+
1203
+ if (boldBtn) {
1204
+ boldBtn.classList.toggle('active', document.queryCommandState('bold'));
1205
+ }
1206
+ if (italicBtn) {
1207
+ italicBtn.classList.toggle('active', document.queryCommandState('italic'));
1208
+ }
1209
+ if (underlineBtn) {
1210
+ underlineBtn.classList.toggle('active', document.queryCommandState('underline'));
1211
+ }
1212
+ }
1213
+
1214
+ setupTextItemDrag(element, id) {
1215
+ let isDragging = false;
1216
+ let startX, startY, origX, origY;
1217
+
1218
+ const startDrag = (e) => {
1219
+ // Don't drag from text editor, format buttons, or resize handle
1220
+ if (e.target.closest('.text-input') || e.target.closest('.format-btn') || e.target.closest('.text-resize-handle')) return;
1221
+ e.preventDefault();
1222
+ isDragging = true;
1223
+ startX = e.clientX;
1224
+ startY = e.clientY;
1225
+ origX = parseInt(element.style.left) || 0;
1226
+ origY = parseInt(element.style.top) || 0;
1227
+ element.style.cursor = 'grabbing';
1228
+ element.classList.add('dragging');
1229
+ };
1230
+
1231
+ const drag = (e) => {
1232
+ if (!isDragging) return;
1233
+ const dx = e.clientX - startX;
1234
+ const dy = e.clientY - startY;
1235
+ element.style.left = (origX + dx) + 'px';
1236
+ element.style.top = (origY + dy) + 'px';
1237
+
1238
+ const textItem = this.textItems.find(t => t.id === id);
1239
+ if (textItem) {
1240
+ textItem.x = origX + dx;
1241
+ textItem.y = origY + dy;
1242
+ }
1243
+ };
1244
+
1245
+ const stopDrag = () => {
1246
+ if (isDragging) {
1247
+ isDragging = false;
1248
+ element.style.cursor = 'grab';
1249
+ element.classList.remove('dragging');
1250
+ }
1251
+ };
1252
+
1253
+ element.addEventListener('mousedown', startDrag);
1254
+ document.addEventListener('mousemove', drag);
1255
+ document.addEventListener('mouseup', stopDrag);
1256
+ }
1257
+
1258
+ setupTextResize(element, id, handle) {
1259
+ let isResizing = false;
1260
+ let startX, startY, initialWidth, initialHeight;
1261
+
1262
+ handle.addEventListener('mousedown', (e) => {
1263
+ isResizing = true;
1264
+ startX = e.clientX;
1265
+ startY = e.clientY;
1266
+ initialWidth = element.offsetWidth;
1267
+ initialHeight = element.offsetHeight;
1268
+ element.classList.add('resizing');
1269
+ e.preventDefault();
1270
+ e.stopPropagation();
1271
+ });
1272
+
1273
+ document.addEventListener('mousemove', (e) => {
1274
+ if (!isResizing) return;
1275
+
1276
+ const dx = (e.clientX - startX) / this.zoomLevel;
1277
+ const dy = (e.clientY - startY) / this.zoomLevel;
1278
+
1279
+ // Allow independent width/height resize
1280
+ const newWidth = Math.max(100, initialWidth + dx);
1281
+ const newHeight = Math.max(40, initialHeight + dy);
1282
+
1283
+ element.style.width = newWidth + 'px';
1284
+ element.style.minHeight = newHeight + 'px';
1285
+
1286
+ // Update editor width to match
1287
+ const editor = element.querySelector('.text-input');
1288
+ if (editor) {
1289
+ editor.style.width = '100%';
1290
+ }
1291
+ });
1292
+
1293
+ document.addEventListener('mouseup', () => {
1294
+ if (isResizing) {
1295
+ isResizing = false;
1296
+ element.classList.remove('resizing');
1297
+
1298
+ // Update stored dimensions
1299
+ const textItem = this.textItems.find(t => t.id === id);
1300
+ if (textItem) {
1301
+ textItem.width = element.offsetWidth;
1302
+ textItem.height = element.offsetHeight;
1303
+ }
1304
+ }
1305
+ });
1306
+ }
1307
+
1308
+ selectTextItem(id) {
1309
+ this.deselectAllText();
1310
+ this.selectedTextId = id;
1311
+ const element = this.textContainer.querySelector(`[data-id="${id}"]`);
1312
+ if (element) {
1313
+ element.classList.add('selected');
1314
+ const editor = element.querySelector('.text-input');
1315
+ if (editor) {
1316
+ editor.focus();
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ deselectAllText() {
1322
+ this.selectedTextId = null;
1323
+ this.textContainer.querySelectorAll('.text-item').forEach(item => {
1324
+ item.classList.remove('selected');
1325
+ });
1326
+ }
1327
+
1328
+ deleteTextItem(id) {
1329
+ const element = this.textContainer.querySelector(`[data-id="${id}"]`);
1330
+ if (element) {
1331
+ element.remove();
1332
+ }
1333
+ this.textItems = this.textItems.filter(t => t.id !== id);
1334
+ if (this.selectedTextId === id) {
1335
+ this.selectedTextId = null;
1336
+ }
1337
+ this.saveState();
1338
+ }
1339
+
1340
+ setupKeyboardShortcuts() {
1341
+ document.addEventListener('keydown', (e) => {
1342
+ // Check if we're in a text input
1343
+ const isInTextInput = e.target.closest('.text-input');
1344
+
1345
+ // Cmd+N - do nothing (prevent default new window behavior)
1346
+ if ((e.metaKey || e.ctrlKey) && e.key === 'n') {
1347
+ e.preventDefault();
1348
+ return;
1349
+ }
1350
+
1351
+ // Cmd+W to close window
1352
+ if ((e.metaKey || e.ctrlKey) && e.key === 'w') {
1353
+ e.preventDefault();
1354
+ window.glassboard.closeWindow();
1355
+ return;
1356
+ }
1357
+
1358
+ // Cmd+S to export as PNG
1359
+ if ((e.metaKey || e.ctrlKey) && e.key === 's') {
1360
+ e.preventDefault();
1361
+ this.exportAsPNG();
1362
+ return;
1363
+ }
1364
+
1365
+ // Cmd+, to open settings
1366
+ if ((e.metaKey || e.ctrlKey) && e.key === ',') {
1367
+ e.preventDefault();
1368
+ this.toggleSettings();
1369
+ return;
1370
+ }
1371
+
1372
+ // Cmd+P to toggle pin
1373
+ if ((e.metaKey || e.ctrlKey) && e.key === 'p') {
1374
+ e.preventDefault();
1375
+ this.togglePin();
1376
+ return;
1377
+ }
1378
+
1379
+ // Cmd+Z to undo
1380
+ if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
1381
+ if (isInTextInput) return; // Let native undo handle it
1382
+ e.preventDefault();
1383
+ this.undo();
1384
+ return;
1385
+ }
1386
+
1387
+ // Cmd+Shift+Z or Cmd+Y to redo
1388
+ if ((e.metaKey || e.ctrlKey) && (e.key === 'z' && e.shiftKey || e.key === 'y')) {
1389
+ if (isInTextInput) return; // Let native redo handle it
1390
+ e.preventDefault();
1391
+ this.redo();
1392
+ return;
1393
+ }
1394
+
1395
+ // Cmd+C to copy
1396
+ if ((e.metaKey || e.ctrlKey) && e.key === 'c') {
1397
+ // Allow native copy in text inputs with selected text
1398
+ if (isInTextInput && window.getSelection().toString()) {
1399
+ return; // Let native copy handle it
1400
+ }
1401
+ // Copy selected object or text item
1402
+ if (this.selectedObjectId) {
1403
+ e.preventDefault();
1404
+ this.copyObject(this.selectedObjectId);
1405
+ return;
1406
+ }
1407
+ if (this.selectedTextId && !isInTextInput) {
1408
+ e.preventDefault();
1409
+ this.copyTextItem(this.selectedTextId);
1410
+ return;
1411
+ }
1412
+ }
1413
+
1414
+ // Cmd+V to smart paste
1415
+ if ((e.metaKey || e.ctrlKey) && e.key === 'v') {
1416
+ // Allow native paste in text inputs
1417
+ if (isInTextInput) {
1418
+ return; // Let native paste handle it
1419
+ }
1420
+ e.preventDefault();
1421
+ this.smartPaste();
1422
+ return;
1423
+ }
1424
+
1425
+ // Cmd+A to select all
1426
+ if ((e.metaKey || e.ctrlKey) && e.key === 'a') {
1427
+ if (isInTextInput) return; // Let native select all handle it
1428
+ e.preventDefault();
1429
+ this.selectAll();
1430
+ return;
1431
+ }
1432
+
1433
+ // 'd' or Delete/Backspace to delete selected object or all selected
1434
+ if ((e.key === 'd' || e.key === 'Delete' || e.key === 'Backspace') && !isInTextInput) {
1435
+ // If all selected (after Cmd+A), delete everything
1436
+ if (this.allSelected) {
1437
+ e.preventDefault();
1438
+ this.deleteAllSelected();
1439
+ return;
1440
+ }
1441
+ // Otherwise delete single selected object
1442
+ if (this.selectedObjectId) {
1443
+ e.preventDefault();
1444
+ this.deleteObject(this.selectedObjectId);
1445
+ return;
1446
+ }
1447
+ // Or delete selected text item
1448
+ if (this.selectedTextId) {
1449
+ e.preventDefault();
1450
+ this.deleteTextItem(this.selectedTextId);
1451
+ return;
1452
+ }
1453
+ }
1454
+
1455
+ // Cmd+Plus to zoom in
1456
+ if ((e.metaKey || e.ctrlKey) && (e.key === '=' || e.key === '+')) {
1457
+ e.preventDefault();
1458
+ this.zoomIn();
1459
+ return;
1460
+ }
1461
+
1462
+ // Cmd+Minus to zoom out
1463
+ if ((e.metaKey || e.ctrlKey) && e.key === '-') {
1464
+ e.preventDefault();
1465
+ this.zoomOut();
1466
+ return;
1467
+ }
1468
+
1469
+ // Cmd+0 to reset view (zoom, pan, rotation)
1470
+ if ((e.metaKey || e.ctrlKey) && e.key === '0') {
1471
+ e.preventDefault();
1472
+ this.resetTransform();
1473
+ return;
1474
+ }
1475
+
1476
+ // Escape to deselect
1477
+ if (e.key === 'Escape') {
1478
+ this.deselectObject();
1479
+ this.deselectAllText();
1480
+ }
1481
+
1482
+ // Note navigation (only when not in text input)
1483
+ if (!isInTextInput) {
1484
+ // [ or ArrowLeft to go to previous note
1485
+ if (e.key === '[' || e.key === 'ArrowLeft') {
1486
+ e.preventDefault();
1487
+ this.previousNote();
1488
+ return;
1489
+ }
1490
+
1491
+ // ] or ArrowRight to go to next note or create new one
1492
+ if (e.key === ']' || e.key === 'ArrowRight') {
1493
+ e.preventDefault();
1494
+ this.nextNote();
1495
+ return;
1496
+ }
1497
+ }
1498
+
1499
+ // Mode shortcuts (only when not in text input)
1500
+ if (!isInTextInput) {
1501
+ if (e.key === 'v' || e.key === 'V') {
1502
+ this.setMode('select');
1503
+ return;
1504
+ }
1505
+ if (e.key === 'b' || e.key === 'B') {
1506
+ this.setMode('draw');
1507
+ return;
1508
+ }
1509
+ if (e.key === 't' || e.key === 'T') {
1510
+ this.setMode('text');
1511
+ return;
1512
+ }
1513
+ // Size shortcuts: Cmd+1=sm, Cmd+2=md, Cmd+3=lg
1514
+ if (e.metaKey && e.key === '1') {
1515
+ e.preventDefault();
1516
+ window.glassboard.setWindowSize('sm');
1517
+ this.updateSizeDropdown('sm');
1518
+ return;
1519
+ }
1520
+ if (e.metaKey && e.key === '2') {
1521
+ e.preventDefault();
1522
+ window.glassboard.setWindowSize('md');
1523
+ this.updateSizeDropdown('md');
1524
+ return;
1525
+ }
1526
+ if (e.metaKey && e.key === '3') {
1527
+ e.preventDefault();
1528
+ window.glassboard.setWindowSize('lg');
1529
+ this.updateSizeDropdown('lg');
1530
+ return;
1531
+ }
1532
+ // N for new note
1533
+ if (e.key === 'n' || e.key === 'N') {
1534
+ this.createNewNoteAndSwitch();
1535
+ return;
1536
+ }
1537
+ }
1538
+
1539
+ // Double right Command key to toggle (hide) Glassboard
1540
+ if (e.code === 'MetaRight' && !e.repeat) {
1541
+ const now = Date.now();
1542
+ if (now - this.lastRightCommandTime < 300) {
1543
+ // Double tap detected - hide the window
1544
+ window.glassboard.hideWindow();
1545
+ this.lastRightCommandTime = 0;
1546
+ } else {
1547
+ this.lastRightCommandTime = now;
1548
+ }
1549
+ }
1550
+ });
1551
+ }
1552
+
1553
+ setupFocusHandling() {
1554
+ window.glassboard.onFocusChange((focused) => {
1555
+ if (focused) {
1556
+ this.app.classList.add('focused');
1557
+ } else {
1558
+ this.app.classList.remove('focused');
1559
+ // Deselect all text items when window loses focus
1560
+ this.deselectAllText();
1561
+ }
1562
+ // Apply background modes via CSS classes
1563
+ this.applyBackgroundModes();
1564
+ // Apply appropriate opacity
1565
+ this.applyBackgroundOpacity();
1566
+ // Update native vibrancy based on current mode
1567
+ const currentMode = focused
1568
+ ? this.settings.activeBgMode
1569
+ : this.settings.inactiveBgMode;
1570
+ window.glassboard.setBackgroundMode(currentMode || 'transparent');
1571
+ });
1572
+ }
1573
+
1574
+ applyBackgroundModes() {
1575
+ // Remove all background classes first
1576
+ this.app.classList.remove('bg-transparent', 'bg-blur', 'bg-dark');
1577
+ this.app.classList.remove('inactive-bg-transparent', 'inactive-bg-blur', 'inactive-bg-dark');
1578
+
1579
+ // Apply active background class
1580
+ const activeMode = this.settings.activeBgMode || 'transparent';
1581
+ this.app.classList.add(`bg-${activeMode}`);
1582
+
1583
+ // Apply inactive background class
1584
+ const inactiveMode = this.settings.inactiveBgMode || 'transparent';
1585
+ this.app.classList.add(`inactive-bg-${inactiveMode}`);
1586
+ }
1587
+
1588
+ setupResize() {
1589
+ // Add resize handles to window (corners and edges)
1590
+ const handles = ['se', 'sw', 'ne', 'nw', 'n', 's', 'e', 'w'];
1591
+ handles.forEach(pos => {
1592
+ const handle = document.createElement('div');
1593
+ handle.className = `resize-handle ${pos}`;
1594
+ this.app.appendChild(handle);
1595
+
1596
+ // Add resizing class when dragging resize handles
1597
+ handle.addEventListener('mousedown', () => {
1598
+ this.app.classList.add('resizing');
1599
+ });
1600
+ });
1601
+
1602
+ // Remove resizing class when mouse is released anywhere
1603
+ document.addEventListener('mouseup', () => {
1604
+ this.app.classList.remove('resizing');
1605
+ });
1606
+
1607
+ // Also detect native window resize events for yellow border
1608
+ let resizeTimeout;
1609
+ window.addEventListener('resize', () => {
1610
+ this.app.classList.add('resizing');
1611
+ clearTimeout(resizeTimeout);
1612
+ resizeTimeout = setTimeout(() => {
1613
+ this.app.classList.remove('resizing');
1614
+ }, 200);
1615
+ });
1616
+ }
1617
+
1618
+ setupZoom() {
1619
+ // Zoom is now handled in setupGestures for trackpad support
1620
+ // This method is kept for future zoom-related setup
1621
+ }
1622
+
1623
+ zoomIn() {
1624
+ this.zoomLevel = Math.min(this.maxZoom, this.zoomLevel + 0.25);
1625
+ this.applyZoom();
1626
+ }
1627
+
1628
+ zoomOut() {
1629
+ this.zoomLevel = Math.max(this.minZoom, this.zoomLevel - 0.25);
1630
+ this.applyZoom();
1631
+ }
1632
+
1633
+ resetZoom() {
1634
+ this.zoomLevel = 1;
1635
+ this.applyZoom();
1636
+ }
1637
+
1638
+ applyZoom() {
1639
+ this.applyTransform();
1640
+ }
1641
+
1642
+ applyTransform() {
1643
+ // Build transform string with translate, scale, and rotate
1644
+ const transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoomLevel}) rotate(${this.rotation}deg)`;
1645
+ this.canvas.style.transform = transform;
1646
+ this.canvas.style.transformOrigin = 'center center';
1647
+ this.textContainer.style.transform = transform;
1648
+ this.textContainer.style.transformOrigin = 'center center';
1649
+
1650
+ // Counter-scale text items so they maintain visual size
1651
+ // Text items should move with pan/zoom but not change visual size
1652
+ const counterScale = 1 / this.zoomLevel;
1653
+ this.textContainer.querySelectorAll('.text-item').forEach(item => {
1654
+ item.style.transform = `scale(${counterScale})`;
1655
+ item.style.transformOrigin = 'top left';
1656
+ });
1657
+
1658
+ // Update zoom indicator if exists
1659
+ const zoomIndicator = document.getElementById('zoom-indicator');
1660
+ if (zoomIndicator) {
1661
+ zoomIndicator.textContent = `${Math.round(this.zoomLevel * 100)}%`;
1662
+ }
1663
+ }
1664
+
1665
+ resetTransform() {
1666
+ this.zoomLevel = 1;
1667
+ this.panX = 0;
1668
+ this.panY = 0;
1669
+ this.rotation = 0;
1670
+ this.applyTransform();
1671
+ }
1672
+
1673
+ setupContextMenu() {
1674
+ this.canvas.addEventListener('contextmenu', (e) => {
1675
+ e.preventDefault();
1676
+
1677
+ const rect = this.canvas.getBoundingClientRect();
1678
+ const x = (e.clientX - rect.left) / this.zoomLevel;
1679
+ const y = (e.clientY - rect.top) / this.zoomLevel;
1680
+
1681
+ const clickedObjectId = this.findObjectAtPoint({ x, y });
1682
+ if (clickedObjectId) {
1683
+ this.selectObject(clickedObjectId);
1684
+ this.showContextMenu(e.clientX, e.clientY, 'object');
1685
+ } else {
1686
+ // Show general context menu for empty area
1687
+ this.showContextMenu(e.clientX, e.clientY, 'general');
1688
+ }
1689
+ });
1690
+
1691
+ // Hide context menu on click elsewhere
1692
+ document.addEventListener('click', () => {
1693
+ this.hideContextMenu();
1694
+ });
1695
+ }
1696
+
1697
+ showContextMenu(x, y, type = 'object') {
1698
+ let menu = document.getElementById('context-menu');
1699
+ if (menu) {
1700
+ menu.remove();
1701
+ }
1702
+
1703
+ menu = document.createElement('div');
1704
+ menu.id = 'context-menu';
1705
+
1706
+ if (type === 'general') {
1707
+ // General context menu for empty area
1708
+ menu.innerHTML = `
1709
+ <button class="context-menu-item" data-action="zoom-in">
1710
+ <span>Zoom In</span>
1711
+ <span class="shortcut">⌘+</span>
1712
+ </button>
1713
+ <button class="context-menu-item" data-action="zoom-out">
1714
+ <span>Zoom Out</span>
1715
+ <span class="shortcut">⌘-</span>
1716
+ </button>
1717
+ <button class="context-menu-item" data-action="reset-zoom">
1718
+ <span>Reset View</span>
1719
+ <span class="shortcut">⌘0</span>
1720
+ </button>
1721
+ <div class="context-menu-divider"></div>
1722
+ <button class="context-menu-item" data-action="paste">
1723
+ <span>Paste</span>
1724
+ <span class="shortcut">⌘V</span>
1725
+ </button>
1726
+ <div class="context-menu-divider"></div>
1727
+ <button class="context-menu-item" data-action="settings">
1728
+ <span>Settings</span>
1729
+ <span class="shortcut">⌘,</span>
1730
+ </button>
1731
+ `;
1732
+
1733
+ menu.querySelector('[data-action="zoom-in"]').addEventListener('click', () => {
1734
+ this.zoomIn();
1735
+ this.hideContextMenu();
1736
+ });
1737
+
1738
+ menu.querySelector('[data-action="zoom-out"]').addEventListener('click', () => {
1739
+ this.zoomOut();
1740
+ this.hideContextMenu();
1741
+ });
1742
+
1743
+ menu.querySelector('[data-action="reset-zoom"]').addEventListener('click', () => {
1744
+ this.resetTransform();
1745
+ this.hideContextMenu();
1746
+ });
1747
+
1748
+ menu.querySelector('[data-action="paste"]').addEventListener('click', () => {
1749
+ this.paste();
1750
+ this.hideContextMenu();
1751
+ });
1752
+
1753
+ menu.querySelector('[data-action="settings"]').addEventListener('click', () => {
1754
+ this.toggleSettings();
1755
+ this.hideContextMenu();
1756
+ });
1757
+ } else {
1758
+ // Object context menu
1759
+ menu.innerHTML = `
1760
+ <button class="context-menu-item" data-action="copy">
1761
+ <span>Copy</span>
1762
+ <span class="shortcut">⌘C</span>
1763
+ </button>
1764
+ <button class="context-menu-item" data-action="paste">
1765
+ <span>Paste</span>
1766
+ <span class="shortcut">⌘V</span>
1767
+ </button>
1768
+ <div class="context-menu-divider"></div>
1769
+ <button class="context-menu-item" data-action="delete">
1770
+ <span>Delete</span>
1771
+ <span class="shortcut">D</span>
1772
+ </button>
1773
+ `;
1774
+
1775
+ menu.querySelector('[data-action="copy"]').addEventListener('click', () => {
1776
+ if (this.selectedObjectId) {
1777
+ this.copyObject(this.selectedObjectId);
1778
+ }
1779
+ this.hideContextMenu();
1780
+ });
1781
+
1782
+ menu.querySelector('[data-action="paste"]').addEventListener('click', () => {
1783
+ this.paste();
1784
+ this.hideContextMenu();
1785
+ });
1786
+
1787
+ menu.querySelector('[data-action="delete"]').addEventListener('click', () => {
1788
+ if (this.selectedObjectId) {
1789
+ this.deleteObject(this.selectedObjectId);
1790
+ }
1791
+ this.hideContextMenu();
1792
+ });
1793
+ }
1794
+
1795
+ document.body.appendChild(menu);
1796
+
1797
+ // Update paste button state
1798
+ const pasteBtn = menu.querySelector('[data-action="paste"]');
1799
+ if (pasteBtn) {
1800
+ pasteBtn.disabled = !this.clipboard;
1801
+ pasteBtn.style.opacity = this.clipboard ? '1' : '0.5';
1802
+ }
1803
+
1804
+ menu.style.left = x + 'px';
1805
+ menu.style.top = y + 'px';
1806
+ menu.classList.add('visible');
1807
+ }
1808
+
1809
+ hideContextMenu() {
1810
+ const menu = document.getElementById('context-menu');
1811
+ if (menu) {
1812
+ menu.classList.remove('visible');
1813
+ }
1814
+ }
1815
+
1816
+ redraw() {
1817
+ const rect = this.canvas.getBoundingClientRect();
1818
+ this.ctx.clearRect(0, 0, rect.width / this.zoomLevel, rect.height / this.zoomLevel);
1819
+
1820
+ // Draw center origin dot (2x2px, zoom-independent)
1821
+ this.drawCenterDot();
1822
+
1823
+ // Draw all saved lines
1824
+ this.lines.forEach(line => this.drawLine(line));
1825
+
1826
+ // Draw current line
1827
+ if (this.currentLine) {
1828
+ this.drawLine(this.currentLine);
1829
+ }
1830
+
1831
+ // Draw selection highlight for single selection
1832
+ if (this.selectedObjectId) {
1833
+ this.drawSelectionHighlight();
1834
+ }
1835
+
1836
+ // Draw selection highlights for multi-selected objects
1837
+ if (this.multiSelectedObjects.length > 0) {
1838
+ this.multiSelectedObjects.forEach(objectId => {
1839
+ this.drawSelectionHighlightForObject(objectId);
1840
+ });
1841
+ }
1842
+
1843
+ // Draw selection highlights for all-selected objects
1844
+ if (this.allSelected && this.selectedObjects.length > 0) {
1845
+ this.selectedObjects.forEach(objectId => {
1846
+ this.drawSelectionHighlightForObject(objectId);
1847
+ });
1848
+ }
1849
+ }
1850
+
1851
+ drawLine(line) {
1852
+ if (line.points.length < 2) return;
1853
+
1854
+ this.ctx.beginPath();
1855
+ this.ctx.strokeStyle = line.color;
1856
+ this.ctx.lineWidth = line.width;
1857
+ this.ctx.lineCap = 'round';
1858
+ this.ctx.lineJoin = 'round';
1859
+
1860
+ this.ctx.moveTo(line.points[0].x, line.points[0].y);
1861
+ for (let i = 1; i < line.points.length; i++) {
1862
+ this.ctx.lineTo(line.points[i].x, line.points[i].y);
1863
+ }
1864
+ this.ctx.stroke();
1865
+ }
1866
+
1867
+ drawCenterDot() {
1868
+ // Draw a 2x2px bright yellow dot at the note's stored origin
1869
+ // Size is compensated for zoom so it always appears 2x2px visually
1870
+ const dotSize = 2 / this.zoomLevel;
1871
+
1872
+ // Get origin from current note (already in content-space)
1873
+ const note = this.notes[this.currentNoteIndex];
1874
+ const rect = this.canvas.getBoundingClientRect();
1875
+ // Fallback for notes without origin (legacy) or if note doesn't exist
1876
+ const originX = note?.originX ?? (rect.width / 2 / this.zoomLevel);
1877
+ const originY = note?.originY ?? (rect.height / 2 / this.zoomLevel);
1878
+
1879
+ this.ctx.fillStyle = '#facc15'; // Bright yellow
1880
+ this.ctx.fillRect(
1881
+ originX - dotSize / 2,
1882
+ originY - dotSize / 2,
1883
+ dotSize,
1884
+ dotSize
1885
+ );
1886
+ }
1887
+
1888
+ drawSelectionHighlight() {
1889
+ // Get bounding box of selected object
1890
+ const selectedLines = this.lines.filter(l => l.objectId === this.selectedObjectId);
1891
+ if (selectedLines.length === 0) return;
1892
+
1893
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1894
+ selectedLines.forEach(line => {
1895
+ line.points.forEach(p => {
1896
+ minX = Math.min(minX, p.x);
1897
+ minY = Math.min(minY, p.y);
1898
+ maxX = Math.max(maxX, p.x);
1899
+ maxY = Math.max(maxY, p.y);
1900
+ });
1901
+ });
1902
+
1903
+ // Add padding
1904
+ const padding = 8;
1905
+ minX -= padding;
1906
+ minY -= padding;
1907
+ maxX += padding;
1908
+ maxY += padding;
1909
+
1910
+ // Draw selection box
1911
+ this.ctx.strokeStyle = '#3b82f6';
1912
+ this.ctx.lineWidth = 2;
1913
+ this.ctx.setLineDash([5, 5]);
1914
+ this.ctx.strokeRect(minX, minY, maxX - minX, maxY - minY);
1915
+ this.ctx.setLineDash([]);
1916
+
1917
+ // Draw corner handles
1918
+ const handleSize = 8;
1919
+ this.ctx.fillStyle = '#3b82f6';
1920
+ const corners = [
1921
+ [minX, minY], [maxX, minY],
1922
+ [minX, maxY], [maxX, maxY]
1923
+ ];
1924
+ corners.forEach(([x, y]) => {
1925
+ this.ctx.fillRect(x - handleSize/2, y - handleSize/2, handleSize, handleSize);
1926
+ });
1927
+ }
1928
+
1929
+ // Draw selection highlight for a specific object (used for multi-select)
1930
+ drawSelectionHighlightForObject(objectId) {
1931
+ const selectedLines = this.lines.filter(l => l.objectId === objectId);
1932
+ if (selectedLines.length === 0) return;
1933
+
1934
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1935
+ selectedLines.forEach(line => {
1936
+ line.points.forEach(p => {
1937
+ minX = Math.min(minX, p.x);
1938
+ minY = Math.min(minY, p.y);
1939
+ maxX = Math.max(maxX, p.x);
1940
+ maxY = Math.max(maxY, p.y);
1941
+ });
1942
+ });
1943
+
1944
+ // Add padding
1945
+ const padding = 8;
1946
+ minX -= padding;
1947
+ minY -= padding;
1948
+ maxX += padding;
1949
+ maxY += padding;
1950
+
1951
+ // Draw selection box (green for multi-select to distinguish)
1952
+ this.ctx.strokeStyle = '#22c55e';
1953
+ this.ctx.lineWidth = 2;
1954
+ this.ctx.setLineDash([5, 5]);
1955
+ this.ctx.strokeRect(minX, minY, maxX - minX, maxY - minY);
1956
+ this.ctx.setLineDash([]);
1957
+ }
1958
+
1959
+ clear() {
1960
+ this.lines = [];
1961
+ this.currentLine = null;
1962
+ this.textItems = [];
1963
+ this.images = [];
1964
+ this.textContainer.innerHTML = '';
1965
+ this.redraw();
1966
+ }
1967
+
1968
+ setupClipboardPaste() {
1969
+ // Check clipboard when window gains focus
1970
+ window.glassboard.onFocusChange((focused) => {
1971
+ if (focused) {
1972
+ this.checkSystemClipboard();
1973
+ } else {
1974
+ this.hidePasteOverlay();
1975
+ }
1976
+ });
1977
+
1978
+ // Also check on initial load
1979
+ setTimeout(() => this.checkSystemClipboard(), 500);
1980
+ }
1981
+
1982
+ // File drag-and-drop support
1983
+ setupFileDrop() {
1984
+ // Prevent default drag behaviors on document
1985
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(event => {
1986
+ document.addEventListener(event, (e) => {
1987
+ e.preventDefault();
1988
+ e.stopPropagation();
1989
+ });
1990
+ });
1991
+
1992
+ // Visual feedback on dragover
1993
+ this.app.addEventListener('dragenter', () => {
1994
+ this.app.classList.add('drag-over');
1995
+ });
1996
+
1997
+ this.app.addEventListener('dragleave', (e) => {
1998
+ if (!this.app.contains(e.relatedTarget)) {
1999
+ this.app.classList.remove('drag-over');
2000
+ }
2001
+ });
2002
+
2003
+ // Handle drop
2004
+ this.app.addEventListener('drop', (e) => {
2005
+ this.app.classList.remove('drag-over');
2006
+ this.handleFileDrop(e);
2007
+ });
2008
+ }
2009
+
2010
+ handleFileDrop(e) {
2011
+ const files = e.dataTransfer.files;
2012
+ if (!files.length) return;
2013
+
2014
+ const dropX = e.clientX - this.app.getBoundingClientRect().left;
2015
+ const dropY = e.clientY - this.app.getBoundingClientRect().top;
2016
+
2017
+ // Process each file
2018
+ Array.from(files).forEach((file, index) => {
2019
+ const x = dropX + (index * 20); // Offset multiple files
2020
+ const y = dropY + (index * 20);
2021
+
2022
+ if (this.isImageFile(file)) {
2023
+ this.handleDroppedImage(file, x, y);
2024
+ } else {
2025
+ this.handleDroppedFile(file, x, y);
2026
+ }
2027
+ });
2028
+
2029
+ this.saveState();
2030
+ }
2031
+
2032
+ isImageFile(file) {
2033
+ const imageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp'];
2034
+ return imageTypes.includes(file.type) || /\.(png|jpe?g|gif|webp|svg|bmp)$/i.test(file.name);
2035
+ }
2036
+
2037
+ handleDroppedImage(file, x, y) {
2038
+ const reader = new FileReader();
2039
+ reader.onload = (e) => {
2040
+ const img = new Image();
2041
+ img.onload = () => {
2042
+ this.pasteImage(e.target.result, x, y, img.width, img.height);
2043
+ };
2044
+ img.src = e.target.result;
2045
+ };
2046
+ reader.readAsDataURL(file);
2047
+ }
2048
+
2049
+ handleDroppedFile(file, x, y) {
2050
+ const id = Date.now().toString();
2051
+
2052
+ // Create attachment element
2053
+ const attachment = document.createElement('div');
2054
+ attachment.className = 'file-attachment';
2055
+ attachment.dataset.id = id;
2056
+ attachment.style.left = (x / this.zoomLevel) + 'px';
2057
+ attachment.style.top = (y / this.zoomLevel) + 'px';
2058
+
2059
+ // File icon based on extension
2060
+ const icon = this.getFileIcon(file.name);
2061
+
2062
+ attachment.innerHTML = `
2063
+ <div class="attachment-icon">${icon}</div>
2064
+ <div class="attachment-name" title="${file.name}">${file.name}</div>
2065
+ <button class="attachment-delete-btn">✕</button>
2066
+ `;
2067
+
2068
+ this.textContainer.appendChild(attachment);
2069
+
2070
+ // Store attachment data
2071
+ this.attachments.push({
2072
+ id,
2073
+ x: x / this.zoomLevel,
2074
+ y: y / this.zoomLevel,
2075
+ filePath: file.path, // Electron provides full path
2076
+ fileName: file.name,
2077
+ fileType: file.type || this.getExtension(file.name)
2078
+ });
2079
+
2080
+ // Setup interactions
2081
+ this.setupAttachmentDrag(attachment, id);
2082
+ this.setupAttachmentClick(attachment, id);
2083
+
2084
+ // Delete button
2085
+ attachment.querySelector('.attachment-delete-btn').addEventListener('click', (e) => {
2086
+ e.stopPropagation();
2087
+ this.deleteAttachment(id);
2088
+ });
2089
+ }
2090
+
2091
+ getFileIcon(fileName) {
2092
+ const ext = this.getExtension(fileName).toLowerCase();
2093
+ const icons = {
2094
+ pdf: '📄', doc: '📝', docx: '📝', txt: '📃',
2095
+ xls: '📊', xlsx: '📊', csv: '📊',
2096
+ ppt: '📽️', pptx: '📽️',
2097
+ zip: '🗜️', rar: '🗜️', '7z': '🗜️',
2098
+ mp3: '🎵', wav: '🎵', m4a: '🎵',
2099
+ mp4: '🎬', mov: '🎬', avi: '🎬',
2100
+ js: '📜', py: '📜', html: '📜', css: '📜',
2101
+ default: '📎'
2102
+ };
2103
+ return icons[ext] || icons.default;
2104
+ }
2105
+
2106
+ getExtension(fileName) {
2107
+ return fileName.split('.').pop() || '';
2108
+ }
2109
+
2110
+ setupAttachmentDrag(element, id) {
2111
+ let isDragging = false;
2112
+ let startX, startY, initialX, initialY;
2113
+
2114
+ element.addEventListener('mousedown', (e) => {
2115
+ if (e.target.classList.contains('attachment-delete-btn')) return;
2116
+ isDragging = true;
2117
+ startX = e.clientX;
2118
+ startY = e.clientY;
2119
+ initialX = parseInt(element.style.left);
2120
+ initialY = parseInt(element.style.top);
2121
+ element.classList.add('dragging');
2122
+ });
2123
+
2124
+ document.addEventListener('mousemove', (e) => {
2125
+ if (!isDragging) return;
2126
+ const dx = (e.clientX - startX) / this.zoomLevel;
2127
+ const dy = (e.clientY - startY) / this.zoomLevel;
2128
+ element.style.left = (initialX + dx) + 'px';
2129
+ element.style.top = (initialY + dy) + 'px';
2130
+ });
2131
+
2132
+ document.addEventListener('mouseup', () => {
2133
+ if (isDragging) {
2134
+ isDragging = false;
2135
+ element.classList.remove('dragging');
2136
+ // Update stored position
2137
+ const att = this.attachments.find(a => a.id === id);
2138
+ if (att) {
2139
+ att.x = parseInt(element.style.left);
2140
+ att.y = parseInt(element.style.top);
2141
+ }
2142
+ }
2143
+ });
2144
+ }
2145
+
2146
+ setupAttachmentClick(element, id) {
2147
+ element.addEventListener('dblclick', () => {
2148
+ const att = this.attachments.find(a => a.id === id);
2149
+ if (att?.filePath) {
2150
+ window.glassboard.openFile(att.filePath);
2151
+ }
2152
+ });
2153
+ }
2154
+
2155
+ deleteAttachment(id) {
2156
+ this.attachments = this.attachments.filter(a => a.id !== id);
2157
+ const element = this.textContainer.querySelector(`.file-attachment[data-id="${id}"]`);
2158
+ if (element) element.remove();
2159
+ this.saveState();
2160
+ }
2161
+
2162
+ restoreAttachment(att) {
2163
+ const attachment = document.createElement('div');
2164
+ attachment.className = 'file-attachment';
2165
+ attachment.dataset.id = att.id;
2166
+ attachment.style.left = att.x + 'px';
2167
+ attachment.style.top = att.y + 'px';
2168
+
2169
+ const icon = this.getFileIcon(att.fileName);
2170
+ attachment.innerHTML = `
2171
+ <div class="attachment-icon">${icon}</div>
2172
+ <div class="attachment-name" title="${att.fileName}">${att.fileName}</div>
2173
+ <button class="attachment-delete-btn">✕</button>
2174
+ `;
2175
+
2176
+ this.textContainer.appendChild(attachment);
2177
+ this.setupAttachmentDrag(attachment, att.id);
2178
+ this.setupAttachmentClick(attachment, att.id);
2179
+
2180
+ attachment.querySelector('.attachment-delete-btn').addEventListener('click', (e) => {
2181
+ e.stopPropagation();
2182
+ this.deleteAttachment(att.id);
2183
+ });
2184
+ }
2185
+
2186
+ checkSystemClipboard() {
2187
+ const content = window.glassboard.getClipboardContent();
2188
+ if (content) {
2189
+ this.systemClipboard = content;
2190
+ } else {
2191
+ this.systemClipboard = null;
2192
+ }
2193
+ }
2194
+
2195
+ showPasteOverlay(x, y) {
2196
+ if (!this.systemClipboard) return;
2197
+
2198
+ // Remove existing overlay
2199
+ this.hidePasteOverlay();
2200
+
2201
+ const overlay = document.createElement('div');
2202
+ overlay.id = 'paste-overlay';
2203
+ overlay.className = 'paste-overlay';
2204
+
2205
+ if (this.systemClipboard.type === 'image') {
2206
+ // Image preview
2207
+ const img = document.createElement('img');
2208
+ img.src = this.systemClipboard.dataUrl;
2209
+ img.className = 'paste-preview-image';
2210
+
2211
+ const label = document.createElement('div');
2212
+ label.className = 'paste-label';
2213
+ label.innerHTML = `
2214
+ <span class="paste-icon">📋</span>
2215
+ <span>Paste Image</span>
2216
+ <span class="paste-size">${this.systemClipboard.width}×${this.systemClipboard.height}</span>
2217
+ `;
2218
+
2219
+ overlay.appendChild(img);
2220
+ overlay.appendChild(label);
2221
+ } else if (this.systemClipboard.type === 'text') {
2222
+ // Text preview
2223
+ const preview = document.createElement('div');
2224
+ preview.className = 'paste-preview-text';
2225
+ preview.textContent = this.systemClipboard.content.substring(0, 100) +
2226
+ (this.systemClipboard.content.length > 100 ? '...' : '');
2227
+
2228
+ const label = document.createElement('div');
2229
+ label.className = 'paste-label';
2230
+ label.innerHTML = `
2231
+ <span class="paste-icon">📋</span>
2232
+ <span>Paste Text</span>
2233
+ `;
2234
+
2235
+ overlay.appendChild(preview);
2236
+ overlay.appendChild(label);
2237
+ }
2238
+
2239
+ // Position the overlay
2240
+ overlay.style.left = x + 'px';
2241
+ overlay.style.top = y + 'px';
2242
+
2243
+ // Click to paste
2244
+ overlay.addEventListener('click', (e) => {
2245
+ e.stopPropagation();
2246
+ this.pasteFromSystemClipboard(x, y);
2247
+ this.hidePasteOverlay();
2248
+ });
2249
+
2250
+ this.app.appendChild(overlay);
2251
+ this.pasteOverlayVisible = true;
2252
+
2253
+ // Auto-hide after a delay if not clicked
2254
+ setTimeout(() => {
2255
+ if (this.pasteOverlayVisible) {
2256
+ this.hidePasteOverlay();
2257
+ }
2258
+ }, 5000);
2259
+ }
2260
+
2261
+ hidePasteOverlay() {
2262
+ const overlay = document.getElementById('paste-overlay');
2263
+ if (overlay) {
2264
+ overlay.remove();
2265
+ }
2266
+ this.pasteOverlayVisible = false;
2267
+ }
2268
+
2269
+ pasteFromSystemClipboard(x, y) {
2270
+ if (!this.systemClipboard) return;
2271
+
2272
+ if (this.systemClipboard.type === 'image') {
2273
+ this.pasteImage(this.systemClipboard.dataUrl, x, y,
2274
+ this.systemClipboard.width, this.systemClipboard.height);
2275
+ } else if (this.systemClipboard.type === 'text') {
2276
+ this.pasteText(this.systemClipboard.content, x, y);
2277
+ }
2278
+ }
2279
+
2280
+ pasteImage(dataUrl, x, y, width, height) {
2281
+ // Scale down if too large
2282
+ const maxSize = 400;
2283
+ let displayWidth = width;
2284
+ let displayHeight = height;
2285
+
2286
+ if (width > maxSize || height > maxSize) {
2287
+ const scale = Math.min(maxSize / width, maxSize / height);
2288
+ displayWidth = width * scale;
2289
+ displayHeight = height * scale;
2290
+ }
2291
+
2292
+ // Create image element
2293
+ const id = Date.now().toString();
2294
+ const imageItem = document.createElement('div');
2295
+ imageItem.className = 'pasted-image';
2296
+ imageItem.dataset.id = id;
2297
+ imageItem.style.left = (x / this.zoomLevel) + 'px';
2298
+ imageItem.style.top = (y / this.zoomLevel) + 'px';
2299
+ imageItem.style.width = displayWidth + 'px';
2300
+ imageItem.style.height = displayHeight + 'px';
2301
+
2302
+ const img = document.createElement('img');
2303
+ img.src = dataUrl;
2304
+ img.draggable = false;
2305
+
2306
+ // Delete button
2307
+ const deleteBtn = document.createElement('button');
2308
+ deleteBtn.className = 'image-delete-btn';
2309
+ deleteBtn.innerHTML = '✕';
2310
+ deleteBtn.addEventListener('click', (e) => {
2311
+ e.stopPropagation();
2312
+ this.deleteImage(id);
2313
+ });
2314
+
2315
+ // Resize handle
2316
+ const resizeHandle = document.createElement('div');
2317
+ resizeHandle.className = 'image-resize-handle';
2318
+
2319
+ imageItem.appendChild(img);
2320
+ imageItem.appendChild(deleteBtn);
2321
+ imageItem.appendChild(resizeHandle);
2322
+ this.textContainer.appendChild(imageItem);
2323
+
2324
+ // Store reference
2325
+ this.images.push({
2326
+ id,
2327
+ x: x / this.zoomLevel,
2328
+ y: y / this.zoomLevel,
2329
+ width: displayWidth,
2330
+ height: displayHeight,
2331
+ dataUrl
2332
+ });
2333
+
2334
+ // Setup drag for image
2335
+ this.setupImageDrag(imageItem, id);
2336
+ this.setupImageResize(imageItem, id, resizeHandle);
2337
+ this.saveState();
2338
+ }
2339
+
2340
+ setupImageDrag(element, id) {
2341
+ let isDragging = false;
2342
+ let startX, startY, initialX, initialY;
2343
+
2344
+ element.addEventListener('mousedown', (e) => {
2345
+ if (e.target.closest('.image-delete-btn') || e.target.closest('.image-resize-handle')) return;
2346
+
2347
+ isDragging = true;
2348
+ startX = e.clientX;
2349
+ startY = e.clientY;
2350
+ initialX = parseInt(element.style.left);
2351
+ initialY = parseInt(element.style.top);
2352
+ element.style.cursor = 'grabbing';
2353
+ e.preventDefault();
2354
+ });
2355
+
2356
+ document.addEventListener('mousemove', (e) => {
2357
+ if (!isDragging) return;
2358
+
2359
+ const dx = (e.clientX - startX) / this.zoomLevel;
2360
+ const dy = (e.clientY - startY) / this.zoomLevel;
2361
+
2362
+ element.style.left = (initialX + dx) + 'px';
2363
+ element.style.top = (initialY + dy) + 'px';
2364
+ });
2365
+
2366
+ document.addEventListener('mouseup', () => {
2367
+ if (isDragging) {
2368
+ isDragging = false;
2369
+ element.style.cursor = 'grab';
2370
+
2371
+ // Update stored position
2372
+ const imageData = this.images.find(i => i.id === id);
2373
+ if (imageData) {
2374
+ imageData.x = parseInt(element.style.left);
2375
+ imageData.y = parseInt(element.style.top);
2376
+ }
2377
+ }
2378
+ });
2379
+ }
2380
+
2381
+ setupImageResize(element, id, handle) {
2382
+ let isResizing = false;
2383
+ let startX, startY, initialWidth, initialHeight;
2384
+
2385
+ handle.addEventListener('mousedown', (e) => {
2386
+ isResizing = true;
2387
+ startX = e.clientX;
2388
+ startY = e.clientY;
2389
+ initialWidth = element.offsetWidth;
2390
+ initialHeight = element.offsetHeight;
2391
+ e.preventDefault();
2392
+ e.stopPropagation();
2393
+ });
2394
+
2395
+ document.addEventListener('mousemove', (e) => {
2396
+ if (!isResizing) return;
2397
+
2398
+ const dx = (e.clientX - startX) / this.zoomLevel;
2399
+ const dy = (e.clientY - startY) / this.zoomLevel;
2400
+
2401
+ // Maintain aspect ratio
2402
+ const scale = Math.max(dx / initialWidth, dy / initialHeight);
2403
+ const newWidth = Math.max(50, initialWidth * (1 + scale));
2404
+ const newHeight = Math.max(50, initialHeight * (1 + scale));
2405
+
2406
+ element.style.width = newWidth + 'px';
2407
+ element.style.height = newHeight + 'px';
2408
+ });
2409
+
2410
+ document.addEventListener('mouseup', () => {
2411
+ if (isResizing) {
2412
+ isResizing = false;
2413
+
2414
+ // Update stored size
2415
+ const imageData = this.images.find(i => i.id === id);
2416
+ if (imageData) {
2417
+ imageData.width = element.offsetWidth;
2418
+ imageData.height = element.offsetHeight;
2419
+ }
2420
+ }
2421
+ });
2422
+ }
2423
+
2424
+ deleteImage(id) {
2425
+ this.images = this.images.filter(i => i.id !== id);
2426
+ const element = this.textContainer.querySelector(`.pasted-image[data-id="${id}"]`);
2427
+ if (element) {
2428
+ element.remove();
2429
+ }
2430
+ this.saveState();
2431
+ }
2432
+
2433
+ pasteText(text, x, y) {
2434
+ // Create a new text item at the paste location
2435
+ const id = Date.now().toString();
2436
+ const item = document.createElement('div');
2437
+ item.className = 'text-item';
2438
+ item.dataset.id = id;
2439
+ item.style.left = (x / this.zoomLevel) + 'px';
2440
+ item.style.top = (y / this.zoomLevel) + 'px';
2441
+ item.style.color = this.currentColor;
2442
+ // Apply counter-scale so text doesn't zoom
2443
+ item.style.transform = `scale(${1 / this.zoomLevel})`;
2444
+ item.style.transformOrigin = 'top left';
2445
+
2446
+ // Add drag handle
2447
+ const dragHandle = document.createElement('div');
2448
+ dragHandle.className = 'text-drag-handle';
2449
+ dragHandle.innerHTML = `
2450
+ <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
2451
+ <path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
2452
+ </svg>
2453
+ `;
2454
+
2455
+ // Add format bar
2456
+ const formatBar = document.createElement('div');
2457
+ formatBar.className = 'text-format-bar';
2458
+ formatBar.innerHTML = `
2459
+ <button class="format-btn" data-format="bold" title="Bold">B</button>
2460
+ <button class="format-btn" data-format="italic" title="Italic">I</button>
2461
+ <button class="format-btn" data-format="underline" title="Underline">U</button>
2462
+ <button class="format-btn delete-btn" title="Delete">✕</button>
2463
+ `;
2464
+
2465
+ // Create contentEditable div with pasted text
2466
+ const editor = document.createElement('div');
2467
+ editor.className = 'text-input';
2468
+ editor.contentEditable = 'true';
2469
+ // Escape HTML to prevent XSS when pasting plain text
2470
+ editor.textContent = text;
2471
+ editor.style.color = this.currentColor;
2472
+
2473
+ // Add resize handle
2474
+ const resizeHandle = document.createElement('div');
2475
+ resizeHandle.className = 'text-resize-handle';
2476
+
2477
+ item.appendChild(dragHandle);
2478
+ item.appendChild(formatBar);
2479
+ item.appendChild(editor);
2480
+ item.appendChild(resizeHandle);
2481
+ this.textContainer.appendChild(item);
2482
+
2483
+ this.textItems.push({
2484
+ id,
2485
+ x: x / this.zoomLevel,
2486
+ y: y / this.zoomLevel,
2487
+ content: editor.innerHTML,
2488
+ color: this.currentColor
2489
+ });
2490
+
2491
+ // Setup event handlers
2492
+ this.setupPastedTextItem(item, id, editor, formatBar);
2493
+ this.saveState();
2494
+ }
2495
+
2496
+ // Settings panel methods
2497
+ setupSettings() {
2498
+ this.settingsPanel = document.getElementById('settings-panel');
2499
+ const closeBtn = document.getElementById('settings-close');
2500
+
2501
+ // Close button
2502
+ closeBtn.addEventListener('click', () => {
2503
+ this.hideSettings();
2504
+ });
2505
+
2506
+ // Close on click outside
2507
+ this.settingsPanel.addEventListener('click', (e) => {
2508
+ if (e.target === this.settingsPanel) {
2509
+ this.hideSettings();
2510
+ }
2511
+ });
2512
+
2513
+ // Close on Escape
2514
+ document.addEventListener('keydown', (e) => {
2515
+ if (e.key === 'Escape' && this.settingsPanel.classList.contains('visible')) {
2516
+ e.preventDefault();
2517
+ this.hideSettings();
2518
+ }
2519
+ });
2520
+
2521
+ // Gesture toggles
2522
+ document.getElementById('setting-pinch-zoom').addEventListener('change', (e) => {
2523
+ this.settings.pinchZoom = e.target.checked;
2524
+ this.autoSave();
2525
+ });
2526
+
2527
+ document.getElementById('setting-pan').addEventListener('change', (e) => {
2528
+ this.settings.pan = e.target.checked;
2529
+ this.autoSave();
2530
+ });
2531
+
2532
+ document.getElementById('setting-rotate').addEventListener('change', (e) => {
2533
+ this.settings.rotate = e.target.checked;
2534
+ this.autoSave();
2535
+ });
2536
+
2537
+ // Zoom controls toggle
2538
+ document.getElementById('setting-zoom-controls').addEventListener('change', (e) => {
2539
+ this.settings.showZoomControls = e.target.checked;
2540
+ const zoomControls = document.getElementById('zoom-controls');
2541
+ if (zoomControls) {
2542
+ zoomControls.style.display = e.target.checked ? '' : 'none';
2543
+ }
2544
+ this.autoSave();
2545
+ });
2546
+
2547
+ // Active background mode options
2548
+ document.querySelectorAll('input[name="bg-mode"]').forEach(radio => {
2549
+ radio.addEventListener('change', (e) => {
2550
+ const mode = e.target.value;
2551
+ this.settings.activeBgMode = mode;
2552
+ this.app.dataset.activeBg = mode;
2553
+ if (this.app.classList.contains('focused')) {
2554
+ window.glassboard.setBackgroundMode(mode);
2555
+ }
2556
+ this.applyBackgroundModes();
2557
+ this.applyBackgroundOpacity();
2558
+ this.autoSave();
2559
+ });
2560
+ });
2561
+
2562
+ // Inactive background mode options
2563
+ document.querySelectorAll('input[name="inactive-bg-mode"]').forEach(radio => {
2564
+ radio.addEventListener('change', (e) => {
2565
+ const mode = e.target.value;
2566
+ this.settings.inactiveBgMode = mode;
2567
+ this.app.dataset.inactiveBg = mode;
2568
+ if (!this.app.classList.contains('focused')) {
2569
+ window.glassboard.setBackgroundMode(mode);
2570
+ }
2571
+ this.applyBackgroundModes();
2572
+ this.applyBackgroundOpacity();
2573
+ this.autoSave();
2574
+ });
2575
+ });
2576
+
2577
+ // Active opacity slider
2578
+ const activeOpacitySlider = document.getElementById('setting-active-opacity');
2579
+ const activeOpacityValue = document.getElementById('active-opacity-value');
2580
+ if (activeOpacitySlider) {
2581
+ activeOpacitySlider.addEventListener('input', (e) => {
2582
+ const value = parseInt(e.target.value);
2583
+ this.settings.activeOpacity = value;
2584
+ if (activeOpacityValue) {
2585
+ activeOpacityValue.textContent = `${value}%`;
2586
+ }
2587
+ this.applyBackgroundOpacity();
2588
+ this.autoSave();
2589
+ });
2590
+ }
2591
+
2592
+ // Inactive opacity slider
2593
+ const inactiveOpacitySlider = document.getElementById('setting-inactive-opacity');
2594
+ const inactiveOpacityValue = document.getElementById('inactive-opacity-value');
2595
+ if (inactiveOpacitySlider) {
2596
+ inactiveOpacitySlider.addEventListener('input', (e) => {
2597
+ const value = parseInt(e.target.value);
2598
+ this.settings.inactiveOpacity = value;
2599
+ if (inactiveOpacityValue) {
2600
+ inactiveOpacityValue.textContent = `${value}%`;
2601
+ }
2602
+ this.applyBackgroundOpacity();
2603
+ this.autoSave();
2604
+ });
2605
+ }
2606
+
2607
+ // Clean slate setting
2608
+ const cleanSlateCheckbox = document.getElementById('setting-clean-slate');
2609
+ if (cleanSlateCheckbox) {
2610
+ cleanSlateCheckbox.addEventListener('change', (e) => {
2611
+ this.settings.openWithCleanSlate = e.target.checked;
2612
+ this.autoSave();
2613
+ });
2614
+ }
2615
+
2616
+ // Auto-save to folder setting
2617
+ const autoSaveFolderCheckbox = document.getElementById('setting-auto-save-folder');
2618
+ if (autoSaveFolderCheckbox) {
2619
+ autoSaveFolderCheckbox.addEventListener('change', (e) => {
2620
+ this.settings.autoSaveToFolder = e.target.checked;
2621
+ this.autoSave();
2622
+ });
2623
+ }
2624
+
2625
+ // Open folder button
2626
+ const openFolderBtn = document.getElementById('open-folder-btn');
2627
+ if (openFolderBtn) {
2628
+ openFolderBtn.addEventListener('click', () => {
2629
+ this.openFloatnoteFolder();
2630
+ });
2631
+ }
2632
+ }
2633
+
2634
+ // Apply background opacity based on focus state
2635
+ applyBackgroundOpacity() {
2636
+ const isFocused = this.app.classList.contains('focused');
2637
+ const opacity = isFocused ? this.settings.activeOpacity : this.settings.inactiveOpacity;
2638
+ this.app.style.setProperty('--bg-opacity', opacity / 100);
2639
+ }
2640
+
2641
+ toggleSettings() {
2642
+ if (this.settingsPanel.classList.contains('visible')) {
2643
+ this.hideSettings();
2644
+ } else {
2645
+ this.showSettings();
2646
+ }
2647
+ }
2648
+
2649
+ showSettings() {
2650
+ this.settingsPanel.classList.add('visible');
2651
+ }
2652
+
2653
+ hideSettings() {
2654
+ this.settingsPanel.classList.remove('visible');
2655
+ }
2656
+
2657
+ // Trackpad gesture support
2658
+ setupGestures() {
2659
+ // Pinch-to-zoom via wheel event (trackpad sends wheel events with ctrlKey)
2660
+ this.app.addEventListener('wheel', (e) => {
2661
+ // Pinch-to-zoom (trackpad sends ctrlKey with pinch)
2662
+ if (e.ctrlKey && this.settings.pinchZoom) {
2663
+ e.preventDefault();
2664
+ const delta = -e.deltaY * 0.01;
2665
+ const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.zoomLevel + delta));
2666
+ this.zoomLevel = newZoom;
2667
+ this.applyTransform();
2668
+ return;
2669
+ }
2670
+
2671
+ // Two-finger pan (no modifier key)
2672
+ if (this.settings.pan && !e.ctrlKey && !e.metaKey) {
2673
+ // Only pan if not drawing
2674
+ if (this.isDrawing) return;
2675
+
2676
+ e.preventDefault();
2677
+ this.panX -= e.deltaX;
2678
+ this.panY -= e.deltaY;
2679
+ this.applyTransform();
2680
+ }
2681
+ }, { passive: false });
2682
+
2683
+ // Safari/WebKit gesture events for pinch and rotate
2684
+ this.app.addEventListener('gesturestart', (e) => {
2685
+ e.preventDefault();
2686
+ this.gestureStartZoom = this.zoomLevel;
2687
+ this.gestureStartRotation = this.rotation;
2688
+ });
2689
+
2690
+ this.app.addEventListener('gesturechange', (e) => {
2691
+ e.preventDefault();
2692
+
2693
+ // Pinch-to-zoom
2694
+ if (this.settings.pinchZoom) {
2695
+ const newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, this.gestureStartZoom * e.scale));
2696
+ this.zoomLevel = newZoom;
2697
+ }
2698
+
2699
+ // Rotation
2700
+ if (this.settings.rotate) {
2701
+ this.rotation = this.gestureStartRotation + e.rotation;
2702
+ }
2703
+
2704
+ this.applyTransform();
2705
+ });
2706
+
2707
+ this.app.addEventListener('gestureend', (e) => {
2708
+ e.preventDefault();
2709
+ });
2710
+
2711
+ // Double-tap to reset (via double-click)
2712
+ let lastTap = 0;
2713
+ this.app.addEventListener('click', (e) => {
2714
+ const now = Date.now();
2715
+ if (now - lastTap < 300) {
2716
+ // Double-tap detected - reset transform
2717
+ this.resetTransform();
2718
+ }
2719
+ lastTap = now;
2720
+ });
2721
+ }
2722
+
2723
+ // Undo/Redo functionality
2724
+ saveState() {
2725
+ if (this.isUndoRedoAction) return;
2726
+
2727
+ // Create a snapshot of current state
2728
+ const state = {
2729
+ lines: JSON.parse(JSON.stringify(this.lines)),
2730
+ textItems: this.textItems.map(t => ({ ...t })),
2731
+ images: this.images.map(i => ({ ...i }))
2732
+ };
2733
+
2734
+ // Remove any states after current index (new branch)
2735
+ this.history = this.history.slice(0, this.historyIndex + 1);
2736
+
2737
+ // Add new state
2738
+ this.history.push(state);
2739
+ this.historyIndex++;
2740
+
2741
+ // Limit history size
2742
+ if (this.history.length > this.maxHistorySize) {
2743
+ this.history.shift();
2744
+ this.historyIndex--;
2745
+ }
2746
+
2747
+ // Trigger auto-save
2748
+ this.autoSave();
2749
+ }
2750
+
2751
+ undo() {
2752
+ if (this.historyIndex <= 0) return;
2753
+
2754
+ this.isUndoRedoAction = true;
2755
+ this.historyIndex--;
2756
+ this.restoreState(this.history[this.historyIndex]);
2757
+ this.isUndoRedoAction = false;
2758
+ this.autoSave();
2759
+ }
2760
+
2761
+ redo() {
2762
+ if (this.historyIndex >= this.history.length - 1) return;
2763
+
2764
+ this.isUndoRedoAction = true;
2765
+ this.historyIndex++;
2766
+ this.restoreState(this.history[this.historyIndex]);
2767
+ this.isUndoRedoAction = false;
2768
+ this.autoSave();
2769
+ }
2770
+
2771
+ restoreState(state) {
2772
+ // Restore lines
2773
+ this.lines = JSON.parse(JSON.stringify(state.lines));
2774
+
2775
+ // Clear and restore text items
2776
+ this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
2777
+ this.textItems = [];
2778
+ state.textItems.forEach(item => {
2779
+ this.restoreTextItem(item);
2780
+ });
2781
+
2782
+ // Clear and restore images
2783
+ this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
2784
+ this.images = [];
2785
+ state.images.forEach(img => {
2786
+ this.restoreImage(img);
2787
+ });
2788
+
2789
+ this.redraw();
2790
+ }
2791
+
2792
+ restoreTextItem(item) {
2793
+ const element = document.createElement('div');
2794
+ element.className = 'text-item';
2795
+ element.dataset.id = item.id;
2796
+ element.style.left = item.x + 'px';
2797
+ element.style.top = item.y + 'px';
2798
+ element.style.color = item.color;
2799
+ if (item.width) element.style.width = item.width + 'px';
2800
+ // Apply counter-scale so text doesn't zoom
2801
+ element.style.transform = `scale(${1 / this.zoomLevel})`;
2802
+ element.style.transformOrigin = 'top left';
2803
+
2804
+ const dragHandle = document.createElement('div');
2805
+ dragHandle.className = 'text-drag-handle';
2806
+ dragHandle.innerHTML = `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/></svg>`;
2807
+
2808
+ const formatBar = document.createElement('div');
2809
+ formatBar.className = 'text-format-bar';
2810
+ formatBar.innerHTML = `
2811
+ <button class="format-btn" data-format="bold" title="Bold">B</button>
2812
+ <button class="format-btn" data-format="italic" title="Italic">I</button>
2813
+ <button class="format-btn" data-format="underline" title="Underline">U</button>
2814
+ <button class="format-btn delete-btn" title="Delete">✕</button>
2815
+ `;
2816
+
2817
+ const editor = document.createElement('div');
2818
+ editor.className = 'text-input';
2819
+ editor.contentEditable = 'true';
2820
+ // Support both old 'text' and new 'content' formats for backwards compatibility
2821
+ editor.innerHTML = item.content || item.text || '';
2822
+ editor.style.color = item.color;
2823
+
2824
+ const resizeHandle = document.createElement('div');
2825
+ resizeHandle.className = 'text-resize-handle';
2826
+
2827
+ element.appendChild(dragHandle);
2828
+ element.appendChild(formatBar);
2829
+ element.appendChild(editor);
2830
+ element.appendChild(resizeHandle);
2831
+ this.textContainer.appendChild(element);
2832
+
2833
+ // Normalize to new format
2834
+ const normalizedItem = { ...item, content: item.content || item.text || '' };
2835
+ delete normalizedItem.text;
2836
+ this.textItems.push(normalizedItem);
2837
+ this.setupPastedTextItem(element, item.id, editor, formatBar);
2838
+ }
2839
+
2840
+ restoreImage(img) {
2841
+ const imageItem = document.createElement('div');
2842
+ imageItem.className = 'pasted-image';
2843
+ imageItem.dataset.id = img.id;
2844
+ imageItem.style.left = img.x + 'px';
2845
+ imageItem.style.top = img.y + 'px';
2846
+ imageItem.style.width = img.width + 'px';
2847
+ imageItem.style.height = img.height + 'px';
2848
+
2849
+ const imgEl = document.createElement('img');
2850
+ imgEl.src = img.dataUrl;
2851
+ imgEl.draggable = false;
2852
+
2853
+ const deleteBtn = document.createElement('button');
2854
+ deleteBtn.className = 'image-delete-btn';
2855
+ deleteBtn.innerHTML = '✕';
2856
+ deleteBtn.addEventListener('click', (e) => {
2857
+ e.stopPropagation();
2858
+ this.deleteImage(img.id);
2859
+ });
2860
+
2861
+ const resizeHandle = document.createElement('div');
2862
+ resizeHandle.className = 'image-resize-handle';
2863
+
2864
+ imageItem.appendChild(imgEl);
2865
+ imageItem.appendChild(deleteBtn);
2866
+ imageItem.appendChild(resizeHandle);
2867
+ this.textContainer.appendChild(imageItem);
2868
+
2869
+ this.images.push({ ...img });
2870
+ this.setupImageDrag(imageItem, img.id);
2871
+ this.setupImageResize(imageItem, img.id, resizeHandle);
2872
+ }
2873
+
2874
+ // Smart paste - checks system clipboard first, then internal clipboard
2875
+ smartPaste() {
2876
+ // Check system clipboard first
2877
+ this.checkSystemClipboard();
2878
+
2879
+ if (this.systemClipboard) {
2880
+ // Paste from system clipboard at center of canvas
2881
+ const rect = this.app.getBoundingClientRect();
2882
+ const x = rect.width / 2;
2883
+ const y = rect.height / 2;
2884
+ this.pasteFromSystemClipboard(x, y);
2885
+ this.saveState();
2886
+ } else if (this.clipboard) {
2887
+ // Paste from internal clipboard
2888
+ this.paste();
2889
+ this.saveState();
2890
+ }
2891
+ }
2892
+
2893
+ // Data persistence
2894
+ autoSave() {
2895
+ clearTimeout(this.saveTimeout);
2896
+ this.saveTimeout = setTimeout(() => {
2897
+ this.saveData();
2898
+ }, 1000); // Debounce saves to 1 second
2899
+ }
2900
+
2901
+ async saveData() {
2902
+ const data = {
2903
+ notes: this.notes,
2904
+ currentNoteIndex: this.currentNoteIndex,
2905
+ settings: this.settings,
2906
+ transform: {
2907
+ zoomLevel: this.zoomLevel,
2908
+ panX: this.panX,
2909
+ panY: this.panY,
2910
+ rotation: this.rotation
2911
+ }
2912
+ };
2913
+
2914
+ try {
2915
+ await window.glassboard.saveData(data);
2916
+
2917
+ // Also save to ~/.floatnote folder if enabled
2918
+ if (this.settings.autoSaveToFolder && this.notes[this.currentNoteIndex]) {
2919
+ await window.glassboard.exportToFloatnote(this.notes[this.currentNoteIndex]);
2920
+ }
2921
+ } catch (error) {
2922
+ console.error('Failed to save data:', error);
2923
+ }
2924
+ }
2925
+
2926
+ // Export current note as PNG
2927
+ async exportAsPNG() {
2928
+ try {
2929
+ // Create a temporary canvas to render everything
2930
+ const exportCanvas = document.createElement('canvas');
2931
+ const rect = this.canvas.getBoundingClientRect();
2932
+ exportCanvas.width = rect.width;
2933
+ exportCanvas.height = rect.height;
2934
+ const exportCtx = exportCanvas.getContext('2d');
2935
+
2936
+ // Fill with transparent background
2937
+ exportCtx.clearRect(0, 0, exportCanvas.width, exportCanvas.height);
2938
+
2939
+ // Draw all lines from current note
2940
+ this.lines.forEach(line => {
2941
+ if (line.points.length < 2) return;
2942
+ exportCtx.beginPath();
2943
+ exportCtx.strokeStyle = line.color;
2944
+ exportCtx.lineWidth = line.width;
2945
+ exportCtx.lineCap = 'round';
2946
+ exportCtx.lineJoin = 'round';
2947
+ exportCtx.moveTo(line.points[0].x, line.points[0].y);
2948
+ for (let i = 1; i < line.points.length; i++) {
2949
+ exportCtx.lineTo(line.points[i].x, line.points[i].y);
2950
+ }
2951
+ exportCtx.stroke();
2952
+ });
2953
+
2954
+ // Draw text items
2955
+ this.textItems.forEach(item => {
2956
+ const element = this.textContainer.querySelector(`[data-id="${item.id}"]`);
2957
+ if (element) {
2958
+ const editor = element.querySelector('.text-input');
2959
+ if (editor && editor.textContent) {
2960
+ exportCtx.font = '16px system-ui, -apple-system, sans-serif';
2961
+ exportCtx.fillStyle = item.color;
2962
+ const lines = editor.textContent.split('\n');
2963
+ lines.forEach((line, i) => {
2964
+ exportCtx.fillText(line, item.x, item.y + 20 + (i * 20));
2965
+ });
2966
+ }
2967
+ }
2968
+ });
2969
+
2970
+ // Draw images
2971
+ for (const img of this.images) {
2972
+ const imgElement = document.querySelector(`.pasted-image[data-id="${img.id}"] img`);
2973
+ if (imgElement) {
2974
+ exportCtx.drawImage(imgElement, img.x, img.y, img.width, img.height);
2975
+ }
2976
+ }
2977
+
2978
+ // Get data URL and export
2979
+ const dataUrl = exportCanvas.toDataURL('image/png');
2980
+ const result = await window.glassboard.exportPNG(dataUrl);
2981
+
2982
+ if (result.success) {
2983
+ console.log('Exported to:', result.path);
2984
+ }
2985
+ } catch (error) {
2986
+ console.error('Failed to export PNG:', error);
2987
+ }
2988
+ }
2989
+
2990
+ // Open ~/.floatnote folder in Finder
2991
+ async openFloatnoteFolder() {
2992
+ try {
2993
+ await window.glassboard.openFloatnoteFolder();
2994
+ } catch (error) {
2995
+ console.error('Failed to open folder:', error);
2996
+ }
2997
+ }
2998
+
2999
+ async loadSavedData() {
3000
+ try {
3001
+ const result = await window.glassboard.loadData();
3002
+ if (result.success && result.data) {
3003
+ const data = result.data;
3004
+
3005
+ // Restore settings first
3006
+ if (data.settings) {
3007
+ this.settings = { ...this.settings, ...data.settings };
3008
+ // Update settings UI
3009
+ document.getElementById('setting-pinch-zoom').checked = this.settings.pinchZoom;
3010
+ document.getElementById('setting-pan').checked = this.settings.pan;
3011
+ document.getElementById('setting-rotate').checked = this.settings.rotate;
3012
+ document.getElementById('setting-zoom-controls').checked = this.settings.showZoomControls;
3013
+ const cleanSlateCheckbox = document.getElementById('setting-clean-slate');
3014
+ if (cleanSlateCheckbox) {
3015
+ cleanSlateCheckbox.checked = this.settings.openWithCleanSlate;
3016
+ }
3017
+ const autoSaveFolderCheckbox = document.getElementById('setting-auto-save-folder');
3018
+ if (autoSaveFolderCheckbox) {
3019
+ autoSaveFolderCheckbox.checked = this.settings.autoSaveToFolder || false;
3020
+ }
3021
+ const zoomControls = document.getElementById('zoom-controls');
3022
+ if (zoomControls) {
3023
+ zoomControls.style.display = this.settings.showZoomControls ? '' : 'none';
3024
+ }
3025
+
3026
+ // Restore background settings
3027
+ const activeBgRadio = document.querySelector(`input[name="bg-mode"][value="${this.settings.activeBgMode || 'transparent'}"]`);
3028
+ if (activeBgRadio) activeBgRadio.checked = true;
3029
+ this.app.dataset.activeBg = this.settings.activeBgMode || 'transparent';
3030
+
3031
+ const inactiveBgRadio = document.querySelector(`input[name="inactive-bg-mode"][value="${this.settings.inactiveBgMode || 'transparent'}"]`);
3032
+ if (inactiveBgRadio) inactiveBgRadio.checked = true;
3033
+ this.app.dataset.inactiveBg = this.settings.inactiveBgMode || 'transparent';
3034
+
3035
+ // Restore opacity settings
3036
+ const activeOpacitySlider = document.getElementById('setting-active-opacity');
3037
+ const activeOpacityValue = document.getElementById('active-opacity-value');
3038
+ if (activeOpacitySlider) {
3039
+ activeOpacitySlider.value = this.settings.activeOpacity || 100;
3040
+ if (activeOpacityValue) activeOpacityValue.textContent = `${this.settings.activeOpacity || 100}%`;
3041
+ }
3042
+
3043
+ const inactiveOpacitySlider = document.getElementById('setting-inactive-opacity');
3044
+ const inactiveOpacityValue = document.getElementById('inactive-opacity-value');
3045
+ if (inactiveOpacitySlider) {
3046
+ inactiveOpacitySlider.value = this.settings.inactiveOpacity || 50;
3047
+ if (inactiveOpacityValue) inactiveOpacityValue.textContent = `${this.settings.inactiveOpacity || 50}%`;
3048
+ }
3049
+
3050
+ this.applyBackgroundModes();
3051
+ this.applyBackgroundOpacity();
3052
+
3053
+ // Apply native vibrancy based on restored settings
3054
+ const currentBgMode = this.app.classList.contains('focused')
3055
+ ? this.settings.activeBgMode
3056
+ : this.settings.inactiveBgMode;
3057
+ if (currentBgMode === 'blur') {
3058
+ window.glassboard.setBackgroundMode('blur');
3059
+ }
3060
+ }
3061
+
3062
+ // Check if we should open with clean slate
3063
+ if (this.settings.openWithCleanSlate) {
3064
+ // Keep the notes but create/go to a new blank note
3065
+ if (data.notes && data.notes.length > 0) {
3066
+ this.notes = data.notes;
3067
+ this.notes.push(this.createEmptyNote());
3068
+ this.currentNoteIndex = this.notes.length - 1;
3069
+ }
3070
+ // Don't restore transform - start fresh
3071
+ this.redraw();
3072
+ this.saveState();
3073
+ return;
3074
+ }
3075
+
3076
+ // Restore notes (new multi-note format)
3077
+ if (data.notes && data.notes.length > 0) {
3078
+ this.notes = data.notes;
3079
+ this.currentNoteIndex = data.currentNoteIndex || 0;
3080
+
3081
+ // Make sure index is valid
3082
+ if (this.currentNoteIndex >= this.notes.length) {
3083
+ this.currentNoteIndex = this.notes.length - 1;
3084
+ }
3085
+
3086
+ // Load current note display
3087
+ this.loadCurrentNote();
3088
+ }
3089
+ // Legacy format support (single note)
3090
+ else if (data.lines || data.textItems || data.images) {
3091
+ // Migrate old format to new format
3092
+ this.notes = [{
3093
+ id: Date.now().toString(),
3094
+ lines: data.lines || [],
3095
+ textItems: data.textItems || [],
3096
+ images: data.images || [],
3097
+ createdAt: Date.now(),
3098
+ lastModified: Date.now()
3099
+ }];
3100
+ this.currentNoteIndex = 0;
3101
+ this.loadCurrentNote();
3102
+ }
3103
+
3104
+ // Restore transform
3105
+ if (data.transform) {
3106
+ this.zoomLevel = data.transform.zoomLevel || 1;
3107
+ this.panX = data.transform.panX || 0;
3108
+ this.panY = data.transform.panY || 0;
3109
+ this.rotation = data.transform.rotation || 0;
3110
+ this.applyTransform();
3111
+ }
3112
+
3113
+ // Initialize history with loaded state
3114
+ this.saveState();
3115
+ }
3116
+ } catch (error) {
3117
+ console.error('Failed to load data:', error);
3118
+ }
3119
+ }
3120
+
3121
+ setupLeftResize() {
3122
+ const handle = document.getElementById('left-resize-handle');
3123
+ if (!handle) return;
3124
+
3125
+ let isResizing = false;
3126
+ let startX = 0;
3127
+
3128
+ handle.addEventListener('mousedown', (e) => {
3129
+ isResizing = true;
3130
+ startX = e.screenX;
3131
+ e.preventDefault();
3132
+ });
3133
+
3134
+ document.addEventListener('mousemove', (e) => {
3135
+ if (!isResizing) return;
3136
+
3137
+ const deltaX = e.screenX - startX;
3138
+ if (deltaX !== 0) {
3139
+ window.glassboard.resizeWindowLeft(deltaX);
3140
+ startX = e.screenX;
3141
+ }
3142
+ });
3143
+
3144
+ document.addEventListener('mouseup', () => {
3145
+ isResizing = false;
3146
+ });
3147
+ }
3148
+
3149
+ // Select all items
3150
+ selectAll() {
3151
+ // Select all drawn objects (by selecting the most recent object group)
3152
+ const objectIds = [...new Set(this.lines.map(l => l.objectId))];
3153
+ this.selectedObjects = objectIds;
3154
+
3155
+ // Select all text items
3156
+ this.textItems.forEach(item => {
3157
+ const element = this.textContainer.querySelector(`[data-id="${item.id}"]`);
3158
+ if (element) {
3159
+ element.classList.add('selected');
3160
+ }
3161
+ });
3162
+
3163
+ // Select all images
3164
+ this.images.forEach(img => {
3165
+ const element = this.textContainer.querySelector(`.pasted-image[data-id="${img.id}"]`);
3166
+ if (element) {
3167
+ element.classList.add('selected');
3168
+ }
3169
+ });
3170
+
3171
+ this.allSelected = true;
3172
+ // Redraw to show selection highlights on drawn objects
3173
+ this.redraw();
3174
+ }
3175
+
3176
+ // Delete all selected items
3177
+ deleteAllSelected() {
3178
+ if (!this.allSelected) return;
3179
+
3180
+ // Delete all lines
3181
+ this.lines = [];
3182
+
3183
+ // Delete all text items
3184
+ this.textContainer.querySelectorAll('.text-item').forEach(el => el.remove());
3185
+ this.textItems = [];
3186
+
3187
+ // Delete all images
3188
+ this.textContainer.querySelectorAll('.pasted-image').forEach(el => el.remove());
3189
+ this.images = [];
3190
+
3191
+ this.allSelected = false;
3192
+ this.selectedObjectId = null;
3193
+ this.selectedTextId = null;
3194
+
3195
+ this.redraw();
3196
+ this.saveState();
3197
+ }
3198
+ }
3199
+
3200
+ // Initialize when DOM is ready
3201
+ document.addEventListener('DOMContentLoaded', () => {
3202
+ new Glassboard();
3203
+ });