node-mac-recorder 1.5.0 → 1.7.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.
@@ -8,12 +8,16 @@ Bu modül, macOS'ta sistem imleci ile pencere seçimi yapabilmenizi sağlayan g
8
8
 
9
9
  - **Real-time Window Detection**: İmleç hangi pencereye gelirse otomatik olarak tespit eder
10
10
  - **Visual Overlay**: Seçilebilir pencereleri mavi transparant kapsayıcı ile highlight eder
11
- - **Interactive Selection**: Merkeze yerleştirilen "Select Window" butonu ile kolay seçim
11
+ - **Interactive Selection**: Merkeze yerleştirilen "Start Record" butonu ile kolay seçim
12
12
  - **Multi-display Support**: Çoklu ekran kurulumlarında çalışır
13
13
  - **Detailed Window Info**: Pencere pozisyonu, boyutu ve hangi ekranda olduğunu döndürür
14
14
  - **Event-driven API**: Pencere hover, seçim ve hata durumları için event'ler
15
15
  - **Window Focus Control**: Detect edilen pencereyi otomatik olarak en öne getirir
16
16
  - **Auto Bring-to-Front**: Cursor hangi pencereye gelirse otomatik focus yapar
17
+ - **Recording Preview Overlay**: Kayıt alanını görselleştiren tam ekran overlay sistemi
18
+ - **Screen Selection**: Tam ekran overlay ile ekran seçimi (menu bar dahil, ESC ile iptal)
19
+ - **Screen Recording Preview**: Seçilen ekran için kayıt önizleme sistemi
20
+ - **ESC Key Support**: Tüm seçim modlarında ESC tuşu ile iptal
17
21
  - **Permission Management**: macOS izin kontrolü ve yönetimi
18
22
 
19
23
  ## 🚀 Kurulum
