node-mac-recorder 2.24.4 → 2.24.5

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/index.js CHANGED
@@ -899,79 +899,58 @@ class MacRecorder extends EventEmitter {
899
899
  }
900
900
  };
901
901
 
902
- let videoStartTimestamp = 0;
903
- const waitStart = Date.now();
904
-
905
- if (usesScreenCaptureKit) {
906
- while (Date.now() - waitStart < VIDEO_START_TIMEOUT_MS) {
907
- videoStartTimestamp = readVideoStart();
908
- if (videoStartTimestamp > 0) {
909
- console.log(
910
- `✅ SYNC: Video ilk karesi ${Date.now() - waitStart}ms'de yakalandi`
911
- );
912
- break;
913
- }
914
- await new Promise(r => setTimeout(r, VIDEO_START_POLL_MS));
915
- }
916
-
917
- if (!videoStartTimestamp) {
918
- console.warn(
919
- `⚠️ SYNC: Video baslangici ${VIDEO_START_TIMEOUT_MS}ms icinde okunamadi — heuristik hizalamaya dusuluyor`
920
- );
921
- }
922
- } else {
923
- // AVFoundation yolunda video-start damgasi yok; eski
924
- // hazir-olma beklemesi korunuyor.
925
- while (Date.now() - waitStart < 600) {
926
- try {
927
- if (
928
- nativeBinding &&
929
- nativeBinding.getRecordingStatus &&
930
- nativeBinding.getRecordingStatus()
931
- ) {
932
- break;
933
- }
934
- } catch (_) {}
935
- await new Promise(r => setTimeout(r, 30));
936
- }
937
- }
938
-
902
+ // ONEMLI: Bu bekleme BLOKLAMAZ.
903
+ // Ilk kare, yakalandigi andan ~200ms+ sonra islenip bize ulasiyor
904
+ // (clamshell'de saniyeler). startRecording bunu beklerse kayit
905
+ // coktan basladigi halde UI "Starting..." gostergesinde takili
906
+ // kaliyor. Bu yuzden cursor/klavye HEMEN baslatiliyor (veri kaybi
907
+ // olmasin), video baslangici ARKA PLANDA yakalanip saklaniyor;
908
+ // stop sirasinda cursor JSON'una gercek offset yaziliyor ve
909
+ // editor bunu birebir telafi ediyor.
939
910
  this.sessionTimestamp = sessionTimestamp;
940
- this.videoStartTimestamp = videoStartTimestamp;
941
- // Onceki kayittan kalan referans sizmasin
942
- this.timelineStartTimestamp = 0;
911
+ this.videoStartTimestamp = 0;
943
912
 
944
913
  const syncTimestamp = Date.now();
945
914
  this.syncTimestamp = syncTimestamp;
946
915
  this.recordingStartTime = syncTimestamp;
916
+ this.timelineStartTimestamp = syncTimestamp;
947
917
 
