mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-27 16:13:13 +08:00
[fix] hide privacy controls on locked desktops across clients
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user