@@ -56,7 +60,7 @@ async function selectWindow() {
56
60
  const selector = new WindowSelector();
57
61
 
58
62
  try {
59
- console.log('Bir pencere seçin...');
63
+ console.log('Bir pencere seçin (ESC ile iptal)...');
60
64
  const selectedWindow = await selector.selectWindow();
61
65
 
62
66
  console.log('Seçilen pencere:', {
@@ -175,6 +179,84 @@ selector.setBringToFrontEnabled(true); // Auto mode ON
175
179
  selector.setBringToFrontEnabled(false); // Auto mode OFF
176
180
  ```
177
181
 
182
+ ##### `async showRecordingPreview(windowInfo)`
183
+ Seçilen pencere için kayıt önizleme overlay'ini gösterir. Tüm ekranı siyah yapar, sadece pencere alanını şeffaf bırakır.
184
+
185
+ **Parameters:**
186
+ - `windowInfo` (WindowInfo) - Pencere bilgileri
187
+
188
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
189
+
190
+ ```javascript
191
+ const success = await selector.showRecordingPreview(selectedWindow);
192
+ ```
193
+
194
+ ##### `async hideRecordingPreview()`
195
+ Kayıt önizleme overlay'ini gizler.
196
+
197
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
198
+
199
+ ```javascript
200
+ const success = await selector.hideRecordingPreview();
201
+ ```
202
+
203
+ ##### `async startScreenSelection()`
204
+ Ekran seçim modunu başlatır. Tüm ekranları overlay ile gösterir.
205
+
206
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
207
+
208
+ ```javascript
209
+ const success = await selector.startScreenSelection();
210
+ ```
211
+
212
+ ##### `async stopScreenSelection()`
213
+ Ekran seçim modunu durdurur.
214
+
215
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
216
+
217
+ ```javascript
218
+ const success = await selector.stopScreenSelection();
219
+ ```
220
+
221
+ ##### `getSelectedScreen()`
222
+ Son seçilen ekran bilgisini döndürür.
223
+
224
+ **Returns:** `ScreenInfo | null`
225
+
226
+ ```javascript
227
+ const screenInfo = selector.getSelectedScreen();
228
+ ```
229
+
230
+ ##### `async selectScreen()`
231
+ Promise tabanlı ekran seçimi. Kullanıcı bir ekran seçene kadar bekler.
232
+
233
+ **Returns:** `Promise<ScreenInfo>`
234
+
235
+ ```javascript
236
+ const selectedScreen = await selector.selectScreen();
237
+ ```
238
+
239
+ ##### `async showScreenRecordingPreview(screenInfo)`
240
+ Seçilen ekran için kayıt önizleme overlay'ini gösterir. Diğer ekranları siyah yapar, sadece seçili ekranı şeffaf bırakır.
241
+
242
+ **Parameters:**
243
+ - `screenInfo` (ScreenInfo) - Ekran bilgileri
244
+
245
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
246
+
247
+ ```javascript
248
+ const success = await selector.showScreenRecordingPreview(selectedScreen);
249
+ ```
250
+
251
+ ##### `async hideScreenRecordingPreview()`
252
+ Ekran kayıt önizleme overlay'ini gizler.
253
+
254
+ **Returns:** `Promise<boolean>` - Başarı/başarısızlık
255
+
256
+ ```javascript
257
+ const success = await selector.hideScreenRecordingPreview();
258
+ ```
259
+
178
260
  ##### `async cleanup()`
179
261
  Tüm kaynakları temizler ve seçimi durdurur.
180
262
 
@@ -267,6 +349,20 @@ selector.on('error', (error) => {
267
349
  }
268
350
  ```
269
351
 
352
+ ### ScreenInfo
353
+ ```javascript
354
+ {
355
+ id: number, // Ekran ID'si (0, 1, 2, ...)
356
+ name: string, // Ekran adı ("Display 1", "Display 2", ...)
357
+ x: number, // Global X pozisyonu
358
+ y: number, // Global Y pozisyonu
359
+ width: number, // Ekran genişliği
360
+ height: number, // Ekran yüksekliği
361
+ resolution: string, // Çözünürlük string'i ("1920x1080")
362
+ isPrimary: boolean // Ana ekran mı?
363
+ }
364
+ ```
365
+
270
366
  ## 🎮 Test Etme
271
367
 
272
368
  ### Test Dosyasını Çalıştır
@@ -298,6 +394,7 @@ node examples/window-selector-example.js --help
298
394
 
299
395
  ## ⚡ Nasıl Çalışır?
300
396
 
397
+ ### Pencere Seçim Süreci
301
398
  1. **Window Detection**: macOS `CGWindowListCopyWindowInfo` API'si ile açık pencereleri tespit eder
302
399
  2. **Cursor Tracking**: Real-time olarak imleç pozisyonunu takip eder
303
400
  3. **Overlay Rendering**: NSWindow ile transparant overlay penceresi oluşturur
@@ -306,6 +403,29 @@ node examples/window-selector-example.js --help
306
403
  6. **User Interaction**: Merkeze yerleştirilen button ile seçim yapar
307
404
  7. **Data Collection**: Seçilen pencerenin tüm bilgilerini toplar
308
405
 
406
+ ### Kayıt Önizleme Sistemi (Pencere)
407
+ 1. **Full Screen Overlay**: Tüm ekranı kaplayan siyah transparan katman oluşturur
408
+ 2. **Window Cutout**: Seçilen pencere alanını şeffaf hale getirir (cut-out effect)
409
+ 3. **Coordinate Conversion**: CGWindow koordinatlarını NSView koordinatlarına dönüştürür
410
+ 4. **Multi-Display Support**: Çoklu ekran kurulumlarında doğru pozisyonlama yapar
411
+ 5. **Non-Interactive**: Mouse events'leri geçirir, kullanıcı etkileşimini engellemeZ
412
+ 6. **Clean Management**: Programatik açma/kapama kontrolü sağlar
413
+
414
+ ### Ekran Seçim Sistemi
415
+ 1. **Multi-Screen Detection**: NSScreen.screens ile tüm ekranları tespit eder
416
+ 2. **Full Screen Coverage**: Her ekran için tam kaplama overlay oluşturur (menu bar dahil)
417
+ 3. **Interactive Overlays**: Her ekranda merkezi "Select Screen" butonu
418
+ 4. **Screen Information Display**: Ekran adı ve çözünürlük bilgilerini gösterir
419
+ 5. **Automatic Assignment**: Her overlay'i kendi ekranına otomatik atar
420
+ 6. **Selection Feedback**: Seçim yapıldığında anında geri bildirim
421
+
422
+ ### Ekran Kayıt Önizleme Sistemi
423
+ 1. **Multi-Screen Management**: Birden fazla ekranı aynı anda yönetir
424
+ 2. **Selective Darkening**: Sadece seçilmeyen ekranları siyah overlay ile kaplar
425
+ 3. **Recording Area Highlight**: Seçilen ekran tamamen şeffaf kalır
426
+ 4. **Screen-Specific Overlays**: Her ekran için ayrı overlay penceresi
427
+ 5. **Coordinate Independence**: Her ekranın kendi koordinat sistemini kullanır
428
+
309
429
  ## 🔧 Troubleshooting
310
430
 
311
431
  ### Build Hataları
@@ -375,7 +495,161 @@ async function manualFocus() {
375
495
  }
376
496
  ```
377
497
 
378
- ### Otomatik Pencere Kaydı
498
+ ### Ekran Seçimi ile Kayıt
499
+ ```javascript
500
+ const WindowSelector = require('./window-selector');
501
+ const MacRecorder = require('./index');
502
+
503
+ async function recordScreenWithPreview() {
504
+ const selector = new WindowSelector();
505
+ const recorder = new MacRecorder();
506
+
507
+ try {
508
+ // Ekran seç
509
+ const screen = await selector.selectScreen();
510
+ console.log(`Selected: ${screen.name} (${screen.resolution})`);
511
+
512
+ // Kayıt önizlemesi göster (diğer ekranlar siyah, seçili ekran şeffaf)
513
+ await selector.showScreenRecordingPreview(screen);
514
+ console.log('🎬 Screen recording preview shown');
515
+
516
+ // 3 saniye bekle
517
+ await new Promise(resolve => setTimeout(resolve, 3000));
518
+
519
+ // Ekran kaydını başlat
520
+ const outputPath = `./recordings/screen-${screen.id}-${Date.now()}.mov`;
521
+ await recorder.startRecording(outputPath, {
522
+ displayId: screen.id,
523
+ captureCursor: true,
524
+ includeMicrophone: true
525
+ });
526
+
527
+ console.log('🔴 Screen recording started...');
528
+
529
+ // 10 saniye kaydet
530
+ setTimeout(async () => {
531
+ await recorder.stopRecording();
532
+
533
+ // Önizleme overlay'ini gizle
534
+ await selector.hideScreenRecordingPreview();
535
+ console.log(`✅ Recording saved: ${outputPath}`);
536
+ }, 10000);
537
+
538
+ } finally {
539
+ await selector.cleanup();
540
+ }
541
+ }
542
+ ```
543
+
544
+ ### Kayıt Önizleme ile Pencere Kaydı
545
+ ```javascript
546
+ const WindowSelector = require('./window-selector');
547
+ const MacRecorder = require('./index');
548
+
549
+ async function recordWithPreview() {
550
+ const selector = new WindowSelector();
551
+ const recorder = new MacRecorder();
552
+
553
+ try {
554
+ // Pencere seç
555
+ const window = await selector.selectWindow();
556
+ console.log(`Selected: ${window.title}`);
557
+
558
+ // Kayıt önizlemesi göster (siyah overlay + şeffaf pencere alanı)
559
+ await selector.showRecordingPreview(window);
560
+ console.log('🎬 Recording preview shown - you can see exact recording area');
561
+
562
+ // 3 saniye bekle (kullanıcı görebilsin)
563
+ await new Promise(resolve => setTimeout(resolve, 3000));
564
+
565
+ // Kaydı başlat
566
+ const outputPath = `./recordings/${window.appName}-${Date.now()}.mov`;
567
+ await recorder.startRecording(outputPath, {
568
+ windowId: window.id,
569
+ captureCursor: true,
570
+ includeMicrophone: true
571
+ });
572
+
573
+ console.log('🔴 Recording started...');
574
+
575
+ // 10 saniye kaydet
576
+ setTimeout(async () => {
577
+ await recorder.stopRecording();
578
+
579
+ // Önizleme overlay'ini gizle
580
+ await selector.hideRecordingPreview();
581
+ console.log(`✅ Recording saved: ${outputPath}`);
582
+ }, 10000);
583
+
584
+ } finally {
585
+ await selector.cleanup();
586
+ }
587
+ }
588
+ ```
589
+
590
+ ### Basit Ekran Seçimi
591
+ ```javascript
592
+ const WindowSelector = require('./window-selector');
593
+
594
+ async function selectScreen() {
595
+ const selector = new WindowSelector();
596
+
597
+ try {
598
+ console.log('Bir ekran seçin (ESC ile iptal)...');
599
+ const selectedScreen = await selector.selectScreen();
600
+
601
+ console.log('Seçilen ekran:', {
602
+ name: selectedScreen.name,
603
+ resolution: selectedScreen.resolution,
604
+ position: `(${selectedScreen.x}, ${selectedScreen.y})`,
605
+ isPrimary: selectedScreen.isPrimary
606
+ });
607
+
608
+ return selectedScreen;
609
+
610
+ } catch (error) {
611
+ if (error.message.includes('cancelled')) {
612
+ console.log('❌ Seçim iptal edildi');
613
+ } else {
614
+ console.error('Hata:', error.message);
615
+ }
616
+ } finally {
617
+ await selector.cleanup();
618
+ }
619
+ }
620
+ ```
621
+
622
+ ### Manuel Ekran Kontrolü
623
+ ```javascript
624
+ const WindowSelector = require('./window-selector');
625
+
626
+ async function manualScreenSelection() {
627
+ const selector = new WindowSelector();
628
+
629
+ try {
630
+ // Ekran seçimini başlat
631
+ await selector.startScreenSelection();
632
+ console.log('🖥️ Screen overlays shown - click Start Record button (ESC to cancel)');
633
+
634
+ // Polling ile seçim bekle
635
+ const checkSelection = () => {
636
+ const selected = selector.getSelectedScreen();
637
+ if (selected) {
638
+ console.log(`✅ Screen selected: ${selected.name}`);
639
+ return selected;
640
+ }
641
+ setTimeout(checkSelection, 100);
642
+ };
643
+
644
+ checkSelection();
645
+
646
+ } catch (error) {
647
+ console.error('Hata:', error.message);
648
+ }
649
+ }
650
+ ```
651
+
652
+ ### Otomatik Pencere Kaydı (Basit)
379
653
  ```javascript
380
654
  const WindowSelector = require('./window-selector');
381
655
  const MacRecorder = require('./index');
@@ -450,6 +724,7 @@ Bu modül ana projenin lisansı altındadır.
450
724
 
451
725
  ## ⭐ Özellik İstekleri
452
726
 
727
+ ### Pencere Seçimi
453
728
  - [ ] Pencere gruplandırma
454
729
  - [ ] Hotkey desteği
455
730
  - [ ] Pencere filtreleme
@@ -457,6 +732,14 @@ Bu modül ana projenin lisansı altındadır.
457
732
  - [ ] Screenshot alma
458
733
  - [ ] Window history
459
734
 
735
+ ### Ekran Seçimi
736
+ - [x] Tam ekran overlay (menu bar dahil) ✅
737
+ - [x] Multi-display desteği ✅
738
+ - [x] Kayıt önizleme sistemi ✅
739
+ - [ ] Hotkey desteği
740
+ - [ ] Çoklu ekran seçimi
741
+ - [ ] Ekran thumbnail'ları
742
+
460
743
  ---
461
744
 
462
745
  **Not**: Bu modül sadece macOS'ta çalışır ve sistem izinleri gerektirir.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -17,12 +17,33 @@ static NSMutableArray *g_allWindows = nil;
17
17
  static NSDictionary *g_currentWindowUnderCursor = nil;
18
18
  static bool g_bringToFrontEnabled = true; // Default enabled
19
19
 
20
+ // Recording preview overlay state
21
+ static NSWindow *g_recordingPreviewWindow = nil;
22
+ static NSView *g_recordingPreviewView = nil;
23
+ static NSDictionary *g_recordingWindowInfo = nil;
24
+
25
+ // Screen selection overlay state
26
+ static bool g_isScreenSelecting = false;
27
+ static NSMutableArray *g_screenOverlayWindows = nil;
28
+ static NSDictionary *g_selectedScreenInfo = nil;
29
+ static NSArray *g_allScreens = nil;
30
+ static id g_screenKeyEventMonitor = nil;
31
+
20
32
  // Forward declarations
21
33
  void cleanupWindowSelector();
22
34
  void updateOverlay();
23
35
  NSDictionary* getWindowUnderCursor(CGPoint point);
24
36
  NSArray* getAllSelectableWindows();
25
37
  bool bringWindowToFront(int windowId);
38
+ void cleanupRecordingPreview();
39
+ bool showRecordingPreview(NSDictionary *windowInfo);
40
+ bool hideRecordingPreview();
41
+ void cleanupScreenSelector();
42
+ bool startScreenSelection();
43
+ bool stopScreenSelection();
44
+ NSDictionary* getSelectedScreenInfo();
45
+ bool showScreenRecordingPreview(NSDictionary *screenInfo);
46
+ bool hideScreenRecordingPreview();
26
47
 
27
48
  // Custom overlay view class
28
49
  @interface WindowSelectorOverlayView : NSView
@@ -35,7 +56,7 @@ bool bringWindowToFront(int windowId);
35
56
  self = [super initWithFrame:frameRect];
36
57
  if (self) {
37
58
  self.wantsLayer = YES;
38
- self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.6] CGColor];
59
+ self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
39
60
  self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
40
61
  self.layer.borderWidth = 5.0;
41
62
  self.layer.cornerRadius = 8.0;
@@ -49,7 +70,7 @@ bool bringWindowToFront(int windowId);
49
70
  if (!self.windowInfo) return;
50
71
 
51
72
  // Background with transparency
52
- [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.6] setFill];
73
+ [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
53
74
  NSRectFill(dirtyRect);
54
75
 
55
76
  // Border
@@ -80,9 +101,118 @@ bool bringWindowToFront(int windowId);
80
101
 
81
102
  @end
82
103
 
104
+ // Recording preview overlay view - full screen with cutout
105
+ @interface RecordingPreviewView : NSView
106
+ @property (nonatomic, strong) NSDictionary *recordingWindowInfo;
107
+ @end
108
+
109
+ @implementation RecordingPreviewView
110
+
111
+ - (instancetype)initWithFrame:(NSRect)frameRect {
112
+ self = [super initWithFrame:frameRect];
113
+ if (self) {
114
+ self.wantsLayer = YES;
115
+ self.layer.backgroundColor = [[NSColor clearColor] CGColor];
116
+ }
117
+ return self;
118
+ }
119
+
120
+ - (void)drawRect:(NSRect)dirtyRect {
121
+ [super drawRect:dirtyRect];
122
+
123
+ if (!self.recordingWindowInfo) {
124
+ // No window info, fill with semi-transparent black
125
+ [[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
126
+ NSRectFill(dirtyRect);
127
+ return;
128
+ }
129
+
130
+ // Get window coordinates
131
+ int windowX = [[self.recordingWindowInfo objectForKey:@"x"] intValue];
132
+ int windowY = [[self.recordingWindowInfo objectForKey:@"y"] intValue];
133
+ int windowWidth = [[self.recordingWindowInfo objectForKey:@"width"] intValue];
134
+ int windowHeight = [[self.recordingWindowInfo objectForKey:@"height"] intValue];
135
+
136
+ // Convert from CGWindow coordinates (top-left) to NSView coordinates (bottom-left)
137
+ NSScreen *mainScreen = [NSScreen mainScreen];
138
+ CGFloat screenHeight = [mainScreen frame].size.height;
139
+ CGFloat convertedY = screenHeight - windowY - windowHeight;
140
+
141
+ NSRect windowRect = NSMakeRect(windowX, convertedY, windowWidth, windowHeight);
142
+
143
+ // Create a path that covers the entire view but excludes the window area
144
+ NSBezierPath *maskPath = [NSBezierPath bezierPathWithRect:self.bounds];
145
+ NSBezierPath *windowPath = [NSBezierPath bezierPathWithRect:windowRect];
146
+ [maskPath appendBezierPath:windowPath];
147
+ [maskPath setWindingRule:NSWindingRuleEvenOdd]; // Creates hole effect
148
+
149
+ // Fill with semi-transparent black, excluding window area
150
+ [[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5] setFill];
151
+ [maskPath fill];
152
+ }
153
+
154
+ @end
155
+
156
+ // Screen selection overlay view
157
+ @interface ScreenSelectorOverlayView : NSView
158
+ @property (nonatomic, strong) NSDictionary *screenInfo;
159
+ @end
160
+
161
+ @implementation ScreenSelectorOverlayView
162
+
163
+ - (instancetype)initWithFrame:(NSRect)frameRect {
164
+ self = [super initWithFrame:frameRect];
165
+ if (self) {
166
+ self.wantsLayer = YES;
167
+ self.layer.backgroundColor = [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] CGColor];
168
+ self.layer.borderColor = [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor];
169
+ self.layer.borderWidth = 5.0;
170
+ self.layer.cornerRadius = 8.0;
171
+ }
172
+ return self;
173
+ }
174
+
175
+ - (void)drawRect:(NSRect)dirtyRect {
176
+ [super drawRect:dirtyRect];
177
+
178
+ if (!self.screenInfo) return;
179
+
180
+ // Background with transparency
181
+ [[NSColor colorWithRed:0.0 green:0.5 blue:1.0 alpha:0.45] setFill];
182
+ NSRectFill(dirtyRect);
183
+
184
+ // Border
185
+ [[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] setStroke];
186
+ NSBezierPath *border = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:8 yRadius:8];
187
+ [border setLineWidth:3.0];
188
+ [border stroke];
189
+
190
+ // Screen info text
191
+ NSString *screenName = [self.screenInfo objectForKey:@"name"] ?: @"Unknown Screen";
192
+ NSString *resolution = [self.screenInfo objectForKey:@"resolution"] ?: @"Unknown Resolution";
193
+ NSString *infoText = [NSString stringWithFormat:@"%@\n%@", screenName, resolution];
194
+
195
+ NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
196
+ [style setAlignment:NSTextAlignmentCenter];
197
+
198
+ NSDictionary *attributes = @{
199
+ NSFontAttributeName: [NSFont systemFontOfSize:21 weight:NSFontWeightMedium],
200
+ NSForegroundColorAttributeName: [NSColor whiteColor],
201
+ NSParagraphStyleAttributeName: style,
202
+ NSStrokeColorAttributeName: [NSColor blackColor],
203
+ NSStrokeWidthAttributeName: @(-2.0)
204
+ };
205
+
206
+ NSRect textRect = NSMakeRect(10, self.bounds.size.height - 90, self.bounds.size.width - 20, 80);
207
+ [infoText drawInRect:textRect withAttributes:attributes];
208
+ }
209
+
210
+ @end
211
+
83
212
  // Button action handler and timer target
84
213
  @interface WindowSelectorDelegate : NSObject
85
214
  - (void)selectButtonClicked:(id)sender;
215
+ - (void)screenSelectButtonClicked:(id)sender;
86
216
  - (void)timerUpdate:(NSTimer *)timer;
87
217
  @end
88
218
 
@@ -94,6 +224,23 @@ bool bringWindowToFront(int windowId);
94
224
  }
95
225
  }
96
226
 
227
+ - (void)screenSelectButtonClicked:(id)sender {
228
+ NSButton *button = (NSButton *)sender;
229
+ NSInteger screenIndex = [button tag];
230
+
231
+ // Get screen info from global array using button tag
232
+ if (g_allScreens && screenIndex >= 0 && screenIndex < [g_allScreens count]) {
233
+ NSDictionary *screenInfo = [g_allScreens objectAtIndex:screenIndex];
234
+ g_selectedScreenInfo = [screenInfo retain];
235
+
236
+ NSLog(@"🖥️ SCREEN BUTTON CLICKED: %@ (%@)",
237
+ [screenInfo objectForKey:@"name"],
238
+ [screenInfo objectForKey:@"resolution"]);
239
+
240
+ cleanupScreenSelector();
241
+ }
242
+ }
243
+
97
244
  - (void)timerUpdate:(NSTimer *)timer {
98
245
  updateOverlay();
99
246
  }
@@ -313,7 +460,14 @@ void updateOverlay() {
313
460
  CGFloat screenHeight = [mainScreen frame].size.height;
314
461
  CGFloat adjustedY = screenHeight - y - height;
315
462
 
316
- NSRect overlayFrame = NSMakeRect(x, adjustedY, width, height);
463
+ // Clamp overlay to screen bounds to avoid partial off-screen issues
464
+ NSRect screenFrame = [mainScreen frame];
465
+ CGFloat clampedX = MAX(screenFrame.origin.x, MIN(x, screenFrame.origin.x + screenFrame.size.width - width));
466
+ CGFloat clampedY = MAX(screenFrame.origin.y, MIN(adjustedY, screenFrame.origin.y + screenFrame.size.height - height));
467
+ CGFloat clampedWidth = MIN(width, screenFrame.size.width - (clampedX - screenFrame.origin.x));
468
+ CGFloat clampedHeight = MIN(height, screenFrame.size.height - (clampedY - screenFrame.origin.y));
469
+
470
+ NSRect overlayFrame = NSMakeRect(clampedX, clampedY, clampedWidth, clampedHeight);
317
471
 
318
472
  NSString *windowTitle = [windowUnderCursor objectForKey:@"title"] ?: @"Untitled";
319
473
  NSString *appName = [windowUnderCursor objectForKey:@"appName"] ?: @"Unknown";
@@ -407,6 +561,320 @@ void cleanupWindowSelector() {
407
561
  }
408
562
  }
409
563
 
564
+ // Recording preview functions
565
+ void cleanupRecordingPreview() {
566
+ if (g_recordingPreviewWindow) {
567
+ [g_recordingPreviewWindow close];
568
+ g_recordingPreviewWindow = nil;
569
+ g_recordingPreviewView = nil;
570
+ }
571
+
572
+ if (g_recordingWindowInfo) {
573
+ [g_recordingWindowInfo release];
574
+ g_recordingWindowInfo = nil;
575
+ }
576
+ }
577
+
578
+ bool showRecordingPreview(NSDictionary *windowInfo) {
579
+ @try {
580
+ // Clean up any existing preview
581
+ cleanupRecordingPreview();
582
+
583
+ if (!windowInfo) return false;
584
+
585
+ // Store window info
586
+ g_recordingWindowInfo = [windowInfo retain];
587
+
588
+ // Get main screen bounds for full screen overlay
589
+ NSScreen *mainScreen = [NSScreen mainScreen];
590
+ NSRect screenFrame = [mainScreen frame];
591
+
592
+ // Create full-screen overlay window
593
+ g_recordingPreviewWindow = [[NSWindow alloc] initWithContentRect:screenFrame
594
+ styleMask:NSWindowStyleMaskBorderless
595
+ backing:NSBackingStoreBuffered
596
+ defer:NO];
597
+
598
+ [g_recordingPreviewWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)]; // High level but below selection
599
+ [g_recordingPreviewWindow setOpaque:NO];
600
+ [g_recordingPreviewWindow setBackgroundColor:[NSColor clearColor]];
601
+ [g_recordingPreviewWindow setIgnoresMouseEvents:YES]; // Don't interfere with user interaction
602
+ [g_recordingPreviewWindow setAcceptsMouseMovedEvents:NO];
603
+ [g_recordingPreviewWindow setHasShadow:NO];
604
+ [g_recordingPreviewWindow setAlphaValue:1.0];
605
+ [g_recordingPreviewWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
606
+
607
+ // Create preview view
608
+ g_recordingPreviewView = [[RecordingPreviewView alloc] initWithFrame:screenFrame];
609
+ [(RecordingPreviewView *)g_recordingPreviewView setRecordingWindowInfo:windowInfo];
610
+ [g_recordingPreviewWindow setContentView:g_recordingPreviewView];
611
+
612
+ // Show the preview
613
+ [g_recordingPreviewWindow orderFront:nil];
614
+ [g_recordingPreviewWindow makeKeyAndOrderFront:nil];
615
+
616
+ NSLog(@"🎬 RECORDING PREVIEW: Showing overlay for %@ - \"%@\"",
617
+ [windowInfo objectForKey:@"appName"],
618
+ [windowInfo objectForKey:@"title"]);
619
+
620
+ return true;
621
+
622
+ } @catch (NSException *exception) {
623
+ NSLog(@"❌ Error showing recording preview: %@", exception);
624
+ cleanupRecordingPreview();
625
+ return false;
626
+ }
627
+ }
628
+
629
+ bool hideRecordingPreview() {
630
+ @try {
631
+ if (g_recordingPreviewWindow) {
632
+ NSLog(@"🎬 RECORDING PREVIEW: Hiding overlay");
633
+ cleanupRecordingPreview();
634
+ return true;
635
+ }
636
+ return false;
637
+
638
+ } @catch (NSException *exception) {
639
+ NSLog(@"❌ Error hiding recording preview: %@", exception);
640
+ return false;
641
+ }
642
+ }
643
+
644
+ // Screen selection functions
645
+ void cleanupScreenSelector() {
646
+ g_isScreenSelecting = false;
647
+
648
+ // Remove key event monitor
649
+ if (g_screenKeyEventMonitor) {
650
+ [NSEvent removeMonitor:g_screenKeyEventMonitor];
651
+ g_screenKeyEventMonitor = nil;
652
+ }
653
+
654
+ // Close all screen overlay windows
655
+ if (g_screenOverlayWindows) {
656
+ for (NSWindow *overlayWindow in g_screenOverlayWindows) {
657
+ [overlayWindow close];
658
+ }
659
+ [g_screenOverlayWindows release];
660
+ g_screenOverlayWindows = nil;
661
+ }
662
+
663
+ // Clean up screen data
664
+ if (g_allScreens) {
665
+ [g_allScreens release];
666
+ g_allScreens = nil;
667
+ }
668
+ }
669
+
670
+ bool startScreenSelection() {
671
+ @try {
672
+ if (g_isScreenSelecting) return false;
673
+
674
+ // Get all available screens
675
+ NSArray *screens = [NSScreen screens];
676
+ if (!screens || [screens count] == 0) return false;
677
+
678
+ // Create screen info array
679
+ NSMutableArray *screenInfoArray = [[NSMutableArray alloc] init];
680
+ g_screenOverlayWindows = [[NSMutableArray alloc] init];
681
+
682
+ for (NSInteger i = 0; i < [screens count]; i++) {
683
+ NSScreen *screen = [screens objectAtIndex:i];
684
+ NSRect screenFrame = [screen frame];
685
+
686
+ // Create screen info dictionary
687
+ NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
688
+ [screenInfo setObject:[NSNumber numberWithInteger:i] forKey:@"id"];
689
+ [screenInfo setObject:[NSString stringWithFormat:@"Display %ld", (long)(i + 1)] forKey:@"name"];
690
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.x] forKey:@"x"];
691
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.origin.y] forKey:@"y"];
692
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.width] forKey:@"width"];
693
+ [screenInfo setObject:[NSNumber numberWithInt:(int)screenFrame.size.height] forKey:@"height"];
694
+ [screenInfo setObject:[NSString stringWithFormat:@"%.0fx%.0f", screenFrame.size.width, screenFrame.size.height] forKey:@"resolution"];
695
+ [screenInfo setObject:[NSNumber numberWithBool:(i == 0)] forKey:@"isPrimary"]; // First screen is primary
696
+ [screenInfoArray addObject:screenInfo];
697
+
698
+ // Create overlay window for this screen (FULL screen including menu bar)
699
+ NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
700
+ styleMask:NSWindowStyleMaskBorderless
701
+ backing:NSBackingStoreBuffered
702
+ defer:NO
703
+ screen:screen];
704
+
705
+ [overlayWindow setLevel:CGWindowLevelForKey(kCGMaximumWindowLevelKey)];
706
+ [overlayWindow setOpaque:NO];
707
+ [overlayWindow setBackgroundColor:[NSColor clearColor]];
708
+ [overlayWindow setIgnoresMouseEvents:NO];
709
+ [overlayWindow setAcceptsMouseMovedEvents:YES];
710
+ [overlayWindow setHasShadow:NO];
711
+ [overlayWindow setAlphaValue:1.0];
712
+ [overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
713
+
714
+ // Create overlay view
715
+ ScreenSelectorOverlayView *overlayView = [[ScreenSelectorOverlayView alloc] initWithFrame:screenFrame];
716
+ [overlayView setScreenInfo:screenInfo];
717
+ [overlayWindow setContentView:overlayView];
718
+
719
+ // Create select button
720
+ NSButton *selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 180, 60)];
721
+ [selectButton setTitle:@"Start Record"];
722
+ [selectButton setButtonType:NSButtonTypeMomentaryPushIn];
723
+ [selectButton setBezelStyle:NSBezelStyleRounded];
724
+ [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
725
+ [selectButton setTag:i]; // Set screen index as tag
726
+
727
+ // Blue background with white text
728
+ [selectButton setWantsLayer:YES];
729
+ [selectButton.layer setBackgroundColor:[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor]];
730
+ [selectButton.layer setCornerRadius:8.0];
731
+ [selectButton.layer setBorderColor:[[NSColor colorWithRed:0.0 green:0.3 blue:0.7 alpha:1.0] CGColor]];
732
+ [selectButton.layer setBorderWidth:2.0];
733
+
734
+ // White text color
735
+ NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
736
+ initWithString:[selectButton title]];
737
+ [titleString addAttribute:NSForegroundColorAttributeName
738
+ value:[NSColor whiteColor]
739
+ range:NSMakeRange(0, [titleString length])];
740
+ [selectButton setAttributedTitle:titleString];
741
+
742
+ // Add shadow for better visibility
743
+ [selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
744
+ [selectButton.layer setShadowOffset:NSMakeSize(0, -2)];
745
+ [selectButton.layer setShadowRadius:4.0];
746
+ [selectButton.layer setShadowOpacity:0.3];
747
+
748
+ // Set button target and action (reuse global delegate)
749
+ if (!g_delegate) {
750
+ g_delegate = [[WindowSelectorDelegate alloc] init];
751
+ }
752
+ [selectButton setTarget:g_delegate];
753
+ [selectButton setAction:@selector(screenSelectButtonClicked:)];
754
+
755
+ // Position button in center of screen
756
+ NSPoint buttonCenter = NSMakePoint(
757
+ (screenFrame.size.width - [selectButton frame].size.width) / 2,
758
+ (screenFrame.size.height - [selectButton frame].size.height) / 2
759
+ );
760
+ [selectButton setFrameOrigin:buttonCenter];
761
+
762
+ [overlayView addSubview:selectButton];
763
+ [overlayWindow orderFront:nil];
764
+ [overlayWindow makeKeyAndOrderFront:nil];
765
+
766
+ [g_screenOverlayWindows addObject:overlayWindow];
767
+ [screenInfo release];
768
+ }
769
+
770
+ g_allScreens = [screenInfoArray retain];
771
+ [screenInfoArray release];
772
+ g_isScreenSelecting = true;
773
+
774
+ // Add ESC key event monitor to cancel selection
775
+ g_screenKeyEventMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyDown
776
+ handler:^(NSEvent *event) {
777
+ if ([event keyCode] == 53) { // ESC key
778
+ NSLog(@"🖥️ SCREEN SELECTION: ESC pressed - cancelling selection");
779
+ cleanupScreenSelector();
780
+ }
781
+ }];
782
+
783
+ NSLog(@"🖥️ SCREEN SELECTION: Started with %lu screens (ESC to cancel)", (unsigned long)[screens count]);
784
+
785
+ return true;
786
+
787
+ } @catch (NSException *exception) {
788
+ NSLog(@"❌ Error starting screen selection: %@", exception);
789
+ cleanupScreenSelector();
790
+ return false;
791
+ }
792
+ }
793
+
794
+ bool stopScreenSelection() {
795
+ @try {
796
+ if (!g_isScreenSelecting) return false;
797
+
798
+ cleanupScreenSelector();
799
+ NSLog(@"🖥️ SCREEN SELECTION: Stopped");
800
+ return true;
801
+
802
+ } @catch (NSException *exception) {
803
+ NSLog(@"❌ Error stopping screen selection: %@", exception);
804
+ return false;
805
+ }
806
+ }
807
+
808
+ NSDictionary* getSelectedScreenInfo() {
809
+ if (!g_selectedScreenInfo) return nil;
810
+
811
+ NSDictionary *result = [g_selectedScreenInfo retain];
812
+ [g_selectedScreenInfo release];
813
+ g_selectedScreenInfo = nil;
814
+
815
+ return [result autorelease];
816
+ }
817
+
818
+ bool showScreenRecordingPreview(NSDictionary *screenInfo) {
819
+ @try {
820
+ // Clean up any existing preview
821
+ cleanupRecordingPreview();
822
+
823
+ if (!screenInfo) return false;
824
+
825
+ // For screen recording preview, we show all OTHER screens as black overlay
826
+ // and keep the selected screen transparent
827
+ NSArray *screens = [NSScreen screens];
828
+ if (!screens || [screens count] == 0) return false;
829
+
830
+ int selectedScreenId = [[screenInfo objectForKey:@"id"] intValue];
831
+
832
+ // Create overlay for each screen except the selected one
833
+ for (NSInteger i = 0; i < [screens count]; i++) {
834
+ if (i == selectedScreenId) continue; // Skip selected screen
835
+
836
+ NSScreen *screen = [screens objectAtIndex:i];
837
+ NSRect screenFrame = [screen frame];
838
+
839
+ // Create full-screen black overlay for non-selected screens
840
+ NSWindow *overlayWindow = [[NSWindow alloc] initWithContentRect:screenFrame
841
+ styleMask:NSWindowStyleMaskBorderless
842
+ backing:NSBackingStoreBuffered
843
+ defer:NO
844
+ screen:screen];
845
+
846
+ [overlayWindow setLevel:CGWindowLevelForKey(kCGOverlayWindowLevelKey)];
847
+ [overlayWindow setOpaque:NO];
848
+ [overlayWindow setBackgroundColor:[NSColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5]];
849
+ [overlayWindow setIgnoresMouseEvents:YES];
850
+ [overlayWindow setAcceptsMouseMovedEvents:NO];
851
+ [overlayWindow setHasShadow:NO];
852
+ [overlayWindow setAlphaValue:1.0];
853
+ [overlayWindow setCollectionBehavior:NSWindowCollectionBehaviorStationary | NSWindowCollectionBehaviorCanJoinAllSpaces];
854
+
855
+ [overlayWindow orderFront:nil];
856
+ [overlayWindow makeKeyAndOrderFront:nil];
857
+
858
+ // Store for cleanup (reuse recording preview window variable)
859
+ if (!g_recordingPreviewWindow) {
860
+ g_recordingPreviewWindow = overlayWindow;
861
+ }
862
+ }
863
+
864
+ NSLog(@"🎬 SCREEN RECORDING PREVIEW: Showing overlay for Screen %d", selectedScreenId);
865
+
866
+ return true;
867
+
868
+ } @catch (NSException *exception) {
869
+ NSLog(@"❌ Error showing screen recording preview: %@", exception);
870
+ return false;
871
+ }
872
+ }
873
+
874
+ bool hideScreenRecordingPreview() {
875
+ return hideRecordingPreview(); // Reuse existing function
876
+ }
877
+
410
878
  // NAPI Function: Start Window Selection
