mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
[feat] add privacy unlock controls and remote relocking
This commit is contained in:
@@ -195,6 +195,7 @@ typedef struct {
|
||||
- (void)sendFileAtURL:(NSURL *)fileURL;
|
||||
- (void)sendSecureAttentionSequence;
|
||||
- (void)sendPrivacyCommand:(BOOL)enable
|
||||
lock:(BOOL)lock
|
||||
blockLocalInput:(BOOL)blockLocalInput
|
||||
requestID:(uint32_t)requestID;
|
||||
|
||||
|
||||
@@ -1044,6 +1044,7 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
}
|
||||
|
||||
- (void)sendPrivacyCommand:(BOOL)enable
|
||||
lock:(BOOL)lock
|
||||
blockLocalInput:(BOOL)blockLocalInput
|
||||
requestID:(uint32_t)requestID {
|
||||
const uint64_t generation = _activeControllerGeneration.load();
|
||||
@@ -1051,7 +1052,8 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
|
||||
if (![self isControllerGenerationActive:generation]) return;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::privacy_command;
|
||||
action.pc = {enable ? crossdesk::PrivacyCommandFlag::enable
|
||||
action.pc = {lock ? crossdesk::PrivacyCommandFlag::lock
|
||||
: enable ? crossdesk::PrivacyCommandFlag::enable
|
||||
: crossdesk::PrivacyCommandFlag::disable,
|
||||
static_cast<bool>(blockLocalInput)};
|
||||
const auto message = action.to_json();
|
||||
|
||||
@@ -9,11 +9,13 @@ struct RemotePrivacyStatus: Decodable {
|
||||
let remotePaused: Bool
|
||||
let revision: UInt32
|
||||
let reason: String
|
||||
let localUnlockRevision: UInt32?
|
||||
}
|
||||
|
||||
struct RemotePrivacyScreen {
|
||||
private(set) var desktopAvailable = true
|
||||
private(set) var snapshot: RemotePrivacyStatus?
|
||||
var unlockNotice = false
|
||||
private(set) var pending = false
|
||||
private(set) var requestID: UInt32 = 0
|
||||
private var requestRevision: UInt32 = 0
|
||||
@@ -64,14 +66,22 @@ struct RemotePrivacyScreen {
|
||||
!s.reason.contains("\0"),
|
||||
s.state != 3 || (s.supported && s.overlayActive && !s.remotePaused) else { return }
|
||||
if let previous = snapshot, Int32(bitPattern: s.revision &- previous.revision) < 0 { return }
|
||||
if let previous = snapshot,
|
||||
Int32(bitPattern: (s.localUnlockRevision ?? 0) &- (previous.localUnlockRevision ?? 0)) > 0 {
|
||||
unlockNotice = true
|
||||
}
|
||||
if s.state == 3 { unlockNotice = false }
|
||||
snapshot = s; receivedAt = now; failure = ""
|
||||
if pending && Int32(bitPattern: s.revision &- requestRevision) > 0 && ![2, 4].contains(s.state) {
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
mutating func begin(at now: TimeInterval) -> UInt32? {
|
||||
func canLock(at now: TimeInterval) -> Bool {
|
||||
desktopAvailable && !pending && fresh(at: now) && snapshot?.supported == true
|
||||
}
|
||||
mutating func begin(at now: TimeInterval, lock: Bool = false) -> UInt32? {
|
||||
tick(at: now)
|
||||
guard canToggle(at: now), let s = snapshot else { return nil }
|
||||
guard (lock ? canLock(at: now) : canToggle(at: now)), let s = snapshot else { return nil }
|
||||
pending = true; requestedAt = now; requestRevision = s.revision; failure = ""
|
||||
requestID &+= 1
|
||||
return requestID
|
||||
|
||||
@@ -1106,12 +1106,14 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
privacyTimer = timer
|
||||
}
|
||||
|
||||
func togglePrivacyScreen() {
|
||||
func lockPrivacyScreen() { sendPrivacyScreen(lock: true) }
|
||||
func togglePrivacyScreen() { sendPrivacyScreen(lock: false) }
|
||||
private func sendPrivacyScreen(lock: Bool) {
|
||||
guard hasNetworkConsent, isConnected else { return }
|
||||
privacyClock = ProcessInfo.processInfo.systemUptime
|
||||
let enable = !privacyScreen.active
|
||||
guard let request = privacyScreen.begin(at: privacyClock) else { return }
|
||||
bridge.sendPrivacyCommand(enable, blockLocalInput: true, requestID: request)
|
||||
guard let request = privacyScreen.begin(at: privacyClock, lock: lock) else { return }
|
||||
bridge.sendPrivacyCommand(enable, lock: lock, blockLocalInput: true, requestID: request)
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceivePrivacyStatus data: Data) {
|
||||
|
||||
@@ -181,19 +181,28 @@ struct RemoteSessionView: View {
|
||||
floatingStatusControls
|
||||
.zIndex(30)
|
||||
|
||||
if session.isConnected && session.privacyScreen.unlockNotice {
|
||||
privacyUnlockOverlay.zIndex(60)
|
||||
}
|
||||
if disconnectConfirmationVisible {
|
||||
disconnectConfirmationOverlay
|
||||
.zIndex(50)
|
||||
}
|
||||
}
|
||||
.alert("升级提示", isPresented: Binding(
|
||||
get: { session.remoteUpdateMessage != nil },
|
||||
set: { if !$0 { session.dismissRemoteUpdate() } }
|
||||
get: { !session.privacyScreen.unlockNotice && session.remoteUpdateMessage != nil },
|
||||
set: { if !$0 && !session.privacyScreen.unlockNotice { session.dismissRemoteUpdate() } }
|
||||
)) {
|
||||
Button("知道了", role: .cancel) { session.dismissRemoteUpdate() }
|
||||
} message: {
|
||||
Text(session.remoteUpdateMessage ?? "")
|
||||
}
|
||||
.onChange(of: session.privacyScreen.unlockNotice) { visible in
|
||||
if visible {
|
||||
mouseInput.releaseAll()
|
||||
keyboardInputVisible = false
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
mouseInput.send = { x, y, flag, wheel in
|
||||
cursorPosition = CGPoint(x: CGFloat(x), y: CGFloat(y))
|
||||
@@ -268,6 +277,35 @@ struct RemoteSessionView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var privacyUnlockOverlay: some View {
|
||||
ZStack {
|
||||
Color.black.opacity(0.28).ignoresSafeArea().contentShape(Rectangle()).onTapGesture { }
|
||||
VStack(spacing: 0) {
|
||||
VStack(spacing: 12) {
|
||||
Text("隐私屏已被解锁").font(.headline)
|
||||
Text("被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。")
|
||||
.font(.subheadline).foregroundStyle(.secondary)
|
||||
let feedback = session.privacyScreen.feedback(at: session.privacyClock)
|
||||
if !feedback.isEmpty { Text(feedback).font(.caption).foregroundStyle(.secondary) }
|
||||
}.padding(24)
|
||||
Divider()
|
||||
HStack(spacing: 0) {
|
||||
Button("确认") { session.privacyScreen.unlockNotice = false }
|
||||
.frame(maxWidth: .infinity, minHeight: 48)
|
||||
Divider().frame(height: 48)
|
||||
Button("锁定") { session.lockPrivacyScreen() }
|
||||
.fontWeight(.semibold).frame(maxWidth: .infinity, minHeight: 48)
|
||||
.disabled(!session.privacyScreen.canLock(at: session.privacyClock))
|
||||
}
|
||||
}
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.shadow(color: .black.opacity(0.2), radius: 18, y: 7)
|
||||
.frame(maxWidth: 380).padding(20)
|
||||
}
|
||||
.accessibilityAddTraits(.isModal)
|
||||
}
|
||||
|
||||
private var disconnectConfirmationOverlay: some View {
|
||||
GeometryReader { proxy in
|
||||
ZStack {
|
||||
|
||||
Reference in New Issue
Block a user