node-mac-recorder 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,7 @@ A powerful native macOS screen recording Node.js package with advanced window se
11
11
  - šŸŽÆ **Area Selection** - Record custom screen regions
12
12
  - šŸ–±ļø **Multi-Display Support** - Automatic display detection and selection
13
13
  - šŸŽØ **Cursor Control** - Toggle cursor visibility in recordings
14
+ - šŸ–±ļø **Cursor Tracking** - Track mouse position, cursor types, and click events
14
15
 
15
16
  šŸŽµ **Granular Audio Controls**
16
17
 
@@ -231,6 +232,64 @@ const thumbnail = await recorder.getDisplayThumbnail(0, {
231
232
  // Perfect for display selection UI
232
233
  ```
233
234
 
235
+ ### Cursor Tracking Methods
236
+
237
+ #### `startCursorTracking(outputPath)`
238
+
239
+ Starts tracking cursor movements and saves data to JSON file.
240
+
241
+ ```javascript
242
+ await recorder.startCursorTracking("./cursor-data.json");
243
+ // Cursor tracking started - will record position, cursor type, and events
244
+ ```
245
+
246
+ #### `stopCursorTracking()`
247
+
248
+ Stops cursor tracking and saves collected data.
249
+
250
+ ```javascript
251
+ await recorder.stopCursorTracking();
252
+ // Data saved to specified JSON file
253
+ ```
254
+
255
+ #### `getCursorPosition()`
256
+
257
+ Gets current cursor position and type.
258
+
259
+ ```javascript
260
+ const position = recorder.getCursorPosition();
261
+ console.log(position);
262
+ // {
263
+ // x: 1234,
264
+ // y: 567,
265
+ // cursorType: "default" // "default", "pointer", "grabbing", "text"
266
+ // }
267
+ ```
268
+
269
+ #### `getCursorTrackingStatus()`
270
+
271
+ Returns cursor tracking status and data count.
272
+
273
+ ```javascript
274
+ const status = recorder.getCursorTrackingStatus();
275
+ console.log(status);
276
+ // {
277
+ // isTracking: true,
278
+ // dataCount: 1250,
279
+ // hasEventTap: true,
280
+ // hasRunLoopSource: true
281
+ // }
282
+ ```
283
+
284
+ #### `saveCursorData(outputPath)`
285
+
286
+ Manually saves current cursor data to file.
287
+
288
+ ```javascript
289
+ await recorder.saveCursorData("./cursor-backup.json");
290
+ // Data saved to file
291
+ ```
292
+
234
293
  ## Usage Examples
235
294
 
236
295
  ### Window-Specific Recording
@@ -404,6 +463,99 @@ async function createDisplaySelector() {
404
463
  }
405
464
  ```
406
465
 
466
+ ### Cursor Tracking Usage
467
+
468
+ ```javascript
469
+ const recorder = new MacRecorder();
470
+
471
+ async function trackUserInteraction() {
472
+ // Start cursor tracking
473
+ await recorder.startCursorTracking("./user-interactions.json");
474
+ console.log("Cursor tracking started...");
475
+
476
+ // Monitor real-time cursor position
477
+ const monitorInterval = setInterval(() => {
478
+ const position = recorder.getCursorPosition();
479
+ console.log(
480
+ `Cursor: ${position.x}, ${position.y} (${position.cursorType})`
481
+ );
482
+
483
+ const status = recorder.getCursorTrackingStatus();
484
+ console.log(`Tracking status: ${status.dataCount} positions recorded`);
485
+ }, 100); // Check every 100ms
486
+
487
+ // Track for 10 seconds
488
+ setTimeout(async () => {
489
+ clearInterval(monitorInterval);
490
+
491
+ // Stop tracking and save data
492
+ await recorder.stopCursorTracking();
493
+ console.log("Cursor tracking completed!");
494
+
495
+ // Load and analyze the data
496
+ const fs = require("fs");
497
+ const data = JSON.parse(
498
+ fs.readFileSync("./user-interactions.json", "utf8")
499
+ );
500
+
501
+ console.log(`Total interactions recorded: ${data.length}`);
502
+
503
+ // Analyze cursor types
504
+ const cursorTypes = {};
505
+ data.forEach((item) => {
506
+ cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
507
+ });
508
+
509
+ console.log("Cursor types distribution:", cursorTypes);
510
+
511
+ // Analyze event types
512
+ const eventTypes = {};
513
+ data.forEach((item) => {
514
+ eventTypes[item.type] = (eventTypes[item.type] || 0) + 1;
515
+ });
516
+
517
+ console.log("Event types distribution:", eventTypes);
518
+ }, 10000);
519
+ }
520
+
521
+ trackUserInteraction();
522
+ ```
523
+
524
+ ### Combined Screen Recording + Cursor Tracking
525
+
526
+ ```javascript
527
+ const recorder = new MacRecorder();
528
+
529
+ async function recordWithCursorTracking() {
530
+ // Start both screen recording and cursor tracking
531
+ await Promise.all([
532
+ recorder.startRecording("./screen-recording.mov", {
533
+ captureCursor: false, // Don't show cursor in video
534
+ includeSystemAudio: true,
535
+ quality: "high",
536
+ }),
537
+ recorder.startCursorTracking("./cursor-data.json"),
538
+ ]);
539
+
540
+ console.log("Recording screen and tracking cursor...");
541
+
542
+ // Record for 30 seconds
543
+ setTimeout(async () => {
544
+ await Promise.all([
545
+ recorder.stopRecording(),
546
+ recorder.stopCursorTracking(),
547
+ ]);
548
+
549
+ console.log("Screen recording and cursor tracking completed!");
550
+ console.log("Files created:");
551
+ console.log("- screen-recording.mov");
552
+ console.log("- cursor-data.json");
553
+ }, 30000);
554
+ }
555
+
556
+ recordWithCursorTracking();
557
+ ```
558
+
407
559
  ## Integration Examples
408
560
 
409
561
  ### Electron Integration
@@ -577,6 +729,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
577
729
 
578
730
  ### Latest Updates
579
731
 
732
+ - āœ… **Cursor Tracking**: Track mouse position, cursor types, and click events with JSON export
580
733
  - āœ… **Window Recording**: Automatic coordinate conversion for multi-display setups
581
734
  - āœ… **Audio Controls**: Separate microphone and system audio controls
582
735
  - āœ… **Display Selection**: Multi-monitor support with automatic detection
package/binding.gyp CHANGED
@@ -5,7 +5,8 @@
5
5
  "sources": [
6
6
  "src/mac_recorder.mm",
7
7
  "src/screen_capture.mm",
8
- "src/audio_capture.mm"
8
+ "src/audio_capture.mm",
9
+ "src/cursor_tracker.mm"
9
10
  ],
10
11
  "include_dirs": [
11
12
  "<!@(node -p \"require('node-addon-api').include\")"
@@ -30,7 +31,10 @@
30
31
  "-framework CoreVideo",
31
32
  "-framework Foundation",
32
33
  "-framework AppKit",
33
- "-framework ScreenCaptureKit"
34
+ "-framework ScreenCaptureKit",
35
+ "-framework ApplicationServices",
36
+ "-framework Carbon",
37
+ "-framework Accessibility"
34
38
  ]
35
39
  },
36
40
  "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
@@ -0,0 +1,93 @@
1
+ [
2
+ {
3
+ "x": 1119,
4
+ "y": 1090,
5
+ "timestamp": 106,
6
+ "cursorType": "default",
7
+ "type": "move"
8
+ },
9
+ {
10
+ "x": 1119,
11
+ "y": 1080,
12
+ "timestamp": 407,
13
+ "cursorType": "default",
14
+ "type": "move"
15
+ },
16
+ {
17
+ "x": 1119,
18
+ "y": 1066,
19
+ "timestamp": 423,
20
+ "cursorType": "default",
21
+ "type": "move"
22
+ },
23
+ {
24
+ "x": 1119,
25
+ "y": 1042,
26
+ "timestamp": 446,
27
+ "cursorType": "default",
28
+ "type": "move"
29
+ },
30
+ {
31
+ "x": 1119,
32
+ "y": 968,
33
+ "timestamp": 466,
34
+ "cursorType": "default",
35
+ "type": "move"
36
+ },
37
+ {
38
+ "x": 1129,
39
+ "y": 906,
40
+ "timestamp": 487,
41
+ "cursorType": "default",
42
+ "type": "move"
43
+ },
44
+ {
45
+ "x": 1153,
46
+ "y": 820,
47
+ "timestamp": 507,
48
+ "cursorType": "default",
49
+ "type": "move"
50
+ },
51
+ {
52
+ "x": 1168,
53
+ "y": 782,
54
+ "timestamp": 528,
55
+ "cursorType": "default",
56
+ "type": "move"
57
+ },
58
+ {
59
+ "x": 1176,
60
+ "y": 764,
61
+ "timestamp": 549,
62
+ "cursorType": "default",
63
+ "type": "move"
64
+ },
65
+ {
66
+ "x": 1176,
67
+ "y": 764,
68
+ "timestamp": 2578,
69
+ "cursorType": "default",
70
+ "type": "mousedown"
71
+ },
72
+ {
73
+ "x": 1176,
74
+ "y": 764,
75
+ "timestamp": 2599,
76
+ "cursorType": "default",
77
+ "type": "move"
78
+ },
79
+ {
80
+ "x": 1176,
81
+ "y": 764,
82
+ "timestamp": 2724,
83
+ "cursorType": "default",
84
+ "type": "mouseup"
85
+ },
86
+ {
87
+ "x": 1176,
88
+ "y": 764,
89
+ "timestamp": 2744,
90
+ "cursorType": "default",
91
+ "type": "move"
92
+ }
93
+ ]
package/cursor-test.js ADDED
@@ -0,0 +1,50 @@
1
+ const MacRecorder = require("./index");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+
5
+ async function testCursorCapture() {
6
+ console.log("šŸŽÆ Cursor Capture Demo\n");
7
+
8
+ const recorder = new MacRecorder();
9
+ const outputPath = path.join(__dirname, "cursor-data.json");
10
+
11
+ try {
12
+ // Başlat
13
+ await recorder.startCursorCapture(outputPath);
14
+ console.log("āœ… Kayıt başladı...");
15
+
16
+ // 5 saniye bekle
17
+ console.log("šŸ“± 5 saniye hareket ettirin, tıklayın...");
18
+
19
+ for (let i = 5; i > 0; i--) {
20
+ process.stdout.write(`ā³ ${i}... `);
21
+ await new Promise((resolve) => setTimeout(resolve, 1000));
22
+ }
23
+ console.log("\n");
24
+
25
+ // Durdur
26
+ await recorder.stopCursorCapture();
27
+ console.log("āœ… Kayıt tamamlandı!");
28
+
29
+ // SonuƧ
30
+ if (fs.existsSync(outputPath)) {
31
+ const data = JSON.parse(fs.readFileSync(outputPath, "utf8"));
32
+ console.log(`šŸ“„ ${data.length} event kaydedildi -> ${outputPath}`);
33
+
34
+ // Basit istatistik
35
+ const clicks = data.filter((d) => d.type === "mousedown").length;
36
+ if (clicks > 0) {
37
+ console.log(`šŸ–±ļø ${clicks} click algılandı`);
38
+ }
39
+ }
40
+ } catch (error) {
41
+ console.error("āŒ Hata:", error.message);
42
+ }
43
+ }
44
+
45
+ // Direkt çalıştır
46
+ if (require.main === module) {
47
+ testCursorCapture().catch(console.error);
48
+ }
49
+
50
+ module.exports = { testCursorCapture };
package/index.js CHANGED
@@ -25,6 +25,14 @@ class MacRecorder extends EventEmitter {
25
25
  this.outputPath = null;
26
26
  this.recordingTimer = null;
27
27
  this.recordingStartTime = null;
28
+
29
+ // Cursor capture variables
30
+ this.cursorCaptureInterval = null;
31
+ this.cursorCaptureFile = null;
32
+ this.cursorCaptureStartTime = null;
33
+ this.cursorCaptureFirstWrite = true;
34
+ this.lastCapturedData = null;
35
+
28
36
  this.options = {
29
37
  includeMicrophone: false, // Default olarak mikrofon kapalı
30
38
  includeSystemAudio: true, // Default olarak sistem sesi aƧık
@@ -413,6 +421,159 @@ class MacRecorder extends EventEmitter {
413
421
  });
414
422
  }
415
423
 
424
+ /**
425
+ * Event'in kaydedilip kaydedilmeyeceğini belirler
426
+ */
427
+ shouldCaptureEvent(currentData) {
428
+ if (!this.lastCapturedData) {
429
+ return true; // İlk event
430
+ }
431
+
432
+ const last = this.lastCapturedData;
433
+
434
+ // Event type değişmişse
435
+ if (currentData.type !== last.type) {
436
+ return true;
437
+ }
438
+
439
+ // Pozisyon değişmişse (minimum 2 pixel tolerans)
440
+ if (
441
+ Math.abs(currentData.x - last.x) >= 2 ||
442
+ Math.abs(currentData.y - last.y) >= 2
443
+ ) {
444
+ return true;
445
+ }
446
+
447
+ // Cursor type değişmişse
448
+ if (currentData.cursorType !== last.cursorType) {
449
+ return true;
450
+ }
451
+
452
+ // Hiçbir değişiklik yoksa kaydetme
453
+ return false;
454
+ }
455
+
456
+ /**
457
+ * Cursor capture başlatır - otomatik olarak dosyaya yazmaya başlar
458
+ */
459
+ async startCursorCapture(filepath) {
460
+ if (!filepath) {
461
+ throw new Error("File path is required");
462
+ }
463
+
464
+ if (this.cursorCaptureInterval) {
465
+ throw new Error("Cursor capture is already running");
466
+ }
467
+
468
+ return new Promise((resolve, reject) => {
469
+ try {
470
+ // Dosyayı oluştur ve temizle
471
+ const fs = require("fs");
472
+ fs.writeFileSync(filepath, "[");
473
+
474
+ this.cursorCaptureFile = filepath;
475
+ this.cursorCaptureStartTime = Date.now();
476
+ this.cursorCaptureFirstWrite = true;
477
+ this.lastCapturedData = null;
478
+
479
+ // JavaScript interval ile polling yap (daha sık - mouse event'leri yakalamak iƧin)
480
+ this.cursorCaptureInterval = setInterval(() => {
481
+ try {
482
+ const position = nativeBinding.getCursorPosition();
483
+ const timestamp = Date.now() - this.cursorCaptureStartTime;
484
+
485
+ const cursorData = {
486
+ x: position.x,
487
+ y: position.y,
488
+ timestamp: timestamp,
489
+ cursorType: position.cursorType,
490
+ type: position.eventType || "move",
491
+ };
492
+
493
+ // Sadece eventType değiştiğinde veya pozisyon değiştiğinde kaydet
494
+ if (this.shouldCaptureEvent(cursorData)) {
495
+ // Dosyaya ekle
496
+ const jsonString = JSON.stringify(cursorData);
497
+
498
+ if (this.cursorCaptureFirstWrite) {
499
+ fs.appendFileSync(filepath, jsonString);
500
+ this.cursorCaptureFirstWrite = false;
501
+ } else {
502
+ fs.appendFileSync(filepath, "," + jsonString);
503
+ }
504
+
505
+ // Son pozisyonu sakla
506
+ this.lastCapturedData = { ...cursorData };
507
+ }
508
+ } catch (error) {
509
+ console.error("Cursor capture error:", error);
510
+ }
511
+ }, 20); // 50 FPS - mouse event'leri yakalamak iƧin daha hızlı
512
+
513
+ this.emit("cursorCaptureStarted", filepath);
514
+ resolve(true);
515
+ } catch (error) {
516
+ reject(error);
517
+ }
518
+ });
519
+ }
520
+
521
+ /**
522
+ * Cursor capture durdurur - dosya yazma işlemini sonlandırır
523
+ */
524
+ async stopCursorCapture() {
525
+ return new Promise((resolve, reject) => {
526
+ try {
527
+ if (!this.cursorCaptureInterval) {
528
+ return resolve(false);
529
+ }
530
+
531
+ // Interval'ı durdur
532
+ clearInterval(this.cursorCaptureInterval);
533
+ this.cursorCaptureInterval = null;
534
+
535
+ // Dosyayı kapat
536
+ if (this.cursorCaptureFile) {
537
+ const fs = require("fs");
538
+ fs.appendFileSync(this.cursorCaptureFile, "]");
539
+ this.cursorCaptureFile = null;
540
+ }
541
+
542
+ // Değişkenleri temizle
543
+ this.lastCapturedData = null;
544
+ this.cursorCaptureStartTime = null;
545
+ this.cursorCaptureFirstWrite = true;
546
+
547
+ this.emit("cursorCaptureStopped");
548
+ resolve(true);
549
+ } catch (error) {
550
+ reject(error);
551
+ }
552
+ });
553
+ }
554
+
555
+ /**
556
+ * Anlık cursor pozisyonunu ve tipini döndürür
557
+ */
558
+ getCursorPosition() {
559
+ try {
560
+ return nativeBinding.getCursorPosition();
561
+ } catch (error) {
562
+ throw new Error("Failed to get cursor position: " + error.message);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Cursor capture durumunu döndürür
568
+ */
569
+ getCursorCaptureStatus() {
570
+ return {
571
+ isCapturing: !!this.cursorCaptureInterval,
572
+ outputFile: this.cursorCaptureFile || null,
573
+ startTime: this.cursorCaptureStartTime || null,
574
+ };
575
+ }
576
+
416
577
  /**
417
578
  * Native modül bilgilerini dƶndürür
418
579
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,457 @@
1
+ #import <napi.h>
2
+ #import <AppKit/AppKit.h>
3
+ #import <Foundation/Foundation.h>
4
+ #import <CoreGraphics/CoreGraphics.h>
5
+ #import <ApplicationServices/ApplicationServices.h>
6
+ #import <Carbon/Carbon.h>
7
+ #import <Accessibility/Accessibility.h>
8
+
9
+ // Global state for cursor tracking
10
+ static bool g_isCursorTracking = false;
11
+ static CFMachPortRef g_eventTap = NULL;
12
+ static CFRunLoopSourceRef g_runLoopSource = NULL;
13
+ static NSDate *g_trackingStartTime = nil;
14
+ static NSString *g_outputPath = nil;
15
+ static NSTimer *g_cursorTimer = nil;
16
+ static int g_debugCallbackCount = 0;
17
+ static NSFileHandle *g_fileHandle = nil;
18
+ static bool g_isFirstWrite = true;
19
+
20
+ // Forward declaration
21
+ void cursorTimerCallback();
22
+ void writeToFile(NSDictionary *cursorData);
23
+
24
+ // Timer helper class
25
+ @interface CursorTimerTarget : NSObject
26
+ - (void)timerCallback:(NSTimer *)timer;
27
+ @end
28
+
29
+ @implementation CursorTimerTarget
30
+ - (void)timerCallback:(NSTimer *)timer {
31
+ cursorTimerCallback();
32
+ }
33
+ @end
34
+
35
+ static CursorTimerTarget *g_timerTarget = nil;
36
+
37
+ // Global cursor state tracking
38
+ static NSString *g_lastDetectedCursorType = nil;
39
+ static int g_cursorTypeCounter = 0;
40
+
41
+ // Mouse button state tracking
42
+ static bool g_leftMouseDown = false;
43
+ static bool g_rightMouseDown = false;
44
+ static NSString *g_lastEventType = @"move";
45
+
46
+ // Cursor type detection helper - gerƧek cursor type'ı al
47
+ NSString* getCursorType() {
48
+ @autoreleasepool {
49
+ g_cursorTypeCounter++;
50
+
51
+ @try {
52
+ // NSCursor.currentCursor kullanarak gerƧek cursor type'ı al
53
+ NSCursor *currentCursor = [NSCursor currentCursor];
54
+
55
+ if (currentCursor == [NSCursor arrowCursor]) {
56
+ g_lastDetectedCursorType = @"default";
57
+ return @"default";
58
+ } else if (currentCursor == [NSCursor pointingHandCursor]) {
59
+ g_lastDetectedCursorType = @"pointer";
60
+ return @"pointer";
61
+ } else if (currentCursor == [NSCursor IBeamCursor]) {
62
+ g_lastDetectedCursorType = @"text";
63
+ return @"text";
64
+ } else if (currentCursor == [NSCursor openHandCursor]) {
65
+ g_lastDetectedCursorType = @"grab";
66
+ return @"grab";
67
+ } else if (currentCursor == [NSCursor closedHandCursor]) {
68
+ g_lastDetectedCursorType = @"grabbing";
69
+ return @"grabbing";
70
+ } else if (currentCursor == [NSCursor resizeLeftRightCursor]) {
71
+ g_lastDetectedCursorType = @"ew-resize";
72
+ return @"ew-resize";
73
+ } else if (currentCursor == [NSCursor resizeUpDownCursor]) {
74
+ g_lastDetectedCursorType = @"ns-resize";
75
+ return @"ns-resize";
76
+ } else if (currentCursor == [NSCursor crosshairCursor]) {
77
+ g_lastDetectedCursorType = @"crosshair";
78
+ return @"crosshair";
79
+ } else {
80
+ // Bilinmeyen cursor - default olarak dƶn
81
+ g_lastDetectedCursorType = @"default";
82
+ return @"default";
83
+ }
84
+ } @catch (NSException *exception) {
85
+ // Hata durumunda default dƶn
86
+ g_lastDetectedCursorType = @"default";
87
+ return @"default";
88
+ }
89
+ }
90
+ }
91
+
92
+ // Dosyaya yazma helper fonksiyonu
93
+ void writeToFile(NSDictionary *cursorData) {
94
+ @autoreleasepool {
95
+ if (!g_fileHandle || !cursorData) {
96
+ return;
97
+ }
98
+
99
+ @try {
100
+ NSError *error;
101
+ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:cursorData
102
+ options:0
103
+ error:&error];
104
+ if (jsonData && !error) {
105
+ NSString *jsonString = [[[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] autorelease];
106
+
107
+ if (g_isFirstWrite) {
108
+ // İlk yazma - array başlat
109
+ [g_fileHandle writeData:[@"[" dataUsingEncoding:NSUTF8StringEncoding]];
110
+ [g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
111
+ g_isFirstWrite = false;
112
+ } else {
113
+ // Sonraki yazmalar - virgül + json
114
+ [g_fileHandle writeData:[@"," dataUsingEncoding:NSUTF8StringEncoding]];
115
+ [g_fileHandle writeData:[jsonString dataUsingEncoding:NSUTF8StringEncoding]];
116
+ }
117
+
118
+ [g_fileHandle synchronizeFile];
119
+ }
120
+ } @catch (NSException *exception) {
121
+ // Hata durumunda sessizce devam et
122
+ }
123
+ }
124
+ }
125
+
126
+ // Event callback for mouse events
127
+ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
128
+ @autoreleasepool {
129
+ g_debugCallbackCount++; // Callback çağrıldığını say
130
+
131
+ if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
132
+ return event;
133
+ }
134
+
135
+ CGPoint location = CGEventGetLocation(event);
136
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
137
+ NSString *cursorType = getCursorType();
138
+ NSString *eventType = @"move";
139
+
140
+ // Event tipini belirle
141
+ switch (type) {
142
+ case kCGEventLeftMouseDown:
143
+ case kCGEventRightMouseDown:
144
+ case kCGEventOtherMouseDown:
145
+ eventType = @"mousedown";
146
+ break;
147
+ case kCGEventLeftMouseUp:
148
+ case kCGEventRightMouseUp:
149
+ case kCGEventOtherMouseUp:
150
+ eventType = @"mouseup";
151
+ break;
152
+ case kCGEventLeftMouseDragged:
153
+ case kCGEventRightMouseDragged:
154
+ case kCGEventOtherMouseDragged:
155
+ eventType = @"drag";
156
+ break;
157
+ case kCGEventMouseMoved:
158
+ default:
159
+ eventType = @"move";
160
+ break;
161
+ }
162
+
163
+ // Cursor data oluştur
164
+ NSDictionary *cursorInfo = @{
165
+ @"x": @((int)location.x),
166
+ @"y": @((int)location.y),
167
+ @"timestamp": @((int)timestamp),
168
+ @"cursorType": cursorType,
169
+ @"type": eventType
170
+ };
171
+
172
+ // Direkt dosyaya yaz
173
+ writeToFile(cursorInfo);
174
+
175
+ return event;
176
+ }
177
+ }
178
+
179
+ // Timer callback for periodic cursor position updates
180
+ void cursorTimerCallback() {
181
+ @autoreleasepool {
182
+ g_debugCallbackCount++; // Timer callback çağrıldığını say
183
+
184
+ if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
185
+ return;
186
+ }
187
+
188
+ // Ana thread'de mouse pozisyonu al
189
+ __block NSPoint mouseLocation;
190
+ __block CGPoint location;
191
+
192
+ if ([NSThread isMainThread]) {
193
+ mouseLocation = [NSEvent mouseLocation];
194
+ } else {
195
+ dispatch_sync(dispatch_get_main_queue(), ^{
196
+ mouseLocation = [NSEvent mouseLocation];
197
+ });
198
+ }
199
+
200
+ CGDirectDisplayID mainDisplay = CGMainDisplayID();
201
+ size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
202
+ location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
203
+
204
+ NSTimeInterval timestamp = [[NSDate date] timeIntervalSinceDate:g_trackingStartTime] * 1000; // milliseconds
205
+ NSString *cursorType = getCursorType();
206
+
207
+ // Cursor data oluştur
208
+ NSDictionary *cursorInfo = @{
209
+ @"x": @((int)location.x),
210
+ @"y": @((int)location.y),
211
+ @"timestamp": @((int)timestamp),
212
+ @"cursorType": cursorType,
213
+ @"type": @"move"
214
+ };
215
+
216
+ // Direkt dosyaya yaz
217
+ writeToFile(cursorInfo);
218
+ }
219
+ }
220
+
221
+ // Helper function to cleanup cursor tracking
222
+ void cleanupCursorTracking() {
223
+ g_isCursorTracking = false;
224
+
225
+ // Timer temizle
226
+ if (g_cursorTimer) {
227
+ [g_cursorTimer invalidate];
228
+ g_cursorTimer = nil;
229
+ }
230
+
231
+ if (g_timerTarget) {
232
+ [g_timerTarget autorelease];
233
+ g_timerTarget = nil;
234
+ }
235
+
236
+ // Dosyayı önce kapat (en önemli işlem)
237
+ if (g_fileHandle) {
238
+ @try {
239
+ if (g_isFirstWrite) {
240
+ // Hiç veri yazılmamışsa boş array
241
+ [g_fileHandle writeData:[@"[]" dataUsingEncoding:NSUTF8StringEncoding]];
242
+ } else {
243
+ // JSON array'i kapat
244
+ [g_fileHandle writeData:[@"]" dataUsingEncoding:NSUTF8StringEncoding]];
245
+ }
246
+ [g_fileHandle synchronizeFile];
247
+ [g_fileHandle closeFile];
248
+ } @catch (NSException *exception) {
249
+ // Dosya işlemi hata verirse sessizce devam et
250
+ }
251
+ g_fileHandle = nil;
252
+ }
253
+
254
+ // Event tap'i durdur (non-blocking)
255
+ if (g_eventTap) {
256
+ CGEventTapEnable(g_eventTap, false);
257
+ g_eventTap = NULL; // CFRelease işlemini yapmıyoruz - system handle etsin
258
+ }
259
+
260
+ // Run loop source'unu kaldır (non-blocking)
261
+ if (g_runLoopSource) {
262
+ g_runLoopSource = NULL; // CFRelease işlemini yapmıyoruz
263
+ }
264
+
265
+ // Global değişkenleri sıfırla
266
+ g_trackingStartTime = nil;
267
+ g_outputPath = nil;
268
+ g_debugCallbackCount = 0;
269
+ g_lastDetectedCursorType = nil;
270
+ g_cursorTypeCounter = 0;
271
+ g_isFirstWrite = true;
272
+ }
273
+
274
+ // NAPI Function: Start Cursor Tracking
275
+ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
276
+ Napi::Env env = info.Env();
277
+
278
+ if (info.Length() < 1) {
279
+ Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
280
+ return env.Null();
281
+ }
282
+
283
+ if (g_isCursorTracking) {
284
+ return Napi::Boolean::New(env, false);
285
+ }
286
+
287
+ std::string outputPath = info[0].As<Napi::String>().Utf8Value();
288
+
289
+ @try {
290
+ // Dosyayı oluştur ve aç
291
+ g_outputPath = [NSString stringWithUTF8String:outputPath.c_str()];
292
+ g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
293
+
294
+ if (!g_fileHandle) {
295
+ // Dosya yoksa oluştur
296
+ [[NSFileManager defaultManager] createFileAtPath:g_outputPath contents:nil attributes:nil];
297
+ g_fileHandle = [[NSFileHandle fileHandleForWritingAtPath:g_outputPath] retain];
298
+ }
299
+
300
+ if (!g_fileHandle) {
301
+ return Napi::Boolean::New(env, false);
302
+ }
303
+
304
+ // Dosyayı temizle (baştan başla)
305
+ [g_fileHandle truncateFileAtOffset:0];
306
+ g_isFirstWrite = true;
307
+
308
+ g_trackingStartTime = [NSDate date];
309
+
310
+ // Create event tap for mouse events
311
+ CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
312
+ CGEventMaskBit(kCGEventLeftMouseUp) |
313
+ CGEventMaskBit(kCGEventRightMouseDown) |
314
+ CGEventMaskBit(kCGEventRightMouseUp) |
315
+ CGEventMaskBit(kCGEventOtherMouseDown) |
316
+ CGEventMaskBit(kCGEventOtherMouseUp) |
317
+ CGEventMaskBit(kCGEventMouseMoved) |
318
+ CGEventMaskBit(kCGEventLeftMouseDragged) |
319
+ CGEventMaskBit(kCGEventRightMouseDragged) |
320
+ CGEventMaskBit(kCGEventOtherMouseDragged));
321
+
322
+ g_eventTap = CGEventTapCreate(kCGSessionEventTap,
323
+ kCGHeadInsertEventTap,
324
+ kCGEventTapOptionListenOnly,
325
+ eventMask,
326
+ eventCallback,
327
+ NULL);
328
+
329
+ if (g_eventTap) {
330
+ // Event tap başarılı - detaylı event tracking aktif
331
+ g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
332
+ CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
333
+ CGEventTapEnable(g_eventTap, true);
334
+ }
335
+
336
+ // NSTimer kullan (main thread'de çalışır)
337
+ g_timerTarget = [[CursorTimerTarget alloc] init];
338
+
339
+ g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
340
+ target:g_timerTarget
341
+ selector:@selector(timerCallback:)
342
+ userInfo:nil
343
+ repeats:YES];
344
+
345
+ // Main run loop'a ekle
346
+ [[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
347
+
348
+ g_isCursorTracking = true;
349
+ return Napi::Boolean::New(env, true);
350
+
351
+ } @catch (NSException *exception) {
352
+ cleanupCursorTracking();
353
+ return Napi::Boolean::New(env, false);
354
+ }
355
+ }
356
+
357
+ // NAPI Function: Stop Cursor Tracking
358
+ Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
359
+ Napi::Env env = info.Env();
360
+
361
+ if (!g_isCursorTracking) {
362
+ return Napi::Boolean::New(env, false);
363
+ }
364
+
365
+ @try {
366
+ cleanupCursorTracking();
367
+ return Napi::Boolean::New(env, true);
368
+
369
+ } @catch (NSException *exception) {
370
+ cleanupCursorTracking();
371
+ return Napi::Boolean::New(env, false);
372
+ }
373
+ }
374
+
375
+ // NAPI Function: Get Current Cursor Position
376
+ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
377
+ Napi::Env env = info.Env();
378
+
379
+ @try {
380
+ // NSEvent kullanarak mouse pozisyonu al (daha güvenli)
381
+ NSPoint mouseLocation = [NSEvent mouseLocation];
382
+
383
+ // CGDisplayPixelsHigh ve CGDisplayPixelsWide ile koordinat dönüşümü
384
+ CGDirectDisplayID mainDisplay = CGMainDisplayID();
385
+ size_t displayHeight = CGDisplayPixelsHigh(mainDisplay);
386
+
387
+ // macOS coordinate system (bottom-left origin) to screen coordinates (top-left origin)
388
+ CGPoint location = CGPointMake(mouseLocation.x, displayHeight - mouseLocation.y);
389
+
390
+ NSString *cursorType = getCursorType();
391
+
392
+ // Mouse button state'ini kontrol et
393
+ bool currentLeftMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonLeft);
394
+ bool currentRightMouseDown = CGEventSourceButtonState(kCGEventSourceStateHIDSystemState, kCGMouseButtonRight);
395
+
396
+ NSString *eventType = @"move";
397
+
398
+ // Mouse button state değişikliklerini tespit et
399
+ if (currentLeftMouseDown && !g_leftMouseDown) {
400
+ eventType = @"mousedown";
401
+ g_lastEventType = @"mousedown";
402
+ } else if (!currentLeftMouseDown && g_leftMouseDown) {
403
+ eventType = @"mouseup";
404
+ g_lastEventType = @"mouseup";
405
+ } else if (currentRightMouseDown && !g_rightMouseDown) {
406
+ eventType = @"rightmousedown";
407
+ g_lastEventType = @"rightmousedown";
408
+ } else if (!currentRightMouseDown && g_rightMouseDown) {
409
+ eventType = @"rightmouseup";
410
+ g_lastEventType = @"rightmouseup";
411
+ } else {
412
+ eventType = @"move";
413
+ g_lastEventType = @"move";
414
+ }
415
+
416
+ // State'i güncelle
417
+ g_leftMouseDown = currentLeftMouseDown;
418
+ g_rightMouseDown = currentRightMouseDown;
419
+
420
+ Napi::Object result = Napi::Object::New(env);
421
+ result.Set("x", Napi::Number::New(env, (int)location.x));
422
+ result.Set("y", Napi::Number::New(env, (int)location.y));
423
+ result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
424
+ result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
425
+
426
+ return result;
427
+
428
+ } @catch (NSException *exception) {
429
+ return env.Null();
430
+ }
431
+ }
432
+
433
+ // NAPI Function: Get Cursor Tracking Status
434
+ Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
435
+ Napi::Env env = info.Env();
436
+
437
+ Napi::Object result = Napi::Object::New(env);
438
+ result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
439
+ result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
440
+ result.Set("hasRunLoopSource", Napi::Boolean::New(env, g_runLoopSource != NULL));
441
+ result.Set("hasFileHandle", Napi::Boolean::New(env, g_fileHandle != NULL));
442
+ result.Set("hasTimer", Napi::Boolean::New(env, g_cursorTimer != NULL));
443
+ result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
444
+ result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
445
+
446
+ return result;
447
+ }
448
+
449
+ // Export functions
450
+ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
451
+ exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
452
+ exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
453
+ exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
454
+ exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
455
+
456
+ return exports;
457
+ }
@@ -7,6 +7,9 @@
7
7
  #import <ImageIO/ImageIO.h>
8
8
  #import <CoreAudio/CoreAudio.h>
9
9
 
10
+ // Cursor tracker function declarations
11
+ Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports);
12
+
10
13
  @interface MacRecorderDelegate : NSObject <AVCaptureFileOutputRecordingDelegate>
11
14
  @property (nonatomic, copy) void (^completionHandler)(NSURL *outputURL, NSError *error);
12
15
  @end
@@ -659,6 +662,9 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
659
662
  exports.Set(Napi::String::New(env, "getWindowThumbnail"), Napi::Function::New(env, GetWindowThumbnail));
660
663
  exports.Set(Napi::String::New(env, "getDisplayThumbnail"), Napi::Function::New(env, GetDisplayThumbnail));
661
664
 
665
+ // Initialize cursor tracker
666
+ InitCursorTracker(env, exports);
667
+
662
668
  return exports;
663
669
  }
664
670
 
package/preview-test.js DELETED
@@ -1,329 +0,0 @@
1
- const MacRecorder = require("./index.js");
2
- const fs = require("fs");
3
-
4
- async function testPreviews() {
5
- const recorder = new MacRecorder();
6
-
7
- console.log("šŸ–¼ļø Thumbnail Preview Test\n");
8
-
9
- try {
10
- // 1. Display Previews
11
- console.log("šŸ“ŗ Display Thumbnails Test...");
12
- const displays = await recorder.getDisplays();
13
-
14
- console.log(`Found ${displays.length} displays:`);
15
- for (let i = 0; i < displays.length; i++) {
16
- const display = displays[i];
17
- console.log(
18
- ` Display ${i}: ${display.resolution} ${
19
- display.isPrimary ? "(Primary)" : ""
20
- }`
21
- );
22
-
23
- try {
24
- console.log(` šŸ“ø Capturing thumbnail...`);
25
- const thumbnail = await recorder.getDisplayThumbnail(display.id, {
26
- maxWidth: 300,
27
- maxHeight: 200,
28
- });
29
-
30
- console.log(` āœ… Success: ${thumbnail.length} chars`);
31
-
32
- // Save as HTML file to view
33
- const htmlContent = `
34
- <!DOCTYPE html>
35
- <html>
36
- <head>
37
- <title>Display ${i} Preview</title>
38
- <style>
39
- body {
40
- font-family: Arial, sans-serif;
41
- text-align: center;
42
- padding: 20px;
43
- background: #f5f5f5;
44
- }
45
- .preview-card {
46
- background: white;
47
- padding: 20px;
48
- border-radius: 12px;
49
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
50
- max-width: 400px;
51
- margin: 0 auto;
52
- }
53
- img {
54
- border: 2px solid #007AFF;
55
- border-radius: 8px;
56
- max-width: 100%;
57
- }
58
- .info {
59
- background: #f0f0f0;
60
- padding: 15px;
61
- border-radius: 8px;
62
- margin: 15px 0;
63
- text-align: left;
64
- }
65
- h1 { color: #007AFF; }
66
- </style>
67
- </head>
68
- <body>
69
- <div class="preview-card">
70
- <h1>šŸ“ŗ Display ${i} Preview</h1>
71
- <div class="info">
72
- <strong>Resolution:</strong> ${display.resolution}<br>
73
- <strong>Position:</strong> (${display.x}, ${display.y})<br>
74
- <strong>Primary:</strong> ${display.isPrimary ? "Yes" : "No"}<br>
75
- <strong>Display ID:</strong> ${display.id}
76
- </div>
77
- <img src="${thumbnail}" alt="Display ${i} Preview" />
78
- <p><small>Captured: ${new Date().toLocaleString()}</small></p>
79
- </div>
80
- </body>
81
- </html>`;
82
-
83
- fs.writeFileSync(`display-${i}-preview.html`, htmlContent);
84
- console.log(` šŸ’¾ Saved: display-${i}-preview.html\n`);
85
- } catch (error) {
86
- console.log(` āŒ Failed: ${error.message}\n`);
87
- }
88
- }
89
-
90
- // 2. Window Previews
91
- console.log("🪟 Window Thumbnails Test...");
92
- const windows = await recorder.getWindows();
93
-
94
- // Test first 3 windows
95
- const testWindows = windows.slice(0, 3);
96
- console.log(`Testing ${testWindows.length} windows:`);
97
-
98
- for (let i = 0; i < testWindows.length; i++) {
99
- const window = testWindows[i];
100
- console.log(` Window ${i}: [${window.appName}] ${window.name}`);
101
- console.log(` Size: ${window.width}x${window.height}`);
102
-
103
- try {
104
- console.log(` šŸ“ø Capturing thumbnail...`);
105
- const thumbnail = await recorder.getWindowThumbnail(window.id, {
106
- maxWidth: 300,
107
- maxHeight: 200,
108
- });
109
-
110
- console.log(` āœ… Success: ${thumbnail.length} chars`);
111
-
112
- // Save as HTML file to view
113
- const htmlContent = `
114
- <!DOCTYPE html>
115
- <html>
116
- <head>
117
- <title>${window.appName} Preview</title>
118
- <style>
119
- body {
120
- font-family: Arial, sans-serif;
121
- text-align: center;
122
- padding: 20px;
123
- background: #f5f5f5;
124
- }
125
- .preview-card {
126
- background: white;
127
- padding: 20px;
128
- border-radius: 12px;
129
- box-shadow: 0 4px 12px rgba(0,0,0,0.1);
130
- max-width: 400px;
131
- margin: 0 auto;
132
- }
133
- img {
134
- border: 2px solid #FF3B30;
135
- border-radius: 8px;
136
- max-width: 100%;
137
- }
138
- .info {
139
- background: #f0f0f0;
140
- padding: 15px;
141
- border-radius: 8px;
142
- margin: 15px 0;
143
- text-align: left;
144
- }
145
- h1 { color: #FF3B30; }
146
- .app-name { color: #007AFF; font-weight: bold; }
147
- </style>
148
- </head>
149
- <body>
150
- <div class="preview-card">
151
- <h1>🪟 <span class="app-name">${window.appName}</span></h1>
152
- <div class="info">
153
- <strong>Window:</strong> ${window.name}<br>
154
- <strong>Size:</strong> ${window.width}x${window.height}<br>
155
- <strong>Position:</strong> (${window.x}, ${window.y})<br>
156
- <strong>Window ID:</strong> ${window.id}
157
- </div>
158
- <img src="${thumbnail}" alt="${window.appName} Preview" />
159
- <p><small>Captured: ${new Date().toLocaleString()}</small></p>
160
- </div>
161
- </body>
162
- </html>`;
163
-
164
- const fileName = `window-${window.appName.replace(
165
- /[^a-zA-Z0-9]/g,
166
- ""
167
- )}-${i}-preview.html`;
168
- fs.writeFileSync(fileName, htmlContent);
169
- console.log(` šŸ’¾ Saved: ${fileName}\n`);
170
- } catch (error) {
171
- console.log(` āŒ Failed: ${error.message}\n`);
172
- }
173
- }
174
-
175
- // 3. Create Gallery
176
- console.log("šŸŽØ Creating Preview Gallery...");
177
- const previewFiles = fs
178
- .readdirSync(".")
179
- .filter((file) => file.endsWith("-preview.html"))
180
- .sort();
181
-
182
- if (previewFiles.length > 0) {
183
- const galleryContent = `
184
- <!DOCTYPE html>
185
- <html>
186
- <head>
187
- <title>šŸ“ø Thumbnail Preview Gallery</title>
188
- <style>
189
- body {
190
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
191
- padding: 20px;
192
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
193
- color: white;
194
- min-height: 100vh;
195
- }
196
- .header {
197
- text-align: center;
198
- margin-bottom: 30px;
199
- }
200
- .gallery {
201
- display: grid;
202
- grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
203
- gap: 20px;
204
- margin-top: 20px;
205
- }
206
- .preview-card {
207
- background: rgba(255,255,255,0.1);
208
- border-radius: 16px;
209
- overflow: hidden;
210
- backdrop-filter: blur(10px);
211
- border: 1px solid rgba(255,255,255,0.2);
212
- transition: transform 0.3s ease;
213
- }
214
- .preview-card:hover {
215
- transform: translateY(-8px);
216
- }
217
- .card-header {
218
- padding: 20px;
219
- border-bottom: 1px solid rgba(255,255,255,0.1);
220
- }
221
- .card-title {
222
- margin: 0;
223
- font-size: 18px;
224
- font-weight: 600;
225
- }
226
- .card-subtitle {
227
- margin: 8px 0 0 0;
228
- opacity: 0.8;
229
- font-size: 14px;
230
- }
231
- iframe {
232
- width: 100%;
233
- height: 400px;
234
- border: none;
235
- background: white;
236
- }
237
- .stats {
238
- background: rgba(255,255,255,0.2);
239
- padding: 20px;
240
- border-radius: 12px;
241
- text-align: center;
242
- margin-bottom: 30px;
243
- backdrop-filter: blur(10px);
244
- }
245
- .btn {
246
- background: rgba(255,255,255,0.2);
247
- color: white;
248
- padding: 10px 20px;
249
- border: none;
250
- border-radius: 8px;
251
- cursor: pointer;
252
- margin: 5px;
253
- text-decoration: none;
254
- display: inline-block;
255
- transition: all 0.3s ease;
256
- }
257
- .btn:hover {
258
- background: rgba(255,255,255,0.3);
259
- transform: translateY(-2px);
260
- }
261
- </style>
262
- </head>
263
- <body>
264
- <div class="header">
265
- <h1>šŸ“ø macOS Thumbnail Preview Gallery</h1>
266
- <p>Screen and window thumbnails generated with node-mac-recorder</p>
267
- </div>
268
-
269
- <div class="stats">
270
- <strong>${previewFiles.length}</strong> Thumbnails Generated<br>
271
- <small>Created at ${new Date().toLocaleString()}</small>
272
- </div>
273
-
274
- <div style="text-align: center; margin-bottom: 20px;">
275
- ${previewFiles
276
- .map(
277
- (file) =>
278
- `<a href="${file}" class="btn" target="_blank">${file
279
- .replace("-preview.html", "")
280
- .replace(/-/g, " ")}</a>`
281
- )
282
- .join("")}
283
- </div>
284
-
285
- <div class="gallery">
286
- ${previewFiles
287
- .map(
288
- (file) => `
289
- <div class="preview-card">
290
- <div class="card-header">
291
- <h3 class="card-title">${file
292
- .replace("-preview.html", "")
293
- .replace(/-/g, " ")
294
- .toUpperCase()}</h3>
295
- <p class="card-subtitle">${file}</p>
296
- </div>
297
- <iframe src="${file}"></iframe>
298
- </div>
299
- `
300
- )
301
- .join("")}
302
- </div>
303
-
304
- <div style="text-align: center; margin-top: 40px; opacity: 0.8;">
305
- <p>šŸš€ Generated with <strong>node-mac-recorder v1.1.0</strong></p>
306
- </div>
307
- </body>
308
- </html>`;
309
-
310
- fs.writeFileSync("preview-gallery.html", galleryContent);
311
- console.log(`āœ… Gallery created: preview-gallery.html`);
312
- }
313
-
314
- console.log("\nšŸŽ‰ Preview Test Completed!");
315
- console.log("\nšŸ“ Generated Files:");
316
- previewFiles.forEach((file) => console.log(` - ${file}`));
317
- console.log(" - preview-gallery.html (main gallery)");
318
-
319
- console.log("\n🌐 View Results:");
320
- console.log(" open preview-gallery.html");
321
- console.log("\nšŸ’” Individual files can be opened directly in browser!");
322
- } catch (error) {
323
- console.error("āŒ Preview test failed:", error.message);
324
- console.error(error.stack);
325
- }
326
- }
327
-
328
- // Run preview test
329
- testPreviews();