411
879
  Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
412
880
  Napi::Env env = info.Env();
@@ -446,19 +914,26 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
446
914
  [g_overlayWindow setContentView:g_overlayView];
447
915
 
448
916
  // Create select button with blue theme
449
- g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 140, 50)];
450
- [g_selectButton setTitle:@"Select Window"];
917
+ g_selectButton = [[NSButton alloc] initWithFrame:NSMakeRect(0, 0, 160, 60)];
918
+ [g_selectButton setTitle:@"Start Record"];
451
919
  [g_selectButton setButtonType:NSButtonTypeMomentaryPushIn];
452
920
  [g_selectButton setBezelStyle:NSBezelStyleRounded];
453
921
  [g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightSemibold]];
454
922
 
455
- // Blue themed button styling
923
+ // Blue background with white text
456
924
  [g_selectButton setWantsLayer:YES];
457
925
  [g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:0.0 green:0.4 blue:0.8 alpha:0.9] CGColor]];
458
926
  [g_selectButton.layer setCornerRadius:8.0];
459
927
  [g_selectButton.layer setBorderColor:[[NSColor colorWithRed:0.0 green:0.3 blue:0.7 alpha:1.0] CGColor]];
460
928
  [g_selectButton.layer setBorderWidth:2.0];
461
- [g_selectButton setContentTintColor:[NSColor whiteColor]];
929
+
930
+ // White text color
931
+ NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
932
+ initWithString:[g_selectButton title]];
933
+ [titleString addAttribute:NSForegroundColorAttributeName
934
+ value:[NSColor whiteColor]
935
+ range:NSMakeRange(0, [titleString length])];
936
+ [g_selectButton setAttributedTitle:titleString];
462
937
 
