[feat] add mobile privacy controls and Windows secure text input

This commit is contained in:
dijunkun
2026-10-27 02:37:23 +08:00
parent 1e25ec15e1
commit b15925f857
37 changed files with 795 additions and 80 deletions
@@ -0,0 +1,99 @@
import Foundation
struct RemotePrivacyStatus: Decodable {
let state: Int
let supported: Bool
let inputBlockSupported: Bool
let overlayActive: Bool
let inputBlocked: Bool
let remotePaused: Bool
let revision: UInt32
let reason: String
}
struct RemotePrivacyScreen {
private(set) var snapshot: RemotePrivacyStatus?
private(set) var pending = false
private(set) var requestID: UInt32 = 0
private var requestRevision: UInt32 = 0
private var receivedAt: TimeInterval = 0
private var requestedAt: TimeInterval = 0
private var failure = ""
var active: Bool {
guard let s = snapshot else { return false }
return s.overlayActive || s.remotePaused || [2, 3, 4].contains(s.state)
}
func fresh(at now: TimeInterval) -> Bool { snapshot != nil && now - receivedAt <= 5 }
func enabled(at now: TimeInterval) -> Bool {
guard let s = snapshot else { return false }
return fresh(at: now) && s.state == 3 && s.overlayActive && !s.remotePaused
}
func buttonTitle(at now: TimeInterval) -> String {
if enabled(at: now) { return "隐私屏已开启" }
if fresh(at: now), let s = snapshot, s.supported && s.state == 1 && !active {
return "隐私屏已关闭"
}
return "隐私屏"
}
func feedback(at now: TimeInterval) -> String {
if failure.isEmpty && (pending || fresh(at: now) && [2, 4].contains(snapshot?.state ?? -1)) {
return ""
}
if failure.isEmpty && !pending && fresh(at: now), let s = snapshot, s.supported,
enabled(at: now) || (s.state == 1 && !active) { return "" }
let reason = detail(at: now)
return status(at: now) + (reason.isEmpty ? "" : ":" + reason)
}
func canToggle(at now: TimeInterval) -> Bool {
guard let s = snapshot else { return false }
return !pending && fresh(at: now) && s.state != 4 && (active || s.supported && s.state == 1)
}
mutating func receive(_ data: Data, at now: TimeInterval) {
struct Envelope: Decodable { let type: Int; let privacyStatus: RemotePrivacyStatus }
let decoder = JSONDecoder(); decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let message = try? decoder.decode(Envelope.self, from: data), message.type == 10 else { return }
let s = message.privacyStatus
guard (0...5).contains(s.state), s.reason.utf8.count < 256,
!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 }
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? {
tick(at: now)
guard canToggle(at: now), let s = snapshot else { return nil }
pending = true; requestedAt = now; requestRevision = s.revision; failure = ""
requestID &+= 1
return requestID
}
mutating func failed(request: UInt32) {
if pending && requestID == request { pending = false; failure = "指令发送失败,请重试" }
}
mutating func tick(at now: TimeInterval) {
if pending && now - requestedAt >= 15 {
pending = false; failure = "请求超时,尚未确认电脑的隐私屏状态"
}
}
func status(at now: TimeInterval) -> String {
if !failure.isEmpty { return failure }
guard let s = snapshot else { return "等待电脑返回隐私屏状态;旧版电脑端可能不支持" }
if !fresh(at: now) { return "隐私屏状态已过期,等待电脑确认" }
if s.state == 2 { return "正在开启隐私屏…" }
if s.state == 4 { return "正在关闭隐私屏…" }
if pending { return "指令已发送,等待电脑确认…" }
if s.state == 5 { return "隐私屏异常" }
if !s.supported || s.state == 0 { return "隐私屏暂不可用" }
if s.state == 3 && s.overlayActive && !s.remotePaused {
return "隐私屏已开启"
}
return s.state == 1 && !active ? "隐私屏已关闭" : "隐私屏状态待确认"
}
func detail(at now: TimeInterval) -> String {
guard let s = snapshot, fresh(at: now), s.state == 0 || s.state == 5 || !s.supported else { return "" }
return s.reason
}
}