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 +130 -66
- package/package.json +1 -1
- package/src/mac_recorder.mm +32 -18
- package/src/screen_capture_kit.mm +74 -6
- package/src/window_selector.mm +211 -97
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
|
-
//
|
|
864
|
-
//
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
|
914
|
-
//
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
|
|
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 =
|
|
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
|
-
|
|
1179
|
-
? (stopRequestedAt -
|
|
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
package/src/mac_recorder.mm
CHANGED
|
@@ -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:
|
|
631
|
-
//
|
|
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
|
|
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
|
-
//
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
1045
|
-
// (TCC kontrolu + pencere
|
|
1046
|
-
//
|
|
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
|
-
|
|
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
|
});
|
package/src/window_selector.mm
CHANGED
|
@@ -85,11 +85,31 @@ static bool shouldSkipSelectableWindowOwner(NSString *windowOwner) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Record icon helpers
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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,
|
|
120
|
+
NSRectFill(NSMakeRect(0, 0, width, diameter));
|
|
101
121
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
[
|
|
129
|
+
[iconColor setStroke];
|
|
111
130
|
[outerPath stroke];
|
|
112
131
|
|
|
113
|
-
CGFloat innerDiameter =
|
|
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
|
-
[
|
|
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(
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
[button.layer setBackgroundColor:
|
|
1624
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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];
|