463
938
  // Add shadow for better visibility
464
939
  [g_selectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
@@ -641,6 +1116,193 @@ Napi::Value GetWindowSelectionStatus(const Napi::CallbackInfo& info) {
641
1116
  return result;
642
1117
  }
643
1118
 
1119
+ // NAPI Function: Show Recording Preview
1120
+ Napi::Value ShowRecordingPreview(const Napi::CallbackInfo& info) {
1121
+ Napi::Env env = info.Env();
1122
+
1123
+ if (info.Length() < 1) {
1124
+ Napi::TypeError::New(env, "Window info object required").ThrowAsJavaScriptException();
1125
+ return env.Null();
1126
+ }
1127
+
1128
+ if (!info[0].IsObject()) {
1129
+ Napi::TypeError::New(env, "Window info must be an object").ThrowAsJavaScriptException();
1130
+ return env.Null();
1131
+ }
1132
+
1133
+ @try {
1134
+ Napi::Object windowInfoObj = info[0].As<Napi::Object>();
1135
+
1136
+ // Convert NAPI object to NSDictionary
1137
+ NSMutableDictionary *windowInfo = [[NSMutableDictionary alloc] init];
1138
+
1139
+ if (windowInfoObj.Has("id")) {
1140
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
1141
+ }
1142
+ if (windowInfoObj.Has("title")) {
1143
+ [windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("title").As<Napi::String>().Utf8Value().c_str()] forKey:@"title"];
1144
+ }
1145
+ if (windowInfoObj.Has("appName")) {
1146
+ [windowInfo setObject:[NSString stringWithUTF8String:windowInfoObj.Get("appName").As<Napi::String>().Utf8Value().c_str()] forKey:@"appName"];
1147
+ }
1148
+ if (windowInfoObj.Has("x")) {
1149
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
1150
+ }
1151
+ if (windowInfoObj.Has("y")) {
1152
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
1153
+ }
1154
+ if (windowInfoObj.Has("width")) {
1155
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
1156
+ }
1157
+ if (windowInfoObj.Has("height")) {
1158
+ [windowInfo setObject:[NSNumber numberWithInt:windowInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
1159
+ }
1160
+
1161
+ bool success = showRecordingPreview(windowInfo);
1162
+ [windowInfo release];
1163
+
1164
+ return Napi::Boolean::New(env, success);
1165
+
1166
+ } @catch (NSException *exception) {
1167
+ return Napi::Boolean::New(env, false);
1168
+ }
1169
+ }
1170
+
1171
+ // NAPI Function: Hide Recording Preview
1172
+ Napi::Value HideRecordingPreview(const Napi::CallbackInfo& info) {
1173
+ Napi::Env env = info.Env();
1174
+
1175
+ @try {
1176
+ bool success = hideRecordingPreview();
1177
+ return Napi::Boolean::New(env, success);
1178
+
1179
+ } @catch (NSException *exception) {
1180
+ return Napi::Boolean::New(env, false);
1181
+ }
1182
+ }
1183
+
1184
+ // NAPI Function: Start Screen Selection
1185
+ Napi::Value StartScreenSelection(const Napi::CallbackInfo& info) {
1186
+ Napi::Env env = info.Env();
1187
+
1188
+ @try {
1189
+ bool success = startScreenSelection();
1190
+ return Napi::Boolean::New(env, success);
1191
+
1192
+ } @catch (NSException *exception) {
1193
+ return Napi::Boolean::New(env, false);
1194
+ }
1195
+ }
1196
+
1197
+ // NAPI Function: Stop Screen Selection
1198
+ Napi::Value StopScreenSelection(const Napi::CallbackInfo& info) {
1199
+ Napi::Env env = info.Env();
1200
+
1201
+ @try {
1202
+ bool success = stopScreenSelection();
1203
+ return Napi::Boolean::New(env, success);
1204
+
1205
+ } @catch (NSException *exception) {
1206
+ return Napi::Boolean::New(env, false);
1207
+ }
1208
+ }
1209
+
1210
+ // NAPI Function: Get Selected Screen Info
1211
+ Napi::Value GetSelectedScreenInfo(const Napi::CallbackInfo& info) {
1212
+ Napi::Env env = info.Env();
1213
+
1214
+ @try {
1215
+ NSDictionary *screenInfo = getSelectedScreenInfo();
1216
+ if (!screenInfo) {
1217
+ return env.Null();
1218
+ }
1219
+
1220
+ Napi::Object result = Napi::Object::New(env);
1221
+ result.Set("id", Napi::Number::New(env, [[screenInfo objectForKey:@"id"] intValue]));
1222
+ result.Set("name", Napi::String::New(env, [[screenInfo objectForKey:@"name"] UTF8String]));
1223
+ result.Set("x", Napi::Number::New(env, [[screenInfo objectForKey:@"x"] intValue]));
1224
+ result.Set("y", Napi::Number::New(env, [[screenInfo objectForKey:@"y"] intValue]));
1225
+ result.Set("width", Napi::Number::New(env, [[screenInfo objectForKey:@"width"] intValue]));
1226
+ result.Set("height", Napi::Number::New(env, [[screenInfo objectForKey:@"height"] intValue]));
1227
+ result.Set("resolution", Napi::String::New(env, [[screenInfo objectForKey:@"resolution"] UTF8String]));
1228
+ result.Set("isPrimary", Napi::Boolean::New(env, [[screenInfo objectForKey:@"isPrimary"] boolValue]));
1229
+
1230
+ NSLog(@"🖥️ SCREEN SELECTED: %@ (%@)",
1231
+ [screenInfo objectForKey:@"name"],
1232
+ [screenInfo objectForKey:@"resolution"]);
1233
+
1234
+ return result;
1235
+
1236
+ } @catch (NSException *exception) {
1237
+ return env.Null();
1238
+ }
1239
+ }
1240
+
1241
+ // NAPI Function: Show Screen Recording Preview
1242
+ Napi::Value ShowScreenRecordingPreview(const Napi::CallbackInfo& info) {
1243
+ Napi::Env env = info.Env();
1244
+
1245
+ if (info.Length() < 1) {
1246
+ Napi::TypeError::New(env, "Screen info object required").ThrowAsJavaScriptException();
1247
+ return env.Null();
1248
+ }
1249
+
1250
+ if (!info[0].IsObject()) {
1251
+ Napi::TypeError::New(env, "Screen info must be an object").ThrowAsJavaScriptException();
1252
+ return env.Null();
1253
+ }
1254
+
1255
+ @try {
1256
+ Napi::Object screenInfoObj = info[0].As<Napi::Object>();
1257
+
1258
+ // Convert NAPI object to NSDictionary
1259
+ NSMutableDictionary *screenInfo = [[NSMutableDictionary alloc] init];
1260
+
1261
+ if (screenInfoObj.Has("id")) {
1262
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("id").As<Napi::Number>().Int32Value()] forKey:@"id"];
1263
+ }
1264
+ if (screenInfoObj.Has("name")) {
1265
+ [screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("name").As<Napi::String>().Utf8Value().c_str()] forKey:@"name"];
1266
+ }
1267
+ if (screenInfoObj.Has("resolution")) {
1268
+ [screenInfo setObject:[NSString stringWithUTF8String:screenInfoObj.Get("resolution").As<Napi::String>().Utf8Value().c_str()] forKey:@"resolution"];
1269
+ }
1270
+ if (screenInfoObj.Has("x")) {
1271
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("x").As<Napi::Number>().Int32Value()] forKey:@"x"];
1272
+ }
1273
+ if (screenInfoObj.Has("y")) {
1274
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("y").As<Napi::Number>().Int32Value()] forKey:@"y"];
1275
+ }
1276
+ if (screenInfoObj.Has("width")) {
1277
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("width").As<Napi::Number>().Int32Value()] forKey:@"width"];
1278
+ }
1279
+ if (screenInfoObj.Has("height")) {
1280
+ [screenInfo setObject:[NSNumber numberWithInt:screenInfoObj.Get("height").As<Napi::Number>().Int32Value()] forKey:@"height"];
1281
+ }
1282
+
1283
+ bool success = showScreenRecordingPreview(screenInfo);
1284
+ [screenInfo release];
1285
+
1286
+ return Napi::Boolean::New(env, success);
1287
+
1288
+ } @catch (NSException *exception) {
1289
+ return Napi::Boolean::New(env, false);
1290
+ }
1291
+ }
1292
+
1293
+ // NAPI Function: Hide Screen Recording Preview
1294
+ Napi::Value HideScreenRecordingPreview(const Napi::CallbackInfo& info) {
1295
+ Napi::Env env = info.Env();
1296
+
1297
+ @try {
1298
+ bool success = hideScreenRecordingPreview();
1299
+ return Napi::Boolean::New(env, success);
1300
+
1301
+ } @catch (NSException *exception) {
1302
+ return Napi::Boolean::New(env, false);
1303
+ }
1304
+ }
1305
+
644
1306
  // Export functions
