[fix] show shortcut controls only for Windows hosts

This commit is contained in:
dijunkun
2026-10-27 04:01:39 +08:00
parent a9e3178042
commit 1687816bde
9 changed files with 59 additions and 42 deletions
@@ -338,7 +338,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
public void mute(){muted=!muted;if(session!=null)session.control(2,muted?0:1);controls.setMuted(muted);}
public void mouse(){boolean relative=!preferences.getBoolean("relativeMouse",true);preferences.edit().putBoolean("relativeMouse",relative).apply();video.setRelative(relative);controls.setRelative(relative);}
public void videoSettings(int field,int value){RemoteVideoSettings.Values old=MainActivity.this.videoSettings.selection;updateVideoSettings(new RemoteVideoSettings.Values(field==0?value:old.quality,field==1?value:old.frameRate,field==2?value:old.preference));}
});controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
});controls.setHostPlatform(hostPlatform);controls.setRelative(preferences.getBoolean("relativeMouse",true));controls.setMuted(muted);controls.videoSettings(videoSettings);root.addView(controls,new LinearLayout.LayoutParams(-1,-1));getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
controls.attachMouseBar(canvas,new VirtualMouseBar(ui,video.mouseInput));
privacyChanged();
video.remoteFocusChanged=()->{if(controls!=null)controls.resetKeyboardContext();};
@@ -496,8 +496,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast("等待远端屏幕信息");return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?"显示器":display.optString("name","显示器"));}new AlertDialog.Builder(this).setTitle("显示器").setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.resetVideoStatistics();}}).show();
}
private void secureAttention(){
if(!ready||session==null)return;
if(!remoteServiceAvailable){controlFeedback(hostPlatform.equals("windows")?"请在远端启用 CrossDesk 系统服务后重试":"Ctrl+Alt+Del 需要远端 Windows 系统服务支持");return;}
if(!ready||session==null||!hostPlatform.equals("windows"))return;
if(!remoteServiceAvailable){controlFeedback("请在远端启用 CrossDesk 系统服务后重试");return;}
session.secureAttention();
}
private void chooseFile(){
@@ -650,6 +650,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
JSONObject info=message.optJSONObject("host_info");
if(info!=null){
hostPlatform=info.optString("platform","");JSONArray list=info.optJSONArray("displays");if(list!=null)displays=list;history.metadata(serverKey(),connectedRemote,info.optString("host_name",connectedRemote),hostPlatform);
if(controls!=null)controls.setHostPlatform(hostPlatform);
boolean wasSupported=videoSettings.supported;videoSettings.setSupported(info.optBoolean("supports_video_settings",false));
if(!videoSettings.supported)cancelVideoSettingsTimeout();
if(controls!=null)controls.videoSettings(videoSettings);
@@ -739,7 +740,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
videoSettings=retained.videoSettings;ready=true;retained.attach(this);
if(rebuild)sessionScreen(retained.autoLandscape);
if(retained.width>0&&retained.height>0)videoSize(retained.width,retained.height);
if(controls!=null){controls.setMuted(muted);controls.videoSettings(videoSettings);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
if(controls!=null){controls.setHostPlatform(hostPlatform);controls.setMuted(muted);controls.videoSettings(videoSettings);controls.fileStatus(retained.fileStatus,receivedFile!=null);}
retained.setBackground(false);signaling(signalState,localIdentity);
return true;
}
@@ -51,7 +51,7 @@ final class SessionControls extends FrameLayout {
private LinearLayout[] trafficRows;
private TextView[] networkDetails;
private View encryptionLock,encryptionOpen;
private boolean muted,relative=true,orbPositioned;
private boolean muted,relative=true,orbPositioned,secureAttentionVisible;
SessionControls(Context context,MobileUi ui,Actions actions){
super(context);this.ui=ui;this.actions=actions;
keyboard=new RemoteKeyboard(context,ui,actions,()->{requestLayout();positionMouse();});
@@ -66,6 +66,12 @@ final class SessionControls extends FrameLayout {
drag(orb,orb,()->{if(menu==null)openMenu("controls");else closeMenu();});
}
void setRelative(boolean value){relative=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
void setHostPlatform(String platform){
boolean visible="windows".equals(platform);
if(secureAttentionVisible==visible)return;
secureAttentionVisible=visible;
if(menu!=null&&menuPage.equals("controls"))openMenu("controls");
}
void attachMouseBar(FrameLayout host, VirtualMouseBar bar) {
mouseHost=host;mouseBar=bar;
// A direct, tightly bounded sibling of the video is essential: otherwise
@@ -130,8 +136,13 @@ 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();
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());
java.util.List<Integer> visibleControls=new java.util.ArrayList<>();
for(int index=0;index<labels.length;index++){
if(index==8&&!secureAttentionVisible||index==10&&!privacy.desktopAvailable)continue;
visibleControls.add(index);
}
int controlCount=visibleControls.size();
for(int row=0;row<(controlCount+menuColumns-1)/menuColumns;row++){LinearLayout line=ui.row();for(int col=0;col<menuColumns;col++){int position=row*menuColumns+col;if(position>=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;}int index=visibleControls.get(position);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<controlCount)ui.gap(body,7);}
@@ -2144,7 +2144,7 @@ void GuiApplication::BindStreamCallbacks() {
if (name != "Ctrl+Alt+Del" && name != "Win+L") return;
{
std::lock_guard lock(props->remote_version_mutex_);
if (name == "Ctrl+Alt+Del" && props->remote_platform_ == HostPlatform::HarmonyOS) return;
if (props->remote_platform_ != HostPlatform::Windows) return;
}
RemoteAction action{};
action.type = ControlType::service_command;
@@ -3796,9 +3796,7 @@ void GuiApplication::SyncStreamWindow() {
}
}
(*ui_->stream)->set_remote_update_visible(remote_update_available);
// All desktop clients use the same menu for a Harmony host. Windows SAS is
// not an ordinary key chord and has no public Harmony implementation.
(*ui_->stream)->set_secure_attention_supported(remote_platform != HostPlatform::HarmonyOS);
(*ui_->stream)->set_secure_attention_supported(remote_platform == HostPlatform::Windows);
if (remote_update_available) {
SetStreamKeyboardFocus(false);
}
+20 -18
View File
@@ -335,10 +335,8 @@ export component StreamWindow inherits Window {
callback reorder-tab(int, length, length);
callback close-tab(string);
callback switch-display(int);
in property <bool> secure-attention-supported: true;
private property <[string]> secure-shortcut-items: ["Ctrl+Alt+Del", "Win+L"];
private property <[string]> ordinary-shortcut-items: ["Win+L"];
private property <[string]> shortcut-items: root.secure-attention-supported ? root.secure-shortcut-items : root.ordinary-shortcut-items;
in property <bool> secure-attention-supported: false;
private property <[string]> shortcut-items: ["Ctrl+Alt+Del", "Win+L"];
private property <length> shortcut-menu-height: root.shortcut-items.length * 30px + 16px;
callback send-shortcut(string);
callback toggle-mouse-control;
@@ -364,6 +362,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-shortcut-width: root.secure-attention-supported ? root.control-button-step : 0px;
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;
@@ -416,6 +415,9 @@ export component StreamWindow inherits Window {
root.close-control-menus();
root.cancel-remote-pointer();
}
changed secure-attention-supported => {
if !root.secure-attention-supported { root.shortcut-menu-open = false; }
}
changed control-expanded => {
control-reveal-timer.running = false;
control-collapse-timer.running = false;
@@ -517,7 +519,7 @@ export component StreamWindow inherits Window {
if logo-button.has-hover { return logo-button.tooltip-info; }
if root.control-expanded && root.control-content-visible {
if display-button.has-hover { return display-button.tooltip-info; }
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if root.secure-attention-supported && shortcut-button.has-hover { return shortcut-button.tooltip-info; }
if mouse-button.has-hover { return mouse-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; }
@@ -715,7 +717,7 @@ export component StreamWindow inherits Window {
&& pointer-y >= root.display-menu-y
&& pointer-y <= root.display-menu-y + root.display-menu-height;
let shortcut-menu-x = control.x + shortcut-button.x;
let over-shortcut-menu = root.shortcut-menu-open
let over-shortcut-menu = root.secure-attention-supported && root.shortcut-menu-open
&& pointer-x >= shortcut-menu-x
&& pointer-x <= shortcut-menu-x + 180px
&& pointer-y >= root.shortcut-menu-y
@@ -1085,7 +1087,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 + 8 * root.control-button-step + root.control-privacy-width + root.control-disconnect-gap, parent.width) : root.control-toolbar-height;
width: root.control-visual-expanded ? min(80px + 7 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width + root.control-disconnect-gap, parent.width) : root.control-toolbar-height;
height: root.control-toolbar-height;
z: 10;
@@ -1130,8 +1132,8 @@ export component StreamWindow inherits Window {
}
shortcut-button := ControlBarButton {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible;
action-enabled: root.control-expanded;
visible: root.control-content-visible && root.secure-attention-supported;
action-enabled: root.control-expanded && root.secure-attention-supported;
x: root.control-buttons-x + root.control-button-step;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
@@ -1149,7 +1151,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 + 2 * root.control-button-step;
x: root.control-buttons-x + root.control-button-step + root.control-shortcut-width;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
@@ -1159,7 +1161,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 + 3 * root.control-button-step + root.control-privacy-width;
x: root.control-buttons-x + 2 * root.control-button-step + root.control-shortcut-width + 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;
@@ -1169,7 +1171,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 + root.control-privacy-width;
x: root.control-buttons-x + 3 * root.control-button-step + root.control-shortcut-width + 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(); }
@@ -1178,7 +1180,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 + root.control-privacy-width;
x: root.control-buttons-x + 4 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width;
y: (parent.height - self.height) / 2;
icon: "\u{f1de}"; selected: root.video-menu-open;
tooltip: StreamStrings.video-settings;
@@ -1193,7 +1195,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 + root.control-privacy-width;
x: root.control-buttons-x + 5 * root.control-button-step + root.control-shortcut-width + 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;
@@ -1203,7 +1205,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 + root.control-privacy-width;
x: root.control-buttons-x + 6 * root.control-button-step + root.control-shortcut-width + 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;
@@ -1219,7 +1221,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 + root.control-privacy-width + root.control-disconnect-gap;
x: root.control-buttons-x + 7 * root.control-button-step + root.control-shortcut-width + root.control-privacy-width + root.control-disconnect-gap;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
danger: true;
@@ -1255,7 +1257,7 @@ export component StreamWindow inherits Window {
privacy-button := ControlBarButton {
reveal-progress: root.control-content-progress;
visible: root.control-content-visible && root.privacy-visible;
x: root.control-buttons-x + 3 * root.control-button-step;
x: root.control-buttons-x + 2 * root.control-button-step + root.control-shortcut-width;
y: (parent.height - self.height) / 2;
icon: FontAwesomeIcons.user-secret;
slashed: root.privacy-off && !root.privacy-unsupported;
@@ -1698,7 +1700,7 @@ export component StreamWindow inherits Window {
}
}
if root.shortcut-menu-open: StreamControlSurface {
if root.secure-attention-supported && root.shortcut-menu-open: StreamControlSurface {
x: control.x + shortcut-button.x; y: root.shortcut-menu-y;
width: 180px; height: root.shortcut-menu-height; z: 11;
TouchArea { mouse-cursor: default; scroll-event(event) => { accept } }
+1 -1
View File
@@ -86,7 +86,7 @@ python3 apps/harmony/scripts/build_slint_desktop.py --desktop --reuse-runtime \
`--desktop-permissions` 生成权限声明、用途说明和授权流程开关;`--signing-config` 生成签名配置并选择 `entry-default-signed.hap` 安装。生成的配置仅位于忽略的 `.native` 缓存,文件权限为 `0600`。无签名参数时仍输出普通 `entry-default-unsigned.hap`。可单独用 `--desktop-permissions` 做带权限声明的编译检查,不能据此认为已获得权限或可安装到真机。
远程 `lock_workstation` 命令已接到经过采集/输入授权检查的 Meta+L 路径,先释放残留键鼠,发送后配对释放,失败保留待清理状态。Windows `send_sas` 没有等价的鸿蒙公开接口,明确返回不支持;所有 PC 客户端共用的 Slint 快捷键菜单在目标为鸿蒙时不显示 Ctrl+Alt+Del,仍保留 Win+L。普通 Ctrl/Alt/Meta 组合键由原生钩子按原顺序转发,系统最终允许拦截哪些保留键仍需授权后的真机验证。
远程 `lock_workstation` 命令已接到经过采集/输入授权检查的 Meta+L 路径,先释放残留键鼠,发送后配对释放,失败保留待清理状态。Windows `send_sas` 没有等价的鸿蒙公开接口,明确返回不支持;PC 和移动端仅在被控端为 Windows 时显示系统组合键按钮,目标为鸿蒙或其他系统时隐藏。普通 Ctrl/Alt/Meta 组合键由原生钩子按原顺序转发,系统最终允许拦截哪些保留键仍需授权后的真机验证。
本轮验证:普通 HAP 与带权限声明的 HAP 构建通过;普通 HAP 安装本机 2in1 模拟器后,全局钩子实际返回 `201`,无授权弹窗循环,仍收到共享窗口回退发送的 `keyboard` 数据流。真实 Ctrl+A/C、End、Ctrl+V 把 `246` 复制为 `246 246`。生产 C++/ArkTS 的模拟 SDK 回归覆盖 Alt+Tab、Meta+L、重复键、小键盘 Enter、取消、失焦、撤权、重复注册、卸载失败、后台通知、缓存失效、读取中变更/撤权,以及权限拒绝后不重复引导。macOS 公共 Slint 冒烟测试新增快捷键菜单的真实点击回归,验证移除 SAS 后首项为锁屏、弹层命中范围随菜单缩小、切回 Windows 菜单仍正常。成功授权的钩子和后台跨应用读取没有在无 ACL 包中冒充验证通过。
@@ -311,7 +311,8 @@ export class MobileController {
catch { this.toast('复制失败,请重试'); }
}
sas(): void {
if (!this.serviceAvailable) { this.toast(this.state.platform === 'windows' ? '请在远端启用 CrossDesk 系统服务后重试' : 'Ctrl+Alt+Del 需要远端 Windows 系统服务支持'); return; }
if (this.state.platform !== 'windows') return;
if (!this.serviceAvailable) { this.toast('请在远端启用 CrossDesk 系统服务后重试'); return; }
this.send({ op: 'sas' });
}
async chooseFile(): Promise<void> {
@@ -136,9 +136,11 @@ export struct MobileRemoteView {
if (index === 10) this.controller?.togglePrivacy();
}
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;
return ['键盘', this.state.relative ? '相对鼠标' : '绝对鼠标', this.mouseVisible ? '收起鼠标' : '虚拟鼠标', '显示器', '画面设置', this.state.muted ? '静音' : '声音', '发送文件', '网络状态', 'Ctrl+Alt+Del', '剪贴板', this.state.privacyButtonTitle];
}
private controlIds(): number[] {
return [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].filter((index: number) =>
(index !== 8 || this.state.platform === 'windows') && (index !== 10 || this.state.privacyVisible));
}
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; }
@@ -215,20 +217,20 @@ export struct MobileRemoteView {
Column({ space: 8 }) {
if (this.menu === 'controls') {
GridRow({ columns: this.columns(), gutter: 7 }) {
ForEach(this.labels(), (label: string, index: number) => {
ForEach(this.controlIds(), (index: number) => {
GridCol() {
Column({ space: 4 }) {
MobileIcon({ name: this.icons()[index], iconExtent: 19 })
Text(label).fontSize(10).fontColor(MobileTheme.ink).maxLines(1)
Text(this.labels()[index]).fontSize(10).fontColor(MobileTheme.ink).maxLines(1)
}
.height(52).width('100%').justifyContent(FlexAlign.Center)
.backgroundColor('#F0F0F2').borderRadius(10)
.enabled(index !== 10 || this.state.ready && this.state.privacyCanToggle)
.opacity(index === 10 && (!this.state.ready || !this.state.privacyCanToggle) ? 0.45 : 1)
.accessibilitySelected(index === 10 && this.state.privacyEnabled)
.accessibilityText(index === 10 ? `${label},${this.state.privacyStatus}` : label).accessibilityLevel('yes').onClick(() => this.action(index))
.accessibilityText(index === 10 ? `${this.labels()[index]},${this.state.privacyStatus}` : this.labels()[index]).accessibilityLevel('yes').onClick(() => this.action(index))
}
}, (label: string) => label)
}, (index: number) => index.toString())
}
if (this.state.privacyFeedback) { Text(this.state.privacyFeedback).fontSize(11).fontColor(MobileTheme.secondary).width('100%') }
if (this.state.transfer) { Text(this.state.transfer).fontSize(11).fontColor(MobileTheme.secondary).width('100%') }
@@ -1125,7 +1125,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
}
func sendSecureAttention() {
guard hasNetworkConsent, isConnected else { return }
guard hasNetworkConsent, isConnected, remotePlatform == .windows else { return }
guard remoteServiceAvailable else {
controlStatus = "Ctrl+Alt+Del 需要远端启用 Windows 系统服务"
return
@@ -1245,9 +1245,11 @@ private struct FloatingSessionMenu: View {
FloatingControlButton(title: "网络状态", symbol: "chart.bar.xaxis") {
page = .networkStats
}
FloatingControlButton(title: "Ctrl+Alt+Del",
symbol: "lock.trianglebadge.exclamationmark",
action: session.sendSecureAttention)
if session.remotePlatform == .windows {
FloatingControlButton(title: "Ctrl+Alt+Del",
symbol: "lock.trianglebadge.exclamationmark",
action: session.sendSecureAttention)
}
FloatingControlButton(title: "剪贴板", symbol: "doc.on.clipboard",
action: session.sendClipboard)
if session.privacyScreen.desktopAvailable {