mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-02-23 18:48:36 +08:00
Compare commits
18 Commits
hdie-tray
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1274935106 | ||
|
|
85b3ed5276 | ||
|
|
5f3ceef592 | ||
|
|
1a90e6b6c7 | ||
|
|
f112d097dc | ||
|
|
45cab7f808 | ||
|
|
216ec9d52b | ||
|
|
56a8f6b97b | ||
|
|
c76d10a438 | ||
|
|
f05f2178e5 | ||
|
|
226d7417b2 | ||
|
|
b0c8e65c6e | ||
|
|
4ae577c3c4 | ||
|
|
204e81a700 | ||
|
|
1f35830570 | ||
|
|
6b334f2977 | ||
|
|
0dc3c12aa5 | ||
|
|
ceffcce20e |
@@ -25,6 +25,7 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
GestureDragStartCallback? onOneFingerPanStart;
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate;
|
||||
GestureDragEndCallback? onOneFingerPanEnd;
|
||||
GestureDragCancelCallback? onOneFingerPanCancel;
|
||||
|
||||
// twoFingerScale : scale + pan event
|
||||
GestureScaleStartCallback? onTwoFingerScaleStart;
|
||||
@@ -169,6 +170,27 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
|
||||
|
||||
DragEndDetails _getDragEndDetails(ScaleEndDetails d) =>
|
||||
DragEndDetails(velocity: d.velocity);
|
||||
|
||||
@override
|
||||
void rejectGesture(int pointer) {
|
||||
super.rejectGesture(pointer);
|
||||
switch (_currentState) {
|
||||
case GestureState.oneFingerPan:
|
||||
if (onOneFingerPanCancel != null) {
|
||||
onOneFingerPanCancel!();
|
||||
}
|
||||
break;
|
||||
case GestureState.twoFingerScale:
|
||||
// Reset scale state if needed, currently self-contained
|
||||
break;
|
||||
case GestureState.threeFingerVerticalDrag:
|
||||
// Reset drag state if needed, currently self-contained
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
_currentState = GestureState.none;
|
||||
}
|
||||
}
|
||||
|
||||
class HoldTapMoveGestureRecognizer extends GestureRecognizer {
|
||||
@@ -717,6 +739,7 @@ RawGestureDetector getMixinGestureDetector({
|
||||
GestureDragStartCallback? onOneFingerPanStart,
|
||||
GestureDragUpdateCallback? onOneFingerPanUpdate,
|
||||
GestureDragEndCallback? onOneFingerPanEnd,
|
||||
GestureDragCancelCallback? onOneFingerPanCancel,
|
||||
GestureScaleUpdateCallback? onTwoFingerScaleUpdate,
|
||||
GestureScaleEndCallback? onTwoFingerScaleEnd,
|
||||
GestureDragUpdateCallback? onThreeFingerVerticalDragUpdate,
|
||||
@@ -765,6 +788,7 @@ RawGestureDetector getMixinGestureDetector({
|
||||
..onOneFingerPanStart = onOneFingerPanStart
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
|
||||
|
||||
@@ -158,7 +158,8 @@ class _RawTouchGestureDetectorRegionState
|
||||
final isMoved =
|
||||
await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||
if (isMoved) {
|
||||
if (lastTapDownDetails != null) {
|
||||
// If pan already handled 'down', don't send it again.
|
||||
if (lastTapDownDetails != null && !_touchModePanStarted) {
|
||||
await inputModel.tapDown(MouseButtons.left);
|
||||
}
|
||||
await inputModel.tapUp(MouseButtons.left);
|
||||
@@ -424,6 +425,14 @@ class _RawTouchGestureDetectorRegionState
|
||||
}
|
||||
}
|
||||
|
||||
// Reset `_touchModePanStarted` if the one-finger pan gesture is cancelled
|
||||
// or rejected by the gesture arena. Without this, the flag can remain
|
||||
// stuck in the "started" state and cause issues such as the Magic Mouse
|
||||
// double-click problem on iPad with magic mouse.
|
||||
onOneFingerPanCancel() {
|
||||
_touchModePanStarted = false;
|
||||
}
|
||||
|
||||
// scale + pan event
|
||||
onTwoFingerScaleStart(ScaleStartDetails d) {
|
||||
_lastTapDownDetails = null;
|
||||
@@ -557,6 +566,7 @@ class _RawTouchGestureDetectorRegionState
|
||||
instance
|
||||
..onOneFingerPanUpdate = onOneFingerPanUpdate
|
||||
..onOneFingerPanEnd = onOneFingerPanEnd
|
||||
..onOneFingerPanCancel = onOneFingerPanCancel
|
||||
..onTwoFingerScaleStart = onTwoFingerScaleStart
|
||||
..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
|
||||
..onTwoFingerScaleEnd = onTwoFingerScaleEnd
|
||||
|
||||
@@ -164,6 +164,13 @@ class _TerminalPageState extends State<TerminalPage>
|
||||
autofocus: true,
|
||||
textStyle: _getTerminalStyle(),
|
||||
backgroundOpacity: 0.7,
|
||||
// The following comment is from xterm.dart source code:
|
||||
// Workaround to detect delete key for platforms and IMEs that do not
|
||||
// emit a hardware delete event. Preferred on mobile platforms. [false] by
|
||||
// default.
|
||||
//
|
||||
// Android works fine without this workaround.
|
||||
deleteDetection: isIOS,
|
||||
padding: _calculatePadding(heightPx),
|
||||
onSecondaryTapDown: (details, offset) async {
|
||||
final selection = _terminalModel.terminalController.selection;
|
||||
|
||||
@@ -1048,6 +1048,14 @@ class InputModel {
|
||||
if (isViewOnly && !showMyCursor) return;
|
||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||
|
||||
// May fix https://github.com/rustdesk/rustdesk/issues/13009
|
||||
if (isIOS && e.synthesized && e.position == Offset.zero && e.buttons == 0) {
|
||||
// iOS may emit a synthesized hover event at (0,0) when the mouse is disconnected.
|
||||
// Ignore this event to prevent cursor jumping.
|
||||
debugPrint('Ignored synthesized hover at (0,0) on iOS');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update pointer region when relative mouse mode is enabled.
|
||||
// This avoids unnecessary tracking when not in relative mode.
|
||||
if (_relativeMouse.enabled.value) {
|
||||
|
||||
@@ -562,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("id_input_tip", "Sie können eine ID, eine direkte IP oder eine Domäne mit einem Port (<domain>:<port>) eingeben.\nWenn Sie auf ein Gerät auf einem anderen Server zugreifen wollen, fügen Sie bitte die Serveradresse (<id>@<server_address>?key=<key_value>) hinzu, zum Beispiel\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nWenn Sie auf ein Gerät auf einem öffentlichen Server zugreifen wollen, geben Sie bitte \"<id>@public\" ein. Der Schlüssel wird für öffentliche Server nicht benötigt.\n\nWenn Sie bei der ersten Verbindung die Verwendung einer Relay-Verbindung erzwingen wollen, fügen Sie \"/r\" am Ende der ID hinzu, zum Beispiel \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "Modus 1"),
|
||||
("privacy_mode_impl_virtual_display_tip", "Modus 2"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktivieren"),
|
||||
("Exit privacy mode", "Datenschutzmodus beenden"),
|
||||
("Enter privacy mode", "Datenschutzmodus aktiviert"),
|
||||
("Exit privacy mode", "Datenschutzmodus beendet"),
|
||||
("idd_not_support_under_win10_2004_tip", "Indirekter Grafiktreiber wird nicht unterstützt. Windows 10, Version 2004 oder neuer ist erforderlich."),
|
||||
("input_source_1_tip", "Eingangsquelle 1"),
|
||||
("input_source_2_tip", "Eingangsquelle 2"),
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Drücken Sie {} zum Beenden."),
|
||||
("rel-mouse-permission-lost-tip", "Die Tastaturberechtigung wurde widerrufen. Der relative Mausmodus wurde deaktiviert."),
|
||||
("Changelog", "Änderungsprotokoll"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Bildschirm während ausgehender Sitzungen aktiv halten"),
|
||||
("keep-awake-during-incoming-sessions-label", "Bildschirm während eingehender Sitzungen aktiv halten"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"),
|
||||
("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"),
|
||||
("Do this for all conflicts", "Appliquer à tous les conflits"),
|
||||
("This is irreversible!", "Ceci est irréversible !"),
|
||||
("This is irreversible!", "Cette action est irréversible !"),
|
||||
("Deleting", "Suppression"),
|
||||
("files", "fichiers"),
|
||||
("Waiting", "En attente"),
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Appuyez sur {} pour quitter."),
|
||||
("rel-mouse-permission-lost-tip", "L’autorisation de contrôle du clavier a été révoquée. Le mode souris relative a été désactivé."),
|
||||
("Changelog", "Journal des modifications"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Maintenir l’écran allumé lors des sessions sortantes"),
|
||||
("keep-awake-during-incoming-sessions-label", "Maintenir l’écran allumé lors des sessions entrantes"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"),
|
||||
("Configure", "Beállítás"),
|
||||
("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell adnia."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a „Képernyőfelvétel” jogosultságot."),
|
||||
("config_screen", "Ahhoz, hogy távolról hozzáférhessen a számítógépéhez, meg kell adnia a RustDesknek a \"Képernyőfelvétel\" jogosultságot."),
|
||||
("Installing ...", "Telepítés…"),
|
||||
("Install", "Telepítés"),
|
||||
("Installation", "Telepítés"),
|
||||
@@ -276,13 +276,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Do you accept?", "Elfogadás?"),
|
||||
("Open System Setting", "Rendszerbeállítások megnyitása"),
|
||||
("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a „Hozzáférhetőség” szolgáltatás használatát."),
|
||||
("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"Hozzáférhetőség\" szolgáltatás használatát."),
|
||||
("android_input_permission_tip2", "A következő rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."),
|
||||
("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"),
|
||||
("android_service_will_start_tip", "A képernyőmegosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."),
|
||||
("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."),
|
||||
("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a „Kapcsolási szolgáltatás indítása” gombra, vagy aktiválja a „Képernyőfelvétel” engedélyt."),
|
||||
("android_start_service_tip", "A képernyőmegosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"Képernyőfelvétel\" engedélyt."),
|
||||
("android_permission_may_not_change_tip", "A meglévő kapcsolatok engedélyei csak új kapcsolódás után módosulnak."),
|
||||
("Account", "Fiók"),
|
||||
("Overwrite", "Felülírás"),
|
||||
@@ -408,15 +408,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"),
|
||||
("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztőprogramra való váltás és a szoftveres leképezés alkalmazása segíthet. A szoftvert újra kell indítani."),
|
||||
("Always use software rendering", "Mindig szoftveres leképezést használjon"),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a „Bemenet figyelése” jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a „Hangfelvétel” jogosultságot."),
|
||||
("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."),
|
||||
("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."),
|
||||
("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."),
|
||||
("Wait", "Várjon"),
|
||||
("Elevation Error", "Emelt szintű hozzáférési hiba"),
|
||||
("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"),
|
||||
("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"),
|
||||
("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az „Igen” gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"),
|
||||
("Request Elevation", "Emelt szintű jogok igénylése"),
|
||||
("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."),
|
||||
("Elevate successfully", "Emelt szintű jogok megadva"),
|
||||
@@ -442,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Voice call", "Hanghívás"),
|
||||
("Text chat", "Szöveges csevegés"),
|
||||
("Stop voice call", "Hanghívás leállítása"),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az „/r” utótagot. Az azonosítóhoz vagy a „Mindig továbbító-kiszolgálón keresztül kapcsolódom” opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az első próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. Az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."),
|
||||
("Reconnect", "Újrakapcsolódás"),
|
||||
("Codec", "Kodek"),
|
||||
("Resolution", "Felbontás"),
|
||||
@@ -490,7 +490,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Update", "Frissítés"),
|
||||
("Enable", "Engedélyezés"),
|
||||
("Disable", "Letiltás"),
|
||||
("Options", "Beállítások"),
|
||||
("Options", "Opciók"),
|
||||
("resolution_original_tip", "Eredeti felbontás"),
|
||||
("resolution_fit_local_tip", "Helyi felbontás beállítása"),
|
||||
("resolution_custom_tip", "Testre szabható felbontás"),
|
||||
@@ -559,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Plug out all", "Kapcsolja ki az összeset"),
|
||||
("True color (4:4:4)", "Valódi szín (4:4:4)"),
|
||||
("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az „/r” az azonosítót a végén, például „9123456234/r”."),
|
||||
("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (<domain>:<port>).\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" lehetőséget. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az első kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."),
|
||||
("privacy_mode_impl_mag_tip", "1. mód"),
|
||||
("privacy_mode_impl_virtual_display_tip", "2. mód"),
|
||||
("Enter privacy mode", "Lépjen be az adatvédelmi módba"),
|
||||
@@ -622,7 +622,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Power", "Főkapcsoló"),
|
||||
("Telegram bot", "Telegram bot"),
|
||||
("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a „/newbot” parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel („/”) kezdetű, pl. „/hello” az aktiváláshoz.\n"),
|
||||
("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"),
|
||||
("cancel-2fa-confirm-tip", "Biztosan vissza akarja vonni a 2FA-hitelesítést?"),
|
||||
("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"),
|
||||
("About RustDesk", "A RustDesk névjegye"),
|
||||
@@ -643,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."),
|
||||
("Authentication Required", "Hitelesítés szükséges"),
|
||||
("Authenticate", "Hitelesítés"),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a „<id>@public” betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("web_id_input_tip", "Azonos kiszolgálón lévő azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (<id>@<kiszolgáló_cím>?key=<key_value>), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévő eszközhöz szeretne hozzáférni, adja meg a \"<id>@public\" betűt. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."),
|
||||
("Download", "Letöltés"),
|
||||
("Upload folder", "Mappa feltöltése"),
|
||||
("Upload files", "Fájlok feltöltése"),
|
||||
@@ -682,9 +682,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("Downloading {}", "{} letöltése"),
|
||||
("{} Update", "{} frissítés"),
|
||||
("{}-to-update-tip", "A(z) {} bezárása és az új verzió telepítése."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a „Letöltés” gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."),
|
||||
("Auto update", "Automatikus frissítés"),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a „Letöltés” gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."),
|
||||
("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."),
|
||||
("Use WebSocket", "WebSocket használata"),
|
||||
("Trackpad speed", "Érintőpad sebessége"),
|
||||
@@ -730,14 +730,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("input note here", "Megjegyzés beírása"),
|
||||
("note-at-conn-end-tip", "Kérjen megjegyzést a kapcsolat végén"),
|
||||
("Show terminal extra keys", "További terminálgombok megjelenítése"),
|
||||
("Relative mouse mode", "Relatív egérmód"),
|
||||
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egérmódot."),
|
||||
("rel-mouse-not-ready-tip", "A relatív egérmód még nem elérhető. Próbálja meg újra."),
|
||||
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egérmód le lett tiltva."),
|
||||
("Relative mouse mode", "Relatív egér mód"),
|
||||
("rel-mouse-not-supported-peer-tip", "A kapcsolódott partner nem támogatja a relatív egér módot."),
|
||||
("rel-mouse-not-ready-tip", "A relatív egér mód még nem elérhető. Próbálja meg újra."),
|
||||
("rel-mouse-lock-failed-tip", "Nem sikerült zárolni a kurzort. A relatív egér mód le lett tiltva."),
|
||||
("rel-mouse-exit-{}-tip", "A kilépéshez nyomja meg a(z) {} gombot."),
|
||||
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egérmód le lett tilva."),
|
||||
("rel-mouse-permission-lost-tip", "A billentyűzet-hozzáférés vissza lett vonva. A relatív egér mód le lett tilva."),
|
||||
("Changelog", "Változáslista"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Képernyő aktív állapotban tartása a kimenő munkamenetek során"),
|
||||
("keep-awake-during-incoming-sessions-label", "Képernyő aktív állapotban tartása a bejövő munkamenetek során"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Premi {} per uscire."),
|
||||
("rel-mouse-permission-lost-tip", "È stata revocato l'accesso alla tastiera. La modalità mouse relativa è stata disabilitata."),
|
||||
("Changelog", "Novità programma"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Mantieni lo schermo attivo durante le sessioni in uscita"),
|
||||
("keep-awake-during-incoming-sessions-label", "Mantieni lo schermo attivo durante le sessioni in ingresso"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "종료하려면 {}을(를) 누르세요."),
|
||||
("rel-mouse-permission-lost-tip", "키보드 권한이 취소되었습니다. 상대 마우스 모드가 비활성화되었습니다."),
|
||||
("Changelog", "변경 기록"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "발신 세션 중 화면 켜짐 유지"),
|
||||
("keep-awake-during-incoming-sessions-label", "수신 세션 중 화면 켜짐 유지"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Druk op {} om af te sluiten."),
|
||||
("rel-mouse-permission-lost-tip", "De toetsenbordcontrole is uitgeschakeld. De relatieve muismodus is uitgeschakeld."),
|
||||
("Changelog", "Wijzigingenlogboek"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Houd het scherm open tijdens de uitgaande sessies."),
|
||||
("keep-awake-during-incoming-sessions-label", "Houd het scherm open tijdens de inkomende sessies."),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("rel-mouse-exit-{}-tip", "Aby wyłączyć tryb przechwytywania myszy, naciśnij {}"),
|
||||
("rel-mouse-permission-lost-tip", "Utracono uprawnienia do trybu przechwytywania myszy"),
|
||||
("Changelog", "Dziennik zmian"),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji wychodzących"),
|
||||
("keep-awake-during-incoming-sessions-label", "Utrzymuj urządzenie w stanie aktywnym podczas sesji przychodzących"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
102
src/lang/ptbr.rs
102
src/lang/ptbr.rs
@@ -672,72 +672,72 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
|
||||
("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."),
|
||||
("save-settings-tip", "Salvar configurações"),
|
||||
("dont-show-again-tip", "Não mostrar novamente"),
|
||||
("Take screenshot", ""),
|
||||
("Taking screenshot", ""),
|
||||
("Take screenshot", "Capturar de tela"),
|
||||
("Taking screenshot", "Capturando tela"),
|
||||
("screenshot-merged-screen-not-supported-tip", ""),
|
||||
("screenshot-action-tip", ""),
|
||||
("Save as", ""),
|
||||
("Copy to clipboard", ""),
|
||||
("Enable remote printer", ""),
|
||||
("Save as", "Salvar como"),
|
||||
("Copy to clipboard", "Copiar para área de transferência"),
|
||||
("Enable remote printer", "Habilitar impressora remota"),
|
||||
("Downloading {}", ""),
|
||||
("{} Update", ""),
|
||||
("{}-to-update-tip", ""),
|
||||
("download-new-version-failed-tip", ""),
|
||||
("Auto update", ""),
|
||||
("update-failed-check-msi-tip", ""),
|
||||
("websocket_tip", ""),
|
||||
("Use WebSocket", ""),
|
||||
("Trackpad speed", ""),
|
||||
("download-new-version-failed-tip", "Falha no download. Você pode tentar novamente ou clicar no botão \"Download\" para baixar da página releases e atualizar manualmente."),
|
||||
("Auto update", "Atualização automática"),
|
||||
("update-failed-check-msi-tip", "Falha na verificação do método de instalação. Clique no botão \"Download\" para baixar da página releases e atualizar manualmente."),
|
||||
("websocket_tip", "Usando WebSocket, apenas conexões via relay são suportadas."),
|
||||
("Use WebSocket", "Usar WebSocket"),
|
||||
("Trackpad speed", "Velocidade do trackpad"),
|
||||
("Default trackpad speed", ""),
|
||||
("Numeric one-time password", ""),
|
||||
("Enable IPv6 P2P connection", ""),
|
||||
("Enable UDP hole punching", ""),
|
||||
("Numeric one-time password", "Senha numérica de uso único"),
|
||||
("Enable IPv6 P2P connection", "Habilitar conexão IPv6 P2P"),
|
||||
("Enable UDP hole punching", "Habilitar UDP hole punching"),
|
||||
("View camera", "Visualizar câmera"),
|
||||
("Enable camera", "Ativar câmera"),
|
||||
("No cameras", "Sem câmeras"),
|
||||
("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."),
|
||||
("Terminal", ""),
|
||||
("Enable terminal", ""),
|
||||
("New tab", ""),
|
||||
("Keep terminal sessions on disconnect", ""),
|
||||
("Terminal (Run as administrator)", ""),
|
||||
("terminal-admin-login-tip", ""),
|
||||
("Failed to get user token.", ""),
|
||||
("Incorrect username or password.", ""),
|
||||
("The user is not an administrator.", ""),
|
||||
("Failed to check if the user is an administrator.", ""),
|
||||
("Supported only in the installed version.", ""),
|
||||
("elevation_username_tip", ""),
|
||||
("Preparing for installation ...", ""),
|
||||
("Show my cursor", ""),
|
||||
("Terminal", "Terminal"),
|
||||
("Enable terminal", "Habilitar Terminal"),
|
||||
("New tab", "Nova aba"),
|
||||
("Keep terminal sessions on disconnect", "Manter sessões de terminal ao desconectar"),
|
||||
("Terminal (Run as administrator)", "Terminal (Executar como administrador)"),
|
||||
("terminal-admin-login-tip", "Insira o nome do usuário e senha de administrador do dispositivo controlado."),
|
||||
("Failed to get user token.", "Falha ao obter token do usuário."),
|
||||
("Incorrect username or password.", "Usuário ou senha incorretos"),
|
||||
("The user is not an administrator.", "O usuário não é administrador"),
|
||||
("Failed to check if the user is an administrator.", "Falha ao verificar se o usuário é administrador"),
|
||||
("Supported only in the installed version.", "Funciona somente na versão instalada"),
|
||||
("elevation_username_tip", "Insira o nome do usuário ou domínio\\usuário"),
|
||||
("Preparing for installation ...", "Preparando para instalação ..."),
|
||||
("Show my cursor", "Mostrar meu cursor"),
|
||||
("Scale custom", "Escala personalizada"),
|
||||
("Custom scale slider", "Controle deslizante de escala personalizada"),
|
||||
("Decrease", "Diminuir"),
|
||||
("Increase", "Aumentar"),
|
||||
("Show virtual mouse", ""),
|
||||
("Virtual mouse size", ""),
|
||||
("Small", ""),
|
||||
("Large", ""),
|
||||
("Show virtual mouse", "Mostrar mouse virtual"),
|
||||
("Virtual mouse size", "Tamanho do mouse virtual"),
|
||||
("Small", "Pequeno"),
|
||||
("Large", "Grande"),
|
||||
("Show virtual joystick", ""),
|
||||
("Edit note", ""),
|
||||
("Alias", ""),
|
||||
("ScrollEdge", ""),
|
||||
("Edit note", "Editar nota"),
|
||||
("Alias", "Apelido"),
|
||||
("ScrollEdge", "Rolagem nas bordas"),
|
||||
("Allow insecure TLS fallback", ""),
|
||||
("allow-insecure-tls-fallback-tip", ""),
|
||||
("Disable UDP", ""),
|
||||
("disable-udp-tip", ""),
|
||||
("server-oss-not-support-tip", ""),
|
||||
("input note here", ""),
|
||||
("note-at-conn-end-tip", ""),
|
||||
("Show terminal extra keys", ""),
|
||||
("Relative mouse mode", ""),
|
||||
("rel-mouse-not-supported-peer-tip", ""),
|
||||
("rel-mouse-not-ready-tip", ""),
|
||||
("rel-mouse-lock-failed-tip", ""),
|
||||
("rel-mouse-exit-{}-tip", ""),
|
||||
("rel-mouse-permission-lost-tip", ""),
|
||||
("Changelog", ""),
|
||||
("keep-awake-during-outgoing-sessions-label", ""),
|
||||
("keep-awake-during-incoming-sessions-label", ""),
|
||||
("allow-insecure-tls-fallback-tip", "Por padrão, o RustDesk verifica o certificado do servidor para protocolos que usam TLS.\nCom esta opção habilitada, o RustDesk ignorará a verificação e prosseguirá em caso de falha."),
|
||||
("Disable UDP", "Desabilitar UDP"),
|
||||
("disable-udp-tip", "Controla se deve usar somente TCP.\nCom esta opção habilitada, o RustDesk não usará mais UDP 21116, TCP 21116 será usado no lugar."),
|
||||
("server-oss-not-support-tip", "NOTA: O servidor RustDesk OSS não inclui este recurso."),
|
||||
("input note here", "Insira uma nota aqui"),
|
||||
("note-at-conn-end-tip", "Solicitar nota ao final da conexão"),
|
||||
("Show terminal extra keys", "Mostrar teclas extras do terminal"),
|
||||
("Relative mouse mode", "Modo de Mouse Relativo"),
|
||||
("rel-mouse-not-supported-peer-tip", "O Modo de Mouse Relativo não é suportado pelo parceiro conectado."),
|
||||
("rel-mouse-not-ready-tip", "O Modo de Mouse Relativo ainda não está pronto. Por favor, tente novamente."),
|
||||
("rel-mouse-lock-failed-tip", "Falha ao bloquear o cursor. O Modo de Mouse Relativo foi desabilitado."),
|
||||
("rel-mouse-exit-{}-tip", "Pressione {} para sair."),
|
||||
("rel-mouse-permission-lost-tip", "Permissão de teclado revogada. O Modo Mouse Relativo foi desabilitado."),
|
||||
("Changelog", "Registro de alterações"),
|
||||
("keep-awake-during-outgoing-sessions-label", "Manter tela ativa durante sessões de saída"),
|
||||
("keep-awake-during-incoming-sessions-label", "Manter tela ativa durante sessões de entrada"),
|
||||
].iter().cloned().collect();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,13 @@
|
||||
#include <Security/Authorization.h>
|
||||
#include <Security/AuthorizationTags.h>
|
||||
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
extern "C" bool CanUseNewApiForScreenCaptureCheck() {
|
||||
#ifdef NO_InputMonitoringAuthStatus
|
||||
return false;
|
||||
@@ -292,3 +299,624 @@ extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t h
|
||||
CFRelease(allModes);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static CFMachPortRef g_eventTap = NULL;
|
||||
static CFRunLoopSourceRef g_runLoopSource = NULL;
|
||||
static std::mutex g_privacyModeMutex;
|
||||
static bool g_privacyModeActive = false;
|
||||
|
||||
// Flag to request asynchronous shutdown of privacy mode.
|
||||
// This is set by DisplayReconfigurationCallback when an error occurs, instead of calling
|
||||
// TurnOffPrivacyModeInternal() directly from within the callback. This avoids potential
|
||||
// issues with unregistering a callback from within itself, which is not explicitly
|
||||
// guaranteed to be safe by Apple documentation.
|
||||
static bool g_privacyModeShutdownRequested = false;
|
||||
|
||||
// Timestamp of the last display reconfiguration event (in milliseconds).
|
||||
// Used for debouncing rapid successive changes (e.g., multiple resolution changes).
|
||||
static uint64_t g_lastReconfigTimestamp = 0;
|
||||
|
||||
// Flag indicating whether a delayed blackout reapplication is already scheduled.
|
||||
// Prevents multiple concurrent delayed tasks from being created.
|
||||
static bool g_blackoutReapplicationScheduled = false;
|
||||
|
||||
// Use CFStringRef (UUID) as key instead of CGDirectDisplayID for stability across reconnections
|
||||
// CGDirectDisplayID can change when displays are reconnected, but UUID remains stable
|
||||
static std::map<std::string, std::vector<CGGammaValue>> g_originalGammas;
|
||||
|
||||
// The event source user data value used by enigo library for injected events.
|
||||
// This allows us to distinguish remote input (which should be allowed) from local physical input.
|
||||
// See: libs/enigo/src/macos/macos_impl.rs - ENIGO_INPUT_EXTRA_VALUE
|
||||
static const int64_t ENIGO_INPUT_EXTRA_VALUE = 100;
|
||||
|
||||
// Duration in milliseconds to monitor and enforce blackout after display reconfiguration.
|
||||
// macOS may restore default gamma (via ColorSync) at unpredictable times after display changes,
|
||||
// so we need to actively monitor and reapply blackout during this period.
|
||||
static const int64_t DISPLAY_RECONFIG_MONITOR_DURATION_MS = 5000;
|
||||
|
||||
// Interval in milliseconds between gamma checks during the monitoring period.
|
||||
static const int64_t GAMMA_CHECK_INTERVAL_MS = 200;
|
||||
|
||||
// Helper function to get UUID string from DisplayID
|
||||
static std::string GetDisplayUUID(CGDirectDisplayID displayId) {
|
||||
CFUUIDRef uuid = CGDisplayCreateUUIDFromDisplayID(displayId);
|
||||
if (uuid == NULL) {
|
||||
return "";
|
||||
}
|
||||
CFStringRef uuidStr = CFUUIDCreateString(kCFAllocatorDefault, uuid);
|
||||
CFRelease(uuid);
|
||||
if (uuidStr == NULL) {
|
||||
return "";
|
||||
}
|
||||
char buffer[128];
|
||||
if (CFStringGetCString(uuidStr, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
|
||||
CFRelease(uuidStr);
|
||||
return std::string(buffer);
|
||||
}
|
||||
CFRelease(uuidStr);
|
||||
return "";
|
||||
}
|
||||
|
||||
// Helper function to get display name from DisplayID
|
||||
static std::string GetDisplayName(CGDirectDisplayID displayId) {
|
||||
// Note: NSScreen.localizedName is only available on macOS 10.15+
|
||||
// Since we target 10.14, we avoid using @available checks that would
|
||||
// require __isPlatformVersionAtLeast runtime function.
|
||||
// For now, we just return "Unknown" for all displays.
|
||||
// This is only used for logging purposes, so it doesn't affect functionality.
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
// Helper function to find DisplayID by UUID from current online displays
|
||||
static CGDirectDisplayID FindDisplayIdByUUID(const std::string& targetUuid) {
|
||||
uint32_t count = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &count);
|
||||
if (count == 0) return kCGNullDirectDisplay;
|
||||
|
||||
std::vector<CGDirectDisplayID> displays(count);
|
||||
CGGetOnlineDisplayList(count, displays.data(), &count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
std::string uuid = GetDisplayUUID(displays[i]);
|
||||
if (uuid == targetUuid) {
|
||||
return displays[i];
|
||||
}
|
||||
}
|
||||
return kCGNullDirectDisplay;
|
||||
}
|
||||
|
||||
// Helper function to restore gamma values for all displays in g_originalGammas.
|
||||
// Returns true if all displays were restored successfully, false if any failed.
|
||||
// Note: This function does NOT clear g_originalGammas - caller should do that if needed.
|
||||
static bool RestoreAllGammas() {
|
||||
bool allSuccess = true;
|
||||
for (auto const& [uuid, gamma] : g_originalGammas) {
|
||||
CGDirectDisplayID d = FindDisplayIdByUUID(uuid);
|
||||
if (d == kCGNullDirectDisplay) {
|
||||
NSLog(@"Display with UUID %s no longer online, skipping gamma restore", uuid.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t sampleCount = gamma.size() / 3;
|
||||
if (sampleCount > 0) {
|
||||
const CGGammaValue* red = gamma.data();
|
||||
const CGGammaValue* green = red + sampleCount;
|
||||
const CGGammaValue* blue = green + sampleCount;
|
||||
CGError error = CGSetDisplayTransferByTable(d, sampleCount, red, green, blue);
|
||||
if (error != kCGErrorSuccess) {
|
||||
std::string displayName = GetDisplayName(d);
|
||||
NSLog(@"Failed to restore gamma for display (Name: %s, ID: %u, UUID: %s, error: %d)",
|
||||
displayName.c_str(), (unsigned)d, uuid.c_str(), error);
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
// Helper function to apply blackout to a single display
|
||||
static bool ApplyBlackoutToDisplay(CGDirectDisplayID display) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> zeros(capacity, 0.0f);
|
||||
CGError error = CGSetDisplayTransferByTable(display, capacity, zeros.data(), zeros.data(), zeros.data());
|
||||
if (error != kCGErrorSuccess) {
|
||||
NSLog(@"ApplyBlackoutToDisplay: Failed to set gamma for display %u (error %d)", (unsigned)display, error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
NSLog(@"ApplyBlackoutToDisplay: Display %u has zero gamma table capacity, blackout not supported", (unsigned)display);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Forward declaration - defined later in the file
|
||||
// Must be called while holding g_privacyModeMutex
|
||||
static bool TurnOffPrivacyModeInternal();
|
||||
|
||||
// Helper function to schedule asynchronous shutdown of privacy mode.
|
||||
// This is called from DisplayReconfigurationCallback when an error occurs,
|
||||
// instead of calling TurnOffPrivacyModeInternal() directly. This avoids
|
||||
// potential issues with unregistering a callback from within itself.
|
||||
// Note: This function should be called while holding g_privacyModeMutex.
|
||||
static void ScheduleAsyncPrivacyModeShutdown(const char* reason) {
|
||||
if (g_privacyModeShutdownRequested) {
|
||||
// Already requested, no need to schedule again
|
||||
return;
|
||||
}
|
||||
g_privacyModeShutdownRequested = true;
|
||||
NSLog(@"Privacy mode shutdown requested: %s", reason);
|
||||
|
||||
// Schedule the actual shutdown on the main queue asynchronously
|
||||
// This ensures we're outside the callback when we unregister it
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
if (g_privacyModeShutdownRequested && g_privacyModeActive) {
|
||||
NSLog(@"Executing deferred privacy mode shutdown");
|
||||
TurnOffPrivacyModeInternal();
|
||||
}
|
||||
g_privacyModeShutdownRequested = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to apply blackout to all online displays.
|
||||
// Must be called while holding g_privacyModeMutex.
|
||||
static void ApplyBlackoutToAllDisplays() {
|
||||
uint32_t onlineCount = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &onlineCount);
|
||||
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
|
||||
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
|
||||
|
||||
for (uint32_t i = 0; i < onlineCount; i++) {
|
||||
ApplyBlackoutToDisplay(onlineDisplays[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get current timestamp in milliseconds
|
||||
static uint64_t GetCurrentTimestampMs() {
|
||||
return (uint64_t)(CFAbsoluteTimeGetCurrent() * 1000.0);
|
||||
}
|
||||
|
||||
// Helper function to check if a display's gamma is currently blacked out (all zeros).
|
||||
// Returns true if gamma appears to be blacked out, false otherwise.
|
||||
static bool IsDisplayBlackedOut(CGDirectDisplayID display) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity == 0) {
|
||||
return true; // Can't check, assume it's fine
|
||||
}
|
||||
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) != kCGErrorSuccess) {
|
||||
return true; // Can't read, assume it's fine
|
||||
}
|
||||
|
||||
// Check if all values are zero (or very close to zero)
|
||||
for (uint32_t i = 0; i < sampleCount; i++) {
|
||||
if (red[i] > 0.01f || green[i] > 0.01f || blue[i] > 0.01f) {
|
||||
return false; // Not blacked out
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Internal function that monitors and enforces blackout for a period after display reconfiguration.
|
||||
// This function checks gamma values periodically and reapplies blackout if needed.
|
||||
// Must NOT be called while holding g_privacyModeMutex (it acquires the lock internally).
|
||||
static void RunBlackoutMonitor() {
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(GAMMA_CHECK_INTERVAL_MS * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
|
||||
if (!g_privacyModeActive) {
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t now = GetCurrentTimestampMs();
|
||||
|
||||
// Calculate effective end time based on the last reconfig event
|
||||
uint64_t effectiveEndTime = g_lastReconfigTimestamp + DISPLAY_RECONFIG_MONITOR_DURATION_MS;
|
||||
|
||||
// Check all displays and reapply blackout if any has been restored
|
||||
uint32_t onlineCount = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &onlineCount);
|
||||
std::vector<CGDirectDisplayID> onlineDisplays(onlineCount);
|
||||
CGGetOnlineDisplayList(onlineCount, onlineDisplays.data(), &onlineCount);
|
||||
|
||||
bool needsReapply = false;
|
||||
for (uint32_t i = 0; i < onlineCount; i++) {
|
||||
if (!IsDisplayBlackedOut(onlineDisplays[i])) {
|
||||
needsReapply = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsReapply) {
|
||||
NSLog(@"Gamma was restored by system, reapplying blackout");
|
||||
ApplyBlackoutToAllDisplays();
|
||||
}
|
||||
|
||||
// Continue monitoring if we haven't reached the end time
|
||||
if (now < effectiveEndTime) {
|
||||
RunBlackoutMonitor();
|
||||
} else {
|
||||
NSLog(@"Blackout monitoring period ended");
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to start monitoring and enforcing blackout after display reconfiguration.
|
||||
// This is used after display reconfiguration events because macOS may restore
|
||||
// default gamma (via ColorSync) at unpredictable times after display changes.
|
||||
// Note: This function should be called while holding g_privacyModeMutex.
|
||||
static void ScheduleDelayedBlackoutReapplication(const char* reason) {
|
||||
// Update timestamp to current time
|
||||
g_lastReconfigTimestamp = GetCurrentTimestampMs();
|
||||
|
||||
NSLog(@"Starting blackout monitor: %s", reason);
|
||||
|
||||
// Only schedule if not already scheduled
|
||||
if (!g_blackoutReapplicationScheduled) {
|
||||
g_blackoutReapplicationScheduled = true;
|
||||
RunBlackoutMonitor();
|
||||
}
|
||||
// If already scheduled, the running monitor will see the updated timestamp
|
||||
// and extend its monitoring period
|
||||
}
|
||||
|
||||
// Display reconfiguration callback to handle display connect/disconnect events
|
||||
//
|
||||
// IMPORTANT: When errors occur in this callback, we use ScheduleAsyncPrivacyModeShutdown()
|
||||
// instead of calling TurnOffPrivacyModeInternal() directly. This is because:
|
||||
// 1. TurnOffPrivacyModeInternal() calls CGDisplayRemoveReconfigurationCallback to unregister
|
||||
// this callback, and unregistering a callback from within itself is not explicitly
|
||||
// guaranteed to be safe by Apple documentation.
|
||||
// 2. Using async dispatch ensures we're completely outside the callback context when
|
||||
// performing the cleanup, avoiding any potential undefined behavior.
|
||||
static void DisplayReconfigurationCallback(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo) {
|
||||
(void)userInfo;
|
||||
|
||||
// Note: We need to handle the callback carefully because:
|
||||
// 1. macOS may call this callback multiple times during display reconfiguration
|
||||
// 2. The system may restore ColorSync settings after our gamma change
|
||||
// 3. We should not hold the lock for too long in the callback
|
||||
|
||||
// Skip begin configuration flag - wait for the actual change
|
||||
if (flags & kCGDisplayBeginConfigurationFlag) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
|
||||
if (!g_privacyModeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags & kCGDisplayAddFlag) {
|
||||
// A display was added - apply blackout to it
|
||||
NSLog(@"Display %u added during privacy mode, applying blackout", (unsigned)display);
|
||||
std::string uuid = GetDisplayUUID(display);
|
||||
if (uuid.empty()) {
|
||||
NSLog(@"Failed to get UUID for newly added display %u, exiting privacy mode", (unsigned)display);
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to get UUID for newly added display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save original gamma if not already saved for this UUID
|
||||
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(display);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(display, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
|
||||
std::vector<CGGammaValue> all;
|
||||
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
|
||||
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
|
||||
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
|
||||
g_originalGammas[uuid] = all;
|
||||
} else {
|
||||
NSLog(@"DisplayReconfigurationCallback: Failed to get gamma table for display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to get gamma table for newly added display");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"DisplayReconfigurationCallback: Display %u (UUID: %s) has zero gamma table capacity, exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Newly added display has zero gamma table capacity");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply blackout to the new display immediately
|
||||
if (!ApplyBlackoutToDisplay(display)) {
|
||||
NSLog(@"DisplayReconfigurationCallback: Failed to blackout display %u (UUID: %s), exiting privacy mode", (unsigned)display, uuid.c_str());
|
||||
ScheduleAsyncPrivacyModeShutdown("Failed to blackout newly added display");
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule a delayed re-application to handle ColorSync restoration
|
||||
// macOS may restore default gamma for ALL displays after a new display is added,
|
||||
// so we need to reapply blackout to all online displays, not just the new one
|
||||
ScheduleDelayedBlackoutReapplication("after new display added");
|
||||
} else if (flags & kCGDisplayRemoveFlag) {
|
||||
// A display was removed - update our mapping and reapply blackout to remaining displays
|
||||
NSLog(@"Display %u removed during privacy mode", (unsigned)display);
|
||||
std::string uuid = GetDisplayUUID(display);
|
||||
(void)uuid; // UUID retrieved for potential future use or logging
|
||||
|
||||
// When a display is removed, macOS may reconfigure other displays and restore their gamma.
|
||||
// Schedule a delayed re-application of blackout to all remaining online displays.
|
||||
ScheduleDelayedBlackoutReapplication("after display removal");
|
||||
} else if (flags & kCGDisplaySetModeFlag) {
|
||||
// Display mode changed (resolution change, ColorSync/Night Shift interference, etc.)
|
||||
// macOS resets gamma to default when display mode changes, so we need to reapply blackout.
|
||||
// Schedule a delayed re-application because ColorSync restoration happens asynchronously.
|
||||
NSLog(@"Display %u mode changed during privacy mode, reapplying blackout", (unsigned)display);
|
||||
ScheduleDelayedBlackoutReapplication("after display mode change");
|
||||
}
|
||||
}
|
||||
|
||||
CGEventRef MyEventTapCallback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
||||
(void)proxy;
|
||||
(void)refcon;
|
||||
|
||||
// Handle EventTap being disabled by system timeout
|
||||
if (type == kCGEventTapDisabledByTimeout) {
|
||||
NSLog(@"EventTap was disabled by timeout, re-enabling");
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Handle EventTap being disabled by user input
|
||||
if (type == kCGEventTapDisabledByUserInput) {
|
||||
NSLog(@"EventTap was disabled by user input, re-enabling");
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Allow events explicitly injected by enigo (remote input), identified via custom user data.
|
||||
int64_t userData = CGEventGetIntegerValueField(event, kCGEventSourceUserData);
|
||||
if (userData == ENIGO_INPUT_EXTRA_VALUE) {
|
||||
return event;
|
||||
}
|
||||
// Block local physical HID input.
|
||||
if (CGEventGetIntegerValueField(event, kCGEventSourceStateID) == kCGEventSourceStateHIDSystemState) {
|
||||
return NULL;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
|
||||
// Helper function to set up EventTap on the main thread
|
||||
// Returns true if EventTap was successfully created and enabled
|
||||
static bool SetupEventTapOnMainThread() {
|
||||
__block bool success = false;
|
||||
|
||||
void (^setupBlock)(void) = ^{
|
||||
if (g_eventTap) {
|
||||
// Already set up
|
||||
success = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: kCGEventTapDisabledByTimeout and kCGEventTapDisabledByUserInput are special
|
||||
// notification types (0xFFFFFFFE and 0xFFFFFFFF) that are delivered via the callback's
|
||||
// type parameter, not through the event mask. They should NOT be included in eventMask
|
||||
// as bit-shifting by these values causes undefined behavior.
|
||||
CGEventMask eventMask = (1 << kCGEventKeyDown) | (1 << kCGEventKeyUp) |
|
||||
(1 << kCGEventLeftMouseDown) | (1 << kCGEventLeftMouseUp) |
|
||||
(1 << kCGEventRightMouseDown) | (1 << kCGEventRightMouseUp) |
|
||||
(1 << kCGEventOtherMouseDown) | (1 << kCGEventOtherMouseUp) |
|
||||
(1 << kCGEventLeftMouseDragged) | (1 << kCGEventRightMouseDragged) |
|
||||
(1 << kCGEventOtherMouseDragged) |
|
||||
(1 << kCGEventMouseMoved) | (1 << kCGEventScrollWheel);
|
||||
|
||||
g_eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionDefault,
|
||||
eventMask, MyEventTapCallback, NULL);
|
||||
if (g_eventTap) {
|
||||
g_runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, g_eventTap, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
||||
CGEventTapEnable(g_eventTap, true);
|
||||
success = true;
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to create CGEventTap; input blocking not enabled.");
|
||||
success = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute on main thread to ensure CFRunLoop operations are safe.
|
||||
// Use dispatch_sync if not on main thread, otherwise execute directly to avoid deadlock.
|
||||
//
|
||||
// IMPORTANT: Potential deadlock consideration:
|
||||
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
|
||||
// tries to acquire g_privacyModeMutex. Currently this is safe because:
|
||||
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
|
||||
// 2. The main thread never directly calls MacSetPrivacyMode
|
||||
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
|
||||
// or restructuring the locking strategy.
|
||||
if ([NSThread isMainThread]) {
|
||||
setupBlock();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), setupBlock);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// Helper function to tear down EventTap on the main thread
|
||||
static void TeardownEventTapOnMainThread() {
|
||||
void (^teardownBlock)(void) = ^{
|
||||
if (g_eventTap) {
|
||||
CGEventTapEnable(g_eventTap, false);
|
||||
CFRunLoopRemoveSource(CFRunLoopGetMain(), g_runLoopSource, kCFRunLoopCommonModes);
|
||||
CFRelease(g_runLoopSource);
|
||||
CFRelease(g_eventTap);
|
||||
g_eventTap = NULL;
|
||||
g_runLoopSource = NULL;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute on main thread to ensure CFRunLoop operations are safe.
|
||||
//
|
||||
// NOTE: We use dispatch_sync here instead of dispatch_async because:
|
||||
// 1. TurnOffPrivacyModeInternal() expects EventTap to be fully torn down before
|
||||
// proceeding with gamma restoration - using async would cause race conditions.
|
||||
// 2. The caller (MacSetPrivacyMode) needs deterministic cleanup order.
|
||||
//
|
||||
// IMPORTANT: Potential deadlock consideration (same as SetupEventTapOnMainThread):
|
||||
// Using dispatch_sync while holding g_privacyModeMutex could deadlock if the main thread
|
||||
// tries to acquire g_privacyModeMutex. Currently this is safe because:
|
||||
// 1. MacSetPrivacyMode (which holds the mutex) is only called from background threads
|
||||
// 2. The main thread never directly calls MacSetPrivacyMode
|
||||
// If this assumption changes in the future, consider releasing the mutex before dispatch_sync
|
||||
// or restructuring the locking strategy.
|
||||
if ([NSThread isMainThread]) {
|
||||
teardownBlock();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), teardownBlock);
|
||||
}
|
||||
}
|
||||
|
||||
// Internal function to turn off privacy mode without acquiring the mutex
|
||||
// Must be called while holding g_privacyModeMutex
|
||||
static bool TurnOffPrivacyModeInternal() {
|
||||
if (!g_privacyModeActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Unregister display reconfiguration callback
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
|
||||
// 2. Input - restore (tear down EventTap on main thread)
|
||||
TeardownEventTapOnMainThread();
|
||||
|
||||
// 3. Gamma - restore using UUID to find current DisplayID
|
||||
bool restoreSuccess = RestoreAllGammas();
|
||||
|
||||
// 4. Fallback: Always call CGDisplayRestoreColorSyncSettings as a safety net
|
||||
// This ensures displays return to normal even if our restoration failed or
|
||||
// if the system (ColorSync/Night Shift) modified gamma during privacy mode
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
|
||||
// Clean up
|
||||
g_originalGammas.clear();
|
||||
g_privacyModeActive = false;
|
||||
g_privacyModeShutdownRequested = false;
|
||||
g_lastReconfigTimestamp = 0;
|
||||
g_blackoutReapplicationScheduled = false;
|
||||
|
||||
return restoreSuccess;
|
||||
}
|
||||
|
||||
extern "C" bool MacSetPrivacyMode(bool on) {
|
||||
std::lock_guard<std::mutex> lock(g_privacyModeMutex);
|
||||
if (on) {
|
||||
// Already in privacy mode
|
||||
if (g_privacyModeActive) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 1. Input Blocking - set up EventTap on main thread
|
||||
if (!SetupEventTapOnMainThread()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Register display reconfiguration callback to handle hot-plug events
|
||||
CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
|
||||
// 3. Gamma Blackout
|
||||
uint32_t count = 0;
|
||||
CGGetOnlineDisplayList(0, NULL, &count);
|
||||
std::vector<CGDirectDisplayID> displays(count);
|
||||
CGGetOnlineDisplayList(count, displays.data(), &count);
|
||||
|
||||
uint32_t blackoutSuccessCount = 0;
|
||||
uint32_t blackoutAttemptCount = 0;
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
CGDirectDisplayID d = displays[i];
|
||||
std::string uuid = GetDisplayUUID(d);
|
||||
|
||||
if (uuid.empty()) {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to get UUID for display %u, privacy mode requires all displays", (unsigned)d);
|
||||
// Privacy mode requires ALL connected displays to be successfully blacked out
|
||||
// to ensure user privacy. If we can't identify a display (no UUID),
|
||||
// we can't safely manage its state or restore it later.
|
||||
// Therefore, we must abort the entire operation and clean up any resources
|
||||
// already allocated (like event taps and reconfiguration callbacks).
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
TeardownEventTapOnMainThread();
|
||||
// Restore gamma for displays that were already blacked out before this failure
|
||||
if (!RestoreAllGammas()) {
|
||||
// If any display failed to restore, use system reset as fallback
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
}
|
||||
g_originalGammas.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save original gamma using UUID as key (stable across reconnections)
|
||||
if (g_originalGammas.find(uuid) == g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(d);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> red(capacity), green(capacity), blue(capacity);
|
||||
uint32_t sampleCount = 0;
|
||||
if (CGGetDisplayTransferByTable(d, capacity, red.data(), green.data(), blue.data(), &sampleCount) == kCGErrorSuccess) {
|
||||
std::vector<CGGammaValue> all;
|
||||
all.insert(all.end(), red.begin(), red.begin() + sampleCount);
|
||||
all.insert(all.end(), green.begin(), green.begin() + sampleCount);
|
||||
all.insert(all.end(), blue.begin(), blue.begin() + sampleCount);
|
||||
g_originalGammas[uuid] = all;
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to get gamma table for display %u (UUID: %s)", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity, not supported", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Set to black only if we have saved original gamma for this display
|
||||
if (g_originalGammas.find(uuid) != g_originalGammas.end()) {
|
||||
uint32_t capacity = CGDisplayGammaTableCapacity(d);
|
||||
if (capacity > 0) {
|
||||
std::vector<CGGammaValue> zeros(capacity, 0.0f);
|
||||
blackoutAttemptCount++;
|
||||
CGError error = CGSetDisplayTransferByTable(d, capacity, zeros.data(), zeros.data(), zeros.data());
|
||||
if (error != kCGErrorSuccess) {
|
||||
std::string displayName = GetDisplayName(d);
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout display (Name: %s, ID: %u, UUID: %s, error: %d)", displayName.c_str(), (unsigned)d, uuid.c_str(), error);
|
||||
} else {
|
||||
blackoutSuccessCount++;
|
||||
}
|
||||
} else {
|
||||
NSLog(@"MacSetPrivacyMode: Display %u (UUID: %s) has zero gamma table capacity for blackout", (unsigned)d, uuid.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return false if any display failed to blackout - privacy mode requires ALL displays to be blacked out
|
||||
if (blackoutAttemptCount > 0 && blackoutSuccessCount < blackoutAttemptCount) {
|
||||
NSLog(@"MacSetPrivacyMode: Failed to blackout all displays (%u/%u succeeded)", blackoutSuccessCount, blackoutAttemptCount);
|
||||
// Clean up: unregister callback and disable event tap since we're failing
|
||||
CGDisplayRemoveReconfigurationCallback(DisplayReconfigurationCallback, NULL);
|
||||
TeardownEventTapOnMainThread();
|
||||
// Restore gamma for displays that were successfully blacked out
|
||||
if (!RestoreAllGammas()) {
|
||||
// If any display failed to restore, use system reset as fallback
|
||||
NSLog(@"Some displays failed to restore gamma during cleanup, using CGDisplayRestoreColorSyncSettings as fallback");
|
||||
CGDisplayRestoreColorSyncSettings();
|
||||
}
|
||||
g_originalGammas.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
g_privacyModeActive = true;
|
||||
return true;
|
||||
|
||||
} else {
|
||||
return TurnOffPrivacyModeInternal();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@ pub mod win_mag;
|
||||
#[cfg(windows)]
|
||||
pub mod win_topmost_window;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod win_virtual_display;
|
||||
#[cfg(windows)]
|
||||
@@ -105,7 +108,14 @@ lazy_static::lazy_static! {
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
"".to_owned()
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
macos::PRIVACY_MODE_IMPL.to_owned()
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
"".to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -127,7 +137,13 @@ pub type PrivacyModeCreator = fn(impl_key: &str) -> Box<dyn PrivacyMode>;
|
||||
lazy_static::lazy_static! {
|
||||
static ref PRIVACY_MODE_CREATOR: Arc<Mutex<HashMap<&'static str, PrivacyModeCreator>>> = {
|
||||
#[cfg(not(windows))]
|
||||
let map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
map.insert(macos::PRIVACY_MODE_IMPL, |impl_key: &str| {
|
||||
Box::new(macos::PrivacyModeImpl::new(impl_key))
|
||||
});
|
||||
}
|
||||
#[cfg(windows)]
|
||||
let mut map: HashMap<&'static str, PrivacyModeCreator> = HashMap::new();
|
||||
#[cfg(windows)]
|
||||
@@ -333,7 +349,14 @@ pub fn get_supported_privacy_mode_impl() -> Vec<(&'static str, &'static str)> {
|
||||
|
||||
vec_impls
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// No translation is intended for privacy_mode_impl_macos_tip as it is a
|
||||
// placeholder for macOS specific privacy mode implementation which currently
|
||||
// doesn't provide multiple modes like Windows does.
|
||||
vec![(macos::PRIVACY_MODE_IMPL, "privacy_mode_impl_macos_tip")]
|
||||
}
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
81
src/privacy_mode/macos.rs
Normal file
81
src/privacy_mode/macos.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use super::{PrivacyMode, PrivacyModeState};
|
||||
use hbb_common::{anyhow::anyhow, ResultType};
|
||||
|
||||
extern "C" {
|
||||
fn MacSetPrivacyMode(on: bool) -> bool;
|
||||
}
|
||||
|
||||
pub const PRIVACY_MODE_IMPL: &str = "privacy_mode_impl_macos";
|
||||
|
||||
pub struct PrivacyModeImpl {
|
||||
impl_key: String,
|
||||
conn_id: i32,
|
||||
}
|
||||
|
||||
impl PrivacyModeImpl {
|
||||
pub fn new(impl_key: &str) -> Self {
|
||||
Self {
|
||||
impl_key: impl_key.to_owned(),
|
||||
conn_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivacyMode for PrivacyModeImpl {
|
||||
fn is_async_privacy_mode(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn init(&self) -> ResultType<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
unsafe {
|
||||
MacSetPrivacyMode(false);
|
||||
}
|
||||
self.conn_id = 0;
|
||||
}
|
||||
|
||||
fn turn_on_privacy(&mut self, conn_id: i32) -> ResultType<bool> {
|
||||
if self.check_on_conn_id(conn_id)? {
|
||||
return Ok(true);
|
||||
}
|
||||
let success = unsafe { MacSetPrivacyMode(true) };
|
||||
if !success {
|
||||
return Err(anyhow!("Failed to turn on privacy mode"));
|
||||
}
|
||||
self.conn_id = conn_id;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn turn_off_privacy(&mut self, conn_id: i32, _state: Option<PrivacyModeState>) -> ResultType<()> {
|
||||
// Note: The `_state` parameter is intentionally ignored on macOS.
|
||||
// On Windows, it's used to notify the connection manager about privacy mode state changes
|
||||
// (see win_topmost_window.rs). macOS currently has a simpler single-mode implementation
|
||||
// without the need for such cross-component state synchronization.
|
||||
self.check_off_conn_id(conn_id)?;
|
||||
let success = unsafe { MacSetPrivacyMode(false) };
|
||||
if !success {
|
||||
return Err(anyhow!("Failed to turn off privacy mode"));
|
||||
}
|
||||
self.conn_id = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pre_conn_id(&self) -> i32 {
|
||||
self.conn_id
|
||||
}
|
||||
|
||||
fn get_impl_key(&self) -> &str {
|
||||
&self.impl_key
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PrivacyModeImpl {
|
||||
fn drop(&mut self) {
|
||||
// Use the same cleanup logic as other code paths to keep conn_id consistent
|
||||
// and ensure all cleanup is centralized in one place.
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
@@ -1420,7 +1420,7 @@ impl Connection {
|
||||
pi.platform = "Android".into();
|
||||
}
|
||||
#[cfg(all(target_os = "macos", not(feature = "unix-file-copy-paste")))]
|
||||
let platform_additions = serde_json::Map::new();
|
||||
let mut platform_additions = serde_json::Map::new();
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "linux",
|
||||
@@ -1453,6 +1453,13 @@ impl Connection {
|
||||
json!(privacy_mode::get_supported_privacy_mode_impl()),
|
||||
);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
platform_additions.insert(
|
||||
"supported_privacy_mode_impl".into(),
|
||||
json!(privacy_mode::get_supported_privacy_mode_impl()),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))]
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user