645
1307
  Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
646
1308
  exports.Set("startWindowSelection", Napi::Function::New(env, StartWindowSelection));
@@ -649,6 +1311,13 @@ Napi::Object InitWindowSelector(Napi::Env env, Napi::Object exports) {
649
1311
  exports.Set("getWindowSelectionStatus", Napi::Function::New(env, GetWindowSelectionStatus));
650
1312
  exports.Set("bringWindowToFront", Napi::Function::New(env, BringWindowToFront));
651
1313
  exports.Set("setBringToFrontEnabled", Napi::Function::New(env, SetBringToFrontEnabled));
1314
+ exports.Set("showRecordingPreview", Napi::Function::New(env, ShowRecordingPreview));
1315
+ exports.Set("hideRecordingPreview", Napi::Function::New(env, HideRecordingPreview));
1316
+ exports.Set("startScreenSelection", Napi::Function::New(env, StartScreenSelection));
1317
+ exports.Set("stopScreenSelection", Napi::Function::New(env, StopScreenSelection));
1318
+ exports.Set("getSelectedScreenInfo", Napi::Function::New(env, GetSelectedScreenInfo));
1319
+ exports.Set("showScreenRecordingPreview", Napi::Function::New(env, ShowScreenRecordingPreview));
1320
+ exports.Set("hideScreenRecordingPreview", Napi::Function::New(env, HideScreenRecordingPreview));
652
1321
 
653
1322
  return exports;
654
1323
  }