948
- if (videoStartTimestamp > 0) {
949
- console.log(
950
- `🎯 SYNC: Video first-frame timestamp: ${videoStartTimestamp} (JS bunu ${(syncTimestamp - videoStartTimestamp).toFixed(0)}ms sonra fark etti)`
951
- );
918
+ if (usesScreenCaptureKit) {
919
+ const watchStart = Date.now();
920
+ this._videoStartWatcherActive = true;
921
+ this._videoStartWatcher = (async () => {
922
+ while (
923
+ this._videoStartWatcherActive &&
924
+ Date.now() - watchStart < VIDEO_START_TIMEOUT_MS
925
+ ) {
926
+ const value = readVideoStart();
927
+ if (value > 0) {
928
+ this.videoStartTimestamp = value;
929
+ console.log(
930
+ `✅ SYNC: Video ilk karesi ${Date.now() - watchStart}ms'de yakalandi (cursor'a gore ${(syncTimestamp - value).toFixed(0)}ms once)`
931
+ );
932
+ return value;
933
+ }
934
+ await new Promise(r => setTimeout(r, VIDEO_START_POLL_MS));
935
+ }
936
+ if (this._videoStartWatcherActive) {
937
+ console.warn(
938
+ `⚠️ SYNC: Video baslangici ${VIDEO_START_TIMEOUT_MS}ms icinde okunamadi — heuristik hizalamaya dusuluyor`
939
+ );
940
+ }
941
+ return 0;
942
+ })();
943
+ // Kayit akisini etkilemesin
944
+ this._videoStartWatcher.catch(() => {});
952
945
  }
953
946
 
954
- // ZAMAN REFERANSI: cursor/klavye timeline'i VIDEONUN ilk karesine
955
- // hizalanir, JS'in "kayit hazir" dedigi ana degil.
956
- //
957
- // NEDEN: syncTimestamp, yukaridaki hazir-olma dongusunden sonraki an.
958
- // Bu an ile videonun gercek t=0'i arasindaki fark, baslatma hizina
959
- // gore DEGISIYOR (ornegin ScreenCaptureKit isitilmissa video cok daha
960
- // erken basliyor). Cursor bu degisken ana baglanirsa her kayitta
961
- // farkli bir kayma olusuyor ve editordeki telafi 1sn limitine
962
- // takilabiliyor. Videonun kendi baslangicini referans alinca fark
963
- // yapisal olarak sifir olur; hizlanma senkronu bozmaz.
964
- const timelineStartTimestamp =
965
- this.videoStartTimestamp > 0
966
- ? this.videoStartTimestamp
967
- : syncTimestamp;
968
- this.timelineStartTimestamp = timelineStartTimestamp;
969
-
970
- if (timelineStartTimestamp !== syncTimestamp) {
971
- console.log(
972
- `🎯 SYNC: Timeline referansi video ilk karesine cekildi (${(syncTimestamp - timelineStartTimestamp).toFixed(0)}ms geri)`
973
- );
974
- }
947
+ // ZAMAN REFERANSI: cursor/klavye timeline'i SIMDI baslar
948
+ // (syncTimestamp). Videonun gercek t=0'i genelde bundan biraz
949
+ // oncedir; aradaki fark stop'ta cursor JSON'una yazilir ve
950
+ // editor birebir telafi eder. Bekleyip "sifir offset" elde
951
+ // etmek yerine bu yol tercih ediliyor cunku bekleme, kayit
952
+ // coktan basladigi halde UI'i "Starting..." halinde tutuyordu.
953
+ const timelineStartTimestamp = syncTimestamp;
975
954
 
976
955
  const standardCursorOptions = {
977
956
  videoRelative: true,
@@ -1085,11 +1064,43 @@ class MacRecorder extends EventEmitter {
1085
1064
  }, 1000);
1086
1065
 
1087
1066
  // Native kayıt gerçekten başladığını kontrol etmek için polling başlat
1067
+ //
1068
+ // KRITIK: getRecordingStatus() TEK BASINA KULLANILAMAZ.
1069
+ // ScreenCaptureKit yolunda `isFullyInitialized` = 10 KARE sarti
1070
+ // arıyor. Ama video writer ILK KAREDE basliyor — yani kayit
1071
+ // coktan basladigi halde bu bayrak false kalabiliyor.
1072
+ // ScreenCaptureKit statik ekranda yeni kare URETMEDIGI icin
1073
+ // (sadece degisiklik oldugunda kare gonderir) kullanici kayda
1074
+ // basip bekledigi anda 10 kare hic dolmuyor ve UI saniyelerce
1075
+ // "Starting recorder..." gosteriyordu; bu sure de videoya giriyordu.
1076
+ //
1077
+ // Dogru sinyal ilk karedir: videoStartTimestamp > 0 (arka plandaki
1078
+ // _videoStartWatcher set eder). Boylece UI, videonun gercek
1079
+ // baslangici ile ayni ana kapanir.
1088
1080
  let recordingStartedEmitted = false;
1089
1081
  let checkRecordingStatus = null;
1082
+ const isNativeRecordingLive = () => {
1083
+ if (this.options.preferScreenCaptureKit === true) {
1084
+ // SCK yolunda TEK dogru sinyal ilk karedir.
1085
+ // getRecordingStatus() burada iki yonlu yaniltiyor:
1086
+ // - SCK stream'i henuz baslamadiysa g_isRecording
1087
+ // fallback'ine dusup ERKEN true doner (video yokken
1088
+ // "kayit basladi" denir)
1089
+ // - SCK basladiysa isFullyInitialized = 10 kare bekler,
1090
+ // statik ekranda bu hic dolmaz ve GEC true doner
1091
+ // Hangisinin kazandigi yarisa bagli; ilk kare ise kesin.
1092
+ return Number(this.videoStartTimestamp) > 0;
1093
+ }
1094
+ // AVFoundation yolu: video-start damgasi yok, eski sinyal
1095
+ try {
1096
+ return nativeBinding.getRecordingStatus();
1097
+ } catch (_) {
1098
+ return false;
1099
+ }
1100
+ };
1090
1101
  const pollRecordingStatus = () => {
1091
1102
  try {
1092
- const nativeStatus = nativeBinding.getRecordingStatus();
1103
+ const nativeStatus = isNativeRecordingLive();
1093
1104
  if (nativeStatus && !recordingStartedEmitted) {
1094
1105
  recordingStartedEmitted = true;
1095
1106
  clearInterval(checkRecordingStatus);
@@ -1229,6 +1240,11 @@ class MacRecorder extends EventEmitter {
1229
1240
  try {
1230
1241
  console.log('🛑 SYNC: Stopping all recording components simultaneously');
1231
1242
 
1243
+ // Video baslangicini bekleyen arka plan izleyicisini sonlandir.
1244
+ // Deger geldiyse zaten this.videoStartTimestamp'e yazildi ve
1245
+ // cursor metadata'sinda kullanilacak.
1246
+ this._videoStartWatcherActive = false;
1247
+
1232
1248
  // SYNC FIX: Stop ALL components at the same time for perfect sync
1233
1249
  // 1. Stop cursor tracking FIRST (it's instant)
1234
1250
  if (this.cursorCaptureInterval) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.4",
3
+ "version": "2.24.5",
4
4
  "description": "Native macOS screen recording package for Node.js applications",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -627,14 +627,39 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
627
627
  // Use ScreenCaptureKit with window exclusion and timeout protection
628
628
  NSError *sckError = nil;
629
629
 
630
- // A/V SYNC: Start camera non-blocking BEFORE ScreenCaptureKit
631
- // Camera warmup overlaps with async SCK initialization
630
+ // A/V SYNC: Kamera SCK'dan ONCE baslatilir VE onayi burada beklenir.
631
+ //
632
+ // ESKIDEN: kamera non-blocking baslatilip SCK hemen schedule
633
+ // ediliyor, kamera onayi SCK'dan SONRA bekleniyordu. Sorun:
634
+ // SCK schedule edilir edilmez video ilk karesini yakalayip
635
+ // KAYDA BASLIYOR, ama JS tarafi hala kamera onayini bekledigi
636
+ // icin (startRecording senkron bir N-API cagrisi, event loop
637
+ // bloke) "kayit basladi" sinyali ~1sn gec gidiyordu.
638
+ // Olculdu: kamerali startRecording 1319ms / kamerasiz 508ms.
639
+ // Bu fark dogrudan videonun basina fazlalik olarak yaziliyor
640
+ // ve UI o sure boyunca "Starting recorder..." gosteriyordu.
641
+ //
642
+ // SIMDI: kamera hazir olduktan SONRA video baslar. Basta
643
+ // fazlalik olusmaz ve kamera ile ekran ayni ana hizalanir.
632
644
  if (captureCamera) {
633
- MRLog(@"🎥 Starting camera recording non-blocking (parallel with SCK init)");
645
+ MRLog(@"🎥 Starting camera recording (before SCK, blocking until ready)");
634
646
  if (!startCameraIfRequested(true, &cameraOutputPath, cameraDeviceId, outputPath, sessionTimestamp)) {
635
647
  MRLog(@"❌ Camera failed to start - aborting recording");
636
648
  return Napi::Boolean::New(env, false);
637
649
  }
650
+
651
+ if (!waitForCameraRecordingStart(8.0)) {
652
+ double cameraStartTs = currentCameraRecordingStartTime();
653
+ if (cameraStartTs > 0 || isCameraRecording()) {
654
+ MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
655
+ } else {
656
+ MRLog(@"❌ Camera did not signal recording start within 8.0s");
657
+ stopCameraRecording();
658
+ return Napi::Boolean::New(env, false);
659
+ }
660
+ } else {
661
+ MRLog(@"✅ Camera recording confirmed (before SCK start)");
662
+ }
638
663
  }
639
664
 
640
665
  // Start SCK immediately - don't wait for camera confirmation
@@ -647,21 +672,10 @@ Napi::Value StartRecording(const Napi::CallbackInfo& info) {
647
672
  MRLog(@"🎬 RECORDING METHOD: ScreenCaptureKit");
648
673
  MRLog(@"✅ SYNC: ScreenCaptureKit recording started successfully");
649
674
 
650
- // A/V SYNC: Wait for camera AFTER SCK started
651
- if (captureCamera) {
652
- if (!waitForCameraRecordingStart(8.0)) {
653
- double cameraStartTs = currentCameraRecordingStartTime();
654
- if (cameraStartTs > 0 || isCameraRecording()) {
655
- MRLog(@"⚠️ Camera did not confirm start within 8.0s but appears running; continuing");
656
- } else {
657
- MRLog(@"❌ Camera did not signal recording start within 8.0s");
658
- stopCameraRecording();
659
- [ScreenCaptureKitRecorder stopRecording];
660
- return Napi::Boolean::New(env, false);
661
- }
662
- }
663
- MRLog(@"✅ Camera recording confirmed (started in parallel with SCK)");
664
- }
675
+ // NOT: Kamera onayi artik SCK'dan ONCE bekleniyor
676
+ // (yukariya alindi) — burada blocking bir is kalmadi,
677
+ // boylece SCK schedule edildikten sonra JS'e donus
678
+ // gecikmiyor ve videonun basinda fazlalik olusmuyor.
665
679
 
666
680
  g_isRecording = true;
667
681
  MRMarkRecordingStartTimestamp();
@@ -167,6 +167,45 @@ static NSInteger g_frameCount = 0;
167
167
  static CFAbsoluteTime g_firstFrameTime = 0;
168
168
  static const NSInteger kSCKHighQualityVideoBitrate = 100 * 1000 * 1000;
169
169
 
170
+ // ---- Prewarm edilmis SCShareableContent onbellegi ----
171
+ // Kayit baslatilirken envanter cagrisini tamamen atlayabilmek icin saklanir.
172
+ // ARC KAPALI: retain/release elle yonetiliyor.
173
+ static id g_cachedShareableContent = nil;
174
+ static CFAbsoluteTime g_cachedShareableContentTime = 0;
175
+ // Bayat envanter yanlis pencere/ekran secimine yol acabilir; kisa tutuluyor.
176
+ static const CFAbsoluteTime kShareableContentCacheTTLSeconds = 30.0;
177
+
178
+ static void SCKStoreShareableContent(id content) {
179
+ if (!content) return;
180
+ @synchronized([ScreenCaptureKitRecorder class]) {
181
+ if (g_cachedShareableContent != content) {
182
+ [g_cachedShareableContent release];
183
+ g_cachedShareableContent = [content retain];
184
+ }
185
+ g_cachedShareableContentTime = CFAbsoluteTimeGetCurrent();
186
+ }
187
+ }
188
+
189
+ // Taze onbellek varsa doner ve onbellegi bosaltir (her kayit taze envanterle
190
+ // baslasin diye tek kullanimlik). Yoksa nil.
191
+ static id SCKTakeCachedShareableContent(void) {
192
+ @synchronized([ScreenCaptureKitRecorder class]) {
193
+ if (!g_cachedShareableContent) {
194
+ return nil;
195
+ }
196
+
197
+ CFAbsoluteTime age = CFAbsoluteTimeGetCurrent() - g_cachedShareableContentTime;
198
+ id content = [[g_cachedShareableContent retain] autorelease];
199
+ [g_cachedShareableContent release];
200
+ g_cachedShareableContent = nil;
201
+
202
+ if (age > kShareableContentCacheTTLSeconds) {
203
+ return nil;
204
+ }
205
+ return content;
206
+ }
207
+ }
208
+
170
209
  // Quality helpers
171
210
  static NSString *SCKNormalizeQualityPreset(id preset) {
172
211
  if (![preset isKindOfClass:[NSString class]]) {
@@ -1041,16 +1080,24 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1041
1080
 
1042
1081
  + (void)prewarmShareableContent {
1043
1082
  if (@available(macOS 15.0, *)) {
1044
- // Kayit sirasindaki ilk SCShareableContent cagrisi yavas olabiliyor
1045
- // (TCC kontrolu + pencere/ekran envanteri). Kayit baslamadan once bir kez
1046
- // cagirip macOS'un ic cache'ini isitiyoruz. Sonuc kullanilmiyor.
1083
+ // Kayit sirasindaki SCShareableContent cagrisi yavas olabiliyor
1084
+ // (TCC kontrolu + ekran/pencere envanteri). Ozellikle ekran
1085
+ // konfigurasyonu degistikten sonra (clamshell) bu envanter sifirdan
1086
+ // kuruluyor ve saniyeler surebiliyor.
1087
+ //
1088
+ // Sadece "isitmak" yetmiyordu: kayit aninda cagri YINE bastan
1089
+ // yapiliyordu. Artik sonucu saklayip kayitta yeniden kullaniyoruz,
1090
+ // yani bu adim tamamen atlanabiliyor.
1047
1091
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
1092
+ CFAbsoluteTime fetchStart = CFAbsoluteTimeGetCurrent();
1048
1093
  [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1049
- if (contentError) {
1094
+ if (contentError || !content) {
1050
1095
  MRLog(@"⚠️ Prewarm shareable content failed: %@", contentError.localizedDescription);
1051
1096
  return;
1052
1097
  }
1053
- MRLog(@"🔥 Prewarm: shareable content hazir (%lu ekran, %lu pencere)",
1098
+ SCKStoreShareableContent(content);
1099
+ NSLog(@"🔥 Prewarm: shareable content hazir ve saklandi (%.0fms, %lu ekran, %lu pencere)",
1100
+ (CFAbsoluteTimeGetCurrent() - fetchStart) * 1000.0,
1054
1101
  (unsigned long)content.displays.count,
1055
1102
  (unsigned long)content.windows.count);
1056
1103
  }];
@@ -1082,9 +1129,29 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1082
1129
  return NO;
1083
1130
  }
1084
1131
 
1132
+ // Prewarm ile alinmis taze envanter varsa cagriyi TAMAMEN atla.
1133
+ // Bu cagri clamshell'de saniyeler surebiliyor ve kayit baslatmanin
1134
+ // en buyuk gecikme kalemi.
1135
+ if (@available(macOS 15.0, *)) {
1136
+ SCShareableContent *prewarmed = (SCShareableContent *)SCKTakeCachedShareableContent();
1137
+ if (prewarmed) {
1138
+ NSLog(@"⚡ Prewarmed shareable content kullanildi — envanter cagrisi atlandi");
1139
+ dispatch_async(controlQueue, ^{
1140
+ SCKPerformRecordingSetup(configCopy, prewarmed);
1141
+ });
1142
+ // NOT: Burada onbellegi tazelemek YOK.
1143
+ // Kayit setup'i calisirken yeni bir SCShareableContent istegi
1144
+ // baslatmak sistem envanterini kayitla ayni anda sorgular ve
1145
+ // kamera/stream baslatmayla yarisir. Tazeleme, kayit bittikten
1146
+ // sonra desktop tarafindaki periyodik isitmaya birakiliyor.
1147
+ return YES;
1148
+ }
1149
+ }
1150
+
1085
1151
  // CRITICAL FIX: Use dispatch_get_global_queue instead of main_queue
1086
1152
  // because Node.js standalone doesn't run macOS main event loop (only Electron does)
1087
1153
  NSLog(@"🚀 Requesting shareable content...");
1154
+ CFAbsoluteTime contentFetchStart = CFAbsoluteTimeGetCurrent();
1088
1155
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
1089
1156
  [SCShareableContent getShareableContentWithCompletionHandler:^(SCShareableContent *content, NSError *contentError) {
1090
1157
  if (contentError || !content) {
@@ -1092,7 +1159,8 @@ extern "C" NSString *ScreenCaptureKitCurrentAudioPath(void) {
1092
1159
  SCKFailScheduling();
1093
1160
  return;
1094
1161
  }
1095
- NSLog(@"✅ Got shareable content, starting recording setup...");
1162
+ NSLog(@"✅ Got shareable content in %.0fms, starting recording setup...",
1163
+ (CFAbsoluteTimeGetCurrent() - contentFetchStart) * 1000.0);
1096
1164
  dispatch_async(controlQueue, ^{
1097
1165
  SCKPerformRecordingSetup(configCopy, content);
1098
1166
  });
@@ -85,11 +85,31 @@ static bool shouldSkipSelectableWindowOwner(NSString *windowOwner) {
85
85
  }
86
86
 
87
87
  // Record icon helpers
88
- static NSImage *CreateRecordIconImage(CGFloat size) {
89
- const CGFloat leadingInset = 24.0;
90
- const CGFloat trailingSpacing = 6.0;
91
- CGFloat width = leadingInset + size + trailingSpacing;
92
- NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(width, size)] autorelease];
88
+ // ---------------------------------------------------------------------------
89
+ // Marka butonu metrikleri — electron/main.cjs içindeki alan seçici `button.primary`
90
+ // ile bire bir: padding 14px 26px | min-width 220px | radius 18px | font 15px/650
91
+ // gap 10px | ikon 20px halka + 8px nokta | hover: translateY(-1px)
92
+ // ---------------------------------------------------------------------------
93
+ static const CGFloat kBrandFontSize = 15.0;
94
+ static const CGFloat kBrandCornerRadius = 18.0;
95
+ static const CGFloat kBrandPaddingX = 26.0;
96
+ static const CGFloat kBrandPaddingY = 16.0; // web 14px; NSButton'da metin
97
+ // biraz daha sıkışık durduğu için +2
98
+ static const CGFloat kBrandMinWidth = 220.0;
99
+ static const CGFloat kBrandIconDiameter = 20.0;
100
+ static const CGFloat kBrandIconSpacing = 10.0;
101
+ // Hover: web'de renk DEĞİŞMİYOR, buton 1px yukarı kalkıyor.
102
+ static const CGFloat kBrandHoverLift = 1.0;
103
+
104
+ // CSS açısı (0° = yukarı, saat yönü) -> NSGradient açısı (0° = sağa, saat yönü tersi)
105
+ static const CGFloat kBrandBaseAngle = 259.0; // CSS 190.98deg
106
+ static const CGFloat kBrandGlossAngle = 249.25; // CSS 200.71deg
107
+
108
+ // Alan seçicideki .primary-icon ile aynı: 20x20 halka, 2px beyaz kenarlık
109
+ // (%90 alfa), ortasında 8px beyaz nokta. Sağdaki boşluk CSS'teki gap:10px.
110
+ static NSImage *CreateRecordIconImage(CGFloat diameter, CGFloat trailingGap) {
111
+ CGFloat width = diameter + trailingGap;
112
+ NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(width, diameter)] autorelease];
93
113
  if (!image) {
94
114
  return nil;
95
115
  }
@@ -97,26 +117,25 @@ static NSImage *CreateRecordIconImage(CGFloat size) {
97
117
  [image lockFocus];
98
118
 
99
119
  [[NSColor clearColor] setFill];
100
- NSRectFill(NSMakeRect(0, 0, width, size));
120
+ NSRectFill(NSMakeRect(0, 0, width, diameter));
101
121
 
102
- CGFloat strokeWidth = MAX(2.0, size * 0.12);
103
- NSRect iconRect = NSMakeRect(leadingInset,
104
- (size - size) / 2.0,
105
- size,
106
- size);
122
+ NSColor *iconColor = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.9];
123
+
124
+ const CGFloat strokeWidth = 2.0;
125
+ NSRect iconRect = NSMakeRect(0, 0, diameter, diameter);
107
126
  NSRect outerRect = NSInsetRect(iconRect, strokeWidth / 2.0, strokeWidth / 2.0);
108
127
  NSBezierPath *outerPath = [NSBezierPath bezierPathWithOvalInRect:outerRect];
109
128
  [outerPath setLineWidth:strokeWidth];
110
- [[NSColor whiteColor] setStroke];
129
+ [iconColor setStroke];
111
130
  [outerPath stroke];
112
131
 
113
- CGFloat innerDiameter = size * 0.45;
132
+ const CGFloat innerDiameter = 8.0;
114
133
  NSRect innerRect = NSMakeRect(NSMidX(iconRect) - innerDiameter / 2.0,
115
134
  NSMidY(iconRect) - innerDiameter / 2.0,
116
135
  innerDiameter,
117
136
  innerDiameter);
118
137
  NSBezierPath *innerPath = [NSBezierPath bezierPathWithOvalInRect:innerRect];
119
- [[NSColor whiteColor] setFill];
138
+ [iconColor setFill];
120
139
  [innerPath fill];
121
140
 
122
141
  [image unlockFocus];
@@ -128,7 +147,7 @@ static NSImage *CreateRecordIconImage(CGFloat size) {
128
147
  static NSImage *GetStartRecordIcon(void) {
129
148
  static NSImage *recordIcon = nil;
130
149
  if (!recordIcon) {
131
- recordIcon = [CreateRecordIconImage(18.0) retain];
150
+ recordIcon = [CreateRecordIconImage(kBrandIconDiameter, kBrandIconSpacing) retain];
132
151
  }
133
152
  return recordIcon;
134
153
  }
@@ -146,15 +165,54 @@ static void ApplyStartRecordButtonIcon(NSButton *button) {
146
165
  [button setImage:icon];
147
166
  [button setImageScaling:NSImageScaleNone];
148
167
  [button setImagePosition:NSImageLeft];
168
+ // İç boşluk ApplyBrandButtonStyle'da ayarlanıyor (padding 14px 26px).
169
+ }
149
170
 
150
- if ([button respondsToSelector:@selector(setContentInsets:)]) {
151
- NSEdgeInsets insets = NSEdgeInsetsMake(0, 12.0, 0, 12.0);
152
- [button setContentInsets:insets];
171
+ // ---------------------------------------------------------------------------
172
+ // Marka butonu pages/selection.vue içindeki .confirm-button ile aynı görünüm
173
+ //
174
+ // background: linear-gradient(200.71deg, rgba(255,255,255,.57) -0.3%,
175
+ // rgba(255,255,255,0) 70.49%),
176
+ // linear-gradient(190.98deg, #006aff 26.77%, #0030ff);
177
+ // color: #fff | padding: 8px 16px | border-radius: 6px | font-size: 14px
178
+ // hover: filter: brightness(0.92) | transition: 0.2s
179
+ //
180
+ // Arka plan drawRect'te çizilir: CAGradientLayer sublayer olarak eklenince
181
+ // butonun başlığı ve ikonu gradient'in ALTINDA kalıyor.
182
+ // ---------------------------------------------------------------------------
183
+ static NSFont *BrandButtonFont(void) {
184
+ // CSS: font-size 15px, font-weight 650 (semibold ile bire bir yakın).
185
+ NSFont *inter = [NSFont fontWithName:@"Inter-SemiBold" size:kBrandFontSize];
186
+ if (inter) return inter;
187
+ return [NSFont systemFontOfSize:kBrandFontSize weight:NSFontWeightSemibold];
188
+ }
189
+
190
+ static NSGradient *BrandBaseGradient(void) {
191
+ static NSGradient *gradient = nil;
192
+ if (!gradient) {
193
+ NSColor *from = [NSColor colorWithSRGBRed:0.0 green:106.0 / 255.0 blue:1.0 alpha:1.0];
194
+ NSColor *to = [NSColor colorWithSRGBRed:0.0 green:48.0 / 255.0 blue:1.0 alpha:1.0];
195
+ gradient = [[NSGradient alloc] initWithColorsAndLocations:from, 0.2677, to, 1.0, nil];
153
196
  }
197
+ return gradient;
198
+ }
154
199
 
155
- if ([button respondsToSelector:@selector(setContentTintColor:)]) {
156
- [button setContentTintColor:[NSColor whiteColor]];
200
+ static NSGradient *BrandGlossGradient(void) {
201
+ static NSGradient *gradient = nil;
202
+ if (!gradient) {
203
+ NSColor *from = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.57];
204
+ NSColor *to = [NSColor colorWithSRGBRed:1.0 green:1.0 blue:1.0 alpha:0.0];
205
+ gradient = [[NSGradient alloc] initWithColorsAndLocations:from, 0.0, to, 0.7049, nil];
157
206
  }
207
+ return gradient;
208
+ }
209
+
210
+ static void DrawBrandBackground(NSRect bounds) {
211
+ NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds
212
+ xRadius:kBrandCornerRadius
213
+ yRadius:kBrandCornerRadius];
214
+ [BrandBaseGradient() drawInBezierPath:path angle:kBrandBaseAngle];
215
+ [BrandGlossGradient() drawInBezierPath:path angle:kBrandGlossAngle];
158
216
  }
159
217
 
160
218
  // CGWindow (sol-üst orijin) <-> Cocoa (sol-alt orijin) dönüşümünde referans yükseklik
@@ -205,6 +263,10 @@ void updateScreenOverlays();
205
263
  // Custom button with hover effects
206
264
  @interface HoverButton : NSButton
207
265
  @property (nonatomic) BOOL isHovered;
266
+ // Marka butonu (alan seçimindeki mavi buton) görünümü: arka plan drawRect'te
267
+ // çizilir. CAGradientLayer sublayer olarak eklenirse butonun kendi çizimi
268
+ // (başlık + ikon) ALTINDA kalıyor, o yüzden katman değil çizim kullanılıyor.
269
+ @property (nonatomic) BOOL usesBrandStyle;
208
270
  - (void)setupHoverTracking;
209
271
  @end
210
272
 
@@ -368,7 +430,19 @@ void updateScreenOverlays();
368
430
  - (void)mouseEntered:(NSEvent *)event {
369
431
  self.isHovered = YES;
370
432
  [[NSCursor pointingHandCursor] set];
371
-
433
+
434
+ // Marka butonu: hover'da web ile aynı şekilde HAFİF KOYULAŞIR
435
+ // (filter: brightness(0.92)). Gradient katmanlı olduğu için backgroundColor
436
+ // ile oynanamaz, örtü katmanının opacity'si kullanılır.
437
+ if (self.usesBrandStyle) {
438
+ // CSS: transform: translateY(-1px) — renk değişmiyor, buton kalkıyor.
439
+ [CATransaction begin];
440
+ [CATransaction setAnimationDuration:0.11];
441
+ self.layer.transform = CATransform3DMakeTranslation(0, kBrandHoverLift, 0);
442
+ [CATransaction commit];
443
+ return;
444
+ }
445
+
372
446
  // Brighten background on hover
373
447
  if (self.layer.backgroundColor) {
374
448
  CGFloat red, green, blue, alpha;
@@ -388,7 +462,15 @@ void updateScreenOverlays();
388
462
  - (void)mouseExited:(NSEvent *)event {
389
463
  self.isHovered = NO;
390
464
  [[NSCursor arrowCursor] set];
391
-
465
+
466
+ if (self.usesBrandStyle) {
467
+ [CATransaction begin];
468
+ [CATransaction setAnimationDuration:0.11];
469
+ self.layer.transform = CATransform3DIdentity;
470
+ [CATransaction commit];
471
+ return;
472
+ }
473
+
392
474
  // Restore original background color
393
475
  NSString *title = [self title];
394
476
  if ([title isEqualToString:@"Start Record"]) {
@@ -412,18 +494,100 @@ void updateScreenOverlays();
412
494
 
413
495
  - (void)updateTrackingAreas {
414
496
  [super updateTrackingAreas];
415
-
497
+
416
498
  // Remove old tracking areas
417
499
  for (NSTrackingArea *area in self.trackingAreas) {
418
500
  [self removeTrackingArea:area];
419
501
  }
420
-
502
+
421
503
  // Add new tracking area
422
504
  [self setupHoverTracking];
423
505
  }
424
506
 
507
+ // Marka arka planı drawRect'te çizildiği için başlık ve ikon her zaman ÜSTTE kalır.
508
+ - (void)drawRect:(NSRect)dirtyRect {
509
+ if (self.usesBrandStyle) {
510
+ DrawBrandBackground([self bounds]);
511
+ }
512
+ [super drawRect:dirtyRect];
513
+ }
514
+
425
515
  @end
426
516
 
517
+ // Marka butonu stilini uygular: alan seçimindeki .confirm-button ile aynı
518
+ // tipografi, köşe ve iç boşluk. Arka planı HoverButton.drawRect çiziyor.
519
+ static void ApplyBrandButtonStyle(NSButton *button) {
520
+ if (!button) return;
521
+
522
+ [button setWantsLayer:YES];
523
+ [button setBordered:NO];
524
+ [button setFocusRingType:NSFocusRingTypeNone];
525
+ [button setShowsBorderOnlyWhileMouseInside:NO];
526
+
527
+ if ([button isKindOfClass:[HoverButton class]]) {
528
+ [(HoverButton *)button setUsesBrandStyle:YES];
529
+ }
530
+
531
+ CALayer *root = [button layer];
532
+ if (root) {
533
+ // Arka plan drawRect'te; katman tarafında hiçbir dolgu/kenar kalmasın.
534
+ root.backgroundColor = [[NSColor clearColor] CGColor];
535
+ root.borderWidth = 0.0;
536
+ root.borderColor = [[NSColor clearColor] CGColor];
537
+ root.shadowOpacity = 0.0;
538
+ root.shadowRadius = 0.0;
539
+ root.shadowOffset = CGSizeMake(0, 0);
540
+ root.masksToBounds = NO;
541
+ }
542
+
543
+ NSFont *font = BrandButtonFont();
544
+ [button setFont:font];
545
+
546
+ // İç boşluk web ile aynı: padding 14px 26px.
547
+ if ([button respondsToSelector:@selector(setContentInsets:)]) {
548
+ [button setContentInsets:NSEdgeInsetsMake(kBrandPaddingY, kBrandPaddingX,
549
+ kBrandPaddingY, kBrandPaddingX)];
550
+ }
551
+ // Web'de ikon ve metin bir arada ortalanıyor (inline-flex + center).
552
+ // Bu olmadan NSButton ikonu sol kenara dayar, metni ortada bırakır.
553
+ if ([button respondsToSelector:@selector(setImageHugsTitle:)]) {
554
+ [button setImageHugsTitle:YES];
555
+ }
556
+
557
+ NSString *title = [button title] ?: @"";
558
+ NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
559
+ [paragraph setAlignment:NSTextAlignmentCenter];
560
+ NSDictionary *attributes = @{
561
+ NSForegroundColorAttributeName : [NSColor whiteColor],
562
+ NSParagraphStyleAttributeName : paragraph,
563
+ NSFontAttributeName : font
564
+ };
565
+ NSMutableAttributedString *attributed =
566
+ [[NSMutableAttributedString alloc] initWithString:title];
567
+ [attributed addAttributes:attributes range:NSMakeRange(0, [attributed length])];
568
+ [button setAttributedTitle:attributed];
569
+
570
+ // Boyut: içerik + padding, en az min-width 220px (web ile aynı).
571
+ NSSize textSize = [title sizeWithAttributes:attributes];
572
+ CGFloat contentWidth = ceil(textSize.width);
573
+ CGFloat contentHeight = ceil(textSize.height);
574
+ NSImage *icon = [button image];
575
+ if (icon) {
576
+ // İkon görüntüsü sağındaki gap'i (10px) zaten içeriyor.
577
+ contentWidth += [icon size].width;
578
+ contentHeight = MAX(contentHeight, [icon size].height);
579
+ }
580
+ NSRect frame = [button frame];
581
+ frame.size.width = MAX(kBrandMinWidth, contentWidth + kBrandPaddingX * 2.0);
582
+ frame.size.height = contentHeight + kBrandPaddingY * 2.0;
583
+ [button setFrame:frame];
584
+ [button setNeedsDisplay:YES];
585
+
586
+ [attributed release];
587
+ [paragraph release];
588
+ }
589
+
590
+
427
591
  @implementation NoFocusWindow
428
592
 
429
593
  - (BOOL)canBecomeKeyWindow {
@@ -1207,28 +1371,10 @@ void updateOverlay() {
1207
1371
  [targetSelectButton setBordered:NO];
1208
1372
  [targetSelectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1209
1373
 
1210
- // CRITICAL VISUAL FIX: Super aggressive styling for external displays
1211
- [targetSelectButton setWantsLayer:YES];
1212
- [targetSelectButton.layer setBackgroundColor:[[NSColor redColor] CGColor]]; // BRIGHT RED for testing
1213
- [targetSelectButton.layer setCornerRadius:8.0];
1214
- [targetSelectButton.layer setBorderWidth:3.0]; // THICK border
1215
- [targetSelectButton.layer setBorderColor:[[NSColor yellowColor] CGColor]]; // YELLOW border
1216
- [targetSelectButton.layer setMasksToBounds:YES];
1217
-
1218
- // Force very visible styling
1219
- [targetSelectButton.layer setShadowColor:[[NSColor blackColor] CGColor]];
1220
- [targetSelectButton.layer setShadowOffset:CGSizeMake(5, 5)];
1221
- [targetSelectButton.layer setShadowRadius:10.0];
1222
- [targetSelectButton.layer setShadowOpacity:1.0];
1223
-
1224
- // Clean white text - normal weight
1225
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1226
- initWithString:[targetSelectButton title]];
1227
- [titleString addAttribute:NSForegroundColorAttributeName
1228
- value:[NSColor whiteColor]
1229
- range:NSMakeRange(0, [titleString length])];
1230
- [targetSelectButton setAttributedTitle:titleString];
1231
-
1374
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
1375
+ ApplyStartRecordButtonIcon(targetSelectButton);
1376
+ ApplyBrandButtonStyle(targetSelectButton);
1377
+
1232
1378
  // Set target and action
1233
1379
  if (!g_delegate) {
1234
1380
  g_delegate = [[WindowSelectorDelegate alloc] init];
@@ -1244,7 +1390,10 @@ void updateOverlay() {
1244
1390
  NSLog(@"🆕 Added new button to Screen %ld overlay - total subviews now: %lu", targetScreenIndex, [targetOverlay.contentView subviews].count);
1245
1391
  }
1246
1392
 
1393
+ // Butonun her gösterimde marka metriklerinde kalmasını garanti et
1394
+ // (mevcut buton yeniden kullanılıyor olabilir).
1247
1395
  ApplyStartRecordButtonIcon(targetSelectButton);
1396
+ ApplyBrandButtonStyle(targetSelectButton);
1248
1397
 
1249
1398
  // Position buttons - Start Record in center of selected window
1250
1399
  if (targetSelectButton) {
@@ -1614,15 +1763,17 @@ void updateScreenOverlays() {
1614
1763
  if ([subview isKindOfClass:[NSButton class]]) {
1615
1764
  NSButton *button = (NSButton *)subview;
1616
1765
  if ([button.title isEqualToString:@"Start Record"]) {
1617
- if (isActiveScreen) {
1618
- // Active screen: bright, prominent button with new RGB color
1619
- [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:1.0] CGColor]];
1620
- [button setAlphaValue:1.0];
1621
- } else {
1622
- // Inactive screen: dimmer button with new RGB color
1623
- [button.layer setBackgroundColor:[[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0 alpha:0.6] CGColor]];
1624
- [button setAlphaValue:0.7];
1766
+ // Marka butonunun arka planı drawRect'te çiziliyor;
1767
+ // layer.backgroundColor set edilirse hapın altında
1768
+ // köşeli bir dikdörtgen olarak görünüyor.
1769
+ BOOL usesBrand = [button isKindOfClass:[HoverButton class]] &&
1770
+ [(HoverButton *)button usesBrandStyle];
1771
+ if (!usesBrand) {
1772
+ [button.layer setBackgroundColor:
1773
+ [[NSColor colorWithRed:77.0/255.0 green:30.0/255.0 blue:231.0/255.0
1774
+ alpha:(isActiveScreen ? 1.0 : 0.6)] CGColor]];
1625
1775
  }
1776
+ [button setAlphaValue:isActiveScreen ? 1.0 : 0.7];
1626
1777
  }
1627
1778
  }
1628
1779
  if ([subview isKindOfClass:[NSTextField class]]) {
@@ -1827,29 +1978,11 @@ bool startScreenSelection() {
1827
1978
  [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1828
1979
  [selectButton setTag:i]; // Set screen index as tag
1829
1980
 
1830
- // Modern button styling with new RGB color
1831
- [selectButton setWantsLayer:YES];
1832
- [selectButton.layer setBackgroundColor:[[NSColor colorWithRed:90.0/255.0 green:50.0/255.0 blue:250.0/255.0 alpha:1.0] CGColor]];
1833
- [selectButton.layer setCornerRadius:8.0];
1834
- [selectButton.layer setBorderWidth:0.0];
1835
-
1836
- // Remove all button borders and decorations
1837
- [selectButton.layer setShadowOpacity:0.0];
1838
- [selectButton.layer setShadowRadius:0.0];
1839
- [selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
1840
- [selectButton.layer setMasksToBounds:YES];
1841
-
1842
- // Clean white text - normal weight
1843
- [selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
1981
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
1844
1982
  [selectButton setTitle:@"Start Record"];
1845
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
1846
- initWithString:[selectButton title]];
1847
- [titleString addAttribute:NSForegroundColorAttributeName
1848
- value:[NSColor whiteColor]
1849
- range:NSMakeRange(0, [titleString length])];
1850
- [selectButton setAttributedTitle:titleString];
1851
-
1852
1983
  ApplyStartRecordButtonIcon(selectButton);
1984
+ ApplyBrandButtonStyle(selectButton);
1985
+
1853
1986
 
1854
1987
  // Clean button - no shadows or highlights
1855
1988
 
@@ -2314,29 +2447,10 @@ Napi::Value StartWindowSelection(const Napi::CallbackInfo& info) {
2314
2447
  [g_selectButton setBordered:NO];
2315
2448
  [g_selectButton setFont:[NSFont systemFontOfSize:16 weight:NSFontWeightRegular]];
2316
2449
 
2317
- // Modern button styling with new RGB color
2318
- [g_selectButton setWantsLayer:YES];
2319
- [g_selectButton.layer setBackgroundColor:[[NSColor colorWithRed:90.0/255.0 green:50.0/255.0 blue:250.0/255.0 alpha:1.0] CGColor]];
2320
- [g_selectButton.layer setCornerRadius:8.0];
2321
- [g_selectButton.layer setBorderWidth:0.0];
2322
-
2323
- // Remove all button borders and decorations
2324
- [g_selectButton.layer setShadowOpacity:0.0];
2325
- [g_selectButton.layer setShadowRadius:0.0];
2326
- [g_selectButton.layer setShadowOffset:NSMakeSize(0, 0)];
2327
- [g_selectButton.layer setMasksToBounds:YES];
2328
- [g_selectButton.layer setBorderWidth:0.0];
2329
- [g_selectButton.layer setBorderColor:[[NSColor clearColor] CGColor]];
2330
-
2331
- // Clean white text - normal weight
2332
- NSMutableAttributedString *titleString = [[NSMutableAttributedString alloc]
2333
- initWithString:[g_selectButton title]];
2334
- [titleString addAttribute:NSForegroundColorAttributeName
2335
- value:[NSColor whiteColor]
2336
- range:NSMakeRange(0, [titleString length])];
2337
- [g_selectButton setAttributedTitle:titleString];
2338
-
2450
+ // Alan seçimindeki mavi buton ile aynı marka görünümü
2339
2451
  ApplyStartRecordButtonIcon(g_selectButton);
2452
+ ApplyBrandButtonStyle(g_selectButton);
2453
+
2340
2454
 
2341
2455
  // Create delegate for button action and timer
2342
2456
  g_delegate = [[WindowSelectorDelegate alloc] init];