[fix] hide privacy controls on locked desktops across clients

This commit is contained in:
dijunkun
2026-10-27 03:46:19 +08:00
parent 922cd41f70
commit 0e052e6d7a
15 changed files with 92 additions and 37 deletions
@@ -201,6 +201,10 @@ final class NativeSession implements AutoCloseable {
if(event.optInt("type",-1)==10){
privacyScreen.receive(event,android.os.SystemClock.elapsedRealtime());
listener.privacyChanged();
} else if(event.optInt("type",-1)==5){
JSONObject service=event.optJSONObject("service_status");
privacyScreen.setInteractiveStage(service==null?"":service.optString("interactive_stage",""));
listener.privacyChanged();
}
listener.data(event);
});
@@ -4,11 +4,17 @@ import org.json.JSONObject;
/** Host-confirmed privacy state; all access belongs to the main thread. */
final class RemotePrivacyScreen {
boolean desktopAvailable = true;
boolean received, supported, inputBlockSupported, overlay, inputBlocked, remotePaused, pending;
int state;
long revision, requestRevision, requestId, receivedAt, requestedAt;
String reason = "", failure = "";
void setInteractiveStage(String stage) {
desktopAvailable = !"lock-screen".equals(stage) &&
!"credential-ui".equals(stage) && !"secure-desktop".equals(stage);
}
void receive(JSONObject message, long now) {
try {
if (message.getInt("type") != 10) return;
@@ -37,6 +43,7 @@ final class RemotePrivacyScreen {
return "隐私屏";
}
String feedback(long now) {
if (!desktopAvailable) return "";
if (failure.isEmpty() && (pending || fresh(now) && (state == 2 || state == 4))) return "";
if (failure.isEmpty() && !pending && fresh(now) && supported &&
(enabled(now) || state == 1 && !active())) return "";
@@ -44,7 +51,7 @@ final class RemotePrivacyScreen {
return status(now) + (detail.isEmpty() ? "" : ":" + detail);
}
boolean canToggle(long now) {
return !pending && fresh(now) && state != 4 && (active() || supported && state == 1);
return desktopAvailable && !pending && fresh(now) && state != 4 && (active() || supported && state == 1);
}
long begin(long now) {
tick(now);
@@ -130,10 +130,11 @@ final class SessionControls extends FrameLayout {
String[] icons={"keyboard","mouse","mouse","display","sliders","audio","folder","chart","lock","clipboard","lock"};
Runnable[] clicks={()->{closeMenu();toggleKeyboard();},this::toggleMouse,actions::mouse,actions::display,()->openMenu("video"),actions::mute,actions::sendFile,()->openMenu("network"),actions::secureAttention,actions::clipboard,actions::togglePrivacy};
menuColumns=controlColumns();
for(int row=0;row<(labels.length+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col<menuColumns;col++){int index=row*menuColumns+col;if(index>=labels.length){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));control.addView(ui.icon(icons[index],INK),ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setSingleLine();caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run());
int controlCount=privacy.desktopAvailable?labels.length:labels.length-1;
for(int row=0;row<(controlCount+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col<menuColumns;col++){int index=row*menuColumns+col;if(index>=controlCount){View spacer=new View(getContext());LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(spacer,p);continue;}LinearLayout control=ui.column();control.setGravity(Gravity.CENTER);control.setBackground(ui.background(0xFFF0F0F2,10));control.addView(ui.icon(icons[index],INK),ui.size(19,19));ui.gap(control,4);TextView caption=ui.text(labels[index],10,false);caption.setSingleLine();caption.setGravity(Gravity.CENTER);control.addView(caption);control.setContentDescription(labels[index]);control.setClickable(true);control.setFocusable(true);control.setOnClickListener(v->clicks[index].run());
if(index==10){privacyButton=control;privacyCaption=caption;}
LinearLayout.LayoutParams p=new LinearLayout.LayoutParams(0,ui.dp(52),1);if(col>0)p.leftMargin=ui.dp(7);line.addView(control,p);
}body.addView(line);if((row+1)*menuColumns<labels.length)ui.gap(body,7);}
}body.addView(line);if((row+1)*menuColumns<controlCount)ui.gap(body,7);}
privacyFeedback=ui.text("",11,false);privacyFeedback.setTextColor(SECONDARY);privacyFeedback.setPadding(0,ui.dp(7),0,0);privacyFeedback.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);body.addView(privacyFeedback);renderPrivacy();
ui.gap(body,7);transferLabel=ui.text(transferStatus,11,false);transferLabel.setTextColor(SECONDARY);transferLabel.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);body.addView(transferLabel);
saveFileButton=ui.action("保存收到的文件",BLUE,actions::saveFile);body.addView(saveFileButton);renderFileStatus();
@@ -149,14 +150,17 @@ final class SessionControls extends FrameLayout {
positionMouse();
}
private void renderPrivacy(){
if(privacyButton==null)return;
if(menu!=null&&menuPage.equals("controls")&&(privacyButton!=null)!=privacy.desktopAvailable){
openMenu("controls");return;
}
long now=android.os.SystemClock.elapsedRealtime();privacy.tick(now);
if(privacyFeedback!=null){String feedback=privacy.feedback(now);privacyFeedback.setText(feedback);privacyFeedback.setVisibility(feedback.isEmpty()?GONE:VISIBLE);}
if(privacyButton==null)return;
boolean enabled=privacy.enabled(now);String action=privacy.active()?"关闭隐私屏":"开启隐私屏";
privacyCaption.setText(privacy.buttonTitle(now));
privacyButton.setSelected(enabled);
privacyButton.setContentDescription(action+","+privacy.status(now));
privacyButton.setEnabled(privacy.canToggle(now));privacyButton.setAlpha(privacyButton.isEnabled()?1f:.45f);
String feedback=privacy.feedback(now);privacyFeedback.setText(feedback);privacyFeedback.setVisibility(feedback.isEmpty()?GONE:VISIBLE);
}
private final class SettingRow {
final int field;final int[] values;final SegmentedControl group;
@@ -2168,7 +2168,7 @@ void GuiApplication::BindStreamCallbacks() {
const auto controls = GetPrivacyControlState(
status, props->privacy_status_received_, props->privacy_command_pending_,
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
if (!controls.can_toggle) return;
if (!props->privacy_desktop_available_ || !controls.can_toggle) return;
action.pc.flag = controls.active ? PrivacyCommandFlag::disable
: PrivacyCommandFlag::enable;
action.pc.block_local_input = true;
@@ -3831,8 +3831,10 @@ void GuiApplication::SyncStreamWindow() {
(*ui_->stream)->set_privacy_active(controls.active);
(*ui_->stream)->set_privacy_off(connected && controls.off);
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
(*ui_->stream)->set_privacy_visible(props->privacy_desktop_available_);
(*ui_->stream)->set_privacy_can_toggle(
props->control_mouse_ && connected && controls.can_toggle);
props->privacy_desktop_available_ && props->control_mouse_ &&
connected && controls.can_toggle);
const int language = localization_language_index_;
std::string text = !connected || !props->privacy_status_received_
? localization::privacy_screen[language]
@@ -193,6 +193,7 @@ struct RemoteSession {
std::mutex privacy_status_mutex_;
PrivacyStatus privacy_status_{};
bool privacy_status_received_ = false;
bool privacy_desktop_available_ = true;
bool privacy_command_pending_ = false;
uint64_t privacy_command_tick_ = 0;
uint32_t privacy_request_revision_ = 0;
+12 -10
View File
@@ -300,6 +300,7 @@ export component StreamWindow inherits Window {
in property <[string]> displays;
in-out property <int> selected-display: 0;
in property <bool> mouse-control-enabled: true;
in property <bool> privacy-visible: true;
in property <bool> privacy-can-toggle: false;
in property <bool> privacy-active: false;
in property <bool> privacy-off: false;
@@ -363,6 +364,7 @@ export component StreamWindow inherits Window {
private property <bool> control-visual-expanded: false;
private property <length> control-toolbar-height: 38px;
private property <length> control-button-step: 32px;
private property <length> control-privacy-width: root.privacy-visible ? root.control-button-step : 0px;
private property <length> control-disconnect-gap: 8px;
private property <length> control-buttons-x: root.control-docked-left ? 43px : 7px;
private property <length> control-panel-gap: 8px;
@@ -517,7 +519,7 @@ export component StreamWindow inherits Window {
if display-button.has-hover { return display-button.tooltip-info; }
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if mouse-button.has-hover { return mouse-button.tooltip-info; }
if privacy-button.has-hover { return privacy-button.tooltip-info; }
if root.privacy-visible && privacy-button.has-hover { return privacy-button.tooltip-info; }
if video-button.has-hover { return video-button.tooltip-info; }
if audio-button.has-hover { return audio-button.tooltip-info; }
if file-button.has-hover { return file-button.tooltip-info; }
@@ -1083,7 +1085,7 @@ export component StreamWindow inherits Window {
control := Rectangle {
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
y: min(max(0px, root.control-pos-y), parent.height - self.height);
width: root.control-visual-expanded ? min(80px + 9 * root.control-button-step + root.control-disconnect-gap, parent.width) : root.control-toolbar-height;
width: root.control-visual-expanded ? min(80px + 8 * root.control-button-step + root.control-privacy-width + root.control-disconnect-gap, parent.width) : root.control-toolbar-height;
height: root.control-toolbar-height;
z: 10;
@@ -1157,7 +1159,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 4 * root.control-button-step;
x: root.control-buttons-x + 3 * root.control-button-step + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
@@ -1167,7 +1169,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 5 * root.control-button-step;
x: root.control-buttons-x + 4 * root.control-button-step + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
clicked => { root.close-control-menus(); root.select-file(); }
@@ -1176,7 +1178,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 6 * root.control-button-step;
x: root.control-buttons-x + 5 * root.control-button-step + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: "\u{f1de}"; selected: root.video-menu-open;
tooltip: StreamStrings.video-settings;
@@ -1191,7 +1193,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 7 * root.control-button-step;
x: root.control-buttons-x + 6 * root.control-button-step + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
@@ -1201,7 +1203,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 8 * root.control-button-step;
x: root.control-buttons-x + 7 * root.control-button-step + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
@@ -1217,7 +1219,7 @@ export component StreamWindow inherits Window {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
x: root.control-buttons-x + 9 * root.control-button-step + root.control-disconnect-gap;
x: root.control-buttons-x + 8 * root.control-button-step + root.control-privacy-width + root.control-disconnect-gap;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
danger: true;
@@ -1252,13 +1254,13 @@ export component StreamWindow inherits Window {
privacy-button := ControlBarButton {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
visible: root.control-content-visible && root.privacy-visible;
x: root.control-buttons-x + 3 * root.control-button-step;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported;
muted: root.privacy-unsupported;
action-enabled: root.control-expanded && root.privacy-can-toggle;
action-enabled: root.control-expanded && root.privacy-visible && root.privacy-can-toggle;
tooltip: root.privacy-status-text
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
@@ -7,6 +7,7 @@ void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) {
std::lock_guard lock(props.privacy_status_mutex_);
props.privacy_status_ = {};
props.privacy_status_received_ = false;
props.privacy_desktop_available_ = true;
props.privacy_command_pending_ = false;
props.privacy_status_tick_ = 0;
}
@@ -17,6 +18,12 @@ void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) {
void GuiRuntime::ApplyRemoteServiceStatus(RemoteSession& props,
const ServiceStatus& status) {
{
std::lock_guard lock(props.privacy_status_mutex_);
const std::string stage = status.interactive_stage;
props.privacy_desktop_available_ = stage != "lock-screen" &&
stage != "credential-ui" && stage != "secure-desktop";
}
props.remote_service_status_received_ = true;
props.remote_service_available_ = status.available;
props.remote_interactive_stage_ = status.interactive_stage;
@@ -25,7 +25,7 @@ interface NativeEvent {
export interface RemoteDisplay { name: string; }
interface HostInfo { host_name: string; platform: string; app_version: string; supports_video_settings: boolean; displays: RemoteDisplay[]; }
interface SettingAck { quality: number; frame_rate: number; preference: number; request_id: number; accepted: boolean; }
interface ServiceStatus { available: boolean; }
interface ServiceStatus { available: boolean; interactive_stage?: string; }
interface ControlData { type: number; host_info?: HostInfo; video_settings?: SettingAck; service_status?: ServiceStatus; privacy_status?: PrivacyStatus; }
interface PresenceRequest { type: string; devices: string[]; subscribe: boolean; }
interface SignalType { type: string; }
@@ -80,6 +80,7 @@ export class MobileState {
privacyEnabled: boolean = false;
privacyActive: boolean = false;
privacyCanToggle: boolean = false;
privacyVisible: boolean = true;
networkTime: number = 0;
unread: number = 0;
announcements: AnnouncementItem[] = [];
@@ -361,6 +362,7 @@ export class MobileController {
const now = this.now(); this.privacyScreen.tick(now);
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.privacyEnabled = this.privacyScreen.enabled(now);
this.state.privacyActive = this.privacyScreen.active(); this.state.privacyCanToggle = this.privacyScreen.canToggle(now);
}
@@ -463,7 +465,11 @@ export class MobileController {
}
private data(message: ControlData): void {
if (message.type === 10 && message.privacy_status) { this.privacyScreen.receive(message.privacy_status, this.now()); this.syncPrivacy(); return; }
if (message.type === 5) { this.serviceAvailable = message.service_status?.available === true; return; }
if (message.type === 5) {
this.serviceAvailable = message.service_status?.available === true;
this.privacyScreen.setInteractiveStage(message.service_status?.interactive_stage ?? '');
this.syncPrivacy(); return;
}
if (message.type === 12 && message.video_settings) {
const v = message.video_settings;
this.video.receive({ quality: v.quality, frameRate: v.frame_rate, preference: v.preference }, v.request_id, v.accepted === true);
@@ -135,7 +135,11 @@ export struct MobileRemoteView {
if (index === 9) { this.state.modal = 'clipboard'; this.menu = ''; }
if (index === 10) this.controller?.togglePrivacy();
}
private labels(): string[] { return ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板', this.state.privacyButtonTitle]; }
private labels(): string[] {
const labels: string[] = ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板'];
if (this.state.privacyVisible) labels.push(this.state.privacyButtonTitle);
return labels;
}
private icons(): string[] { return ['keyboard', 'mouse', 'mouse', 'display', 'sliders', this.state.muted ? 'muted' : 'audio', 'folder', 'chart', 'lock', 'clipboard', 'lock']; }
private columns(): number { return this.viewHeight < 400 && this.viewWidth >= 400 ? 4 : 3; }
private menuWidth(): number { return Math.max(1, Math.min(this.viewWidth - 16, this.columns() === 4 ? 380 : Math.max(280, Math.min(340, this.viewWidth - 160)))); }
@@ -5,6 +5,7 @@ export interface PrivacyStatus {
}
export class RemotePrivacyScreen {
desktopAvailable: boolean = true;
snapshot?: PrivacyStatus;
pending: boolean = false;
requestId: number = 0;
@@ -13,6 +14,10 @@ export class RemotePrivacyScreen {
private requestedAt: number = 0;
private failure: string = '';
setInteractiveStage(stage: string): void {
this.desktopAvailable = stage !== 'lock-screen' && stage !== 'credential-ui' && stage !== 'secure-desktop';
}
receive(s: PrivacyStatus, now: number): void {
if (!Number.isInteger(s.state) || s.state < 0 || s.state > 5 ||
!Number.isInteger(s.revision) || s.revision < 0 || s.revision > 0xffffffff ||
@@ -40,6 +45,7 @@ export class RemotePrivacyScreen {
return '隐私屏';
}
feedback(now: number): string {
if (!this.desktopAvailable) return '';
const s = this.snapshot;
if (!this.failure && (this.pending || this.fresh(now) && (s?.state === 2 || s?.state === 4))) return '';
if (!this.failure && !this.pending && this.fresh(now) && s?.supported &&
@@ -49,7 +55,7 @@ export class RemotePrivacyScreen {
}
canToggle(now: number): boolean {
const s = this.snapshot;
return s !== undefined && !this.pending && this.fresh(now) && s.state !== 4 && (this.active() || s.supported && s.state === 1);
return this.desktopAvailable && s !== undefined && !this.pending && this.fresh(now) && s.state !== 4 && (this.active() || s.supported && s.state === 1);
}
begin(now: number): number {
this.tick(now);
@@ -120,7 +120,8 @@ typedef struct {
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveClipboardText:(NSString *)text;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didReceiveServiceAvailable:(BOOL)available;
didReceiveServiceAvailable:(BOOL)available
interactiveStage:(NSString *)stage;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
didSendSecureAttention:(BOOL)success;
- (void)rtcBridge:(CrossDeskRTCBridge *)bridge
@@ -1736,11 +1736,12 @@ Params MakeParams(const RTCState &state, const std::string &user_id,
}
if (action.type == ControlType::service_status) {
const BOOL available = action.ss.available;
NSString *stage = [NSString stringWithUTF8String:action.ss.interactive_stage];
DispatchMain(^{
if (![self isControllerGenerationActive:generation]) return;
id<CrossDeskRTCBridgeDelegate> delegate = self.delegate;
if ([delegate respondsToSelector:@selector(rtcBridge:didReceiveServiceAvailable:)]) {
[delegate rtcBridge:self didReceiveServiceAvailable:available];
if ([delegate respondsToSelector:@selector(rtcBridge:didReceiveServiceAvailable:interactiveStage:)]) {
[delegate rtcBridge:self didReceiveServiceAvailable:available interactiveStage:stage ?: @""];
}
});
return;
@@ -12,6 +12,7 @@ struct RemotePrivacyStatus: Decodable {
}
struct RemotePrivacyScreen {
private(set) var desktopAvailable = true
private(set) var snapshot: RemotePrivacyStatus?
private(set) var pending = false
private(set) var requestID: UInt32 = 0
@@ -20,6 +21,10 @@ struct RemotePrivacyScreen {
private var requestedAt: TimeInterval = 0
private var failure = ""
mutating func setInteractiveStage(_ stage: String) {
desktopAvailable = !["lock-screen", "credential-ui", "secure-desktop"].contains(stage)
}
var active: Bool {
guard let s = snapshot else { return false }
return s.overlayActive || s.remotePaused || [2, 3, 4].contains(s.state)
@@ -37,6 +42,7 @@ struct RemotePrivacyScreen {
return "隐私屏"
}
func feedback(at now: TimeInterval) -> String {
if !desktopAvailable { return "" }
if failure.isEmpty && (pending || fresh(at: now) && [2, 4].contains(snapshot?.state ?? -1)) {
return ""
}
@@ -47,7 +53,7 @@ struct RemotePrivacyScreen {
}
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)
return desktopAvailable && !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 }
@@ -1439,9 +1439,11 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
clipboardStatus = "已接收远端剪贴板"
}
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceiveServiceAvailable available: Bool) {
func rtcBridge(_ bridge: CrossDeskRTCBridge, didReceiveServiceAvailable available: Bool,
interactiveStage stage: String) {
guard hasNetworkConsent, isConnecting || isConnected else { return }
remoteServiceAvailable = available
privacyScreen.setInteractiveStage(stage)
}
func rtcBridge(_ bridge: CrossDeskRTCBridge, didSendSecureAttention success: Bool) {
@@ -1250,17 +1250,19 @@ private struct FloatingSessionMenu: View {
action: session.sendSecureAttention)
FloatingControlButton(title: "剪贴板", symbol: "doc.on.clipboard",
action: session.sendClipboard)
FloatingControlButton(
title: session.privacyScreen.buttonTitle(at: session.privacyClock),
symbol: "eye.slash",
selected: session.privacyScreen.enabled(at: session.privacyClock),
action: session.togglePrivacyScreen
)
.disabled(!session.isConnected || !session.privacyScreen.canToggle(at: session.privacyClock))
.opacity(session.isConnected && session.privacyScreen.canToggle(at: session.privacyClock) ? 1 : 0.45)
.accessibilityLabel("隐私屏")
.accessibilityValue(session.privacyScreen.status(at: session.privacyClock))
.accessibilityHint(session.privacyScreen.active ? "关闭隐私屏" : "开启隐私屏")
if session.privacyScreen.desktopAvailable {
FloatingControlButton(
title: session.privacyScreen.buttonTitle(at: session.privacyClock),
symbol: "eye.slash",
selected: session.privacyScreen.enabled(at: session.privacyClock),
action: session.togglePrivacyScreen
)
.disabled(!session.isConnected || !session.privacyScreen.canToggle(at: session.privacyClock))
.opacity(session.isConnected && session.privacyScreen.canToggle(at: session.privacyClock) ? 1 : 0.45)
.accessibilityLabel("隐私屏")
.accessibilityValue(session.privacyScreen.status(at: session.privacyClock))
.accessibilityHint(session.privacyScreen.active ? "关闭隐私屏" : "开启隐私屏")
}
}
let privacyFeedback = session.privacyScreen.feedback(at: session.privacyClock)