@@ -272,6 +272,181 @@ class WindowSelector extends EventEmitter {
272
272
  this.isSelecting = false;
273
273
  }
274
274
 
275
+ /**
276
+ * Seçilen pencere için kayıt önizleme overlay'ini gösterir
277
+ * Tüm ekranı siyah yapar, sadece pencere alanını şeffaf bırakır
278
+ * @param {Object} windowInfo - Pencere bilgileri
279
+ * @returns {Promise<boolean>} Success/failure
280
+ */
281
+ async showRecordingPreview(windowInfo) {
282
+ if (!windowInfo) {
283
+ throw new Error("Window info is required");
284
+ }
285
+
286
+ try {
287
+ const success = nativeBinding.showRecordingPreview(windowInfo);
288
+ return success;
289
+ } catch (error) {
290
+ throw new Error(`Failed to show recording preview: ${error.message}`);
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Kayıt önizleme overlay'ini gizler
296
+ * @returns {Promise<boolean>} Success/failure
297
+ */
298
+ async hideRecordingPreview() {
299
+ try {
300
+ const success = nativeBinding.hideRecordingPreview();
301
+ return success;
302
+ } catch (error) {
303
+ throw new Error(`Failed to hide recording preview: ${error.message}`);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Ekran seçimi başlatır
309
+ * Tüm ekranları overlay ile gösterir ve seçim yapılmasını bekler
310
+ * @returns {Promise<boolean>} Success/failure
311
+ */
312
+ async startScreenSelection() {
313
+ try {
314
+ const success = nativeBinding.startScreenSelection();
315
+ if (success) {
316
+ this._isScreenSelecting = true;
317
+ }
318
+ return success;
319
+ } catch (error) {
320
+ throw new Error(`Failed to start screen selection: ${error.message}`);
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Ekran seçimini durdurur
326
+ * @returns {Promise<boolean>} Success/failure
327
+ */
328
+ async stopScreenSelection() {
329
+ try {
330
+ const success = nativeBinding.stopScreenSelection();
331
+ this._isScreenSelecting = false;
332
+ return success;
333
+ } catch (error) {
334
+ throw new Error(`Failed to stop screen selection: ${error.message}`);
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Seçilen ekran bilgisini döndürür
340
+ * @returns {Object|null} Screen info or null
341
+ */
342
+ getSelectedScreen() {
343
+ try {
344
+ const selectedScreen = nativeBinding.getSelectedScreenInfo();
345
+ if (selectedScreen) {
346
+ // Screen selected, update status
347
+ this._isScreenSelecting = false;
348
+ }
349
+ return selectedScreen;
350
+ } catch (error) {
351
+ console.error(`Failed to get selected screen: ${error.message}`);
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Ekran seçim durumunu döndürür
358
+ * @returns {boolean} Is selecting screens
359
+ */
360
+ get isScreenSelecting() {
361
+ // Screen selection durum bilgisi için native taraftan status alalım
362
+ try {
363
+ // Bu fonksiyon henüz yok, eklemek gerekiyor
364
+ return this._isScreenSelecting || false;
365
+ } catch (error) {
366
+ return false;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Promise tabanlı ekran seçimi
372
+ * Kullanıcı bir ekran seçene kadar bekler
373
+ * @returns {Promise<Object>} Selected screen info
374
+ */
375
+ async selectScreen() {
376
+ try {
377
+ // Start screen selection
378
+ await this.startScreenSelection();
379
+
380
+ // Poll for selection completion
381
+ return new Promise((resolve, reject) => {
382
+ let isResolved = false;
383
+
384
+ const checkSelection = () => {
385
+ if (isResolved) return; // Prevent multiple resolutions
386
+
387
+ const selectedScreen = this.getSelectedScreen();
388
+ if (selectedScreen) {
389
+ isResolved = true;
390
+ resolve(selectedScreen);
391
+ } else if (this.isScreenSelecting) {
392
+ // Still selecting, check again
393
+ setTimeout(checkSelection, 100);
394
+ } else {
395
+ // Selection was cancelled (probably ESC key)
396
+ isResolved = true;
397
+ reject(new Error('Screen selection was cancelled'));
398
+ }
399
+ };
400
+
401
+ // Start polling
402
+ checkSelection();
403
+
404
+ // Timeout after 60 seconds
405
+ setTimeout(() => {
406
+ if (!isResolved) {
407
+ isResolved = true;
408
+ this.stopScreenSelection();
409
+ reject(new Error('Screen selection timed out'));
410
+ }
411
+ }, 60000);
412
+ });
413
+ } catch (error) {
414
+ throw new Error(`Failed to select screen: ${error.message}`);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Seçilen ekran için kayıt önizleme overlay'ini gösterir
420
+ * Diğer ekranları siyah yapar, sadece seçili ekranı şeffaf bırakır
421
+ * @param {Object} screenInfo - Ekran bilgileri
422
+ * @returns {Promise<boolean>} Success/failure
423
+ */
424
+ async showScreenRecordingPreview(screenInfo) {
425
+ if (!screenInfo) {
426
+ throw new Error("Screen info is required");
427
+ }
428
+
429
+ try {
430
+ const success = nativeBinding.showScreenRecordingPreview(screenInfo);
431
+ return success;
432
+ } catch (error) {
433
+ throw new Error(`Failed to show screen recording preview: ${error.message}`);
434
+ }
435
+ }
436
+
437
+ /**
438
+ * Ekran kayıt önizleme overlay'ini gizler
439
+ * @returns {Promise<boolean>} Success/failure
440
+ */
441
+ async hideScreenRecordingPreview() {
442
+ try {
443
+ const success = nativeBinding.hideScreenRecordingPreview();
444
+ return success;
445
+ } catch (error) {
446
+ throw new Error(`Failed to hide screen recording preview: ${error.message}`);
447
+ }
448
+ }
449
+
275
450
  /**
276
451
  * macOS'ta pencere seçim izinlerini kontrol eder
277
452
  */