[feat] add privacy unlock controls and remote relocking

This commit is contained in:
dijunkun
2026-10-27 10:22:25 +08:00
parent d1e4a31efb
commit ea3a2e1faa
41 changed files with 470 additions and 72 deletions
@@ -81,6 +81,7 @@ export class MobileState {
privacyEnabled: boolean = false;
privacyActive: boolean = false;
privacyCanToggle: boolean = false;
privacyCanLock: boolean = false;
privacyVisible: boolean = true;
networkTime: number = 0;
unread: number = 0;
@@ -266,9 +267,9 @@ export class MobileController {
if (this.foreground && this.state.consent) this.checkLocal();
}
release(): void { this.releaseInput(); this.send({ op: 'release' }); }
pointer(x: number, y: number, flag: number, scroll: number): void { this.send({ op: 'pointer', x: x, y: y, flag: flag, scroll: scroll }); }
physicalKey(code: number, down: boolean): void { this.send({ op: 'key', code: code, down: down }); }
key(code: number, down: boolean): void { this.send({ op: 'virtualKey', code: code, down: down }); }
pointer(x: number, y: number, flag: number, scroll: number): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'pointer', x: x, y: y, flag: flag, scroll: scroll }); }
physicalKey(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'key', code: code, down: down }); }
key(code: number, down: boolean): void { if (this.state.modal === 'privacyUnlock') return; this.send({ op: 'virtualKey', code: code, down: down }); }
chord(code: number, modifiers: number[] = []): void {
modifiers.forEach((key: number) => this.key(key, true)); this.key(code, true); this.key(code, false);
modifiers.slice().reverse().forEach((key: number) => this.key(key, false));
@@ -352,11 +353,17 @@ export class MobileController {
this.state.documentTitle = title; this.state.documentBack = back; this.state.page = 'document'; this.state.modal = '';
} catch { this.toast('无法读取文档'); }
}
togglePrivacy(): void {
dismissPrivacyUnlock(): void {
this.privacyScreen.unlockNotice = false;
if (this.state.modal === 'privacyUnlock') this.state.modal = '';
}
lockPrivacy(): void { this.sendPrivacy(true); }
togglePrivacy(): void { this.sendPrivacy(false); }
private sendPrivacy(lock: boolean): void {
if (!this.state.ready || !this.state.consent) return;
const enable = !this.privacyScreen.active(), request = this.privacyScreen.begin(this.now());
const enable = !this.privacyScreen.active(), request = this.privacyScreen.begin(this.now(), lock);
if (request < 0) return;
this.send({ op: 'privacy', flag: enable ? 1 : 2, request: request,
this.send({ op: 'privacy', flag: lock ? 3 : enable ? 1 : 2, request: request,
blockLocalInput: true });
this.syncPrivacy();
}
@@ -365,6 +372,13 @@ export class MobileController {
this.state.privacyStatus = this.privacyScreen.status(now); this.state.privacyFeedback = this.privacyScreen.feedback(now);
this.state.privacyButtonTitle = this.privacyScreen.buttonTitle(now);
this.state.privacyVisible = this.privacyScreen.desktopAvailable;
this.state.privacyCanLock = this.privacyScreen.canLock(now);
if (this.foreground && this.state.ready && this.privacyScreen.unlockNotice && !this.state.modal) {
this.release();
this.state.modal = 'privacyUnlock';
} else if (!this.privacyScreen.unlockNotice && this.state.modal === 'privacyUnlock') {
this.state.modal = '';
}
this.state.privacyEnabled = this.privacyScreen.enabled(now);
this.state.privacyActive = this.privacyScreen.active(); this.state.privacyCanToggle = this.privacyScreen.canToggle(now);
}
@@ -1,13 +1,14 @@
export interface PrivacyStatus {
state: number; supported: boolean; input_block_supported: boolean;
overlay_active: boolean; input_blocked: boolean; remote_paused: boolean;
revision: number; reason: string;
revision: number; reason: string; local_unlock_revision?: number;
}
export class RemotePrivacyScreen {
desktopAvailable: boolean = true;
snapshot?: PrivacyStatus;
pending: boolean = false;
unlockNotice: boolean = false;
requestId: number = 0;
private requestRevision: number = 0;
private receivedAt: number = 0;
@@ -26,6 +27,10 @@ export class RemotePrivacyScreen {
typeof s.remote_paused !== 'boolean' || typeof s.reason !== 'string' || s.reason.includes('\0')) return;
if (s.state === 3 && (!s.supported || !s.overlay_active || s.remote_paused)) return;
if (this.snapshot && ((s.revision - this.snapshot.revision) | 0) < 0) return;
const unlock = s.local_unlock_revision ?? 0;
if (!Number.isInteger(unlock) || unlock < 0 || unlock > 0xffffffff) return;
if (this.snapshot && ((unlock - (this.snapshot.local_unlock_revision ?? 0)) | 0) > 0) this.unlockNotice = true;
if (s.state === 3) this.unlockNotice = false;
this.snapshot = s; this.receivedAt = now; this.failure = '';
if (this.pending && ((s.revision - this.requestRevision) | 0) > 0 && s.state !== 2 && s.state !== 4) this.pending = false;
}
@@ -57,9 +62,12 @@ export class RemotePrivacyScreen {
const s = this.snapshot;
return this.desktopAvailable && s !== undefined && !this.pending && this.fresh(now) && s.state !== 4 && (this.active() || s.supported && s.state === 1);
}
begin(now: number): number {
canLock(now: number): boolean {
return this.desktopAvailable && !this.pending && this.fresh(now) && this.snapshot?.supported === true;
}
begin(now: number, lock: boolean = false): number {
this.tick(now);
if (!this.canToggle(now) || !this.snapshot) return -1;
if (!(lock ? this.canLock(now) : this.canToggle(now)) || !this.snapshot) return -1;
this.pending = true; this.requestedAt = now; this.requestRevision = this.snapshot.revision; this.failure = '';
this.requestId = (this.requestId + 1) >>> 0;
return this.requestId;
@@ -91,6 +91,7 @@ struct Mobile {
return saved;
}
private dismissModal(): void {
if (this.state.modal === 'privacyUnlock') this.controller?.dismissPrivacyUnlock();
if (this.state.modal === 'password') { this.state.password = ''; this.showPassword = false; }
this.previewToggle = this.state.previews; this.state.modal = '';
}
@@ -537,6 +538,21 @@ struct Mobile {
}.padding({ left: 26, right: 26, top: 24, bottom: 20 }).width('100%')
this.Rule()
this.Action('取消连接', () => this.controller?.end(''), true)
} else if (this.state.modal === 'privacyUnlock') {
Column({ space: 12 }) {
Text('隐私屏已被解锁').fontSize(20).fontWeight(600).fontColor(MobileTheme.ink)
Text('被控端已通过组合键解除隐私屏。点击“锁定”将重新开启隐私屏,并禁止本机使用组合键解锁。')
.fontSize(14).lineHeight(21).fontColor(MobileTheme.secondary).width('100%')
if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(12).fontColor(MobileTheme.secondary) }
}.padding(24).width('100%')
this.Rule()
Row() {
Button('确认', { type: ButtonType.Normal }).fontSize(16).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
.backgroundColor(Color.White).onClick(() => this.dismissModal())
Divider().vertical(true).height(50).strokeWidth(0.5).color(MobileTheme.line)
Button('锁定', { type: ButtonType.Normal }).fontSize(16).fontWeight(600).fontColor(MobileTheme.blue).height(50).layoutWeight(1)
.backgroundColor(Color.White).enabled(this.state.privacyCanLock).onClick(() => this.controller?.lockPrivacy())
}.width('100%')
} else if (this.state.modal === 'clipboard') {
Column({ space: 16 }) {
this.SheetHeader('剪贴板')