node-mac-recorder 2.24.3 → 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
@@ -2,6 +2,12 @@ const { EventEmitter } = require("events");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
+ // Videonun ilk karesi yakalanana kadar beklenecek ust sinir.
6
+ // Normalde birkac yuz ms; clamshell'de ekran envanteri yeniden kuruldugu icin
7
+ // saniyelere cikabiliyor. Beklemek, cursor'i yanlis referansa baglamaktan iyidir.
8
+ const VIDEO_START_TIMEOUT_MS = 12000;
9
+ const VIDEO_START_POLL_MS = 10;
10
+
5
11
  // Auto-switch to Electron-safe implementation when running under Electron and binary exists
6
12
  let USE_ELECTRON_SAFE = false;
7
13
  let ElectronSafeMacRecorder = null;
@@ -860,77 +866,91 @@ class MacRecorder extends EventEmitter {
860
866
 
861
867
  // Only start cursor if native recording started successfully
862
868
  if (success) {
863
- // For ScreenCaptureKit (async startup), wait briefly until native fully initialized
864
- // ScreenCaptureKit needs ~150-300ms to start + ~150ms for first 10 frames
865
- const waitStart = Date.now();
866
- try {
867
- while (Date.now() - waitStart < 600) {
868
- try {
869
- const nativeStatus = nativeBinding && nativeBinding.getRecordingStatus ? nativeBinding.getRecordingStatus() : true;
870
- if (nativeStatus) {
871
- console.log(`✅ SYNC: Native recording fully ready after ${Date.now() - waitStart}ms`);
872
- break;
873
- }
874
- } catch (_) {}
875
- await new Promise(r => setTimeout(r, 30));
869
+ // ==========================================================
870
+ // VIDEONUN ILK KARESINI BEKLE SENKRONUN TEMELI
871
+ // ==========================================================
872
+ // getRecordingStatus() BU AMAC ICIN KULLANILAMAZ:
873
+ // ScreenCaptureKit stream'i henuz baslamadiysa native taraf
874
+ // `g_isRecording` fallback'ine dusup TRUE donuyor, yani
875
+ // "kayit komutu verildi"yi "kayit basladi" saniyor.
876
+ // Olculdu: "fully ready after 0ms" + videoStartTimestamp = 0.
877
+ // Sonucu: cursor timeline'i video'dan ONCE basliyor. Normal
878
+ // kosulda fark ~150-300ms, clamshell'de (ekran envanteri
879
+ // yeniden kuruldugu icin) SANIYELER — editordeki 1sn'lik
880
+ // telafi limiti bunu kapatamiyor ve cursor gorunur sekilde kayiyor.
881
+ //
882
+ // Dogru sinyal getVideoStartTimestamp(): native ilk kareyi
883
+ // yakalayip g_videoStartTime'i set ettiginde gecerli bir
884
+ // wall-clock degeri doner. Timeline'i buna baglayinca
885
+ // baslatma hizi (prewarm vb.) senkronu ETKILEMEZ.
886
+ const usesScreenCaptureKit =
887
+ this.options.preferScreenCaptureKit === true;
888
+
889
+ const readVideoStart = () => {
890
+ try {
891
+ const value = Number(
892
+ typeof nativeBinding.getVideoStartTimestamp === 'function'
893
+ ? nativeBinding.getVideoStartTimestamp()
894
+ : 0
895
+ );
896
+ return Number.isFinite(value) && value > 0 ? value : 0;
897
+ } catch (_) {
898
+ return 0;
876
899
  }
877
- } catch (_) {}
900
+ };
901
+
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.
878
910
  this.sessionTimestamp = sessionTimestamp;
911
+ this.videoStartTimestamp = 0;
879
912
 
880
- // Native sync_timeline handles A/V alignment - no JS-level delay needed
881
913
  const syncTimestamp = Date.now();
882
914
  this.syncTimestamp = syncTimestamp;
883
915
  this.recordingStartTime = syncTimestamp;
884
- console.log(`🎯 CURSOR SYNC: Cursor tracking will use timestamp: ${syncTimestamp}`);
885
-
886
- // CURSOR/VIDEO TIME ALIGNMENT:
887
- // Cursor timeline'in t=0'i bu andir (syncTimestamp), ama videonun
888
- // ILK KARESI daha once yakalanmis olabilir (ScreenCaptureKit
889
- // baslatma gecikmesi + yukaridaki hazir-olma beklemesi). Editor
890
- // "cursor'un ilk ornegi = video t=0" varsayarsa aradaki fark sabit
891
- // bir zaman kaymasi olarak kalir. Native gercek video baslangicini
892
- // biliyor; okuyup sakla ki stop'ta cursor JSON'una yazabilelim.
893
- this.videoStartTimestamp = 0;
894
- // Onceki kayittan kalan referans sizmasin
895
- this.timelineStartTimestamp = 0;
896
- try {
897
- const nativeVideoStart =
898
- typeof nativeBinding.getVideoStartTimestamp === 'function'
899
- ? Number(nativeBinding.getVideoStartTimestamp())
900
- : 0;
901
- if (Number.isFinite(nativeVideoStart) && nativeVideoStart > 0) {
902
- this.videoStartTimestamp = nativeVideoStart;
903
- console.log(
904
- `🎯 SYNC: Video first-frame timestamp: ${nativeVideoStart} (cursor starts ${(syncTimestamp - nativeVideoStart).toFixed(0)}ms later)`
905
- );
906
- } else {
907
- console.warn('⚠️ SYNC: Video start timestamp unavailable — cursor sync metadata will be skipped');
908
- }
909
- } catch (videoStartError) {
910
- console.warn('⚠️ SYNC: Video start timestamp read failed:', videoStartError.message);
916
+ this.timelineStartTimestamp = syncTimestamp;
917
+
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(() => {});
911
945
  }
912
946
 
913
- // ZAMAN REFERANSI: cursor/klavye timeline'i VIDEONUN ilk karesine
914
- // hizalanir, JS'in "kayit hazir" dedigi ana degil.
915
- //
916
- // NEDEN: syncTimestamp, yukaridaki hazir-olma dongusunden sonraki an.
917
- // Bu an ile videonun gercek t=0'i arasindaki fark, baslatma hizina
918
- // gore DEGISIYOR (ornegin ScreenCaptureKit isitilmissa video cok daha
919
- // erken basliyor). Cursor bu degisken ana baglanirsa her kayitta
920
- // farkli bir kayma olusuyor ve editordeki telafi 1sn limitine
921
- // takilabiliyor. Videonun kendi baslangicini referans alinca fark
922
- // yapisal olarak sifir olur; hizlanma senkronu bozmaz.
923
- const timelineStartTimestamp =
924
- this.videoStartTimestamp > 0
925
- ? this.videoStartTimestamp
926
- : syncTimestamp;
927
- this.timelineStartTimestamp = timelineStartTimestamp;
928
-
929
- if (timelineStartTimestamp !== syncTimestamp) {
930
- console.log(
931
- `🎯 SYNC: Timeline referansi video ilk karesine cekildi (${(syncTimestamp - timelineStartTimestamp).toFixed(0)}ms geri)`
932
- );
933
- }
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;
934
954
 
935
955
  const standardCursorOptions = {
936
956
  videoRelative: true,
@@ -1044,11 +1064,43 @@ class MacRecorder extends EventEmitter {
1044
1064
  }, 1000);
1045
1065
 
1046
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.
1047
1080
  let recordingStartedEmitted = false;
1048
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
+ };
1049
1101
  const pollRecordingStatus = () => {
1050
1102
  try {
1051
- const nativeStatus = nativeBinding.getRecordingStatus();
1103
+ const nativeStatus = isNativeRecordingLive();
1052
1104
  if (nativeStatus && !recordingStartedEmitted) {
1053
1105
  recordingStartedEmitted = true;
1054
1106
  clearInterval(checkRecordingStatus);
@@ -1174,13 +1226,25 @@ class MacRecorder extends EventEmitter {
1174
1226
 
1175
1227
  return new Promise(async (resolve, reject) => {
1176
1228
  const stopRequestedAt = Date.now();
1229
+ // Sure, VIDEONUN baslangicindan olculur. recordingStartTime (JS'in
1230
+ // hazir oldugunu fark ettigi an) videodan birkac on ms sonra oldugu
1231
+ // icin buradan hesaplanan stopLimit videoyu kuyrugundan kirpiyordu.
1232
+ const durationReference =
1233
+ this.timelineStartTimestamp && this.timelineStartTimestamp > 0
1234
+ ? this.timelineStartTimestamp
1235
+ : this.recordingStartTime;
1177
1236
  const elapsedSeconds =
1178
- this.recordingStartTime && this.recordingStartTime > 0
1179
- ? (stopRequestedAt - this.recordingStartTime) / 1000
1237
+ durationReference && durationReference > 0
1238
+ ? (stopRequestedAt - durationReference) / 1000
1180
1239
  : -1;
1181
1240
  try {
1182
1241
  console.log('🛑 SYNC: Stopping all recording components simultaneously');
1183
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
+
1184
1248
  // SYNC FIX: Stop ALL components at the same time for perfect sync
1185
1249
  // 1. Stop cursor tracking FIRST (it's instant)
1186
1250
  if (this.cursorCaptureInterval) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-mac-recorder",
3
- "version": "2.24.3",
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];