node-mac-recorder 1.2.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/cursor-data.json +93 -0
- package/cursor-test.js +23 -149
- package/index.js +123 -52
- package/package.json +1 -1
- package/src/cursor_tracker.mm +166 -108
- package/manual-cursor-data.json +0 -352
package/cursor-data.json
ADDED
|
@@ -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
CHANGED
|
@@ -2,175 +2,49 @@ const MacRecorder = require("./index");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
|
|
5
|
-
async function
|
|
6
|
-
console.log("🎯 Cursor
|
|
5
|
+
async function testCursorCapture() {
|
|
6
|
+
console.log("🎯 Cursor Capture Demo\n");
|
|
7
7
|
|
|
8
8
|
const recorder = new MacRecorder();
|
|
9
|
+
const outputPath = path.join(__dirname, "cursor-data.json");
|
|
9
10
|
|
|
10
11
|
try {
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
console.log("
|
|
14
|
-
console.log("📁 Output dosyası:", outputPath);
|
|
12
|
+
// Başlat
|
|
13
|
+
await recorder.startCursorCapture(outputPath);
|
|
14
|
+
console.log("✅ Kayıt başladı...");
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
console.log("
|
|
18
|
-
|
|
19
|
-
// Durum kontrolü
|
|
20
|
-
const status = recorder.getCursorTrackingStatus();
|
|
21
|
-
console.log("📊 Tracking durumu:", status);
|
|
22
|
-
|
|
23
|
-
// 5 saniye bekle ve pozisyon örnekleri al
|
|
24
|
-
console.log(
|
|
25
|
-
"\n🎬 5 saniye boyunca cursor hareketlerinizi takip ediyoruz..."
|
|
26
|
-
);
|
|
27
|
-
console.log("💡 Fare hareket ettirin, tıklayın ve sürükleyin!");
|
|
28
|
-
|
|
29
|
-
// Manuel data collection - JavaScript tarafında polling
|
|
30
|
-
const manualData = [];
|
|
31
|
-
const startTime = Date.now();
|
|
16
|
+
// 5 saniye bekle
|
|
17
|
+
console.log("📱 5 saniye hareket ettirin, tıklayın...");
|
|
32
18
|
|
|
33
19
|
for (let i = 5; i > 0; i--) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// 100ms'de bir pozisyon al (10 FPS)
|
|
37
|
-
for (let j = 0; j < 10; j++) {
|
|
38
|
-
const position = recorder.getCursorPosition();
|
|
39
|
-
const timestamp = Date.now() - startTime;
|
|
40
|
-
|
|
41
|
-
manualData.push({
|
|
42
|
-
x: position.x,
|
|
43
|
-
y: position.y,
|
|
44
|
-
timestamp: timestamp,
|
|
45
|
-
cursorType: position.cursorType,
|
|
46
|
-
type: "move",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Son pozisyonu göster
|
|
53
|
-
if (manualData.length > 0) {
|
|
54
|
-
const lastPos = manualData[manualData.length - 1];
|
|
55
|
-
console.log(
|
|
56
|
-
`📌 Pozisyon: x=${lastPos.x}, y=${lastPos.y}, tip=${lastPos.cursorType}`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
20
|
+
process.stdout.write(`⏳ ${i}... `);
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
59
22
|
}
|
|
23
|
+
console.log("\n");
|
|
60
24
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
const manualPath = path.join(__dirname, "manual-cursor-data.json");
|
|
66
|
-
fs.writeFileSync(manualPath, JSON.stringify(manualData, null, 2));
|
|
67
|
-
console.log(`📄 Manuel veriler kaydedildi: ${manualPath}`);
|
|
68
|
-
|
|
69
|
-
// Tracking durdur
|
|
70
|
-
console.log("\n🛑 Cursor tracking durduruluyor...");
|
|
71
|
-
await recorder.stopCursorTracking();
|
|
72
|
-
console.log("✅ Cursor tracking durduruldu!");
|
|
25
|
+
// Durdur
|
|
26
|
+
await recorder.stopCursorCapture();
|
|
27
|
+
console.log("✅ Kayıt tamamlandı!");
|
|
73
28
|
|
|
74
|
-
//
|
|
75
|
-
const finalStatus = recorder.getCursorTrackingStatus();
|
|
76
|
-
console.log("📊 Final durumu:", finalStatus);
|
|
77
|
-
|
|
78
|
-
// Dosya kontrolü
|
|
29
|
+
// Sonuç
|
|
79
30
|
if (fs.existsSync(outputPath)) {
|
|
80
31
|
const data = JSON.parse(fs.readFileSync(outputPath, "utf8"));
|
|
81
|
-
console.log(
|
|
82
|
-
`\n📄 JSON dosyası oluşturuldu: ${data.length} adet cursor verisi kaydedildi`
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// İlk birkaç veriyi göster
|
|
86
|
-
if (data.length > 0) {
|
|
87
|
-
console.log("\n📝 İlk 3 cursor verisi:");
|
|
88
|
-
data.slice(0, 3).forEach((item, index) => {
|
|
89
|
-
console.log(
|
|
90
|
-
`${index + 1}. x:${item.x}, y:${item.y}, timestamp:${
|
|
91
|
-
item.timestamp
|
|
92
|
-
}, cursorType:${item.cursorType}, type:${item.type}`
|
|
93
|
-
);
|
|
94
|
-
});
|
|
32
|
+
console.log(`📄 ${data.length} event kaydedildi -> ${outputPath}`);
|
|
95
33
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
34
|
+
// Basit istatistik
|
|
35
|
+
const clicks = data.filter((d) => d.type === "mousedown").length;
|
|
36
|
+
if (clicks > 0) {
|
|
37
|
+
console.log(`🖱️ ${clicks} click algılandı`);
|
|
99
38
|
}
|
|
100
|
-
|
|
101
|
-
// Cursor tipleri istatistiği
|
|
102
|
-
const cursorTypes = {};
|
|
103
|
-
const eventTypes = {};
|
|
104
|
-
data.forEach((item) => {
|
|
105
|
-
cursorTypes[item.cursorType] = (cursorTypes[item.cursorType] || 0) + 1;
|
|
106
|
-
eventTypes[item.type] = (eventTypes[item.type] || 0) + 1;
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
console.log("\n📈 Cursor Tipleri İstatistiği:");
|
|
110
|
-
Object.keys(cursorTypes).forEach((type) => {
|
|
111
|
-
console.log(` ${type}: ${cursorTypes[type]} adet`);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
console.log("\n🎭 Event Tipleri İstatistiği:");
|
|
115
|
-
Object.keys(eventTypes).forEach((type) => {
|
|
116
|
-
console.log(` ${type}: ${eventTypes[type]} adet`);
|
|
117
|
-
});
|
|
118
|
-
} else {
|
|
119
|
-
console.log("❌ JSON dosyası oluşturulamadı!");
|
|
120
39
|
}
|
|
121
40
|
} catch (error) {
|
|
122
41
|
console.error("❌ Hata:", error.message);
|
|
123
42
|
}
|
|
124
|
-
|
|
125
|
-
console.log("\n🎉 Test tamamlandı!");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Ek fonksiyonlar test et
|
|
129
|
-
async function testCursorPositionOnly() {
|
|
130
|
-
console.log("\n🎯 Anlık Cursor Pozisyon Testi...");
|
|
131
|
-
|
|
132
|
-
const recorder = new MacRecorder();
|
|
133
|
-
|
|
134
|
-
try {
|
|
135
|
-
for (let i = 0; i < 5; i++) {
|
|
136
|
-
const position = recorder.getCursorPosition();
|
|
137
|
-
console.log(
|
|
138
|
-
`📌 Pozisyon ${i + 1}: x=${position.x}, y=${position.y}, tip=${
|
|
139
|
-
position.cursorType
|
|
140
|
-
}`
|
|
141
|
-
);
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
143
|
-
}
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.error("❌ Pozisyon alma hatası:", error.message);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Test menüsü
|
|
150
|
-
async function main() {
|
|
151
|
-
console.log("🚀 MacRecorder Cursor Tracking Test Menüsü\n");
|
|
152
|
-
|
|
153
|
-
const args = process.argv.slice(2);
|
|
154
|
-
|
|
155
|
-
if (args.includes("--position")) {
|
|
156
|
-
await testCursorPositionOnly();
|
|
157
|
-
} else if (args.includes("--full")) {
|
|
158
|
-
await testCursorTracking();
|
|
159
|
-
} else {
|
|
160
|
-
console.log("Kullanım:");
|
|
161
|
-
console.log(
|
|
162
|
-
" node cursor-test.js --full # Tam cursor tracking testi (5 saniye)"
|
|
163
|
-
);
|
|
164
|
-
console.log(
|
|
165
|
-
" node cursor-test.js --position # Sadece anlık pozisyon testi"
|
|
166
|
-
);
|
|
167
|
-
console.log("\nÖrnek:");
|
|
168
|
-
console.log(" node cursor-test.js --full");
|
|
169
|
-
}
|
|
170
43
|
}
|
|
171
44
|
|
|
45
|
+
// Direkt çalıştır
|
|
172
46
|
if (require.main === module) {
|
|
173
|
-
|
|
47
|
+
testCursorCapture().catch(console.error);
|
|
174
48
|
}
|
|
175
49
|
|
|
176
|
-
module.exports = {
|
|
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
|
|
@@ -414,26 +422,96 @@ class MacRecorder extends EventEmitter {
|
|
|
414
422
|
}
|
|
415
423
|
|
|
416
424
|
/**
|
|
417
|
-
*
|
|
425
|
+
* Event'in kaydedilip kaydedilmeyeceğini belirler
|
|
418
426
|
*/
|
|
419
|
-
|
|
420
|
-
if (!
|
|
421
|
-
|
|
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");
|
|
422
466
|
}
|
|
423
467
|
|
|
424
468
|
return new Promise((resolve, reject) => {
|
|
425
469
|
try {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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);
|
|
437
515
|
} catch (error) {
|
|
438
516
|
reject(error);
|
|
439
517
|
}
|
|
@@ -441,18 +519,33 @@ class MacRecorder extends EventEmitter {
|
|
|
441
519
|
}
|
|
442
520
|
|
|
443
521
|
/**
|
|
444
|
-
* Cursor
|
|
522
|
+
* Cursor capture durdurur - dosya yazma işlemini sonlandırır
|
|
445
523
|
*/
|
|
446
|
-
async
|
|
524
|
+
async stopCursorCapture() {
|
|
447
525
|
return new Promise((resolve, reject) => {
|
|
448
526
|
try {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
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);
|
|
456
549
|
} catch (error) {
|
|
457
550
|
reject(error);
|
|
458
551
|
}
|
|
@@ -471,36 +564,14 @@ class MacRecorder extends EventEmitter {
|
|
|
471
564
|
}
|
|
472
565
|
|
|
473
566
|
/**
|
|
474
|
-
* Cursor
|
|
567
|
+
* Cursor capture durumunu döndürür
|
|
475
568
|
*/
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Cursor verilerini JSON dosyasına kaydeder
|
|
486
|
-
*/
|
|
487
|
-
async saveCursorData(outputPath) {
|
|
488
|
-
if (!outputPath) {
|
|
489
|
-
throw new Error("Output path is required");
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return new Promise((resolve, reject) => {
|
|
493
|
-
try {
|
|
494
|
-
const success = nativeBinding.saveCursorData(outputPath);
|
|
495
|
-
if (success) {
|
|
496
|
-
resolve(true);
|
|
497
|
-
} else {
|
|
498
|
-
reject(new Error("Failed to save cursor data"));
|
|
499
|
-
}
|
|
500
|
-
} catch (error) {
|
|
501
|
-
reject(error);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
569
|
+
getCursorCaptureStatus() {
|
|
570
|
+
return {
|
|
571
|
+
isCapturing: !!this.cursorCaptureInterval,
|
|
572
|
+
outputFile: this.cursorCaptureFile || null,
|
|
573
|
+
startTime: this.cursorCaptureStartTime || null,
|
|
574
|
+
};
|
|
504
575
|
}
|
|
505
576
|
|
|
506
577
|
/**
|
package/package.json
CHANGED
package/src/cursor_tracker.mm
CHANGED
|
@@ -8,16 +8,18 @@
|
|
|
8
8
|
|
|
9
9
|
// Global state for cursor tracking
|
|
10
10
|
static bool g_isCursorTracking = false;
|
|
11
|
-
static NSMutableArray *g_cursorData = nil;
|
|
12
11
|
static CFMachPortRef g_eventTap = NULL;
|
|
13
12
|
static CFRunLoopSourceRef g_runLoopSource = NULL;
|
|
14
13
|
static NSDate *g_trackingStartTime = nil;
|
|
15
14
|
static NSString *g_outputPath = nil;
|
|
16
15
|
static NSTimer *g_cursorTimer = nil;
|
|
17
16
|
static int g_debugCallbackCount = 0;
|
|
17
|
+
static NSFileHandle *g_fileHandle = nil;
|
|
18
|
+
static bool g_isFirstWrite = true;
|
|
18
19
|
|
|
19
20
|
// Forward declaration
|
|
20
|
-
void cursorTimerCallback(
|
|
21
|
+
void cursorTimerCallback();
|
|
22
|
+
void writeToFile(NSDictionary *cursorData);
|
|
21
23
|
|
|
22
24
|
// Timer helper class
|
|
23
25
|
@interface CursorTimerTarget : NSObject
|
|
@@ -26,7 +28,7 @@ void cursorTimerCallback(NSTimer *timer);
|
|
|
26
28
|
|
|
27
29
|
@implementation CursorTimerTarget
|
|
28
30
|
- (void)timerCallback:(NSTimer *)timer {
|
|
29
|
-
cursorTimerCallback(
|
|
31
|
+
cursorTimerCallback();
|
|
30
32
|
}
|
|
31
33
|
@end
|
|
32
34
|
|
|
@@ -36,31 +38,87 @@ static CursorTimerTarget *g_timerTarget = nil;
|
|
|
36
38
|
static NSString *g_lastDetectedCursorType = nil;
|
|
37
39
|
static int g_cursorTypeCounter = 0;
|
|
38
40
|
|
|
39
|
-
//
|
|
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
|
|
40
47
|
NSString* getCursorType() {
|
|
41
48
|
@autoreleasepool {
|
|
42
49
|
g_cursorTypeCounter++;
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
case 0:
|
|
51
|
+
@try {
|
|
52
|
+
// NSCursor.currentCursor kullanarak gerçek cursor type'ı al
|
|
53
|
+
NSCursor *currentCursor = [NSCursor currentCursor];
|
|
54
|
+
|
|
55
|
+
if (currentCursor == [NSCursor arrowCursor]) {
|
|
50
56
|
g_lastDetectedCursorType = @"default";
|
|
51
57
|
return @"default";
|
|
52
|
-
|
|
58
|
+
} else if (currentCursor == [NSCursor pointingHandCursor]) {
|
|
53
59
|
g_lastDetectedCursorType = @"pointer";
|
|
54
60
|
return @"pointer";
|
|
55
|
-
|
|
61
|
+
} else if (currentCursor == [NSCursor IBeamCursor]) {
|
|
56
62
|
g_lastDetectedCursorType = @"text";
|
|
57
63
|
return @"text";
|
|
58
|
-
|
|
64
|
+
} else if (currentCursor == [NSCursor openHandCursor]) {
|
|
65
|
+
g_lastDetectedCursorType = @"grab";
|
|
66
|
+
return @"grab";
|
|
67
|
+
} else if (currentCursor == [NSCursor closedHandCursor]) {
|
|
59
68
|
g_lastDetectedCursorType = @"grabbing";
|
|
60
69
|
return @"grabbing";
|
|
61
|
-
|
|
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
|
|
62
81
|
g_lastDetectedCursorType = @"default";
|
|
63
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
|
|
64
122
|
}
|
|
65
123
|
}
|
|
66
124
|
}
|
|
@@ -68,7 +126,9 @@ NSString* getCursorType() {
|
|
|
68
126
|
// Event callback for mouse events
|
|
69
127
|
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
|
70
128
|
@autoreleasepool {
|
|
71
|
-
|
|
129
|
+
g_debugCallbackCount++; // Callback çağrıldığını say
|
|
130
|
+
|
|
131
|
+
if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
|
|
72
132
|
return event;
|
|
73
133
|
}
|
|
74
134
|
|
|
@@ -109,21 +169,19 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef eve
|
|
|
109
169
|
@"type": eventType
|
|
110
170
|
};
|
|
111
171
|
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
[g_cursorData addObject:cursorInfo];
|
|
115
|
-
}
|
|
172
|
+
// Direkt dosyaya yaz
|
|
173
|
+
writeToFile(cursorInfo);
|
|
116
174
|
|
|
117
175
|
return event;
|
|
118
176
|
}
|
|
119
177
|
}
|
|
120
178
|
|
|
121
179
|
// Timer callback for periodic cursor position updates
|
|
122
|
-
void cursorTimerCallback(
|
|
180
|
+
void cursorTimerCallback() {
|
|
123
181
|
@autoreleasepool {
|
|
124
|
-
g_debugCallbackCount++;
|
|
182
|
+
g_debugCallbackCount++; // Timer callback çağrıldığını say
|
|
125
183
|
|
|
126
|
-
if (!g_isCursorTracking || !
|
|
184
|
+
if (!g_isCursorTracking || !g_trackingStartTime || !g_fileHandle) {
|
|
127
185
|
return;
|
|
128
186
|
}
|
|
129
187
|
|
|
@@ -155,10 +213,8 @@ void cursorTimerCallback(NSTimer *timer) {
|
|
|
155
213
|
@"type": @"move"
|
|
156
214
|
};
|
|
157
215
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
[g_cursorData addObject:cursorInfo];
|
|
161
|
-
}
|
|
216
|
+
// Direkt dosyaya yaz
|
|
217
|
+
writeToFile(cursorInfo);
|
|
162
218
|
}
|
|
163
219
|
}
|
|
164
220
|
|
|
@@ -166,39 +222,53 @@ void cursorTimerCallback(NSTimer *timer) {
|
|
|
166
222
|
void cleanupCursorTracking() {
|
|
167
223
|
g_isCursorTracking = false;
|
|
168
224
|
|
|
169
|
-
// Timer
|
|
225
|
+
// Timer temizle
|
|
170
226
|
if (g_cursorTimer) {
|
|
171
227
|
[g_cursorTimer invalidate];
|
|
172
228
|
g_cursorTimer = nil;
|
|
173
229
|
}
|
|
174
230
|
|
|
175
|
-
// Timer target'ı temizle
|
|
176
231
|
if (g_timerTarget) {
|
|
177
|
-
[g_timerTarget
|
|
232
|
+
[g_timerTarget autorelease];
|
|
178
233
|
g_timerTarget = nil;
|
|
179
234
|
}
|
|
180
235
|
|
|
181
|
-
//
|
|
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)
|
|
182
255
|
if (g_eventTap) {
|
|
183
256
|
CGEventTapEnable(g_eventTap, false);
|
|
184
|
-
|
|
185
|
-
CFRelease(g_eventTap);
|
|
186
|
-
g_eventTap = NULL;
|
|
257
|
+
g_eventTap = NULL; // CFRelease işlemini yapmıyoruz - system handle etsin
|
|
187
258
|
}
|
|
188
259
|
|
|
189
|
-
// Run loop source'unu kaldır
|
|
260
|
+
// Run loop source'unu kaldır (non-blocking)
|
|
190
261
|
if (g_runLoopSource) {
|
|
191
|
-
|
|
192
|
-
CFRelease(g_runLoopSource);
|
|
193
|
-
g_runLoopSource = NULL;
|
|
262
|
+
g_runLoopSource = NULL; // CFRelease işlemini yapmıyoruz
|
|
194
263
|
}
|
|
195
264
|
|
|
196
|
-
|
|
265
|
+
// Global değişkenleri sıfırla
|
|
197
266
|
g_trackingStartTime = nil;
|
|
198
267
|
g_outputPath = nil;
|
|
199
268
|
g_debugCallbackCount = 0;
|
|
200
269
|
g_lastDetectedCursorType = nil;
|
|
201
270
|
g_cursorTypeCounter = 0;
|
|
271
|
+
g_isFirstWrite = true;
|
|
202
272
|
}
|
|
203
273
|
|
|
204
274
|
// NAPI Function: Start Cursor Tracking
|
|
@@ -217,10 +287,25 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
217
287
|
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
218
288
|
|
|
219
289
|
@try {
|
|
220
|
-
//
|
|
221
|
-
g_cursorData = [[NSMutableArray alloc] init];
|
|
222
|
-
g_trackingStartTime = [NSDate date];
|
|
290
|
+
// Dosyayı oluştur ve aç
|
|
223
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];
|
|
224
309
|
|
|
225
310
|
// Create event tap for mouse events
|
|
226
311
|
CGEventMask eventMask = (CGEventMaskBit(kCGEventLeftMouseDown) |
|
|
@@ -244,23 +329,21 @@ Napi::Value StartCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
244
329
|
if (g_eventTap) {
|
|
245
330
|
// Event tap başarılı - detaylı event tracking aktif
|
|
246
331
|
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
|
|
247
|
-
CFRunLoopAddSource(
|
|
332
|
+
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
|
248
333
|
CGEventTapEnable(g_eventTap, true);
|
|
249
334
|
}
|
|
250
|
-
// Event tap başarısız olsa da devam et - sadece timer ile tracking yapar
|
|
251
335
|
|
|
252
|
-
//
|
|
336
|
+
// NSTimer kullan (main thread'de çalışır)
|
|
253
337
|
g_timerTarget = [[CursorTimerTarget alloc] init];
|
|
254
338
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
repeats:YES];
|
|
339
|
+
g_cursorTimer = [NSTimer timerWithTimeInterval:0.05 // 50ms (20 FPS)
|
|
340
|
+
target:g_timerTarget
|
|
341
|
+
selector:@selector(timerCallback:)
|
|
342
|
+
userInfo:nil
|
|
343
|
+
repeats:YES];
|
|
261
344
|
|
|
262
|
-
//
|
|
263
|
-
[[NSRunLoop
|
|
345
|
+
// Main run loop'a ekle
|
|
346
|
+
[[NSRunLoop mainRunLoop] addTimer:g_cursorTimer forMode:NSRunLoopCommonModes];
|
|
264
347
|
|
|
265
348
|
g_isCursorTracking = true;
|
|
266
349
|
return Napi::Boolean::New(env, true);
|
|
@@ -280,19 +363,6 @@ Napi::Value StopCursorTracking(const Napi::CallbackInfo& info) {
|
|
|
280
363
|
}
|
|
281
364
|
|
|
282
365
|
@try {
|
|
283
|
-
// JSON dosyasını kaydet
|
|
284
|
-
if (g_cursorData && g_outputPath) {
|
|
285
|
-
@synchronized(g_cursorData) {
|
|
286
|
-
NSError *error;
|
|
287
|
-
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:g_cursorData
|
|
288
|
-
options:NSJSONWritingPrettyPrinted
|
|
289
|
-
error:&error];
|
|
290
|
-
if (jsonData && !error) {
|
|
291
|
-
[jsonData writeToFile:g_outputPath atomically:YES];
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
366
|
cleanupCursorTracking();
|
|
297
367
|
return Napi::Boolean::New(env, true);
|
|
298
368
|
|
|
@@ -319,10 +389,39 @@ Napi::Value GetCursorPosition(const Napi::CallbackInfo& info) {
|
|
|
319
389
|
|
|
320
390
|
NSString *cursorType = getCursorType();
|
|
321
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
|
+
|
|
322
420
|
Napi::Object result = Napi::Object::New(env);
|
|
323
421
|
result.Set("x", Napi::Number::New(env, (int)location.x));
|
|
324
422
|
result.Set("y", Napi::Number::New(env, (int)location.y));
|
|
325
423
|
result.Set("cursorType", Napi::String::New(env, [cursorType UTF8String]));
|
|
424
|
+
result.Set("eventType", Napi::String::New(env, [eventType UTF8String]));
|
|
326
425
|
|
|
327
426
|
return result;
|
|
328
427
|
|
|
@@ -337,63 +436,22 @@ Napi::Value GetCursorTrackingStatus(const Napi::CallbackInfo& info) {
|
|
|
337
436
|
|
|
338
437
|
Napi::Object result = Napi::Object::New(env);
|
|
339
438
|
result.Set("isTracking", Napi::Boolean::New(env, g_isCursorTracking));
|
|
340
|
-
|
|
341
|
-
NSUInteger dataCount = 0;
|
|
342
|
-
if (g_cursorData) {
|
|
343
|
-
@synchronized(g_cursorData) {
|
|
344
|
-
dataCount = [g_cursorData count];
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
result.Set("dataCount", Napi::Number::New(env, (int)dataCount));
|
|
349
439
|
result.Set("hasEventTap", Napi::Boolean::New(env, g_eventTap != NULL));
|
|
350
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));
|
|
351
443
|
result.Set("debugCallbackCount", Napi::Number::New(env, g_debugCallbackCount));
|
|
352
444
|
result.Set("cursorTypeCounter", Napi::Number::New(env, g_cursorTypeCounter));
|
|
353
445
|
|
|
354
446
|
return result;
|
|
355
447
|
}
|
|
356
448
|
|
|
357
|
-
// NAPI Function: Save Cursor Data
|
|
358
|
-
Napi::Value SaveCursorData(const Napi::CallbackInfo& info) {
|
|
359
|
-
Napi::Env env = info.Env();
|
|
360
|
-
|
|
361
|
-
if (info.Length() < 1) {
|
|
362
|
-
Napi::TypeError::New(env, "Output path required").ThrowAsJavaScriptException();
|
|
363
|
-
return env.Null();
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
std::string outputPath = info[0].As<Napi::String>().Utf8Value();
|
|
367
|
-
|
|
368
|
-
@try {
|
|
369
|
-
if (g_cursorData) {
|
|
370
|
-
@synchronized(g_cursorData) {
|
|
371
|
-
NSError *error;
|
|
372
|
-
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:g_cursorData
|
|
373
|
-
options:NSJSONWritingPrettyPrinted
|
|
374
|
-
error:&error];
|
|
375
|
-
if (jsonData && !error) {
|
|
376
|
-
NSString *filePath = [NSString stringWithUTF8String:outputPath.c_str()];
|
|
377
|
-
BOOL success = [jsonData writeToFile:filePath atomically:YES];
|
|
378
|
-
return Napi::Boolean::New(env, success);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return Napi::Boolean::New(env, false);
|
|
384
|
-
|
|
385
|
-
} @catch (NSException *exception) {
|
|
386
|
-
return Napi::Boolean::New(env, false);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
449
|
// Export functions
|
|
391
450
|
Napi::Object InitCursorTracker(Napi::Env env, Napi::Object exports) {
|
|
392
451
|
exports.Set("startCursorTracking", Napi::Function::New(env, StartCursorTracking));
|
|
393
452
|
exports.Set("stopCursorTracking", Napi::Function::New(env, StopCursorTracking));
|
|
394
453
|
exports.Set("getCursorPosition", Napi::Function::New(env, GetCursorPosition));
|
|
395
454
|
exports.Set("getCursorTrackingStatus", Napi::Function::New(env, GetCursorTrackingStatus));
|
|
396
|
-
exports.Set("saveCursorData", Napi::Function::New(env, SaveCursorData));
|
|
397
455
|
|
|
398
456
|
return exports;
|
|
399
457
|
}
|
package/manual-cursor-data.json
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
[
|
|
2
|
-
{
|
|
3
|
-
"x": 1195,
|
|
4
|
-
"y": 590,
|
|
5
|
-
"timestamp": 36,
|
|
6
|
-
"cursorType": "default",
|
|
7
|
-
"type": "move"
|
|
8
|
-
},
|
|
9
|
-
{
|
|
10
|
-
"x": 1195,
|
|
11
|
-
"y": 590,
|
|
12
|
-
"timestamp": 136,
|
|
13
|
-
"cursorType": "default",
|
|
14
|
-
"type": "move"
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
"x": 1195,
|
|
18
|
-
"y": 590,
|
|
19
|
-
"timestamp": 237,
|
|
20
|
-
"cursorType": "default",
|
|
21
|
-
"type": "move"
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"x": 1195,
|
|
25
|
-
"y": 590,
|
|
26
|
-
"timestamp": 340,
|
|
27
|
-
"cursorType": "default",
|
|
28
|
-
"type": "move"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"x": 1210,
|
|
32
|
-
"y": 590,
|
|
33
|
-
"timestamp": 441,
|
|
34
|
-
"cursorType": "default",
|
|
35
|
-
"type": "move"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"x": 1228,
|
|
39
|
-
"y": 591,
|
|
40
|
-
"timestamp": 542,
|
|
41
|
-
"cursorType": "pointer",
|
|
42
|
-
"type": "move"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"x": 1233,
|
|
46
|
-
"y": 593,
|
|
47
|
-
"timestamp": 644,
|
|
48
|
-
"cursorType": "pointer",
|
|
49
|
-
"type": "move"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"x": 1234,
|
|
53
|
-
"y": 593,
|
|
54
|
-
"timestamp": 746,
|
|
55
|
-
"cursorType": "pointer",
|
|
56
|
-
"type": "move"
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
"x": 1234,
|
|
60
|
-
"y": 613,
|
|
61
|
-
"timestamp": 849,
|
|
62
|
-
"cursorType": "pointer",
|
|
63
|
-
"type": "move"
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"x": 1231,
|
|
67
|
-
"y": 696,
|
|
68
|
-
"timestamp": 951,
|
|
69
|
-
"cursorType": "pointer",
|
|
70
|
-
"type": "move"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
"x": 1231,
|
|
74
|
-
"y": 758,
|
|
75
|
-
"timestamp": 1053,
|
|
76
|
-
"cursorType": "pointer",
|
|
77
|
-
"type": "move"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
"x": 1232,
|
|
81
|
-
"y": 771,
|
|
82
|
-
"timestamp": 1156,
|
|
83
|
-
"cursorType": "text",
|
|
84
|
-
"type": "move"
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
"x": 1232,
|
|
88
|
-
"y": 771,
|
|
89
|
-
"timestamp": 1257,
|
|
90
|
-
"cursorType": "text",
|
|
91
|
-
"type": "move"
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
"x": 1232,
|
|
95
|
-
"y": 771,
|
|
96
|
-
"timestamp": 1359,
|
|
97
|
-
"cursorType": "text",
|
|
98
|
-
"type": "move"
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
"x": 1235,
|
|
102
|
-
"y": 782,
|
|
103
|
-
"timestamp": 1460,
|
|
104
|
-
"cursorType": "text",
|
|
105
|
-
"type": "move"
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
"x": 1251,
|
|
109
|
-
"y": 806,
|
|
110
|
-
"timestamp": 1562,
|
|
111
|
-
"cursorType": "text",
|
|
112
|
-
"type": "move"
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"x": 1266,
|
|
116
|
-
"y": 827,
|
|
117
|
-
"timestamp": 1664,
|
|
118
|
-
"cursorType": "text",
|
|
119
|
-
"type": "move"
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
"x": 1268,
|
|
123
|
-
"y": 838,
|
|
124
|
-
"timestamp": 1766,
|
|
125
|
-
"cursorType": "grabbing",
|
|
126
|
-
"type": "move"
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
"x": 1265,
|
|
130
|
-
"y": 853,
|
|
131
|
-
"timestamp": 1867,
|
|
132
|
-
"cursorType": "grabbing",
|
|
133
|
-
"type": "move"
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
"x": 1252,
|
|
137
|
-
"y": 877,
|
|
138
|
-
"timestamp": 1969,
|
|
139
|
-
"cursorType": "grabbing",
|
|
140
|
-
"type": "move"
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
"x": 1240,
|
|
144
|
-
"y": 904,
|
|
145
|
-
"timestamp": 2070,
|
|
146
|
-
"cursorType": "grabbing",
|
|
147
|
-
"type": "move"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
"x": 1232,
|
|
151
|
-
"y": 920,
|
|
152
|
-
"timestamp": 2173,
|
|
153
|
-
"cursorType": "grabbing",
|
|
154
|
-
"type": "move"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
"x": 1229,
|
|
158
|
-
"y": 929,
|
|
159
|
-
"timestamp": 2274,
|
|
160
|
-
"cursorType": "grabbing",
|
|
161
|
-
"type": "move"
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
"x": 1228,
|
|
165
|
-
"y": 931,
|
|
166
|
-
"timestamp": 2375,
|
|
167
|
-
"cursorType": "default",
|
|
168
|
-
"type": "move"
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
"x": 1225,
|
|
172
|
-
"y": 936,
|
|
173
|
-
"timestamp": 2476,
|
|
174
|
-
"cursorType": "default",
|
|
175
|
-
"type": "move"
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
"x": 1222,
|
|
179
|
-
"y": 941,
|
|
180
|
-
"timestamp": 2577,
|
|
181
|
-
"cursorType": "default",
|
|
182
|
-
"type": "move"
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
"x": 1219,
|
|
186
|
-
"y": 945,
|
|
187
|
-
"timestamp": 2679,
|
|
188
|
-
"cursorType": "default",
|
|
189
|
-
"type": "move"
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
"x": 1217,
|
|
193
|
-
"y": 947,
|
|
194
|
-
"timestamp": 2781,
|
|
195
|
-
"cursorType": "default",
|
|
196
|
-
"type": "move"
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
"x": 1214,
|
|
200
|
-
"y": 951,
|
|
201
|
-
"timestamp": 2882,
|
|
202
|
-
"cursorType": "default",
|
|
203
|
-
"type": "move"
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
"x": 1214,
|
|
207
|
-
"y": 951,
|
|
208
|
-
"timestamp": 2983,
|
|
209
|
-
"cursorType": "pointer",
|
|
210
|
-
"type": "move"
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
"x": 1219,
|
|
214
|
-
"y": 937,
|
|
215
|
-
"timestamp": 3084,
|
|
216
|
-
"cursorType": "pointer",
|
|
217
|
-
"type": "move"
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
"x": 1224,
|
|
221
|
-
"y": 928,
|
|
222
|
-
"timestamp": 3185,
|
|
223
|
-
"cursorType": "pointer",
|
|
224
|
-
"type": "move"
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
"x": 1227,
|
|
228
|
-
"y": 923,
|
|
229
|
-
"timestamp": 3286,
|
|
230
|
-
"cursorType": "pointer",
|
|
231
|
-
"type": "move"
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
"x": 1229,
|
|
235
|
-
"y": 919,
|
|
236
|
-
"timestamp": 3390,
|
|
237
|
-
"cursorType": "pointer",
|
|
238
|
-
"type": "move"
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
"x": 1233,
|
|
242
|
-
"y": 910,
|
|
243
|
-
"timestamp": 3491,
|
|
244
|
-
"cursorType": "pointer",
|
|
245
|
-
"type": "move"
|
|
246
|
-
},
|
|
247
|
-
{
|
|
248
|
-
"x": 1234,
|
|
249
|
-
"y": 907,
|
|
250
|
-
"timestamp": 3592,
|
|
251
|
-
"cursorType": "text",
|
|
252
|
-
"type": "move"
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
"x": 1236,
|
|
256
|
-
"y": 904,
|
|
257
|
-
"timestamp": 3694,
|
|
258
|
-
"cursorType": "text",
|
|
259
|
-
"type": "move"
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
"x": 1236,
|
|
263
|
-
"y": 904,
|
|
264
|
-
"timestamp": 3795,
|
|
265
|
-
"cursorType": "text",
|
|
266
|
-
"type": "move"
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
"x": 1236,
|
|
270
|
-
"y": 904,
|
|
271
|
-
"timestamp": 3898,
|
|
272
|
-
"cursorType": "text",
|
|
273
|
-
"type": "move"
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
"x": 1236,
|
|
277
|
-
"y": 904,
|
|
278
|
-
"timestamp": 3999,
|
|
279
|
-
"cursorType": "text",
|
|
280
|
-
"type": "move"
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
"x": 1236,
|
|
284
|
-
"y": 904,
|
|
285
|
-
"timestamp": 4101,
|
|
286
|
-
"cursorType": "text",
|
|
287
|
-
"type": "move"
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
"x": 1236,
|
|
291
|
-
"y": 904,
|
|
292
|
-
"timestamp": 4203,
|
|
293
|
-
"cursorType": "grabbing",
|
|
294
|
-
"type": "move"
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
"x": 1236,
|
|
298
|
-
"y": 903,
|
|
299
|
-
"timestamp": 4304,
|
|
300
|
-
"cursorType": "grabbing",
|
|
301
|
-
"type": "move"
|
|
302
|
-
},
|
|
303
|
-
{
|
|
304
|
-
"x": 1229,
|
|
305
|
-
"y": 964,
|
|
306
|
-
"timestamp": 4406,
|
|
307
|
-
"cursorType": "grabbing",
|
|
308
|
-
"type": "move"
|
|
309
|
-
},
|
|
310
|
-
{
|
|
311
|
-
"x": 1220,
|
|
312
|
-
"y": 991,
|
|
313
|
-
"timestamp": 4507,
|
|
314
|
-
"cursorType": "grabbing",
|
|
315
|
-
"type": "move"
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
"x": 1217,
|
|
319
|
-
"y": 1004,
|
|
320
|
-
"timestamp": 4609,
|
|
321
|
-
"cursorType": "grabbing",
|
|
322
|
-
"type": "move"
|
|
323
|
-
},
|
|
324
|
-
{
|
|
325
|
-
"x": 1217,
|
|
326
|
-
"y": 1005,
|
|
327
|
-
"timestamp": 4709,
|
|
328
|
-
"cursorType": "grabbing",
|
|
329
|
-
"type": "move"
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
"x": 1215,
|
|
333
|
-
"y": 1014,
|
|
334
|
-
"timestamp": 4810,
|
|
335
|
-
"cursorType": "default",
|
|
336
|
-
"type": "move"
|
|
337
|
-
},
|
|
338
|
-
{
|
|
339
|
-
"x": 1214,
|
|
340
|
-
"y": 1025,
|
|
341
|
-
"timestamp": 4912,
|
|
342
|
-
"cursorType": "default",
|
|
343
|
-
"type": "move"
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
"x": 1214,
|
|
347
|
-
"y": 1025,
|
|
348
|
-
"timestamp": 5015,
|
|
349
|
-
"cursorType": "default",
|
|
350
|
-
"type": "move"
|
|
351
|
-
}
|
|
352
|
-
]
|