mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-13 20:25:53 +08:00
Compare commits
12
Commits
cli-v0.2.0
..
mcp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d393cd847e | ||
|
|
bad4569117 | ||
|
|
72dda9bb90 | ||
|
|
dec9e445c4 | ||
|
|
8a1e58d62d | ||
|
|
94a5f57bea | ||
|
|
e6e72ed511 | ||
|
|
7d8daa27eb | ||
|
|
9d61aacffe | ||
|
|
b3be25ad80 | ||
|
|
24f91c608a | ||
|
|
ac8eff5099 |
No files matched your search
+13
-1
@@ -2,6 +2,12 @@
|
||||
|
||||
原生 Android 控制端,使用 Java 界面、JNI 和 MiniRTC,与桌面端使用相同的信令、ICE、SRTP 和输入协议。没有 WebView。支持 Android 8.0(API 26)及以上的 arm64 手机和平板。桌面、Android、iOS 和 HarmonyOS 共用客户端发布版本,各移动平台构建号独立;可在仓库根目录执行 `python3 scripts/version.py show client` 查看,使用 `set client <版本>` 同步修改,详见[组件版本管理](../../docs/VERSIONING.md)。
|
||||
|
||||
## 可信设备鉴权
|
||||
|
||||
“设置 → 本机配对信息 → 查看指纹”可查看本机指纹并复制配对信息。将信息粘贴到被控电脑的“管理可信设备 → 添加可信设备”,核对指纹后添加,即可连接开启“仅允许可信设备控制”的电脑;连接密码仍需验证。
|
||||
|
||||
移动端使用共享 Ed25519 挑战响应协议。私钥通过 Android Keystore / AES-GCM 加密后保存在应用私有持久存储,不写入缓存、日志或配对信息。重启、清理缓存、更换服务器和本机登录 ID 不改变公钥;已有密钥损坏、无法读取或丢失时停止初始化并提示错误,不自动换钥。清除应用数据或卸载后重新安装,需要重新配对。
|
||||
|
||||
## 当前功能
|
||||
|
||||
- 官方或自托管 WSS 信令服务器;授权后进入前台即自动注册或登录本机身份,首页实时显示连接、重连及失败状态。发起桌面连接时复用该身份;切换服务器会重新登录,撤回授权会关闭连接。已有远程会话优先通过系统画中画保持连接,画中画不可用时由带连接通知的前台服务兜底;首页空闲信令在后台关闭。
|
||||
@@ -153,7 +159,7 @@ gh secret set ANDROID_KEY_PASSWORD --repo kunkundi/crossdesk
|
||||
|
||||
身份按主机名和端口分开保存,备份与设备迁移不包含应用数据。密码不写入连接记录,仅在明确开启“保存密码”后写入独立加密存储;未开启时仅用于当前会话。连接成功通知只处理一次,重复的传输就绪回调不会再次保存密码或重建会话页面;密码存储拒绝空值覆盖。旧版本已被空值覆盖的密码无法恢复,遇到此记录时会提示重新输入一次,并保留“保存密码”开启状态。由于 MiniRTC 的诊断日志包含登录身份,Android 桥接层关闭其会话日志。身份存储损坏会显示错误,不会静默重新注册。
|
||||
|
||||
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的 Ctrl / Win / Alt / Shift 可锁定组合键;macOS 的 Command 对应 Win。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
|
||||
会话中的“剪贴板”发送用户输入的文本;收到的远端文本先留在会话内,点击“复制远端文本”才写入手机剪贴板。退出会话后清除会话内容。每次发送最多 128 KB。电脑键盘的修饰键在本地选中,仅应用于下一个按键,发送后自动复位;名称根据远端显示为 Win / Super / Cmd 和 Alt / Option。系统键盘通过 `supports_text_input` 检测远端能力,支持 UTF-8 的 Windows 和 macOS 被控端可直接输入中文、表情和听写文本;其他端或旧版使用 ASCII 按键回退,不支持的文本会提示使用剪贴板,不会先删除远端内容。输入法尚未确认的组字不会发送,自动纠错仅替换已发送文本的变化后缀;点击远端、收起、切换键盘或进入后台会清除本地输入上下文。
|
||||
|
||||
Windows 文本输入使用 Unicode 键盘事件,无需切换电脑输入法或覆盖剪贴板。本次开发版将完整 UTF-8 文本通过 Windows 服务转发到锁屏、登录与 UAC 安全桌面,保留数字、大小写字母、符号、空格及中文等字符,不将密码转换成数字或 ASCII 按键。该通道需要同时更新 Windows 客户端、CrossDesk Service 和会话辅助程序;仅更新 Android,或仅确认旧服务正在运行,均不会启用修复。旧版可能已声明 `supports_text_input`,但只支持普通桌面文本输入,因此“电脑键盘可输入、搜狗等系统输入法不可输入”仍可能出现。锁屏实际输入效果需在更新后的 Windows 环境验证。
|
||||
|
||||
@@ -250,3 +256,9 @@ AV1 硬件路径尚无对应实机验证;Retroid 上使用 dav1d 软件解码
|
||||
设置 → 关于使用与 iOS 相同的分组及入口:版本、软件许可、开源组件、源码与构建说明。软件许可中可分别阅读 GNU GPL 第 3 版及开源软件权利说明;开源组件页单独展示 OpenFEC 告知,其余组件列出名称和许可证,详情包含版本、源码、适用的构建配方和每一份许可文档。内容按 Android 构建生成,不显示未使用的 iOS 依赖,Android 签名说明替换 Apple 签名说明。
|
||||
|
||||
APK 保留完整 `THIRD_PARTY_NOTICES.txt`,并生成轻量的 `ThirdPartyLicenses.json` 索引、72 份独立许可文档及 `SourceMetadata.json`。打开文档时才读取正文,长文按有限长度分块显示,支持文本选择、复制与网页链接。构建继续生成并打包许可资料,CI 和 APK 验证不再检查许可证内容、哈希或组件清单。源码页面显示标签或提交版本及未发布修改提示,源码链接指向基础提交。当前 Android 构建说明、构建脚本、MiniRTC、OpenSSL 及 SVT-AV1 配方随 APK 离线提供,避免开发版链接指向基础提交中尚不存在的 Android 文件。
|
||||
|
||||
## 快捷键与自定义
|
||||
|
||||
系统键盘上方的 44 dp 工具栏提供常用快捷键横向滚动栏(默认 Esc、Tab、复制、粘贴)和“更多”入口。更多面板按“常用 / 按键 / 系统 / 我的”分组,替换原键盘区域并沿用输入法高度。预设根据 Windows、macOS、Linux 远端适配;Linux 系统预设需手动选择 GNOME 或 KDE Plasma,未知平台只提供通用按键。Windows 安全选项沿用 Ctrl+Alt+Del 服务通道。
|
||||
|
||||
“管理与自定义”使用独立本地窗口,编辑内容不发送远端。最多保存 16 个自定义组合,每个远端平台最多固定 8 个,支持排序、取消固定、编辑与删除。自定义组合保留用户选择的实际修饰键。组合键整体排队,按反序释放修饰键;快捷键执行前先发送已确认的文本并重置纠错上下文,尚在组词时提示先选词或取消。
|
||||
@@ -314,6 +314,68 @@ final class LocalizationKeys {
|
||||
IDS.put("被控端分辨率", R.string.text_59c9dca5568a6cd3);
|
||||
IDS.put("设备不支持", R.string.text_36e0073276c0e39f);
|
||||
IDS.put("等待电脑端确认…", R.string.text_7b3440ad0f81b811);
|
||||
IDS.put("将配对信息添加到被控端的可信设备列表,并核对指纹。", R.string.text_728ea431051d269e);
|
||||
IDS.put("本机配对信息", R.string.text_56a998ff5c730e20);
|
||||
IDS.put("查看指纹", R.string.text_976f1504103f24ac);
|
||||
IDS.put("复制配对信息", R.string.text_3aab57c59aba6543);
|
||||
IDS.put("配对信息已复制。", R.string.text_cbacf1eafbca53e0);
|
||||
IDS.put("无法复制配对信息,请重试。", R.string.text_cde42ccdeacbbbd9);
|
||||
IDS.put("安全存储不可用,请解锁设备后重试", R.string.text_7383b2ae09252c59);
|
||||
IDS.put("上一页", R.string.text_c9b9ae7a61444ab7);
|
||||
IDS.put("下一页", R.string.text_8a8542f6964852dc);
|
||||
IDS.put("主按键", R.string.text_0c4f690eb398c12e);
|
||||
IDS.put("任务管理器", R.string.text_62fdb5304cf5cd9b);
|
||||
IDS.put("保存", R.string.text_a3030bf8f16dc63c);
|
||||
IDS.put("修饰键", R.string.text_ba0e2ff8201bc6a6);
|
||||
IDS.put("全选", R.string.text_3a5040b68abf75f9);
|
||||
IDS.put("切换应用", R.string.text_c2d918002afcc3d4);
|
||||
IDS.put("切换窗口", R.string.text_a7fe8e2685849c31);
|
||||
IDS.put("剪切", R.string.text_410a8e8a6bf253ac);
|
||||
IDS.put("发送组合", R.string.text_6309cd49db59d165);
|
||||
IDS.put("名称", R.string.text_d44e9b3d3b31d37b);
|
||||
IDS.put("向上移动", R.string.text_6789dd11afc7c4c5);
|
||||
IDS.put("固定到键盘上方", R.string.text_352c003784b1c89e);
|
||||
IDS.put("复制", R.string.text_63d90d977348ab1f);
|
||||
IDS.put("安全选项", R.string.text_ecb0d13f3f8bfd0d);
|
||||
IDS.put("已有相同组合的快捷键", R.string.text_dd1b1da65a6c89de);
|
||||
IDS.put("常用", R.string.text_4563c57b5143deaa);
|
||||
IDS.put("应用列表", R.string.text_6308bf1373bd0ee8);
|
||||
IDS.put("强制退出面板", R.string.text_8dc0823d700cf875);
|
||||
IDS.put("快捷键", R.string.text_ee2638183d3ea860);
|
||||
IDS.put("我的", R.string.text_7f1d9dd04cd1c7b8);
|
||||
IDS.put("按键", R.string.text_7c7577d4d4fc6009);
|
||||
IDS.put("撤销", R.string.text_926a50b98ece2667);
|
||||
IDS.put("新建", R.string.text_50ef2f4cf6a46924);
|
||||
IDS.put("新建快捷键", R.string.text_c6904bb36f7f62c5);
|
||||
IDS.put("显示桌面", R.string.text_5cfe4846aa8a7228);
|
||||
IDS.put("更多快捷键", R.string.text_feb12adf8e0315e2);
|
||||
IDS.put("最多保存 16 个自定义快捷键", R.string.text_5d6bce93c0649a63);
|
||||
IDS.put("最多固定 8 个快捷键", R.string.text_f33cb21422f62ee8);
|
||||
IDS.put("未知", R.string.text_4d8c1c5b42830791);
|
||||
IDS.put("本地编辑,输入不会发送到远端", R.string.text_6b1fa1d7159de777);
|
||||
IDS.put("添加到常用栏", R.string.text_cb9eaa63c25d383e);
|
||||
IDS.put("移除", R.string.text_6135d4159e892541);
|
||||
IDS.put("窗口菜单", R.string.text_e693ff5832c7f46d);
|
||||
IDS.put("等待远端平台信息", R.string.text_096a247cc5455ba7);
|
||||
IDS.put("管理与自定义", R.string.text_39c7740197c41d44);
|
||||
IDS.put("管理快捷键", R.string.text_2dc8c27b75796b36);
|
||||
IDS.put("粘贴", R.string.text_33517926747180e6);
|
||||
IDS.put("系统", R.string.text_5b50d7c4b5950dc5);
|
||||
IDS.put("终端复制", R.string.text_45123fa07e22fb3e);
|
||||
IDS.put("终端粘贴", R.string.text_3936f3d30a763a35);
|
||||
IDS.put("编辑", R.string.text_051836569928a9f9);
|
||||
IDS.put("编辑快捷键", R.string.text_0ecda10acc934f22);
|
||||
IDS.put("聚焦搜索", R.string.text_d32d2a4e0e83d7cf);
|
||||
IDS.put("请使用系统预设中的安全选项", R.string.text_add54b3ce4b1c661);
|
||||
IDS.put("请先选词或取消组词", R.string.text_ae066f34095261c9);
|
||||
IDS.put("请输入有效的快捷键名称", R.string.text_a895bdb6f1f11e99);
|
||||
IDS.put("请选择有效的按键", R.string.text_6b3213d2afed99e8);
|
||||
IDS.put("运行窗口", R.string.text_819569ec77acb01e);
|
||||
IDS.put("还没有自定义快捷键", R.string.text_22c4107f03c3533e);
|
||||
IDS.put("适用远端", R.string.text_b07f98d4671af3f9);
|
||||
IDS.put("选择桌面方案", R.string.text_4bdabfba3368869d);
|
||||
IDS.put("选择桌面方案以加载系统预设", R.string.text_4fa28857c6f253cc);
|
||||
IDS.put("键盘上方常用栏", R.string.text_fc5f6f7462e8557a);
|
||||
}
|
||||
private LocalizationKeys() {}
|
||||
}
|
||||
@@ -348,7 +348,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
public void togglePrivacy(){if(ready&&session!=null)session.togglePrivacy();}
|
||||
public void key(int code){tapKey(code);}
|
||||
public boolean textEdit(int count,String value){return sendTextEdit(count,value);}
|
||||
public void chord(int code,int[] modifiers){if(session==null)return;for(int modifier:modifiers)session.key(modifier,true);tapKey(code);for(int i=modifiers.length-1;i>=0;i--)session.key(modifiers[i],false);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
|
||||
public void chord(int code,int[] modifiers){if(ready&&session!=null)session.chord(code,modifiers);}public void shortcut(int code){MainActivity.this.shortcut(code);}public void text(String value){sendText(value);}
|
||||
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 displayResolution(RemoteDisplayResolutions.Mode mode){updateDisplayResolution(mode);}
|
||||
@@ -397,6 +397,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
toggle.setOnCheckedChangeListener((button,checked)->{inputs.setVisibility(checked?View.VISIBLE:View.GONE);if(!checked){host="api.crossdesk.cn";port=9099;preferences.edit().putString("host",host).putInt("port",port).apply();ensureSignaling();} });
|
||||
backAction=()->{if(toggle.isChecked()&&!saveServer(address,number))return;hideKeyboard();home("");};
|
||||
identitySummary=text("",12,false);identitySummary.setTextColor(SECONDARY);identitySummary.setGravity(Gravity.CENTER);identitySummary.setPadding(dp(16),dp(12),dp(16),dp(16));identitySummary.setTextIsSelectable(true);content.addView(identitySummary);renderSignaling();
|
||||
LinearLayout trust=section(content,L10n.tr("本机配对信息"),MobileUi.SETTINGS_CARD_RADIUS);
|
||||
link(trust,L10n.tr("查看指纹"),"",this::showPairingInfo);ui.gap(content,28);
|
||||
LinearLayout privacy=section(content,L10n.tr("隐私"),MobileUi.SETTINGS_CARD_RADIUS);link(privacy,L10n.tr("隐私与授权"),preferences.getBoolean("networkConsent",false)?L10n.tr("已授权"):L10n.tr("未授权"),this::privacyPage);ui.divider(privacy);
|
||||
LinearLayout preview=row();preview.setPadding(dp(16),dp(10),dp(16),dp(10));preview.addView(text(L10n.tr("保存远程画面预览"),16,false),new LinearLayout.LayoutParams(0,-2,1));previewSwitch=new Switch(this);previewSwitch.setContentDescription(L10n.tr("保存远程画面预览"));preview.addView(previewSwitch);privacy.addView(preview);
|
||||
previewSwitch.setOnCheckedChangeListener((button,checked)->{if(updatingPreviewSwitch)return;if(checked){renderPreviews();confirmPreviewSaving();}else previews.setEnabled(false);});
|
||||
@@ -406,6 +408,21 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
aboutUpdateLabel=(TextView)aboutLink.getChildAt(1);aboutUpdateLabel.setPadding(0,0,dp(8),0);
|
||||
aboutUpdateDot=updateDot();LinearLayout.LayoutParams dotPosition=new LinearLayout.LayoutParams(dp(8),dp(8));dotPosition.setMarginEnd(dp(8));aboutLink.addView(aboutUpdateDot,2,dotPosition);renderAppUpdates();
|
||||
}
|
||||
private void showPairingInfo(){
|
||||
NativeSession.pairingInfo(this,localIdentity,info->{
|
||||
if(isFinishing()||isDestroyed()||!page.equals("settings"))return;
|
||||
if(info==null){toast(L10n.tr("安全存储不可用,请解锁设备后重试"));return;}
|
||||
TextView fingerprint=text(info.optString("fingerprint").replaceAll("(.{4})(?!$)","$1 "),14,false);
|
||||
fingerprint.setTypeface(android.graphics.Typeface.MONOSPACE);fingerprint.setTextIsSelectable(true);fingerprint.setPadding(dp(24),dp(16),dp(24),dp(16));
|
||||
new AlertDialog.Builder(this).setTitle(L10n.tr("本机配对信息"))
|
||||
.setMessage(L10n.tr("将配对信息添加到被控端的可信设备列表,并核对指纹。"))
|
||||
.setView(fingerprint).setNegativeButton(L10n.tr("取消"),null)
|
||||
.setPositiveButton(L10n.tr("复制配对信息"),(dialog,which)->{
|
||||
try{((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("CrossDesk",info.getString("pairing")));toast(L10n.tr("配对信息已复制。"));}
|
||||
catch(Exception error){toast(L10n.tr("无法复制配对信息,请重试。"));}
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
private void confirmPreviewSaving(){
|
||||
previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("保存远程画面预览?"))
|
||||
.setMessage(L10n.tr("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。"))
|
||||
@@ -507,7 +524,7 @@ public final class MainActivity extends Activity implements NativeSession.Listen
|
||||
toast(L10n.tr("当前远端不支持直接输入此文本,请使用支持文本输入的被控端版本,或通过剪贴板发送"));return false;
|
||||
}
|
||||
private void tapKey(int code){if(session!=null){session.key(code,true);session.key(code,false);}}
|
||||
private void shortcut(int code){if(session!=null){int modifier=hostPlatform.equals("macos")?0x5B:0x11;session.key(modifier,true);tapKey(code);session.key(modifier,false);}}
|
||||
private void shortcut(int code){if(ready&&session!=null)session.chord(code,new int[]{hostPlatform.equals("macos")?0x5B:0x11});}
|
||||
private void selectDisplay(){
|
||||
if(!ready)return;int count=Math.min(displays.length(),8);if(count==0){toast(L10n.tr("等待远端屏幕信息"));return;}String[] labels=new String[count];for(int i=0;i<count;i++){JSONObject display=displays.optJSONObject(i);labels[i]=(i+1)+" · "+(display==null?L10n.tr("显示器"):display.optString("name",L10n.tr("显示器")));}new AlertDialog.Builder(this).setTitle(L10n.tr("显示器")).setItems(labels,(d,w)->{if(session!=null){if(w!=selectedDisplay&&video!=null)video.resetViewport();selectedDisplay=w;session.control(4,w);if(controls!=null)controls.displayResolutions(displayResolutions,selectedDisplay);if(controls!=null)controls.resetVideoStatistics();}}).show();
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ final class NativeSession implements AutoCloseable {
|
||||
File certificates = exportTrust();
|
||||
File logs = new File(context.getNoBackupFilesDir(), "logs");
|
||||
if (!logs.isDirectory() && !logs.mkdirs()) throw new IllegalStateException("Log directory unavailable");
|
||||
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath());
|
||||
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath(), trustKey(context));
|
||||
if (handle == 0) { fail(L10n.tr("原生连接初始化失败")); return; }
|
||||
nFileDirectory(handle,new File(documents.directory,"received").getAbsolutePath());
|
||||
if (surface != null && surface.isValid()) nSurface(handle, surface);
|
||||
@@ -116,6 +116,33 @@ final class NativeSession implements AutoCloseable {
|
||||
});
|
||||
}
|
||||
boolean isClosed() { return closed; }
|
||||
// Called only on RTC, shared by every foreground/background session and UI export.
|
||||
// This key belongs to the installation, not to a signaling server or login ID.
|
||||
private static String trustKey(Context context) throws Exception {
|
||||
SecretStore store = new SecretStore(context);
|
||||
var preferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE);
|
||||
String seed = store.get("device-trust:v1");
|
||||
if (seed.isEmpty()) {
|
||||
if (preferences.getBoolean("deviceTrustInitialized", false))
|
||||
throw new IllegalStateException("Device signing identity missing");
|
||||
seed = nGenerateTrustKey();
|
||||
if (seed == null) throw new IllegalStateException("Device signing identity unavailable");
|
||||
store.put("device-trust:v1", seed);
|
||||
}
|
||||
if (!preferences.edit().putBoolean("deviceTrustInitialized", true).commit())
|
||||
throw new IllegalStateException("Device signing identity marker unavailable");
|
||||
return seed;
|
||||
}
|
||||
static void pairingInfo(Context context, String deviceId, java.util.function.Consumer<JSONObject> completed) {
|
||||
Context app = context.getApplicationContext();
|
||||
RTC.execute(() -> {
|
||||
JSONObject info = null;
|
||||
try { info = new JSONObject(nTrustInfo(trustKey(app), deviceId.split("@", 2)[0])); }
|
||||
catch (Exception ignored) { }
|
||||
JSONObject result = info;
|
||||
new Handler(Looper.getMainLooper()).post(() -> completed.accept(result));
|
||||
});
|
||||
}
|
||||
// UI callbacks run on the main thread. A service-owned state can replace
|
||||
// the Activity listener without recreating either native peer.
|
||||
void setListener(Listener value) { listener = java.util.Objects.requireNonNull(value); }
|
||||
@@ -272,6 +299,15 @@ final class NativeSession implements AutoCloseable {
|
||||
void surface(Surface value) { execute(() -> { surface = value; if (handle != 0) nSurface(handle, value); }); }
|
||||
void pointer(float x, float y, int flag, int wheel) { execute(() -> { if (connected) nPointer(handle, x, y, flag, wheel); }); }
|
||||
void key(int code, boolean down) { execute(() -> { if (connected) nKey(handle, code, down); }); }
|
||||
void chord(int code,int[] modifiers) {
|
||||
if(closed||!connected)return;
|
||||
int[] keys=RemoteShortcuts.normalize(modifiers);
|
||||
execute(()->{
|
||||
if(!connected)return;
|
||||
try{for(int modifier:keys)nKey(handle,modifier,true);nKey(handle,code,true);}
|
||||
finally{nKey(handle,code,false);for(int i=keys.length-1;i>=0;i--)nKey(handle,keys[i],false);}
|
||||
});
|
||||
}
|
||||
boolean textEdit(int deletions,String text) {
|
||||
byte[] bytes=text.getBytes(StandardCharsets.UTF_8);
|
||||
boolean unicode=supportsTextInput;
|
||||
@@ -347,7 +383,9 @@ final class NativeSession implements AutoCloseable {
|
||||
static native boolean nValidAppVersion(String version);
|
||||
static native boolean nHasAppUpdate(String version, byte[] releaseJson);
|
||||
static native String nCheckMobileUpdate(String version, byte[] releaseJson);
|
||||
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots);
|
||||
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots, String trustKey);
|
||||
private static native String nGenerateTrustKey();
|
||||
private static native String nTrustInfo(String seed, String deviceId);
|
||||
private static native boolean nConnect(long handle, String identity, String remote, String password);
|
||||
private static native void nReady(long handle,byte[] displayResolutions);
|
||||
private static native void nPointer(long handle, float x, float y, int flag, int wheel);
|
||||
|
||||
@@ -22,6 +22,12 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
private final Runnable changed;
|
||||
private final LinearLayout toolbar,keys;
|
||||
private final TextView switchButton,lettersButton,functionsButton;
|
||||
private final LinearLayout quickKeys;
|
||||
private final HorizontalScrollView quickScroll;
|
||||
private final TextView panelTitle;
|
||||
private final RemoteShortcutPanel shortcutPanel;
|
||||
private String hostPlatform="unknown";
|
||||
private boolean shortcuts;
|
||||
private final Set<Integer> modifiers=new LinkedHashSet<>();
|
||||
private final SparseIntArray heights=new SparseIntArray();
|
||||
private boolean opened,computer,functions,imeVisible,measuring,switching,targetComputer;
|
||||
@@ -35,15 +41,20 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
public boolean edit(int count,String text){return actions.textEdit(count,text);}
|
||||
public void key(int code){actions.key(code);}
|
||||
});
|
||||
shortcutPanel=new RemoteShortcutPanel(context,ui,this::sendShortcut,()->{render();changed.run();});
|
||||
FrameLayout header=new FrameLayout(context);addView(header,new LayoutParams(-1,ui.dp(44)));
|
||||
header.addView(editor,new FrameLayout.LayoutParams(1,1,Gravity.BOTTOM));
|
||||
toolbar=ui.row();toolbar.setGravity(Gravity.CENTER_VERTICAL);toolbar.setPadding(ui.dp(6),0,ui.dp(6),0);
|
||||
header.addView(toolbar,new FrameLayout.LayoutParams(-1,-1));
|
||||
switchButton=button(L10n.tr("电脑键盘"),this::switchKeyboard);switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
|
||||
switchButton=button(L10n.tr("电脑键盘"),()->{shortcuts=false;switchKeyboard();});switchButton.setPadding(ui.dp(8),0,ui.dp(8),0);
|
||||
toolbar.addView(switchButton,new LayoutParams(-2,-1));
|
||||
LinearLayout pages=ui.row();pages.setGravity(Gravity.CENTER);toolbar.addView(pages,new LayoutParams(0,-1,1));
|
||||
quickScroll=new HorizontalScrollView(context);quickScroll.setHorizontalScrollBarEnabled(false);
|
||||
quickKeys=ui.row();quickScroll.addView(quickKeys);pages.addView(quickScroll,new LayoutParams(-1,-1));
|
||||
panelTitle=ui.text(L10n.tr("快捷键"),13,false);panelTitle.setGravity(Gravity.CENTER);pages.addView(panelTitle,new LayoutParams(-1,-1));
|
||||
lettersButton=button(L10n.tr("字母数字"),()->page(false));functionsButton=button(L10n.tr("符号功能"),()->page(true));
|
||||
pages.addView(lettersButton,new LayoutParams(-2,ui.dp(32)));pages.addView(functionsButton,new LayoutParams(-2,ui.dp(32)));
|
||||
TextView more=button("⋯",this::showShortcuts);more.setContentDescription(L10n.tr("更多快捷键"));toolbar.addView(more,new LayoutParams(ui.dp(44),ui.dp(44)));
|
||||
FrameLayout hide=new FrameLayout(context);hide.setContentDescription(L10n.tr("收起键盘"));hide.setClickable(true);hide.setFocusable(true);
|
||||
hide.addView(ui.icon("keyboard_hide",foreground()),new FrameLayout.LayoutParams(ui.dp(24),ui.dp(24),Gravity.CENTER));
|
||||
hide.setOnClickListener(v->{feedback(v);close();});toolbar.addView(hide,new LayoutParams(ui.dp(44),ui.dp(44)));
|
||||
@@ -60,6 +71,7 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
private void feedback(View view){view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);}
|
||||
boolean isOpen(){return opened;}
|
||||
boolean isComputer(){return computer;}
|
||||
void setPlatform(String value){hostPlatform=RemoteShortcuts.platform(value);shortcutPanel.platform(hostPlatform);if(opened)render();}
|
||||
int bottomInset(){return opened&&!computer?imeHeight:0;}
|
||||
private int sizeKey(){Configuration c=getResources().getConfiguration();return c.screenWidthDp*10000+c.screenHeightDp;}
|
||||
private int keyAreaHeight(){
|
||||
@@ -71,11 +83,11 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
int preferredHeight(){return ui.dp(44)+(computer?keyAreaHeight():0);}
|
||||
void toggle(){if(opened)close();else open();}
|
||||
void open(){
|
||||
opened=true;computer=false;modifiers.clear();editor.start();setVisibility(VISIBLE);render();changed.run();
|
||||
opened=true;computer=shortcuts=false;modifiers.clear();editor.start();setVisibility(VISIBLE);render();changed.run();
|
||||
post(()->{if(opened&&!computer)changeIme(true,false,false);});
|
||||
}
|
||||
boolean close(){
|
||||
if(!opened)return false;opened=false;measuring=switching=false;cancelTransition();modifiers.clear();editor.stop();
|
||||
shortcutPanel.close();if(!opened)return false;opened=false;measuring=switching=false;cancelTransition();modifiers.clear();editor.stop();
|
||||
getContext().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getWindowToken(),0);
|
||||
setVisibility(GONE);changed.run();return true;
|
||||
}
|
||||
@@ -83,6 +95,7 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
private void page(boolean next){functions=next;render();}
|
||||
private void switchKeyboard(){
|
||||
if(!opened||switching)return;
|
||||
if(!computer&&!prepareShortcut())return;
|
||||
targetComputer=!computer;switching=true;modifiers.clear();
|
||||
if(targetComputer){
|
||||
editor.pause();computer=true;render();changed.run();
|
||||
@@ -99,6 +112,20 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
invalidate();
|
||||
}else changeIme(!targetComputer,true,false);
|
||||
}
|
||||
private boolean prepareShortcut(){
|
||||
if(computer)return true;
|
||||
if(editor.hasPendingComposition()){Toast.makeText(getContext(),L10n.tr("请先选词或取消组词"),Toast.LENGTH_SHORT).show();return false;}
|
||||
return editor.prepareShortcut();
|
||||
}
|
||||
private void showShortcuts(){
|
||||
if(!opened||switching)return;
|
||||
if(computer){if(shortcuts)switchKeyboard();else{shortcuts=true;modifiers.clear();render();changed.run();}}
|
||||
else{shortcuts=true;switchKeyboard();}
|
||||
}
|
||||
private void sendShortcut(RemoteShortcuts.Item item){
|
||||
if(!opened||switching||!item.platform.equals(hostPlatform)||!prepareShortcut())return;
|
||||
modifiers.clear();if(item.service)actions.secureAttention();else actions.chord(item.key,item.modifiers);
|
||||
}
|
||||
private void finishSwitch(){
|
||||
if(!switching)return;switching=false;
|
||||
if(computer!=targetComputer){computer=targetComputer;render();}
|
||||
@@ -158,7 +185,9 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
private void render(){
|
||||
setBackgroundColor(dark()?0xFF202124:0xFFE8EAED);
|
||||
switchButton.setText(computer?L10n.tr("系统键盘"):L10n.tr("电脑键盘"));switchButton.setTextColor(foreground());
|
||||
lettersButton.setVisibility(computer?VISIBLE:GONE);functionsButton.setVisibility(computer?VISIBLE:GONE);
|
||||
lettersButton.setVisibility(computer&&!shortcuts?VISIBLE:GONE);functionsButton.setVisibility(computer&&!shortcuts?VISIBLE:GONE);
|
||||
quickScroll.setVisibility(computer?GONE:VISIBLE);panelTitle.setVisibility(computer&&shortcuts?VISIBLE:GONE);panelTitle.setTextColor(foreground());
|
||||
quickKeys.removeAllViews();for(RemoteShortcuts.Item item:shortcutPanel.store.pinned(hostPlatform)){TextView key=button(item.title(),()->sendShortcut(item));key.setContentDescription(item.title()+", "+item.chord());quickKeys.addView(key,new LayoutParams(-2,ui.dp(44)));}
|
||||
for(TextView view:new TextView[]{lettersButton,functionsButton}){
|
||||
boolean selected=(view==functionsButton)==functions;view.setSelected(selected);
|
||||
view.setTextColor(selected?(dark()?0xFFA8C7FA:0xFF185ABC):foreground());
|
||||
@@ -169,6 +198,7 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
private void renderKeysIfNeeded(int width){
|
||||
if(!computer)return;int height=keyAreaHeight();
|
||||
if(layoutWidth==width&&layoutHeight==height)return;layoutWidth=width;layoutHeight=height;keys.removeAllViews();
|
||||
if(shortcuts){keys.addView(shortcutPanel.create(),new LayoutParams(-1,-1));return;}
|
||||
boolean landscape=getResources().getConfiguration().screenWidthDp>getResources().getConfiguration().screenHeightDp;
|
||||
String[][] upper;
|
||||
if(!functions)upper=new String[][]{{"1","2","3","4","5","6","7","8","9","0"},{"q","w","e","r","t","y","u","i","o","p"},{"a","s","d","f","g","h","j","k","l"}};
|
||||
@@ -185,6 +215,7 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
LinearLayout line=ui.row();line.setGravity(Gravity.CENTER);
|
||||
for(String value:values){
|
||||
boolean shift=modifiers.contains(16);String shown=shift&&value.length()==1?shifted(value):value;
|
||||
if(value.equals("Win"))shown=RemoteShortcuts.modifierName(91,hostPlatform);else if(value.equals("Alt"))shown=RemoteShortcuts.modifierName(18,hostPlatform);
|
||||
TextView key=button(L10n.tr(shown),()->key(value));key.setPadding(0,0,0,0);
|
||||
key.setTextSize(value.length()>1?12:18);key.setSingleLine(true);
|
||||
int modifier=modifier(value);boolean selected=modifier!=0&&modifiers.contains(modifier);key.setSelected(selected);
|
||||
@@ -209,6 +240,7 @@ final class RemoteKeyboard extends LinearLayout {
|
||||
int modifier=modifier(value);if(modifier!=0){if(!modifiers.add(modifier))modifiers.remove(modifier);render();return;}
|
||||
int code=switch(value){case "空格"->32;case "Enter"->13;case "⌫"->8;case "Esc"->27;case "Tab"->9;case "Caps"->20;case "Ins"->45;case "Del"->46;case "Home"->36;case "End"->35;case "PgUp"->33;case "PgDn"->34;case "←"->37;case "↑"->38;case "→"->39;case "↓"->40;default->value.startsWith("F")?111+Integer.parseInt(value.substring(1)):KeyMap.ascii(value.charAt(0))&255;};
|
||||
actions.chord(code,modifiers.stream().mapToInt(Integer::intValue).toArray());
|
||||
modifiers.clear();render();
|
||||
}
|
||||
@Override protected void onMeasure(int widthSpec,int heightSpec){
|
||||
renderKeysIfNeeded(MeasureSpec.getSize(widthSpec));super.onMeasure(widthSpec,heightSpec);
|
||||
|
||||
@@ -52,6 +52,15 @@ final class RemoteKeyboardInput extends EditText {
|
||||
void pause() { accepting=false;resetContext(); }
|
||||
void stop() { pause();clearFocus(); }
|
||||
boolean isAcceptingInput() { return accepting; }
|
||||
boolean hasPendingComposition() { return batches>0||composing(); }
|
||||
boolean prepareShortcut() {
|
||||
if(!synchronize())return false;
|
||||
// Navigation and shortcuts invalidate the remote correction context.
|
||||
// Flush committed text first; never send or discard an IME preedit.
|
||||
if(enterPending)sender.key(13);
|
||||
resetContext();
|
||||
return true;
|
||||
}
|
||||
void resetContext() {
|
||||
generation++;batches=0;main.removeCallbacks(synchronize);resetting=true;
|
||||
// EditText.setText replaces the buffer and internally restarts the IME,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/** The preset panel occupies the keyboard keybed; editing uses a separate local window. */
|
||||
final class RemoteShortcutPanel {
|
||||
private final Context context;
|
||||
private final MobileUi ui;
|
||||
final RemoteShortcuts store;
|
||||
private final Consumer<RemoteShortcuts.Item> send;
|
||||
private final Runnable changed;
|
||||
private String platform="unknown";
|
||||
private int category,page;
|
||||
private Dialog manager;
|
||||
RemoteShortcutPanel(Context context,MobileUi ui,Consumer<RemoteShortcuts.Item> send,Runnable changed){this.context=context;this.ui=ui;this.send=send;this.changed=changed;store=new RemoteShortcuts(context);}
|
||||
void platform(String value){String next=RemoteShortcuts.platform(value);if(!platform.equals(next)){platform=next;page=0;close();}}
|
||||
private void hideEditor(){if(manager!=null&&manager.getWindow()!=null)context.getSystemService(android.view.inputmethod.InputMethodManager.class).hideSoftInputFromWindow(manager.getWindow().getDecorView().getWindowToken(),0);}
|
||||
void close(){if(manager!=null){hideEditor();manager.dismiss();manager=null;}}
|
||||
private boolean dark(){return (context.getResources().getConfiguration().uiMode&Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;}
|
||||
private int ink(){return dark()?Color.WHITE:0xFF202124;}
|
||||
private int surface(){return dark()?0xFF202124:0xFFF1F3F6;}
|
||||
private TextView text(String value,int size){TextView text=ui.text(value,size,false);text.setTextColor(ink());return text;}
|
||||
private TextView button(String value,Runnable action){TextView button=ui.action(value,ink(),action);button.setTextSize(13);button.setMinHeight(ui.dp(44));button.setPadding(ui.dp(8),0,ui.dp(8),0);return button;}
|
||||
private void refresh(){changed.run();}
|
||||
View create(){
|
||||
LinearLayout root=ui.column();root.setPadding(ui.dp(6),0,ui.dp(6),0);
|
||||
String[] groups={"common","keys","system","mine"},labels={L10n.tr("常用"),L10n.tr("按键"),L10n.tr("系统"),L10n.tr("我的")};
|
||||
root.addView(new SegmentedControl(ui,labels,category,true,index->{category=index;page=0;refresh();}),new LinearLayout.LayoutParams(-1,ui.dp(36)));
|
||||
if(category==2&&platform.equals("linux")){
|
||||
Spinner desktop=spinner(new String[]{L10n.tr("选择桌面方案"),"GNOME","KDE Plasma"},store.desktop().equals("gnome")?1:store.desktop().equals("kde")?2:0,index->{String next=index==1?"gnome":index==2?"kde":"";if(!store.desktop().equals(next)){store.desktop(next);page=0;refresh();}});
|
||||
root.addView(desktop,new LinearLayout.LayoutParams(-1,ui.dp(40)));
|
||||
}
|
||||
List<RemoteShortcuts.Item> items=new ArrayList<>();for(RemoteShortcuts.Item item:store.items(platform))if(item.group.equals(groups[category]))items.add(item);
|
||||
int count=Math.max(1,(items.size()+5)/6);page=Math.min(page,count-1);
|
||||
ScrollView gridScroll=new ScrollView(context);gridScroll.setFillViewport(true);root.addView(gridScroll,new LinearLayout.LayoutParams(-1,0,1));
|
||||
LinearLayout grid=ui.column();grid.setMinimumHeight(ui.dp(items.isEmpty()?44:108));gridScroll.addView(grid,new FrameLayout.LayoutParams(-1,-2));
|
||||
if(items.isEmpty()){TextView empty=text(L10n.tr(platform.equals("unknown")?"等待远端平台信息":category==2?"选择桌面方案以加载系统预设":"还没有自定义快捷键"),13);empty.setGravity(Gravity.CENTER);grid.addView(empty,new LinearLayout.LayoutParams(-1,-1));}
|
||||
else for(int row=0;row<2;row++){
|
||||
LinearLayout line=ui.row();for(int col=0;col<3;col++){
|
||||
int index=page*6+row*3+col;LinearLayout.LayoutParams params=new LinearLayout.LayoutParams(0,-1,1);params.setMargins(ui.dp(3),ui.dp(3),ui.dp(3),ui.dp(3));
|
||||
if(index>=items.size()){line.addView(new View(context),params);continue;}
|
||||
RemoteShortcuts.Item item=items.get(index);LinearLayout tile=ui.column();tile.setGravity(Gravity.CENTER);tile.setPadding(ui.dp(3),0,ui.dp(3),0);tile.setBackground(ui.background(dark()?0xFF353A43:Color.WHITE,7));
|
||||
TextView name=text(item.title(),13);name.setGravity(Gravity.CENTER);name.setMaxLines(2);tile.addView(name);TextView chord=text(item.chord(),11);chord.setGravity(Gravity.CENTER);chord.setMaxLines(2);tile.addView(chord);
|
||||
tile.setContentDescription(item.title()+", "+item.chord());tile.setClickable(true);tile.setFocusable(true);tile.setOnClickListener(v->send.accept(item));line.addView(tile,params);
|
||||
}grid.addView(line,new LinearLayout.LayoutParams(-1,0,1));
|
||||
}
|
||||
LinearLayout footer=ui.row();footer.setGravity(Gravity.CENTER_VERTICAL);
|
||||
if(count>1){TextView previous=button("‹",()->{page--;refresh();});previous.setEnabled(page>0);previous.setContentDescription(L10n.tr("上一页"));footer.addView(previous);footer.addView(text((page+1)+" / "+count,12));TextView next=button("›",()->{page++;refresh();});next.setEnabled(page+1<count);next.setContentDescription(L10n.tr("下一页"));footer.addView(next);}
|
||||
else footer.addView(text(RemoteShortcuts.platformName(platform),12));
|
||||
footer.addView(new View(context),new LinearLayout.LayoutParams(0,1,1));TextView edit=button(L10n.tr("管理与自定义"),this::manage);edit.setEnabled(!platform.equals("unknown"));footer.addView(edit);root.addView(footer,new LinearLayout.LayoutParams(-1,ui.dp(44)));
|
||||
return root;
|
||||
}
|
||||
private Spinner spinner(String[] labels,int selected,Consumer<Integer> changed){Spinner spinner=new Spinner(context);ArrayAdapter<String> adapter=new ArrayAdapter<>(context,android.R.layout.simple_spinner_dropdown_item,labels);spinner.setAdapter(adapter);spinner.setSelection(selected);spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener(){public void onNothingSelected(android.widget.AdapterView<?> p){}public void onItemSelected(android.widget.AdapterView<?> p,View v,int position,long id){changed.accept(position);}});return spinner;}
|
||||
private LinearLayout screen(String title,Runnable back,String action,Runnable submit){
|
||||
LinearLayout root=ui.column();root.setBackgroundColor(surface());LinearLayout header=ui.row();header.setGravity(Gravity.CENTER_VERTICAL);header.addView(button(L10n.tr("返回"),back));TextView heading=text(title,17);header.addView(heading,new LinearLayout.LayoutParams(0,-2,1));if(action!=null)header.addView(button(action,submit));root.addView(header,new LinearLayout.LayoutParams(-1,ui.dp(52)));
|
||||
TextView note=text(L10n.tr("本地编辑,输入不会发送到远端"),12);note.setPadding(ui.dp(16),ui.dp(8),ui.dp(16),ui.dp(8));root.addView(note);return root;
|
||||
}
|
||||
private LinearLayout scrollBody(LinearLayout root){ScrollView scroll=new ScrollView(context);root.addView(scroll,new LinearLayout.LayoutParams(-1,0,1));LinearLayout body=ui.column();body.setPadding(ui.dp(16),ui.dp(8),ui.dp(16),ui.dp(20));scroll.addView(body);return body;}
|
||||
private void present(View root){
|
||||
if(manager==null){manager=new Dialog(context);manager.requestWindowFeature(Window.FEATURE_NO_TITLE);manager.setOnDismissListener(d->{manager=null;refresh();});}
|
||||
hideEditor();manager.setContentView(root);manager.show();Window window=manager.getWindow();if(window!=null){window.setLayout(-1,-1);window.setBackgroundDrawableResource(android.R.color.transparent);window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);}
|
||||
}
|
||||
private void manage(){
|
||||
LinearLayout root=screen(L10n.tr("管理快捷键"),this::close,L10n.tr("新建"),()->edit(null));LinearLayout body=scrollBody(root);body.addView(text(RemoteShortcuts.platformName(platform)+" · "+L10n.tr("键盘上方常用栏"),14));
|
||||
List<String> selected=store.pins(platform);
|
||||
for(RemoteShortcuts.Item item:store.pinned(platform)){
|
||||
LinearLayout row=ui.row();row.setGravity(Gravity.CENTER_VERTICAL);TextView label=text(item.title()+"\n"+item.chord(),13);row.addView(label,new LinearLayout.LayoutParams(0,-2,1));
|
||||
TextView up=button("↑",()->{List<String> list=store.pins(platform);int index=list.indexOf(item.id);if(index>0){java.util.Collections.swap(list,index,index-1);store.pins(platform,list);manage();}});up.setContentDescription(L10n.tr("向上移动"));up.setEnabled(selected.indexOf(item.id)>0);row.addView(up);
|
||||
row.addView(button(L10n.tr("移除"),()->{List<String> list=store.pins(platform);list.remove(item.id);store.pins(platform,list);manage();}));body.addView(row);ui.divider(body);
|
||||
}
|
||||
ui.gap(body,16);body.addView(text(L10n.tr("添加到常用栏"),14));
|
||||
for(RemoteShortcuts.Item item:store.items(platform)){
|
||||
LinearLayout row=ui.row();row.setGravity(Gravity.CENTER_VERTICAL);TextView label=text(item.title()+"\n"+item.chord(),13);row.addView(label,new LinearLayout.LayoutParams(0,-2,1));
|
||||
if(!selected.contains(item.id))row.addView(button("+",()->{List<String> list=store.pins(platform);if(list.size()>=RemoteShortcuts.MAX_PINNED){Toast.makeText(context,L10n.tr("最多固定 8 个快捷键"),Toast.LENGTH_SHORT).show();return;}list.add(item.id);store.pins(platform,list);manage();}));
|
||||
if(item.custom){row.addView(button(L10n.tr("编辑"),()->edit(item)));row.addView(button(L10n.tr("删除"),()->{store.remove(item.id);manage();}));}
|
||||
body.addView(row);
|
||||
}
|
||||
present(root);
|
||||
}
|
||||
private void edit(RemoteShortcuts.Item existing){
|
||||
EditText name=new EditText(context);name.setTextColor(ink());name.setSingleLine();name.setText(existing==null?"":existing.name);name.setHint(L10n.tr("名称"));name.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(64)});
|
||||
final String[] target={existing==null?platform:existing.platform};final int[] key={existing==null?83:existing.key};List<Integer> modifiers=new ArrayList<>();for(int code:existing==null?new int[]{platform.equals("macos")?91:17}:existing.modifiers)modifiers.add(code);
|
||||
CheckBox pin=new CheckBox(context);pin.setTextColor(ink());pin.setText(L10n.tr("固定到键盘上方"));pin.setChecked(existing==null?store.pins(target[0]).size()<RemoteShortcuts.MAX_PINNED:store.pins(existing.platform).contains(existing.id));
|
||||
TextView error=text("",13);error.setTextColor(0xFFE45757);TextView preview=text("",16);
|
||||
Runnable update=()->preview.setText(new RemoteShortcuts.Item("","",target[0],"mine",key[0],RemoteShortcuts.normalize(modifiers.stream().mapToInt(Integer::intValue).toArray()),false,true).chord());
|
||||
LinearLayout root=screen(L10n.tr(existing==null?"新建快捷键":"编辑快捷键"),this::manage,L10n.tr("保存"),()->{String result=store.save(existing==null?null:existing.id,name.getText().toString(),target[0],key[0],modifiers.stream().mapToInt(Integer::intValue).toArray(),pin.isChecked());if(result.isEmpty())manage();else error.setText(result);});
|
||||
LinearLayout body=scrollBody(root);body.addView(name,new LinearLayout.LayoutParams(-1,ui.dp(52)));ui.gap(body,12);
|
||||
LinearLayout modifierRow=ui.row();CheckBox[] toggles=new CheckBox[4];for(int i=0;i<4;i++){final int code=RemoteShortcuts.MODIFIERS[i];CheckBox toggle=new CheckBox(context);toggle.setTextColor(ink());toggle.setTextSize(12);toggle.setText(RemoteShortcuts.modifierName(code,target[0]));toggle.setChecked(modifiers.contains(code));toggle.setOnCheckedChangeListener((v,checked)->{if(checked){if(!modifiers.contains(code))modifiers.add(code);}else modifiers.remove(Integer.valueOf(code));update.run();});toggles[i]=toggle;modifierRow.addView(toggle,new LinearLayout.LayoutParams(0,ui.dp(48),1));}
|
||||
List<Integer> keys=RemoteShortcuts.keys();
|
||||
Spinner mainKey=spinner(keys.stream().map(code->RemoteShortcuts.keyName(code,target[0])).toArray(String[]::new),keys.indexOf(key[0]),index->{key[0]=keys.get(index);update.run();});
|
||||
body.addView(text(L10n.tr("适用远端"),13));body.addView(spinner(new String[]{"Windows","macOS","Linux"},Arrays.asList(RemoteShortcuts.PLATFORMS).indexOf(target[0]),index->{target[0]=RemoteShortcuts.PLATFORMS[index];mainKey.setAdapter(new ArrayAdapter<>(context,android.R.layout.simple_spinner_dropdown_item,keys.stream().map(code->RemoteShortcuts.keyName(code,target[0])).toArray(String[]::new)));mainKey.setSelection(keys.indexOf(key[0]));for(int i=0;i<4;i++)toggles[i].setText(RemoteShortcuts.modifierName(RemoteShortcuts.MODIFIERS[i],target[0]));update.run();}),new LinearLayout.LayoutParams(-1,ui.dp(48)));
|
||||
body.addView(text(L10n.tr("主按键"),13));body.addView(mainKey,new LinearLayout.LayoutParams(-1,ui.dp(48)));
|
||||
body.addView(text(L10n.tr("修饰键"),13));body.addView(modifierRow);body.addView(text(L10n.tr("发送组合"),13));body.addView(preview);body.addView(pin);body.addView(error);update.run();present(root);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package cn.crossdesk.mobile;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Local controller preferences. Key codes use the existing cross-platform wire mapping. */
|
||||
final class RemoteShortcuts {
|
||||
static final int MAX_CUSTOM=16,MAX_PINNED=8;
|
||||
static final String[] PLATFORMS={"windows","macos","linux"};
|
||||
static final int[] MODIFIERS={17,16,18,91};
|
||||
static final class Item {
|
||||
final String id,name,platform,group;
|
||||
final int key;
|
||||
final int[] modifiers;
|
||||
final boolean service,custom;
|
||||
Item(String id,String name,String platform,String group,int key,int[] modifiers,boolean service,boolean custom){
|
||||
this.id=id;this.name=name;this.platform=platform;this.group=group;this.key=key;
|
||||
this.modifiers=modifiers.clone();this.service=service;this.custom=custom;
|
||||
}
|
||||
String title(){return custom?name:L10n.tr(name);}
|
||||
String chord(){List<String> parts=new ArrayList<>();for(int modifier:modifiers)parts.add(modifierName(modifier,platform));parts.add(keyName(key,platform));return String.join(" + ",parts);}
|
||||
}
|
||||
private final SharedPreferences preferences;
|
||||
RemoteShortcuts(Context context){preferences=context.getSharedPreferences("remote_shortcuts_v1",Context.MODE_PRIVATE);}
|
||||
static String platform(String value){return Arrays.asList(PLATFORMS).contains(value)?value:"unknown";}
|
||||
static String platformName(String value){return switch(value){case "windows"->"Windows";case "macos"->"macOS";case "linux"->"Linux";default->L10n.tr("未知");};}
|
||||
String desktop(){String value=preferences.getString("linux_desktop","");return value.equals("gnome")||value.equals("kde")?value:"";}
|
||||
void desktop(String value){preferences.edit().putString("linux_desktop",value.equals("gnome")||value.equals("kde")?value:"").apply();}
|
||||
static String modifierName(int code,String platform){return switch(code){case 17->"Ctrl";case 16->"Shift";case 18->platform.equals("macos")?"Option":"Alt";case 91->platform.equals("macos")?"Cmd":platform.equals("linux")?"Super":"Win";default->"";};}
|
||||
static String keyName(int code,String platform){
|
||||
if(code>=65&&code<=90||code>=48&&code<=57)return Character.toString((char)code);
|
||||
if(code>=112&&code<=123)return "F"+(code-111);
|
||||
return switch(code){case 8->platform.equals("macos")?"Delete ⌫":"Backspace";case 9->"Tab";case 13->"Enter";case 27->"Esc";case 32->"Space";case 33->"PageUp";case 34->"PageDown";case 35->"End";case 36->"Home";case 37->"←";case 38->"↑";case 39->"→";case 40->"↓";case 45->"Insert";case 46->platform.equals("macos")?"Delete ⌦":"Delete";default->"";};
|
||||
}
|
||||
static List<Integer> keys(){List<Integer> result=new ArrayList<>();for(int i=65;i<=90;i++)result.add(i);for(int i=48;i<=57;i++)result.add(i);for(int i=112;i<=123;i++)result.add(i);for(int i:new int[]{27,9,13,32,8,46,45,36,35,33,34,37,38,39,40})result.add(i);return result;}
|
||||
static int[] normalize(int[] modifiers){return Arrays.stream(MODIFIERS).filter(code->Arrays.stream(modifiers).anyMatch(value->value==code)).toArray();}
|
||||
private static Item preset(String id,String name,String platform,String group,int key,int... modifiers){return new Item(id,name,platform,group,key,modifiers,false,false);}
|
||||
List<Item> items(String remote){
|
||||
String os=platform(remote);List<Item> result=new ArrayList<>();
|
||||
if(!os.equals("unknown")){
|
||||
int primary=os.equals("macos")?91:17;
|
||||
String[] ids={"copy","paste","cut","all","undo","save"},names={"复制","粘贴","剪切","全选","撤销","保存"};int[] keys={67,86,88,65,90,83};
|
||||
for(int i=0;i<ids.length;i++)result.add(preset(ids[i],names[i],os,"common",keys[i],primary));
|
||||
if(os.equals("linux")){result.add(preset("term-copy","终端复制",os,"common",67,17,16));result.add(preset("term-paste","终端粘贴",os,"common",86,17,16));}
|
||||
}
|
||||
for(int key:new int[]{27,9,13,8,46,36,35,33,34,37,38,39,40,112,113,114,115,116,117,118,119,120,121,122,123})
|
||||
result.add(preset(key==27?"esc":key==9?"tab":"key-"+key,keyName(key,os),os,"keys",key));
|
||||
if(os.equals("windows")){
|
||||
result.add(preset("switch","切换窗口",os,"system",9,18));result.add(preset("desktop","显示桌面",os,"system",68,91));result.add(preset("task","任务管理器",os,"system",27,17,16));
|
||||
result.add(new Item("sas","安全选项",os,"system",46,new int[]{17,18},true,false));
|
||||
}else if(os.equals("macos")){
|
||||
result.add(preset("switch","切换应用",os,"system",9,91));result.add(preset("search","聚焦搜索",os,"system",32,91));result.add(preset("forcequit","强制退出面板",os,"system",27,18,91));
|
||||
}else if(os.equals("linux")&&!desktop().isEmpty()){
|
||||
result.add(preset("switch",desktop().equals("gnome")?"切换应用":"切换窗口",os,"system",9,desktop().equals("gnome")?91:18));result.add(preset("run","运行窗口",os,"system",113,18));
|
||||
result.add(desktop().equals("gnome")?preset("apps","应用列表",os,"system",65,91):preset("window-menu","窗口菜单",os,"system",114,18));
|
||||
}
|
||||
for(Item item:custom())if(item.platform.equals(os))result.add(item);
|
||||
return result;
|
||||
}
|
||||
List<Item> custom(){
|
||||
List<Item> result=new ArrayList<>();
|
||||
try{JSONArray entries=new JSONArray(preferences.getString("custom","[]"));for(int i=0;i<entries.length()&&result.size()<MAX_CUSTOM;i++){
|
||||
JSONObject entry=entries.optJSONObject(i);if(entry==null)continue;String os=platform(entry.optString("platform")),id=entry.optString("id"),name=entry.optString("name").trim();int key=entry.optInt("key");JSONArray modifiers=entry.optJSONArray("modifiers");
|
||||
if(os.equals("unknown")||!id.startsWith("custom-")||name.isEmpty()||name.length()>64||!keys().contains(key)||modifiers==null||modifiers.length()>4)continue;
|
||||
int[] values=new int[modifiers.length()];boolean valid=true;for(int j=0;j<values.length;j++){int value=modifiers.optInt(j);values[j]=value;if(!Arrays.stream(MODIFIERS).anyMatch(code->code==value))valid=false;}
|
||||
if(!valid)continue;int[] normalized=normalize(values);if(os.equals("windows")&&key==46&&Arrays.stream(normalized).anyMatch(v->v==17)&&Arrays.stream(normalized).anyMatch(v->v==18))continue;
|
||||
if(result.stream().anyMatch(item->item.id.equals(id)))continue;
|
||||
result.add(new Item(id,name,os,"mine",key,normalized,false,true));
|
||||
}}catch(Exception ignored){ }
|
||||
return result;
|
||||
}
|
||||
List<String> pins(String os){
|
||||
List<String> result=new ArrayList<>();String stored=preferences.getString("pins_"+platform(os),null);
|
||||
if(stored==null)stored="[\"esc\",\"tab\",\"copy\",\"paste\"]";
|
||||
try{JSONArray array=new JSONArray(stored);for(int i=0;i<array.length()&&result.size()<MAX_PINNED;i++){String id=array.optString(i);if(!id.isEmpty()&&!result.contains(id))result.add(id);}}catch(Exception ignored){result.addAll(Arrays.asList("esc","tab","copy","paste"));}
|
||||
List<Item> available=items(os);result.removeIf(id->available.stream().noneMatch(item->item.id.equals(id)));
|
||||
return result;
|
||||
}
|
||||
List<Item> pinned(String os){List<Item> all=items(os),result=new ArrayList<>();for(String id:pins(os))for(Item item:all)if(item.id.equals(id)){result.add(item);break;}return result;}
|
||||
void pins(String os,List<String> values){preferences.edit().putString("pins_"+platform(os),new JSONArray(values.subList(0,Math.min(values.size(),MAX_PINNED))).toString()).apply();}
|
||||
void remove(String id){List<Item> values=custom();values.removeIf(item->item.id.equals(id));write(values);for(String os:PLATFORMS){List<String> selected=pins(os);selected.remove(id);pins(os,selected);}}
|
||||
String save(String id,String name,String os,int key,int[] modifiers,boolean pin){
|
||||
name=name.trim();if(name.isEmpty()||name.length()>64)return L10n.tr("请输入有效的快捷键名称");
|
||||
if(platform(os).equals("unknown")||!keys().contains(key))return L10n.tr("请选择有效的按键");
|
||||
int[] normalized=normalize(modifiers);if(os.equals("windows")&&key==46&&Arrays.stream(normalized).anyMatch(v->v==17)&&Arrays.stream(normalized).anyMatch(v->v==18))return L10n.tr("请使用系统预设中的安全选项");
|
||||
List<Item> values=custom();boolean exists=false;for(Item item:values){if(item.id.equals(id))exists=true;else if(item.platform.equals(os)&&item.key==key&&Arrays.equals(item.modifiers,normalized))return L10n.tr("已有相同组合的快捷键");}
|
||||
if(!exists&&values.size()>=MAX_CUSTOM)return L10n.tr("最多保存 16 个自定义快捷键");
|
||||
List<String> selected=pins(os);if(pin&&!selected.contains(id)&&selected.size()>=MAX_PINNED)return L10n.tr("最多固定 8 个快捷键");
|
||||
String itemId=id==null?"custom-"+UUID.randomUUID():id;values.removeIf(item->item.id.equals(itemId));values.add(new Item(itemId,name,os,"mine",key,normalized,false,true));write(values);
|
||||
for(String target:PLATFORMS){List<String> list=pins(target);int index=list.indexOf(itemId);list.remove(itemId);if(target.equals(os)&&pin)list.add(index>=0?Math.min(index,list.size()):list.size(),itemId);pins(target,list);}return "";
|
||||
}
|
||||
private void write(List<Item> items){JSONArray array=new JSONArray();try{for(Item item:items){JSONArray modifiers=new JSONArray();for(int code:item.modifiers)modifiers.put(code);array.put(new JSONObject().put("id",item.id).put("name",item.name).put("platform",item.platform).put("key",item.key).put("modifiers",modifiers));}}catch(Exception ignored){return;}preferences.edit().putString("custom",array.toString()).apply();}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ final class SessionControls extends FrameLayout {
|
||||
}
|
||||
void setRelative(boolean value){relative=value;if(menu!=null&&menuPage.equals("controls"))openMenu("controls");}
|
||||
void setHostPlatform(String platform){
|
||||
keyboard.setPlatform(platform);
|
||||
boolean visible="windows".equals(platform);
|
||||
if(secureAttentionVisible==visible)return;
|
||||
secureAttentionVisible=visible;
|
||||
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"被控端分辨率"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"设备不支持"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"等待电脑端确认…"</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"将配对信息添加到被控端的可信设备列表,并核对指纹。"</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"本机配对信息"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"查看指纹"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"复制配对信息"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"配对信息已复制。"</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"无法复制配对信息,请重试。"</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"安全存储不可用,请解锁设备后重试"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"上一页"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"下一页"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"主按键"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"任务管理器"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"保存"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修饰键"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"全选"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"切换应用"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"切换窗口"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"剪切"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"发送组合"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"名称"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"向上移动"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"固定到键盘上方"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"复制"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"安全选项"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"已有相同组合的快捷键"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"常用"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"应用列表"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"强制退出面板"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"快捷键"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"我的"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"按键"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"撤销"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"新建"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"新建快捷键"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"显示桌面"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"更多快捷键"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"最多保存 16 个自定义快捷键"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"最多固定 8 个快捷键"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"未知"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"本地编辑,输入不会发送到远端"</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"添加到常用栏"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"移除"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"窗口菜单"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"等待远端平台信息"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"管理与自定义"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"管理快捷键"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"粘贴"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"系统"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"终端复制"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"终端粘贴"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"编辑"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"编辑快捷键"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"聚焦搜索"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"请使用系统预设中的安全选项"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"请先选词或取消组词"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"请输入有效的快捷键名称"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"请选择有效的按键"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"运行窗口"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"还没有自定义快捷键"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"适用远端"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"选择桌面方案"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"选择桌面方案以加载系统预设"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"键盘上方常用栏"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"被控端解析度"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"裝置不支援"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"等待電腦端確認…"</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"將配對資訊新增至被控端的可信裝置清單,並核對指紋。"</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"本機配對資訊"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"查看指紋"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"複製配對資訊"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"配對資訊已複製。"</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"無法複製配對資訊,請重試。"</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"安全儲存不可用,請解鎖裝置後重試"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"上一頁"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"下一頁"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"主按鍵"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"工作管理員"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"儲存"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修飾鍵"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"全選"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"切換應用程式"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"切換視窗"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"剪下"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"傳送組合"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"名稱"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"向上移動"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"固定到鍵盤上方"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"複製"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"安全性選項"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"已有相同組合的快捷鍵"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"常用"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"應用程式列表"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"強制結束面板"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"快速鍵"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"我的"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"按鍵"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"復原"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"新增"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"新增快速鍵"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"顯示桌面"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"更多快速鍵"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"最多儲存 16 個自訂快速鍵"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"最多固定 8 個快速鍵"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"未知"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"本機編輯,輸入不會傳送到遠端"</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"加入常用列"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"移除"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"視窗選單"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"等待遠端平台資訊"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"管理與自訂"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"管理快速鍵"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"貼上"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"系統"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"終端機複製"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"終端機貼上"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"編輯"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"編輯快速鍵"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight 搜尋"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"請使用系統預設中的安全性選項"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"請先選字或取消組字"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"請輸入有效的快速鍵名稱"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"請選擇有效的按鍵"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"執行視窗"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"尚無自訂快速鍵"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"適用遠端"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"選擇桌面環境"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"選擇桌面環境以載入系統預設"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"鍵盤上方常用列"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Auflösung des entfernten Bildschirms"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"Nicht unterstützt"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"Warten auf Bestätigung vom Computer..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Fügen Sie diese Kopplungsdaten auf dem gesteuerten Gerät zur Vertrauensliste hinzu und prüfen Sie den Fingerabdruck."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"Kopplungsdaten dieses Geräts"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"Fingerabdruck anzeigen"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Kopplungsdaten kopieren"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Kopplungsdaten kopiert."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"Der sichere Speicher ist nicht verfügbar. Bitte entsperren Sie das Gerät und versuchen Sie es erneut"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Vorherige Seite"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Nächste Seite"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Haupttaste"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Task-Manager"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Speichern"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modifikatortasten"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Alles auswählen"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Apps wechseln"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Fenster wechseln"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Ausschneiden"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Tastenkombination"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Name"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Nach oben"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Über der Tastatur anheften"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Kopieren"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Sicherheitsoptionen"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"Diese Kombination ist bereits vorhanden"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Favoriten"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"App-Liste"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Dialog zum sofortigen Beenden"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Tastenkürzel"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Eigene"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Tasten"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Rückgängig"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"Neu"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"Neues Tastenkürzel"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Desktop anzeigen"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"Weitere Tastenkürzel"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Bis zu 16 eigene Tastenkürzel speichern"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Bis zu 8 Tastenkürzel anheften"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Unbekannt"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Zur Favoritenleiste hinzufügen"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Entfernen"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Fenstermenü"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"Warten auf Informationen zur Remote-Plattform"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Verwalten und anpassen"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Tastenkürzel verwalten"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Einfügen"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"System"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Im Terminal kopieren"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Im Terminal einfügen"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Bearbeiten"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Tastenkürzel bearbeiten"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight-Suche"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Sicherheitsoptionen in den Systemvorgaben verwenden"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Zuerst die aktuelle Texteingabe bestätigen oder abbrechen"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Namen mit 1–64 Zeichen eingeben"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Eine gültige Taste auswählen"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Ausführen-Dialog"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"Noch keine eigenen Tastenkürzel"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Remote-Plattform"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Desktop-Umgebung auswählen"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Desktop-Umgebung wählen, um Systemvorgaben zu laden"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoriten über der Tastatur"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Resolución de la pantalla remota"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"No compatible"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"Esperando confirmación del ordenador..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Añada esta información de emparejamiento a la lista de confianza del dispositivo controlado y verifique la huella."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"Información de este dispositivo"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"Ver huella"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Copiar información"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Información de emparejamiento copiada."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"No se pudo copiar la información de emparejamiento. Inténtelo de nuevo."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"El almacenamiento seguro no está disponible. Desbloquee el dispositivo e inténtelo nuevamente."</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Página anterior"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Página siguiente"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Tecla principal"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Administrador de tareas"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Guardar"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modificadores"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Seleccionar todo"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Cambiar de aplicación"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Cambiar de ventana"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Cortar"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Combinación de teclas"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nombre"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Subir"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Fijar sobre el teclado"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Copiar"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Opciones de seguridad"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"Esta combinación ya existe"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Favoritos"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"Lista de aplicaciones"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Diálogo Forzar salida"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Atajos"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mis atajos"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Teclas"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Deshacer"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"Nuevo"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"Nuevo atajo"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Mostrar escritorio"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"Más atajos"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Guarda hasta 16 atajos personalizados"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Fija hasta 8 atajos"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Desconocido"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Edición local. Lo escrito permanece en este dispositivo."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Añadir a la barra de favoritos"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Quitar"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Menú de ventana"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"Esperando información del sistema remoto"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Gestionar y personalizar"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Gestionar atajos"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Pegar"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"Sistema"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Copiar en terminal"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Pegar en terminal"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Editar"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Editar atajo"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Búsqueda de Spotlight"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Usa Opciones de seguridad en los ajustes del sistema"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Confirma o cancela primero la composición actual"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Introduce un nombre de 1 a 64 caracteres"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Selecciona una tecla válida"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Diálogo Ejecutar"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"Aún no hay atajos personalizados"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Sistema remoto"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Elegir entorno de escritorio"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Elige un entorno para cargar los ajustes del sistema"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoritos sobre el teclado"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Résolution de l’écran distant"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"Non pris en charge"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"En attente de confirmation de l\'ordinateur..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Ajoutez ces informations à la liste des appareils de confiance de l’appareil contrôlé et vérifiez l’empreinte."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"Informations de cet appareil"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"Afficher l’empreinte"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Copier les informations"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Informations d’association copiées."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"Impossible de copier les informations d’association. Réessayez."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"Le stockage sécurisé n\'est pas disponible, veuillez déverrouiller l\'appareil et réessayer"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Page précédente"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Page suivante"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Touche principale"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Gestionnaire des tâches"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Enregistrer"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Touches de modification"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Sélectionner tout"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Changer d’application"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Changer de fenêtre"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Couper"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Combinaison à envoyer"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nom"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Monter"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Épingler au-dessus du clavier"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Copier"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Options de sécurité"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"Cette combinaison existe déjà"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Favoris"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"Liste des applications"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Fenêtre Forcer à quitter"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Raccourcis"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mes raccourcis"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Touches"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Annuler l’action"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"Nouveau"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"Nouveau raccourci"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Afficher le bureau"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"Autres raccourcis"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Enregistrez jusqu’à 16 raccourcis personnalisés"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Épinglez jusqu’à 8 raccourcis"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Inconnu"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Édition locale. La saisie reste sur cet appareil."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Ajouter à la barre des favoris"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Retirer"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Menu de la fenêtre"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"En attente des informations du système distant"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Gérer et personnaliser"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Gérer les raccourcis"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Coller"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"Système"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Copier dans le terminal"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Coller dans le terminal"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Modifier"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Modifier le raccourci"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Recherche Spotlight"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Utilisez les options de sécurité des préréglages système"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Validez ou annulez d’abord la composition en cours"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Saisissez un nom de 1 à 64 caractères"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Sélectionnez une touche valide"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Fenêtre Exécuter"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"Aucun raccourci personnalisé"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Système distant"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Choisir l’environnement de bureau"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Choisissez un environnement pour charger les préréglages système"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoris au-dessus du clavier"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"接続先の画面解像度"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"サポートされていません"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"コンピューターからの確認を待っています..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"操作されるデバイスの信頼済みリストにこのペアリング情報を追加し、フィンガープリントを確認してください。"</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"このデバイスのペアリング情報"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"フィンガープリントを表示"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"ペアリング情報をコピー"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"ペアリング情報をコピーしました。"</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"ペアリング情報をコピーできません。もう一度お試しください。"</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"前のページ"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"次のページ"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"メインキー"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"タスクマネージャー"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"保存"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"修飾キー"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"全て選択"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"アプリを切り替え"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"ウインドウを切り替え"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"切り取り"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"送信するキーの組み合わせ"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"名前"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"上へ移動"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"キーボードの上に固定"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"コピー"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"セキュリティオプション"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"この組み合わせは登録済みです"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"よく使う項目"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"アプリ一覧"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"強制終了ダイアログ"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"ショートカット"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"マイ項目"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"キー"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"取り消す"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"新規"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"ショートカットを作成"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"デスクトップを表示"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"その他のショートカット"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"カスタムショートカットは16個まで保存できます"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"固定できるショートカットは8個までです"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"不明"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"ローカル編集。入力はリモートに送信されません。"</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"お気に入りバーに追加"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"取り除く"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"ウインドウメニュー"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"リモートのOS情報を待っています"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"管理とカスタマイズ"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"ショートカットを管理"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"貼り付け"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"システム"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"端末でコピー"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"端末に貼り付け"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"編集"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"ショートカットを編集"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight検索"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"システムプリセットのセキュリティオプションを使用してください"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"先に変換を確定またはキャンセルしてください"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"ショートカット名を1〜64文字で入力してください"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"有効なキーを選択してください"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"実行ダイアログ"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"カスタムショートカットはまだありません"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"対象リモートOS"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"デスクトップ環境を選択"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"デスクトップ環境を選択してプリセットを読み込みます"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"キーボード上のお気に入り"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"원격 화면 해상도"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"지원되지 않음"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"컴퓨터의 확인을 기다리는 중..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"제어 대상 장치의 신뢰 목록에 이 페어링 정보를 추가하고 지문을 확인하세요."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"이 장치의 페어링 정보"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"지문 보기"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"페어링 정보 복사"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"페어링 정보를 복사했습니다."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"페어링 정보를 복사할 수 없습니다. 다시 시도하세요."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요."</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"이전 페이지"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"다음 페이지"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"기본 키"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"작업 관리자"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"저장"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"보조 키"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"모두 선택"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"앱 전환"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"창 전환"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"잘라내기"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"전송할 키 조합"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"이름"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"위로 이동"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"키보드 위에 고정"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"복사"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"보안 옵션"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"이미 등록된 키 조합입니다"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"자주 사용"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"앱 목록"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"강제 종료 대화상자"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"단축키"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"내 단축키"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"키"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"실행 취소"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"새로 만들기"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"새 단축키"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"바탕 화면 표시"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"더 많은 단축키"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"단축키는 최대 8개까지 고정할 수 있습니다"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"알 수 없음"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"즐겨찾기 모음에 추가"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"제거"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"창 메뉴"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"원격 플랫폼 정보를 기다리는 중"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"관리 및 사용자 지정"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"단축키 관리"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"붙여넣기"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"시스템"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"터미널 복사"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"터미널 붙여넣기"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"편집"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"단축키 편집"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight 검색"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"시스템 프리셋의 보안 옵션을 사용하세요"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"먼저 조합 중인 문자를 확정하거나 취소하세요"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"단축키 이름을 1~64자로 입력하세요"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"유효한 키를 선택하세요"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"실행 대화상자"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"아직 사용자 지정 단축키가 없습니다"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"대상 원격 플랫폼"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"데스크톱 환경 선택"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"키보드 위 즐겨찾기 모음"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Resolução da tela remota"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"Não suportado"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"Aguardando confirmação do computador..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Adicione estas informações de pareamento à lista de confiança do dispositivo controlado e confira a impressão digital."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"Informações deste dispositivo"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"Ver impressão digital"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Copiar informações"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Informações de pareamento copiadas."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"Não foi possível copiar as informações de pareamento. Tente novamente."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Página anterior"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Próxima página"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Tecla principal"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Gerenciador de Tarefas"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Salvar"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modificadores"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Selecionar tudo"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Alternar aplicativos"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Alternar janelas"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Recortar"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Combinação de teclas"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Nome"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Mover para cima"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Fixar acima do teclado"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Copiar"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Opções de segurança"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"Esta combinação já existe"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Favoritos"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"Lista de aplicativos"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Janela Forçar Encerrar"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Atalhos"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Meus atalhos"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Teclas"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Desfazer"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"Novo"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"Novo atalho"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Mostrar área de trabalho"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"Mais atalhos"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Salve até 16 atalhos personalizados"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Fixe até 8 atalhos"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Desconhecido"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Edição local. A entrada fica neste dispositivo."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Adicionar à barra de favoritos"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Remover"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Menu da janela"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"Aguardando informações do sistema remoto"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Gerenciar e personalizar"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Gerenciar atalhos"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Colar"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"Sistema"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Copiar no terminal"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Colar no terminal"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Editar"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Editar atalho"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Busca do Spotlight"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Use Opções de segurança nas predefinições do sistema"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Confirme ou cancele primeiro a composição atual"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Insira um nome de 1 a 64 caracteres"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Selecione uma tecla válida"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Janela Executar"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"Ainda não há atalhos personalizados"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Sistema remoto"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Escolher ambiente de trabalho"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Escolha um ambiente para carregar as predefinições do sistema"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Favoritos acima do teclado"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Разрешение удалённого экрана"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"Не поддерживается"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"Ожидание подтверждения от компьютера..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Добавьте эти данные сопряжения в список доверенных устройств на управляемом компьютере и сверьте отпечаток."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"Данные этого устройства"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"Показать отпечаток"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Копировать данные"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Данные сопряжения скопированы."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"Не удалось скопировать данные сопряжения. Повторите попытку."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку."</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Предыдущая страница"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Следующая страница"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Основная клавиша"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Диспетчер задач"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Сохранить"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Клавиши-модификаторы"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Выбрать все"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Переключить приложение"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Переключить окно"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Вырезать"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Сочетание клавиш"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Название"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Переместить вверх"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Закрепить над клавиатурой"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Копировать"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Параметры безопасности"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"Такое сочетание уже существует"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Избранное"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"Список приложений"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Окно принудительного завершения"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Сочетания клавиш"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Мои"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Клавиши"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Отменить"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"Создать"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"Новое сочетание"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Показать рабочий стол"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"Другие сочетания"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Можно сохранить до 16 своих сочетаний"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Можно закрепить до 8 сочетаний"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Неизвестно"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Локальное редактирование. Ввод не отправляется на удалённое устройство."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Добавить на панель избранного"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Убрать"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Меню окна"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"Ожидание сведений об удалённой системе"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Управление и настройка"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Управление сочетаниями"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Вставить"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"Система"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Копировать в терминале"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Вставить в терминале"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Изменить"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Изменить сочетание"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Поиск Spotlight"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Используйте параметры безопасности из системных наборов"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Сначала подтвердите или отмените текущий ввод"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Введите название от 1 до 64 символов"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Выберите допустимую клавишу"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Окно запуска"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"Своих сочетаний пока нет"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Удалённая система"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Выберите окружение рабочего стола"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Выберите окружение, чтобы загрузить системные наборы"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Избранное над клавиатурой"</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,66 @@
|
||||
<string name="text_59c9dca5568a6cd3" formatted="false">"Remote display resolution"</string>
|
||||
<string name="text_36e0073276c0e39f" formatted="false">"Unsupported"</string>
|
||||
<string name="text_7b3440ad0f81b811" formatted="false">"Waiting for confirmation from the computer..."</string>
|
||||
<string name="text_728ea431051d269e" formatted="false">"Add this pairing information to the controlled device’s trusted list and verify the fingerprint."</string>
|
||||
<string name="text_56a998ff5c730e20" formatted="false">"This device’s pairing info"</string>
|
||||
<string name="text_976f1504103f24ac" formatted="false">"View fingerprint"</string>
|
||||
<string name="text_3aab57c59aba6543" formatted="false">"Copy pairing info"</string>
|
||||
<string name="text_cbacf1eafbca53e0" formatted="false">"Pairing information copied."</string>
|
||||
<string name="text_cde42ccdeacbbbd9" formatted="false">"Unable to copy pairing information. Please try again."</string>
|
||||
<string name="text_7383b2ae09252c59" formatted="false">"Secure storage is unavailable, please unlock the device and try again"</string>
|
||||
<string name="text_c9b9ae7a61444ab7" formatted="false">"Previous page"</string>
|
||||
<string name="text_8a8542f6964852dc" formatted="false">"Next page"</string>
|
||||
<string name="text_0c4f690eb398c12e" formatted="false">"Main key"</string>
|
||||
<string name="text_62fdb5304cf5cd9b" formatted="false">"Task Manager"</string>
|
||||
<string name="text_a3030bf8f16dc63c" formatted="false">"Save"</string>
|
||||
<string name="text_ba0e2ff8201bc6a6" formatted="false">"Modifiers"</string>
|
||||
<string name="text_3a5040b68abf75f9" formatted="false">"Select all"</string>
|
||||
<string name="text_c2d918002afcc3d4" formatted="false">"Switch apps"</string>
|
||||
<string name="text_a7fe8e2685849c31" formatted="false">"Switch windows"</string>
|
||||
<string name="text_410a8e8a6bf253ac" formatted="false">"Cut"</string>
|
||||
<string name="text_6309cd49db59d165" formatted="false">"Key combination"</string>
|
||||
<string name="text_d44e9b3d3b31d37b" formatted="false">"Name"</string>
|
||||
<string name="text_6789dd11afc7c4c5" formatted="false">"Move up"</string>
|
||||
<string name="text_352c003784b1c89e" formatted="false">"Pin above keyboard"</string>
|
||||
<string name="text_63d90d977348ab1f" formatted="false">"Copy"</string>
|
||||
<string name="text_ecb0d13f3f8bfd0d" formatted="false">"Security options"</string>
|
||||
<string name="text_dd1b1da65a6c89de" formatted="false">"This combination already exists"</string>
|
||||
<string name="text_4563c57b5143deaa" formatted="false">"Favorites"</string>
|
||||
<string name="text_6308bf1373bd0ee8" formatted="false">"App list"</string>
|
||||
<string name="text_8dc0823d700cf875" formatted="false">"Force Quit dialog"</string>
|
||||
<string name="text_ee2638183d3ea860" formatted="false">"Shortcuts"</string>
|
||||
<string name="text_7f1d9dd04cd1c7b8" formatted="false">"Mine"</string>
|
||||
<string name="text_7c7577d4d4fc6009" formatted="false">"Keys"</string>
|
||||
<string name="text_926a50b98ece2667" formatted="false">"Undo"</string>
|
||||
<string name="text_50ef2f4cf6a46924" formatted="false">"New"</string>
|
||||
<string name="text_c6904bb36f7f62c5" formatted="false">"New shortcut"</string>
|
||||
<string name="text_5cfe4846aa8a7228" formatted="false">"Show desktop"</string>
|
||||
<string name="text_feb12adf8e0315e2" formatted="false">"More shortcuts"</string>
|
||||
<string name="text_5d6bce93c0649a63" formatted="false">"Save up to 16 custom shortcuts"</string>
|
||||
<string name="text_f33cb21422f62ee8" formatted="false">"Pin up to 8 shortcuts"</string>
|
||||
<string name="text_4d8c1c5b42830791" formatted="false">"Unknown"</string>
|
||||
<string name="text_6b1fa1d7159de777" formatted="false">"Local editing. Input stays on this device."</string>
|
||||
<string name="text_cb9eaa63c25d383e" formatted="false">"Add to favorites bar"</string>
|
||||
<string name="text_6135d4159e892541" formatted="false">"Remove"</string>
|
||||
<string name="text_e693ff5832c7f46d" formatted="false">"Window menu"</string>
|
||||
<string name="text_096a247cc5455ba7" formatted="false">"Waiting for remote platform information"</string>
|
||||
<string name="text_39c7740197c41d44" formatted="false">"Manage & customize"</string>
|
||||
<string name="text_2dc8c27b75796b36" formatted="false">"Manage shortcuts"</string>
|
||||
<string name="text_33517926747180e6" formatted="false">"Paste"</string>
|
||||
<string name="text_5b50d7c4b5950dc5" formatted="false">"System"</string>
|
||||
<string name="text_45123fa07e22fb3e" formatted="false">"Terminal copy"</string>
|
||||
<string name="text_3936f3d30a763a35" formatted="false">"Terminal paste"</string>
|
||||
<string name="text_051836569928a9f9" formatted="false">"Edit"</string>
|
||||
<string name="text_0ecda10acc934f22" formatted="false">"Edit shortcut"</string>
|
||||
<string name="text_d32d2a4e0e83d7cf" formatted="false">"Spotlight search"</string>
|
||||
<string name="text_add54b3ce4b1c661" formatted="false">"Use Security options in the system presets"</string>
|
||||
<string name="text_ae066f34095261c9" formatted="false">"Confirm or cancel the current composition first"</string>
|
||||
<string name="text_a895bdb6f1f11e99" formatted="false">"Enter a shortcut name (1–64 characters)"</string>
|
||||
<string name="text_6b3213d2afed99e8" formatted="false">"Select a valid key"</string>
|
||||
<string name="text_819569ec77acb01e" formatted="false">"Run dialog"</string>
|
||||
<string name="text_22c4107f03c3533e" formatted="false">"No custom shortcuts yet"</string>
|
||||
<string name="text_b07f98d4671af3f9" formatted="false">"Remote platform"</string>
|
||||
<string name="text_4bdabfba3368869d" formatted="false">"Choose desktop environment"</string>
|
||||
<string name="text_4fa28857c6f253cc" formatted="false">"Choose a desktop environment to load system presets"</string>
|
||||
<string name="text_fc5f6f7462e8557a" formatted="false">"Favorites above keyboard"</string>
|
||||
</resources>
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
#include "controller_protocol.h"
|
||||
#include "../../../libs/controller/include/mobile_device_trust.h"
|
||||
#include "file_transfer_session.h"
|
||||
#include "latest_frame_worker.h"
|
||||
#include "video_picture.h"
|
||||
@@ -54,6 +55,7 @@ struct Session {
|
||||
MiniRtcPeerPtr* controller = nullptr;
|
||||
CallbackContext identity_context{this, false}, controller_context{this, true};
|
||||
std::string host, log_path, identity_login, controller_login, remote;
|
||||
std::unique_ptr<mobile_controller::DeviceTrustIdentity> trust;
|
||||
int port;
|
||||
std::atomic<bool> stopping{false};
|
||||
std::atomic<uint64_t> signal_generation{0};
|
||||
@@ -190,6 +192,11 @@ void Data(const char* data, size_t size, const char*, size_t,
|
||||
if (name == kFileStream) { ctx->owner->files.Receive(data, size); return; }
|
||||
if (name == kFileFeedbackStream) { ctx->owner->files.Ack(data, size); return; }
|
||||
if (name == kControlStream) {
|
||||
if (auto proof = ctx->owner->trust->ChallengeResponse(std::string_view(data, size),
|
||||
ctx->owner->controller_login.substr(2), "CrossDesk Android")) {
|
||||
ctx->owner->Send(kControlStream, *proof);
|
||||
return;
|
||||
}
|
||||
RemoteAction action{};
|
||||
if (action.from_json(std::string(data, size))) {
|
||||
const auto message = action.to_json();
|
||||
@@ -291,6 +298,26 @@ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* java_vm, void*) {
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
#define JNI_METHOD(name) Java_cn_crossdesk_mobile_NativeSession_##name
|
||||
extern "C" JNIEXPORT jstring JNICALL JNI_METHOD(nGenerateTrustKey)(JNIEnv* env, jclass) {
|
||||
try {
|
||||
auto seed = mobile_controller::DeviceTrustIdentity::GeneratePrivateKey();
|
||||
auto result = env->NewStringUTF(seed.c_str());
|
||||
mobile_controller::DeviceTrustIdentity::ClearSecret(seed);
|
||||
return result;
|
||||
} catch (const std::exception&) { return nullptr; }
|
||||
}
|
||||
extern "C" JNIEXPORT jstring JNICALL JNI_METHOD(nTrustInfo)(
|
||||
JNIEnv* env, jclass, jstring key, jstring device_id) {
|
||||
try {
|
||||
auto seed = String(env, key);
|
||||
// Clean the temporary JNI copy even when key validation fails.
|
||||
struct Clean { std::string& value; ~Clean() { mobile_controller::DeviceTrustIdentity::ClearSecret(value); } } clean{seed};
|
||||
mobile_controller::DeviceTrustIdentity trust(seed);
|
||||
const auto info = json{{"pairing", trust.PairingInfo(String(env, device_id), "CrossDesk Android")},
|
||||
{"fingerprint", trust.Fingerprint()}}.dump();
|
||||
return env->NewStringUTF(info.c_str());
|
||||
} catch (const std::exception&) { return nullptr; }
|
||||
}
|
||||
extern "C" JNIEXPORT jboolean JNICALL JNI_METHOD(nValidAppVersion)(
|
||||
JNIEnv* env, jclass, jstring version) {
|
||||
return version && env->GetStringUTFLength(version) < 64 &&
|
||||
@@ -321,7 +348,7 @@ extern "C" JNIEXPORT jstring JNICALL JNI_METHOD(nCheckMobileUpdate)(
|
||||
}
|
||||
extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nCreate)(
|
||||
JNIEnv* env, jclass, jobject owner, jstring host, jint port, jstring identity,
|
||||
jstring log_path, jstring certificates) {
|
||||
jstring log_path, jstring certificates, jstring trust_key) {
|
||||
try {
|
||||
auto s = std::make_unique<Session>();
|
||||
s->owner = env->NewGlobalRef(owner);
|
||||
@@ -332,6 +359,9 @@ extern "C" JNIEXPORT jlong JNICALL JNI_METHOD(nCreate)(
|
||||
env->DeleteLocalRef(cls);
|
||||
s->host = String(env, host); s->port = port;
|
||||
s->identity_login = String(env, identity); s->log_path = String(env, log_path);
|
||||
auto seed = String(env, trust_key);
|
||||
struct Clean { std::string& value; ~Clean() { mobile_controller::DeviceTrustIdentity::ClearSecret(value); } } clean{seed};
|
||||
s->trust = std::make_unique<mobile_controller::DeviceTrustIdentity>(seed);
|
||||
// The bundle is exported from Android's default TrustManager, never the
|
||||
// build machine's certificate store. MiniRTC on Android explicitly
|
||||
// loads this path: OpenSSL's default loader ignores SSL_CERT_FILE in an
|
||||
@@ -376,7 +406,8 @@ extern "C" JNIEXPORT void JNICALL JNI_METHOD(nReady)(JNIEnv* env, jclass, jlong
|
||||
if (!env->ExceptionCheck())
|
||||
ParseDisplayResolutionPreferences(bytes, action.preferred_display_resolutions);
|
||||
}
|
||||
s->Send(kControlStream, action.to_json()); FreeRemoteAction(action);
|
||||
s->Send(kControlStream, mobile_controller::DeviceTrustIdentity::Advertise(action.to_json()));
|
||||
FreeRemoteAction(action);
|
||||
}
|
||||
extern "C" JNIEXPORT void JNICALL JNI_METHOD(nPointer)(
|
||||
JNIEnv*, jclass, jlong handle, jfloat x, jfloat y, jint flag, jint wheel) {
|
||||
|
||||
@@ -5,12 +5,14 @@ add_rules("mode.release", "mode.debug")
|
||||
set_policy("package.precompiled", false)
|
||||
add_cxflags("-fPIC")
|
||||
includes("../../../deps/submodules/minirtc")
|
||||
add_requires("openssl3 3.3.2", {system = false, configs = {shared = false}})
|
||||
|
||||
target("crossdesk_android")
|
||||
set_kind("shared")
|
||||
add_deps("minirtc")
|
||||
add_packages("nlohmann_json", "libyuv")
|
||||
add_files("rtc_bridge.cpp", "../../../libs/wire/src/remote_action.cpp", "../../../libs/wire/src/app_version.cpp", "../../../libs/wire/src/file_transfer.cpp")
|
||||
add_packages("nlohmann_json", "libyuv", "openssl3")
|
||||
add_files("rtc_bridge.cpp", "../../../libs/wire/src/remote_action.cpp", "../../../libs/wire/src/app_version.cpp", "../../../libs/wire/src/file_transfer.cpp", "../../../libs/controller/src/mobile_device_trust.cpp")
|
||||
add_includedirs("../../../libs/controller/include")
|
||||
add_includedirs("../../../deps/submodules/minirtc/src/api", "../../../libs/wire/include")
|
||||
add_syslinks("android", "log")
|
||||
add_shflags("-Wl,-z,max-page-size=16384", "-Wl,-z,common-page-size=16384", {force = true})
|
||||
|
||||
@@ -18,6 +18,8 @@ target("crossdesk_controller")
|
||||
path.join(minirtc_dir, "src/api"), {public = true})
|
||||
add_files(path.join(controller_dir, "src/*.cpp"))
|
||||
remove_files(path.join(controller_dir, "src/file_browser.cpp"))
|
||||
-- Mobile builds compile this helper with their own OpenSSL dependency.
|
||||
remove_files(path.join(controller_dir, "src/mobile_device_trust.cpp"))
|
||||
target_end()
|
||||
|
||||
target("crossdesk_terminal")
|
||||
|
||||
@@ -47,37 +47,6 @@ normalize_app_version() {
|
||||
fi
|
||||
}
|
||||
|
||||
find_slint_runtime() {
|
||||
local needed="$1"
|
||||
local resolved=""
|
||||
|
||||
resolved="$(ldd "$BUILD_BINARY" 2>/dev/null | awk -v needed="$needed" \
|
||||
'$1 == needed && $2 == "=>" && $3 ~ /^\// { print $3; exit }')"
|
||||
if [[ -n "$resolved" && -f "$resolved" ]]; then
|
||||
echo "$resolved"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local package_roots=()
|
||||
if [[ -n "${XMAKE_GLOBALDIR:-}" ]]; then
|
||||
package_roots+=("$XMAKE_GLOBALDIR/.xmake/packages")
|
||||
fi
|
||||
package_roots+=("$HOME/.xmake/packages" "/data/.xmake/packages")
|
||||
|
||||
local package_root=""
|
||||
local candidate=""
|
||||
for package_root in "${package_roots[@]}"; do
|
||||
[[ -d "$package_root/s/slint" ]] || continue
|
||||
candidate="$(find "$package_root/s/slint" -path "*/lib/$needed" -print -quit 2>/dev/null || true)"
|
||||
if [[ -n "$candidate" ]]; then
|
||||
echo "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$INPUT_VERSION")"
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
if [[ ! "$DEB_VERSION" =~ ^[0-9][0-9A-Za-z.+:~_-]*$ ]]; then
|
||||
@@ -127,9 +96,13 @@ fi
|
||||
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
|
||||
's/.*Shared library: \[\(libslint_cpp[^]]*\)\].*/\1/p' | head -n 1)"
|
||||
if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
SLINT_LIBRARY="$(find_slint_runtime "$SLINT_NEEDED" || true)"
|
||||
if [[ -z "$SLINT_LIBRARY" ]]; then
|
||||
echo "Unable to locate the required Slint runtime: $SLINT_NEEDED" >&2
|
||||
# The build rule copies the exact linked package beside the executable.
|
||||
# Never search global caches or trust the packager's LD_LIBRARY_PATH: an
|
||||
# unpatched runtime can share both the version and SONAME of the linked one.
|
||||
SLINT_LIBRARY="$(dirname "$BUILD_BINARY")/$SLINT_NEEDED"
|
||||
if [[ ! -f "$SLINT_LIBRARY" ]]; then
|
||||
echo "Missing build-matched Slint runtime: $SLINT_LIBRARY" >&2
|
||||
echo "Build it first with: xmake b -vy crossdesk" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
@@ -138,6 +111,27 @@ if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
strip --strip-unneeded "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
fi
|
||||
|
||||
# Check the staged files with only their private runtime directory available.
|
||||
# ldd -r can exit successfully even when it reports unresolved symbols, so its
|
||||
# diagnostics must also be checked. This does not start the GUI or updater.
|
||||
for executable in crossdesk-bin crossdesk_updater; do
|
||||
dependency_report="$WORK_DIR/$executable.ldd"
|
||||
if ! env -u LD_PRELOAD -u LD_AUDIT LD_LIBRARY_PATH="$PRIVATE_DIR" \
|
||||
ldd -r "$PRIVATE_DIR/$executable" > "$dependency_report" 2>&1 || \
|
||||
grep -Eq 'not found|undefined symbol:' "$dependency_report"; then
|
||||
echo "Packaged runtime dependency check failed: $executable" >&2
|
||||
cat "$dependency_report" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
if ! grep -Fq "$SLINT_NEEDED => $PRIVATE_DIR/$SLINT_NEEDED (" "$WORK_DIR/crossdesk-bin.ldd"; then
|
||||
echo "Packaged executable does not load its private Slint runtime" >&2
|
||||
cat "$WORK_DIR/crossdesk-bin.ldd" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
|
||||
#!/bin/sh
|
||||
private_lib_dir="/usr/lib/crossdesk"
|
||||
|
||||
@@ -70,28 +70,38 @@ struct PrivateText {
|
||||
void Validate(const nlohmann::json& document) {
|
||||
Require(document.is_object() && document.at("version") == 1);
|
||||
}
|
||||
|
||||
std::optional<nlohmann::json> ReadLocked(const std::filesystem::path& directory) {
|
||||
const auto path = directory / "identity.json";
|
||||
if (std::filesystem::symlink_status(path).type() ==
|
||||
std::filesystem::file_type::not_found) return std::nullopt;
|
||||
PrivateText text{private_file::Read(path)};
|
||||
auto saved = nlohmann::json::parse(text.value, nullptr, false);
|
||||
Require(!saved.is_discarded());
|
||||
Validate(saved);
|
||||
return saved;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void Update(const std::filesystem::path& cache,
|
||||
const std::function<void(nlohmann::json&)>& update) {
|
||||
private_file::EnsureDirectory(cache, true);
|
||||
StoreLock lock(cache / "identity.json.lock");
|
||||
const auto path = cache / "identity.json";
|
||||
const bool exists = std::filesystem::symlink_status(path).type() !=
|
||||
std::filesystem::file_type::not_found;
|
||||
nlohmann::json saved = {{"version", 1}};
|
||||
if (exists) {
|
||||
PrivateText text{private_file::Read(path)};
|
||||
saved = nlohmann::json::parse(text.value, nullptr, false);
|
||||
Require(!saved.is_discarded());
|
||||
}
|
||||
Validate(saved);
|
||||
std::optional<nlohmann::json> Read(const std::filesystem::path& directory) {
|
||||
private_file::EnsureDirectory(directory, true);
|
||||
StoreLock lock(directory / "identity.json.lock");
|
||||
return ReadLocked(directory);
|
||||
}
|
||||
|
||||
void Update(const std::filesystem::path& directory,
|
||||
const std::function<void(nlohmann::json&, bool)>& update) {
|
||||
private_file::EnsureDirectory(directory, true);
|
||||
StoreLock lock(directory / "identity.json.lock");
|
||||
auto previous = ReadLocked(directory);
|
||||
auto saved = previous ? std::move(*previous) : nlohmann::json{{"version", 1}};
|
||||
PrivateText before{saved.dump()};
|
||||
update(saved);
|
||||
update(saved, previous.has_value());
|
||||
Validate(saved);
|
||||
PrivateText after{saved.dump()};
|
||||
Require(after.value.size() <= 64 * 1024);
|
||||
if (!exists || before.value != after.value) private_file::Write(path, after.value);
|
||||
if (!previous || before.value != after.value)
|
||||
private_file::Write(directory / "identity.json", after.value);
|
||||
}
|
||||
|
||||
} // namespace crossdesk::device_identity_store
|
||||
@@ -10,14 +10,17 @@
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
|
||||
namespace crossdesk::device_identity_store {
|
||||
|
||||
// The shared device signing key lives in cache/identity.json. Read the latest
|
||||
// document under a process lock, apply an update, then atomically persist it.
|
||||
// Device signing keys live in the persistent trusted-device directory.
|
||||
// Read the latest document under a process lock, then atomically persist it.
|
||||
// Throw on corruption, unsafe permissions, lock contention or persistence error.
|
||||
void Update(const std::filesystem::path& cache,
|
||||
const std::function<void(nlohmann::json&)>& update);
|
||||
// Read returns nullopt only when the identity file is absent (legacy migration).
|
||||
std::optional<nlohmann::json> Read(const std::filesystem::path& directory);
|
||||
void Update(const std::filesystem::path& directory,
|
||||
const std::function<void(nlohmann::json&, bool exists)>& update);
|
||||
|
||||
} // namespace crossdesk::device_identity_store
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
namespace crossdesk {
|
||||
void GuiRuntime::InitializeTrustedDevices(const std::filesystem::path& directory) {
|
||||
try {
|
||||
trusted_devices_ = std::make_unique<TrustedDevices>(directory, cache_path_);
|
||||
trusted_devices_ = std::make_unique<TrustedDevices>(
|
||||
directory, cache_path_, config_center_ && config_center_->IsTrustedDevicesOnly());
|
||||
} catch (const std::exception& error) {
|
||||
LOG_ERROR("Cannot load trusted devices: {}", error.what());
|
||||
// The independent persisted switch stays enabled on identity/store failure.
|
||||
@@ -44,9 +45,16 @@ bool GuiRuntime::BeginControllerTrust(const std::string& id, PeerEventHandler* e
|
||||
state.pending = required;
|
||||
state.deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
try {
|
||||
if (!trusted_devices_) state.invalid = true;
|
||||
else state.nonce = TrustedDevices::Nonce();
|
||||
} catch (...) { state.invalid = true; }
|
||||
if (!trusted_devices_) {
|
||||
state.invalid = true;
|
||||
state.failure_reason = "identity_unavailable";
|
||||
} else {
|
||||
state.nonce = TrustedDevices::Nonce();
|
||||
}
|
||||
} catch (...) {
|
||||
state.invalid = true;
|
||||
state.failure_reason = "challenge_generation_failed";
|
||||
}
|
||||
controller_trust_[id] = std::move(state);
|
||||
return !required;
|
||||
}
|
||||
@@ -111,8 +119,12 @@ bool GuiRuntime::HandleDeviceTrustMessage(const std::string& id, PeerEventHandle
|
||||
if (state.invalid || state.verified || std::chrono::steady_clock::now() >= state.deadline)
|
||||
return true;
|
||||
// Accept once per fresh challenge; malformed/replayed proofs grant nothing.
|
||||
state.verified = TrustedDevices::Verify(message, state.nonce);
|
||||
if (!state.verified) state.invalid = true;
|
||||
TrustedDevices::ProofFailure failure;
|
||||
state.verified = TrustedDevices::Verify(message, state.nonce, &failure);
|
||||
if (!state.verified) {
|
||||
state.invalid = true;
|
||||
state.failure_reason = TrustedDevices::ProofFailureReason(failure);
|
||||
}
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
// Untrusted protocol input never escapes the transport callback.
|
||||
@@ -127,6 +139,7 @@ void GuiRuntime::TickControllerTrust() {
|
||||
std::string nonce;
|
||||
int kind;
|
||||
std::string host_info;
|
||||
const char* rejection_reason = nullptr;
|
||||
};
|
||||
std::vector<Action> actions;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
@@ -139,9 +152,12 @@ void GuiRuntime::TickControllerTrust() {
|
||||
const bool trusted = state.IsTrusted(trusted_devices_.get());
|
||||
if (required && (state.invalid || (state.verified && !trusted) ||
|
||||
(!state.verified && now >= state.deadline))) {
|
||||
const char* reason = state.invalid
|
||||
? (state.failure_reason ? state.failure_reason : "invalid_proof")
|
||||
: (state.verified ? "public_key_not_trusted" : "proof_timeout");
|
||||
state.invalid = true;
|
||||
state.closing = true;
|
||||
actions.push_back({id, state.events, {}, -1});
|
||||
actions.push_back({id, state.events, {}, -1, {}, reason});
|
||||
} else if (state.pending && (!required || trusted)) {
|
||||
state.pending = false;
|
||||
actions.push_back({id, state.events, {}, 1, std::exchange(state.early_host_info, {})});
|
||||
@@ -159,7 +175,8 @@ void GuiRuntime::TickControllerTrust() {
|
||||
auto callback = events->EnterCallback();
|
||||
if (!callback) continue;
|
||||
if (action.kind < 0) {
|
||||
LOG_WARN("Reject controller: trusted device verification required");
|
||||
LOG_WARN("Reject controller: trusted device verification required; reason={}",
|
||||
action.rejection_reason);
|
||||
transfers_.Cancel(events.get(), action.id);
|
||||
remote_input_.Disconnect(action.id);
|
||||
CloseServerController(action.id);
|
||||
|
||||
@@ -140,6 +140,7 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
std::string nonce;
|
||||
std::optional<TrustedDevices::Device> verified;
|
||||
std::string early_host_info;
|
||||
const char* failure_reason = nullptr;
|
||||
std::chrono::steady_clock::time_point deadline{}, next_challenge{};
|
||||
bool pending = false, invalid = false, closing = false, capable = false;
|
||||
bool IsTrusted(const TrustedDevices* devices) const {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
constexpr size_t kMaxTrustedDevices = 128;
|
||||
void Require(bool ok) {
|
||||
if (!ok) throw std::runtime_error("Invalid or unavailable trusted device identity");
|
||||
void Require(bool ok, const char* error = "Invalid or unavailable trusted device identity") {
|
||||
if (!ok) throw std::runtime_error(error);
|
||||
}
|
||||
std::string Hex(const uint8_t* data, size_t size) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
@@ -55,11 +55,14 @@ struct PrivateText {
|
||||
}
|
||||
|
||||
TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||
const std::filesystem::path& cache)
|
||||
const std::filesystem::path& cache,
|
||||
bool require_existing)
|
||||
: path_(directory / "devices.json") {
|
||||
private_file::EnsureDirectory(directory);
|
||||
const bool exists = std::filesystem::symlink_status(path_).type() !=
|
||||
std::filesystem::file_type::not_found;
|
||||
Require(exists || !require_existing,
|
||||
"Trusted device list is missing while restrictions are enabled; restore the original configuration");
|
||||
PrivateText legacy_key;
|
||||
if (exists) {
|
||||
PrivateText text{private_file::Read(path_)};
|
||||
@@ -81,7 +84,24 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||
}
|
||||
}
|
||||
|
||||
device_identity_store::Update(cache, [&](nlohmann::json& identity) {
|
||||
device_identity_store::Update(directory, [&](nlohmann::json& identity, bool identity_exists) {
|
||||
bool legacy_identity_exists = false;
|
||||
if (!identity_exists) {
|
||||
// The persistent copy is authoritative once migration has completed.
|
||||
// Leave the old cache copy intact so an interrupted migration is retryable.
|
||||
if (auto previous = device_identity_store::Read(cache)) {
|
||||
identity = std::move(*previous);
|
||||
legacy_identity_exists = true;
|
||||
}
|
||||
} else {
|
||||
Require(identity.contains("trust_private_key"),
|
||||
"Persistent device signing key is missing; restore the original identity");
|
||||
}
|
||||
// Only a never-completed first initialization may create an empty list.
|
||||
// Older identity stores predate this marker and must already have a list.
|
||||
Require(exists || (!legacy_identity_exists &&
|
||||
(!identity_exists || !identity.value("trusted_devices_initialized", true))),
|
||||
"Trusted device list is missing from an existing installation; restore the original list");
|
||||
if (!identity.contains("trust_private_key") && !legacy_key.value.empty())
|
||||
identity["trust_private_key"] = legacy_key.value;
|
||||
if (identity.contains("trust_private_key")) {
|
||||
@@ -94,6 +114,8 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||
secret.data(), secret.size()));
|
||||
OPENSSL_cleanse(secret.data(), secret.size());
|
||||
} else {
|
||||
Require(!exists && !legacy_identity_exists,
|
||||
"Device signing key is missing from an existing installation; restore the original identity");
|
||||
key_.reset(EVP_PKEY_Q_keygen(nullptr, nullptr, "ED25519"));
|
||||
Require(bool(key_));
|
||||
std::array<uint8_t, 32> secret{};
|
||||
@@ -103,6 +125,7 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||
OPENSSL_cleanse(secret.data(), secret.size());
|
||||
}
|
||||
Require(bool(key_));
|
||||
identity["trusted_devices_initialized"] = exists;
|
||||
});
|
||||
|
||||
std::array<uint8_t, 32> public_key{};
|
||||
@@ -112,6 +135,13 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
|
||||
// The identity update is durable before removing the legacy secret. If this
|
||||
// write fails, reopening can safely retry with the same key and device list.
|
||||
if (!exists || !legacy_key.value.empty()) Save(devices_);
|
||||
if (!exists) {
|
||||
device_identity_store::Update(directory, [](nlohmann::json& identity, bool present) {
|
||||
Require(present && identity.contains("trust_private_key"),
|
||||
"Device identity disappeared during initialization");
|
||||
identity["trusted_devices_initialized"] = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::string TrustedDevices::Fingerprint(const std::string& public_key) {
|
||||
@@ -172,12 +202,18 @@ std::string TrustedDevices::Sign(std::string_view text) const {
|
||||
}
|
||||
|
||||
std::optional<TrustedDevices::Device> TrustedDevices::Verify(
|
||||
const nlohmann::json& proof, const std::string& nonce) {
|
||||
const nlohmann::json& proof, const std::string& nonce, ProofFailure* failure) {
|
||||
if (failure) *failure = ProofFailure::None;
|
||||
const auto reject = [failure](ProofFailure reason) -> std::optional<Device> {
|
||||
if (failure) *failure = reason;
|
||||
return std::nullopt;
|
||||
};
|
||||
try {
|
||||
if (proof.at("version") != 1 || proof.at("nonce") != nonce || !IsHex(nonce, 32))
|
||||
return std::nullopt;
|
||||
if (proof.at("version") != 1) return reject(ProofFailure::UnsupportedVersion);
|
||||
if (!IsHex(nonce, 32)) return reject(ProofFailure::Malformed);
|
||||
if (proof.at("nonce") != nonce) return reject(ProofFailure::NonceMismatch);
|
||||
Device device{proof.at("public_key"), proof.at("name"), proof.at("device_id")};
|
||||
if (!Label(device.name) || !Label(device.device_id)) return std::nullopt;
|
||||
if (!Label(device.name) || !Label(device.device_id)) return reject(ProofFailure::Malformed);
|
||||
const auto public_key = Unhex(device.public_key, 32);
|
||||
const auto signature = Unhex(proof.at("signature"), 64);
|
||||
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key(
|
||||
@@ -185,12 +221,28 @@ std::optional<TrustedDevices::Device> TrustedDevices::Verify(
|
||||
EVP_PKEY_free);
|
||||
Context context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
const auto text = Transcript(nonce, device);
|
||||
if (key && context && EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) == 1 &&
|
||||
EVP_DigestVerify(context.get(), signature.data(), signature.size(),
|
||||
reinterpret_cast<const uint8_t*>(text.data()), text.size()) == 1)
|
||||
return device;
|
||||
} catch (const std::exception&) {}
|
||||
return std::nullopt;
|
||||
if (!key || !context ||
|
||||
EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) != 1)
|
||||
return reject(ProofFailure::CryptoUnavailable);
|
||||
const int verified = EVP_DigestVerify(context.get(), signature.data(), signature.size(),
|
||||
reinterpret_cast<const uint8_t*>(text.data()), text.size());
|
||||
if (verified == 1) return device;
|
||||
return reject(verified == 0 ? ProofFailure::InvalidSignature : ProofFailure::CryptoUnavailable);
|
||||
} catch (const std::exception&) {
|
||||
return reject(ProofFailure::Malformed);
|
||||
}
|
||||
}
|
||||
|
||||
const char* TrustedDevices::ProofFailureReason(ProofFailure failure) {
|
||||
switch (failure) {
|
||||
case ProofFailure::None: return "none";
|
||||
case ProofFailure::Malformed: return "malformed_proof";
|
||||
case ProofFailure::UnsupportedVersion: return "unsupported_proof_version";
|
||||
case ProofFailure::NonceMismatch: return "nonce_mismatch";
|
||||
case ProofFailure::InvalidSignature: return "invalid_signature";
|
||||
case ProofFailure::CryptoUnavailable: return "crypto_unavailable";
|
||||
}
|
||||
return "invalid_proof";
|
||||
}
|
||||
|
||||
bool TrustedDevices::Contains(const std::string& public_key) const {
|
||||
|
||||
@@ -27,9 +27,18 @@ class TrustedDevices {
|
||||
std::string public_key, name, device_id;
|
||||
bool operator==(const Device&) const = default;
|
||||
};
|
||||
enum class ProofFailure {
|
||||
None,
|
||||
Malformed,
|
||||
UnsupportedVersion,
|
||||
NonceMismatch,
|
||||
InvalidSignature,
|
||||
CryptoUnavailable
|
||||
};
|
||||
|
||||
TrustedDevices(const std::filesystem::path& directory,
|
||||
const std::filesystem::path& cache);
|
||||
const std::filesystem::path& cache,
|
||||
bool require_existing = false);
|
||||
std::string Fingerprint() const;
|
||||
const std::string& PublicKey() const { return public_key_; }
|
||||
// Callers supply a canonical, purpose-specific transcript.
|
||||
@@ -39,7 +48,9 @@ class TrustedDevices {
|
||||
nlohmann::json Prove(const std::string& nonce,
|
||||
const std::string& device_id, const std::string& name) const;
|
||||
static std::optional<Device> Verify(const nlohmann::json& proof,
|
||||
const std::string& nonce);
|
||||
const std::string& nonce, ProofFailure* failure = nullptr);
|
||||
// Stable diagnostic codes contain no peer-supplied data or key material.
|
||||
static const char* ProofFailureReason(ProofFailure failure);
|
||||
static constexpr size_t kMaxPairingInfoSize = 2048;
|
||||
std::string PairingInfo(const std::string& device_id, const std::string& name) const;
|
||||
static std::optional<Device> ParsePairingInfo(std::string_view text);
|
||||
|
||||
@@ -17,7 +17,8 @@ std::filesystem::path Home() {
|
||||
std::filesystem::path EnvOrDefault(const char* name,
|
||||
const std::filesystem::path& fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
return value ? std::filesystem::path(value) : fallback;
|
||||
const std::filesystem::path path = value ? value : "";
|
||||
return path.is_absolute() ? path : fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -16,7 +16,8 @@ std::filesystem::path Home() {
|
||||
std::filesystem::path EnvOrDefault(const char* name,
|
||||
const std::filesystem::path& fallback) {
|
||||
const char* value = std::getenv(name);
|
||||
return value ? std::filesystem::path(value) : fallback;
|
||||
const std::filesystem::path path = value ? value : "";
|
||||
return path.is_absolute() ? path : fallback;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -270,7 +270,9 @@ void SharedHostIdentity::Import() {
|
||||
private_file::EnsureDirectory(cache_, true);
|
||||
private_file::EnsureDirectory(trusted_);
|
||||
Json identity{{"version", 1}};
|
||||
if (std::filesystem::exists(cache_ / "identity.json"))
|
||||
if (std::filesystem::exists(trusted_ / "identity.json"))
|
||||
identity = PrivateDocument(trusted_ / "identity.json");
|
||||
else if (std::filesystem::exists(cache_ / "identity.json"))
|
||||
identity = PrivateDocument(cache_ / "identity.json");
|
||||
Require(identity.is_object() && identity.value("version", 0) == 1,
|
||||
"Invalid existing device identity store");
|
||||
@@ -284,7 +286,7 @@ void SharedHostIdentity::Import() {
|
||||
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
|
||||
// Validate everything before replacing any user identity data. Both stores
|
||||
// stay private to this process's user; their DACLs are never relaxed.
|
||||
private_file::Write(cache_ / "identity.json", identity.dump());
|
||||
private_file::Write(trusted_ / "identity.json", identity.dump());
|
||||
private_file::Write(trusted_ / "devices.json", data.at("trusted_devices").dump());
|
||||
private_file::Write(cache_ / "secure_cache_v3.enc",
|
||||
std::string(reinterpret_cast<const char*>(&cache), sizeof(cache)));
|
||||
@@ -303,7 +305,10 @@ bool SharedHostIdentity::Publish(const std::string& host_config, std::string& er
|
||||
std::memset(cache.base.iv, 0, sizeof(cache.base.iv));
|
||||
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
|
||||
const std::string identity_cache(reinterpret_cast<const char*>(&cache), sizeof(cache));
|
||||
const auto identity = PrivateDocument(cache_ / "identity.json");
|
||||
// CLI setup can publish an installation that has not run GUI migration yet.
|
||||
const auto identity = PrivateDocument(
|
||||
std::filesystem::exists(trusted_ / "identity.json")
|
||||
? trusted_ / "identity.json" : cache_ / "identity.json");
|
||||
const auto key = identity.at("trust_private_key").get<std::string>();
|
||||
Require(Unhex(key).size() == 32, "Invalid device identity key");
|
||||
const auto devices = PrivateDocument(trusted_ / "devices.json");
|
||||
|
||||
@@ -20,7 +20,7 @@ function setup_targets()
|
||||
"CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
||||
|
||||
local function copy_slint_runtime(target)
|
||||
if not target:is_plat("windows") then
|
||||
if not target:is_plat("windows", "linux") then
|
||||
return
|
||||
end
|
||||
|
||||
@@ -28,8 +28,16 @@ function setup_targets()
|
||||
if not slint and target:dep("gui") then
|
||||
slint = target:dep("gui"):pkg("slint")
|
||||
end
|
||||
assert(slint, "the Slint package is required to copy its Windows runtime")
|
||||
assert(slint, "the Slint package is required to copy its runtime")
|
||||
local runtime_dir = path.join(slint:installdir(), "lib")
|
||||
if target:is_plat("linux") then
|
||||
-- Package the runtime selected for this build. Multiple Slint builds
|
||||
-- with the same version/SONAME can have different patched APIs.
|
||||
local runtime_so = path.join(runtime_dir, "libslint_cpp.so")
|
||||
assert(os.isfile(runtime_so), "Slint runtime not found: " .. runtime_so)
|
||||
os.cp(runtime_so, target:targetdir())
|
||||
return
|
||||
end
|
||||
local runtime_dll = path.join(runtime_dir, "slint_cpp.dll")
|
||||
assert(os.isfile(runtime_dll), "Slint runtime not found: " .. runtime_dll)
|
||||
import("utils.binary.deplibs")
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
|
||||
手机控制端的“设置 → 屏幕方向 → 连接建立后锁定横屏”默认关闭并保存在本机。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受此选项影响。退出会话恢复首页方向,返回后台会话沿用该会话的方向设置。已移除检测手机姿态后解除初始横屏的逻辑。
|
||||
|
||||
## 移动端快捷键
|
||||
|
||||
手机和平板在现有悬浮键盘上提供常用快捷键栏和“更多”入口;更多面板替换按键区域,包含“常用 / 按键 / 系统 / 我的”。预设根据远端 Windows、macOS、Linux 适配,Linux 系统预设需选择 GNOME 或 KDE Plasma。当前输入入口仍是 ArkUI 悬浮键盘,未改为系统输入法。
|
||||
|
||||
管理页独立处理本地输入,支持 16 个自定义组合、每个远端平台 8 个固定项,以及排序、编辑和删除。配置保存在本机,组合键使用同一条原生队列任务发送,按反序释放修饰键,并校验连接代次。
|
||||
|
||||
## 移动端可信设备鉴权
|
||||
|
||||
手机和平板的“设置 → 本机配对信息”支持查看指纹和复制配对信息。在被控电脑的“管理可信设备 → 添加可信设备”中粘贴并核对指纹后,可以连接开启“仅允许可信设备控制”的电脑;连接密码仍需验证。
|
||||
|
||||
移动模式使用共享 Ed25519 挑战响应协议,私钥通过 AssetStore 持久保存并禁止同步,不放入缓存、日志或配对信息。重启、清理缓存、更换服务器和本机登录 ID 不改变公钥;已初始化的密钥损坏、不可用或丢失时报告错误,不自动换钥。清除应用数据或重新安装后需要重新配对。
|
||||
|
||||
## 构建
|
||||
|
||||
依赖仓库沿用 Windows/macOS/Linux 的 Xmake 策略:使用默认仓库或调用环境指定的 `XMAKE_GLOBALDIR` / `XMAKE_MAIN_REPO`,不固定或校验仓库提交。鸿蒙 PC 与移动端共用此规则,SDK、编译产物和依赖安装缓存仍按平台隔离。
|
||||
|
||||
@@ -3,6 +3,40 @@
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
napi_value TrustKey(napi_env env, napi_callback_info) {
|
||||
try {
|
||||
const auto seed = crossdesk::harmony::MobileGenerateTrustKey();
|
||||
napi_value result;
|
||||
napi_create_string_utf8(env, seed.data(), seed.size(), &result);
|
||||
return result;
|
||||
} catch (const std::exception&) {
|
||||
napi_throw_error(env, nullptr, "Device signing identity unavailable"); return nullptr;
|
||||
}
|
||||
}
|
||||
napi_value TrustInfo(napi_env env, napi_callback_info info) {
|
||||
size_t count = 2; napi_value arguments[2];
|
||||
if (napi_get_cb_info(env, info, &count, arguments, nullptr, nullptr) != napi_ok || count != 2) {
|
||||
napi_throw_type_error(env, nullptr, "Expected device identity and public ID"); return nullptr;
|
||||
}
|
||||
std::string values[2];
|
||||
for (size_t i = 0; i < 2; ++i) {
|
||||
size_t size = 0;
|
||||
if (napi_get_value_string_utf8(env, arguments[i], nullptr, 0, &size) != napi_ok || size > 128) {
|
||||
napi_throw_type_error(env, nullptr, "Invalid device identity"); return nullptr;
|
||||
}
|
||||
values[i].resize(size + 1);
|
||||
napi_get_value_string_utf8(env, arguments[i], values[i].data(), values[i].size(), &size);
|
||||
values[i].resize(size);
|
||||
}
|
||||
try {
|
||||
const auto info = crossdesk::harmony::MobileTrustInfo(values[0], values[1]);
|
||||
napi_value result;
|
||||
napi_create_string_utf8(env, info.data(), info.size(), &result);
|
||||
return result;
|
||||
} catch (const std::exception&) {
|
||||
napi_throw_error(env, nullptr, "Device signing identity unavailable"); return nullptr;
|
||||
}
|
||||
}
|
||||
napi_value Command(napi_env env, napi_callback_info info) {
|
||||
size_t count = 1; napi_value argument;
|
||||
if (napi_get_cb_info(env, info, &count, &argument, nullptr, nullptr) != napi_ok || count != 1) {
|
||||
@@ -24,6 +58,8 @@ napi_value Poll(napi_env env, napi_callback_info) {
|
||||
}
|
||||
napi_value Init(napi_env env, napi_value exports) {
|
||||
napi_property_descriptor properties[] = {
|
||||
{"mobileGenerateTrustKey", nullptr, TrustKey, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"mobileTrustInfo", nullptr, TrustInfo, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"mobileCommand", nullptr, Command, nullptr, nullptr, nullptr, napi_default, nullptr},
|
||||
{"mobilePoll", nullptr, Poll, nullptr, nullptr, nullptr, napi_default, nullptr}};
|
||||
napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties);
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export const mobileCommand: (json: string) => void;
|
||||
export const mobilePoll: () => string;
|
||||
export const mobileGenerateTrustKey: () => string;
|
||||
export const mobileTrustInfo: (seed: string, deviceId: string) => string;
|
||||
@@ -242,4 +242,64 @@ export const translations: Record<string, string[]> = {
|
||||
"鼠标控制": ["鼠标控制", "Mouse control", "Управление мышью", "滑鼠控制", "マウスコントロール", "마우스 제어", "Maussteuerung", "Contrôle de la souris", "Control del ratón", "Controle do mouse"],
|
||||
"被控端分辨率": ["被控端分辨率", "Remote display resolution", "Разрешение удалённого экрана", "被控端解析度", "接続先の画面解像度", "원격 화면 해상도", "Auflösung des entfernten Bildschirms", "Résolution de l’écran distant", "Resolución de la pantalla remota", "Resolução da tela remota"],
|
||||
"设备不支持": ["设备不支持", "Unsupported", "Не поддерживается", "裝置不支援", "サポートされていません", "지원되지 않음", "Nicht unterstützt", "Non pris en charge", "No compatible", "Não suportado"],
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。": ["将配对信息添加到被控端的可信设备列表,并核对指纹。", "Add this pairing information to the controlled device’s trusted list and verify the fingerprint.", "Добавьте эти данные сопряжения в список доверенных устройств на управляемом компьютере и сверьте отпечаток.", "將配對資訊新增至被控端的可信裝置清單,並核對指紋。", "操作されるデバイスの信頼済みリストにこのペアリング情報を追加し、フィンガープリントを確認してください。", "제어 대상 장치의 신뢰 목록에 이 페어링 정보를 추가하고 지문을 확인하세요.", "Fügen Sie diese Kopplungsdaten auf dem gesteuerten Gerät zur Vertrauensliste hinzu und prüfen Sie den Fingerabdruck.", "Ajoutez ces informations à la liste des appareils de confiance de l’appareil contrôlé et vérifiez l’empreinte.", "Añada esta información de emparejamiento a la lista de confianza del dispositivo controlado y verifique la huella.", "Adicione estas informações de pareamento à lista de confiança do dispositivo controlado e confira a impressão digital."],
|
||||
"本机配对信息": ["本机配对信息", "This device’s pairing info", "Данные этого устройства", "本機配對資訊", "このデバイスのペアリング情報", "이 장치의 페어링 정보", "Kopplungsdaten dieses Geräts", "Informations de cet appareil", "Información de este dispositivo", "Informações deste dispositivo"],
|
||||
"查看指纹": ["查看指纹", "View fingerprint", "Показать отпечаток", "查看指紋", "フィンガープリントを表示", "지문 보기", "Fingerabdruck anzeigen", "Afficher l’empreinte", "Ver huella", "Ver impressão digital"],
|
||||
"复制配对信息": ["复制配对信息", "Copy pairing info", "Копировать данные", "複製配對資訊", "ペアリング情報をコピー", "페어링 정보 복사", "Kopplungsdaten kopieren", "Copier les informations", "Copiar información", "Copiar informações"],
|
||||
"配对信息已复制。": ["配对信息已复制。", "Pairing information copied.", "Данные сопряжения скопированы.", "配對資訊已複製。", "ペアリング情報をコピーしました。", "페어링 정보를 복사했습니다.", "Kopplungsdaten kopiert.", "Informations d’association copiées.", "Información de emparejamiento copiada.", "Informações de pareamento copiadas."],
|
||||
"无法复制配对信息,请重试。": ["无法复制配对信息,请重试。", "Unable to copy pairing information. Please try again.", "Не удалось скопировать данные сопряжения. Повторите попытку.", "無法複製配對資訊,請重試。", "ペアリング情報をコピーできません。もう一度お試しください。", "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", "Impossible de copier les informations d’association. Réessayez.", "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", "Não foi possível copiar as informações de pareamento. Tente novamente."],
|
||||
"上一页": ["上一页", "Previous page", "Предыдущая страница", "上一頁", "前のページ", "이전 페이지", "Vorherige Seite", "Page précédente", "Página anterior", "Página anterior"],
|
||||
"下一页": ["下一页", "Next page", "Следующая страница", "下一頁", "次のページ", "다음 페이지", "Nächste Seite", "Page suivante", "Página siguiente", "Próxima página"],
|
||||
"主按键": ["主按键", "Main key", "Основная клавиша", "主按鍵", "メインキー", "기본 키", "Haupttaste", "Touche principale", "Tecla principal", "Tecla principal"],
|
||||
"任务管理器": ["任务管理器", "Task Manager", "Диспетчер задач", "工作管理員", "タスクマネージャー", "작업 관리자", "Task-Manager", "Gestionnaire des tâches", "Administrador de tareas", "Gerenciador de Tarefas"],
|
||||
"保存": ["保存", "Save", "Сохранить", "儲存", "保存", "저장", "Speichern", "Enregistrer", "Guardar", "Salvar"],
|
||||
"修饰键": ["修饰键", "Modifiers", "Клавиши-модификаторы", "修飾鍵", "修飾キー", "보조 키", "Modifikatortasten", "Touches de modification", "Modificadores", "Modificadores"],
|
||||
"切换应用": ["切换应用", "Switch apps", "Переключить приложение", "切換應用程式", "アプリを切り替え", "앱 전환", "Apps wechseln", "Changer d’application", "Cambiar de aplicación", "Alternar aplicativos"],
|
||||
"切换窗口": ["切换窗口", "Switch windows", "Переключить окно", "切換視窗", "ウインドウを切り替え", "창 전환", "Fenster wechseln", "Changer de fenêtre", "Cambiar de ventana", "Alternar janelas"],
|
||||
"剪切": ["剪切", "Cut", "Вырезать", "剪下", "切り取り", "잘라내기", "Ausschneiden", "Couper", "Cortar", "Recortar"],
|
||||
"发送组合": ["发送组合", "Key combination", "Сочетание клавиш", "傳送組合", "送信するキーの組み合わせ", "전송할 키 조합", "Tastenkombination", "Combinaison à envoyer", "Combinación de teclas", "Combinação de teclas"],
|
||||
"名称": ["名称", "Name", "Название", "名稱", "名前", "이름", "Name", "Nom", "Nombre", "Nome"],
|
||||
"向上移动": ["向上移动", "Move up", "Переместить вверх", "向上移動", "上へ移動", "위로 이동", "Nach oben", "Monter", "Subir", "Mover para cima"],
|
||||
"固定到键盘上方": ["固定到键盘上方", "Pin above keyboard", "Закрепить над клавиатурой", "固定到鍵盤上方", "キーボードの上に固定", "키보드 위에 고정", "Über der Tastatur anheften", "Épingler au-dessus du clavier", "Fijar sobre el teclado", "Fixar acima do teclado"],
|
||||
"安全选项": ["安全选项", "Security options", "Параметры безопасности", "安全性選項", "セキュリティオプション", "보안 옵션", "Sicherheitsoptionen", "Options de sécurité", "Opciones de seguridad", "Opções de segurança"],
|
||||
"已有相同组合的快捷键": ["已有相同组合的快捷键", "This combination already exists", "Такое сочетание уже существует", "已有相同組合的快捷鍵", "この組み合わせは登録済みです", "이미 등록된 키 조합입니다", "Diese Kombination ist bereits vorhanden", "Cette combinaison existe déjà", "Esta combinación ya existe", "Esta combinação já existe"],
|
||||
"常用": ["常用", "Favorites", "Избранное", "常用", "よく使う項目", "자주 사용", "Favoriten", "Favoris", "Favoritos", "Favoritos"],
|
||||
"应用列表": ["应用列表", "App list", "Список приложений", "應用程式列表", "アプリ一覧", "앱 목록", "App-Liste", "Liste des applications", "Lista de aplicaciones", "Lista de aplicativos"],
|
||||
"强制退出面板": ["强制退出面板", "Force Quit dialog", "Окно принудительного завершения", "強制結束面板", "強制終了ダイアログ", "강제 종료 대화상자", "Dialog zum sofortigen Beenden", "Fenêtre Forcer à quitter", "Diálogo Forzar salida", "Janela Forçar Encerrar"],
|
||||
"快捷键": ["快捷键", "Shortcuts", "Сочетания клавиш", "快速鍵", "ショートカット", "단축키", "Tastenkürzel", "Raccourcis", "Atajos", "Atalhos"],
|
||||
"我的": ["我的", "Mine", "Мои", "我的", "マイ項目", "내 단축키", "Eigene", "Mes raccourcis", "Mis atajos", "Meus atalhos"],
|
||||
"按键": ["按键", "Keys", "Клавиши", "按鍵", "キー", "키", "Tasten", "Touches", "Teclas", "Teclas"],
|
||||
"撤销": ["撤销", "Undo", "Отменить", "復原", "取り消す", "실행 취소", "Rückgängig", "Annuler l’action", "Deshacer", "Desfazer"],
|
||||
"新建": ["新建", "New", "Создать", "新增", "新規", "새로 만들기", "Neu", "Nouveau", "Nuevo", "Novo"],
|
||||
"新建快捷键": ["新建快捷键", "New shortcut", "Новое сочетание", "新增快速鍵", "ショートカットを作成", "새 단축키", "Neues Tastenkürzel", "Nouveau raccourci", "Nuevo atajo", "Novo atalho"],
|
||||
"显示桌面": ["显示桌面", "Show desktop", "Показать рабочий стол", "顯示桌面", "デスクトップを表示", "바탕 화면 표시", "Desktop anzeigen", "Afficher le bureau", "Mostrar escritorio", "Mostrar área de trabalho"],
|
||||
"更多快捷键": ["更多快捷键", "More shortcuts", "Другие сочетания", "更多快速鍵", "その他のショートカット", "더 많은 단축키", "Weitere Tastenkürzel", "Autres raccourcis", "Más atajos", "Mais atalhos"],
|
||||
"最多保存 16 个自定义快捷键": ["最多保存 16 个自定义快捷键", "Save up to 16 custom shortcuts", "Можно сохранить до 16 своих сочетаний", "最多儲存 16 個自訂快速鍵", "カスタムショートカットは16個まで保存できます", "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다", "Bis zu 16 eigene Tastenkürzel speichern", "Enregistrez jusqu’à 16 raccourcis personnalisés", "Guarda hasta 16 atajos personalizados", "Salve até 16 atalhos personalizados"],
|
||||
"最多固定 8 个快捷键": ["最多固定 8 个快捷键", "Pin up to 8 shortcuts", "Можно закрепить до 8 сочетаний", "最多固定 8 個快速鍵", "固定できるショートカットは8個までです", "단축키는 최대 8개까지 고정할 수 있습니다", "Bis zu 8 Tastenkürzel anheften", "Épinglez jusqu’à 8 raccourcis", "Fija hasta 8 atajos", "Fixe até 8 atalhos"],
|
||||
"未知": ["未知", "Unknown", "Неизвестно", "未知", "不明", "알 수 없음", "Unbekannt", "Inconnu", "Desconocido", "Desconhecido"],
|
||||
"本地编辑,输入不会发送到远端": ["本地编辑,输入不会发送到远端", "Local editing. Input stays on this device.", "Локальное редактирование. Ввод не отправляется на удалённое устройство.", "本機編輯,輸入不會傳送到遠端", "ローカル編集。入力はリモートに送信されません。", "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.", "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.", "Édition locale. La saisie reste sur cet appareil.", "Edición local. Lo escrito permanece en este dispositivo.", "Edição local. A entrada fica neste dispositivo."],
|
||||
"添加到常用栏": ["添加到常用栏", "Add to favorites bar", "Добавить на панель избранного", "加入常用列", "お気に入りバーに追加", "즐겨찾기 모음에 추가", "Zur Favoritenleiste hinzufügen", "Ajouter à la barre des favoris", "Añadir a la barra de favoritos", "Adicionar à barra de favoritos"],
|
||||
"电脑键盘": ["电脑键盘", "Computer keyboard", "Компьютерная клавиатура", "電腦鍵盤", "コンピューターのキーボード", "컴퓨터 키보드", "Computertastatur", "Clavier d'ordinateur", "Teclado de computadora", "Teclado de computador"],
|
||||
"移除": ["移除", "Remove", "Убрать", "移除", "取り除く", "제거", "Entfernen", "Retirer", "Quitar", "Remover"],
|
||||
"窗口菜单": ["窗口菜单", "Window menu", "Меню окна", "視窗選單", "ウインドウメニュー", "창 메뉴", "Fenstermenü", "Menu de la fenêtre", "Menú de ventana", "Menu da janela"],
|
||||
"等待远端平台信息": ["等待远端平台信息", "Waiting for remote platform information", "Ожидание сведений об удалённой системе", "等待遠端平台資訊", "リモートのOS情報を待っています", "원격 플랫폼 정보를 기다리는 중", "Warten auf Informationen zur Remote-Plattform", "En attente des informations du système distant", "Esperando información del sistema remoto", "Aguardando informações do sistema remoto"],
|
||||
"管理与自定义": ["管理与自定义", "Manage & customize", "Управление и настройка", "管理與自訂", "管理とカスタマイズ", "관리 및 사용자 지정", "Verwalten und anpassen", "Gérer et personnaliser", "Gestionar y personalizar", "Gerenciar e personalizar"],
|
||||
"管理快捷键": ["管理快捷键", "Manage shortcuts", "Управление сочетаниями", "管理快速鍵", "ショートカットを管理", "단축키 관리", "Tastenkürzel verwalten", "Gérer les raccourcis", "Gestionar atajos", "Gerenciar atalhos"],
|
||||
"系统": ["系统", "System", "Система", "系統", "システム", "시스템", "System", "Système", "Sistema", "Sistema"],
|
||||
"系统键盘": ["系统键盘", "System keyboard", "Системная клавиатура", "系統鍵盤", "システムキーボード", "시스템 키보드", "Systemtastatur", "Clavier système", "Teclado del sistema", "Teclado do sistema"],
|
||||
"终端复制": ["终端复制", "Terminal copy", "Копировать в терминале", "終端機複製", "端末でコピー", "터미널 복사", "Im Terminal kopieren", "Copier dans le terminal", "Copiar en terminal", "Copiar no terminal"],
|
||||
"终端粘贴": ["终端粘贴", "Terminal paste", "Вставить в терминале", "終端機貼上", "端末に貼り付け", "터미널 붙여넣기", "Im Terminal einfügen", "Coller dans le terminal", "Pegar en terminal", "Colar no terminal"],
|
||||
"编辑": ["编辑", "Edit", "Изменить", "編輯", "編集", "편집", "Bearbeiten", "Modifier", "Editar", "Editar"],
|
||||
"编辑快捷键": ["编辑快捷键", "Edit shortcut", "Изменить сочетание", "編輯快速鍵", "ショートカットを編集", "단축키 편집", "Tastenkürzel bearbeiten", "Modifier le raccourci", "Editar atajo", "Editar atalho"],
|
||||
"聚焦搜索": ["聚焦搜索", "Spotlight search", "Поиск Spotlight", "Spotlight 搜尋", "Spotlight検索", "Spotlight 검색", "Spotlight-Suche", "Recherche Spotlight", "Búsqueda de Spotlight", "Busca do Spotlight"],
|
||||
"请使用系统预设中的安全选项": ["请使用系统预设中的安全选项", "Use Security options in the system presets", "Используйте параметры безопасности из системных наборов", "請使用系統預設中的安全性選項", "システムプリセットのセキュリティオプションを使用してください", "시스템 프리셋의 보안 옵션을 사용하세요", "Sicherheitsoptionen in den Systemvorgaben verwenden", "Utilisez les options de sécurité des préréglages système", "Usa Opciones de seguridad en los ajustes del sistema", "Use Opções de segurança nas predefinições do sistema"],
|
||||
"请先选词或取消组词": ["请先选词或取消组词", "Confirm or cancel the current composition first", "Сначала подтвердите или отмените текущий ввод", "請先選字或取消組字", "先に変換を確定またはキャンセルしてください", "먼저 조합 중인 문자를 확정하거나 취소하세요", "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen", "Validez ou annulez d’abord la composition en cours", "Confirma o cancela primero la composición actual", "Confirme ou cancele primeiro a composição atual"],
|
||||
"请输入有效的快捷键名称": ["请输入有效的快捷键名称", "Enter a shortcut name (1–64 characters)", "Введите название от 1 до 64 символов", "請輸入有效的快速鍵名稱", "ショートカット名を1〜64文字で入力してください", "단축키 이름을 1~64자로 입력하세요", "Namen mit 1–64 Zeichen eingeben", "Saisissez un nom de 1 à 64 caractères", "Introduce un nombre de 1 a 64 caracteres", "Insira um nome de 1 a 64 caracteres"],
|
||||
"请选择有效的按键": ["请选择有效的按键", "Select a valid key", "Выберите допустимую клавишу", "請選擇有效的按鍵", "有効なキーを選択してください", "유효한 키를 선택하세요", "Eine gültige Taste auswählen", "Sélectionnez une touche valide", "Selecciona una tecla válida", "Selecione uma tecla válida"],
|
||||
"运行窗口": ["运行窗口", "Run dialog", "Окно запуска", "執行視窗", "実行ダイアログ", "실행 대화상자", "Ausführen-Dialog", "Fenêtre Exécuter", "Diálogo Ejecutar", "Janela Executar"],
|
||||
"还没有自定义快捷键": ["还没有自定义快捷键", "No custom shortcuts yet", "Своих сочетаний пока нет", "尚無自訂快速鍵", "カスタムショートカットはまだありません", "아직 사용자 지정 단축키가 없습니다", "Noch keine eigenen Tastenkürzel", "Aucun raccourci personnalisé", "Aún no hay atajos personalizados", "Ainda não há atalhos personalizados"],
|
||||
"适用远端": ["适用远端", "Remote platform", "Удалённая система", "適用遠端", "対象リモートOS", "대상 원격 플랫폼", "Remote-Plattform", "Système distant", "Sistema remoto", "Sistema remoto"],
|
||||
"选择桌面方案": ["选择桌面方案", "Choose desktop environment", "Выберите окружение рабочего стола", "選擇桌面環境", "デスクトップ環境を選択", "데스크톱 환경 선택", "Desktop-Umgebung auswählen", "Choisir l’environnement de bureau", "Elegir entorno de escritorio", "Escolher ambiente de trabalho"],
|
||||
"选择桌面方案以加载系统预设": ["选择桌面方案以加载系统预设", "Choose a desktop environment to load system presets", "Выберите окружение, чтобы загрузить системные наборы", "選擇桌面環境以載入系統預設", "デスクトップ環境を選択してプリセットを読み込みます", "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요", "Desktop-Umgebung wählen, um Systemvorgaben zu laden", "Choisissez un environnement pour charger les préréglages système", "Elige un entorno para cargar los ajustes del sistema", "Escolha um ambiente para carregar as predefinições do sistema"],
|
||||
"键盘上方常用栏": ["键盘上方常用栏", "Favorites above keyboard", "Избранное над клавиатурой", "鍵盤上方常用列", "キーボード上のお気に入り", "키보드 위 즐겨찾기 모음", "Favoriten über der Tastatur", "Favoris au-dessus du clavier", "Favoritos sobre el teclado", "Favoritos acima do teclado"],
|
||||
};
|
||||
@@ -12,6 +12,8 @@ import { AnnouncementInbox, AnnouncementItem, AnnouncementRequest, AnnouncementR
|
||||
import { VideoSettings, VideoValues, NetworkStatistics, NetworkReport, RecentPresence, PresenceMessage, asciiKey } from './ControllerModels';
|
||||
|
||||
interface Command {
|
||||
modifiers?: number[];
|
||||
trustKey?: string;
|
||||
displayResolutions?: ResolutionPreference[];
|
||||
displayId?: number; displayKey?: string; width?: number; height?: number;
|
||||
op: string; session?: number; connection?: number; server?: string; port?: number; identity?: string;
|
||||
@@ -34,9 +36,11 @@ interface VideoCodecStatus { encoder: string; }
|
||||
interface ControlData { display_resolution?: ResolutionRequest; display_resolutions?: ResolutionDisplay[]; type: number; host_info?: HostInfo; video_settings?: SettingAck; service_status?: ServiceStatus; privacy_status?: PrivacyStatus; video_codec?: VideoCodecStatus; }
|
||||
interface PresenceRequest { type: string; devices: string[]; subscribe: boolean; }
|
||||
interface SignalType { type: string; }
|
||||
interface TrustInfo { pairing: string; fingerprint: string; }
|
||||
|
||||
@Observed
|
||||
export class MobileState {
|
||||
trustFingerprint: string = '';
|
||||
page: string = 'home';
|
||||
consent: boolean = false;
|
||||
privacyAvailable: boolean = false;
|
||||
@@ -200,12 +204,13 @@ export class MobileController {
|
||||
this.starting = true; const session = ++this.session, scope = this.scope();
|
||||
try {
|
||||
let identity = await this.store.secret(`identity:${scope}`);
|
||||
const trustKey = await this.store.trustKey();
|
||||
// A public ID alone is not a login credential. Discard legacy incomplete records.
|
||||
if (identity && !/^[0-9]{9}@.+$/.test(identity)) { await this.store.removeSecret(`identity:${scope}`); identity = ''; }
|
||||
if (session !== this.session || !this.foreground || !this.state.consent) return;
|
||||
this.state.signalText = tr("正在连接…"); this.state.signal = 0; this.started = true;
|
||||
this.send({ op: 'start', session: session, server: this.state.server, port: Number(this.state.port), identity: identity,
|
||||
logs: this.context.cacheDir, certificates: '/etc/security/certificates', directory: this.store.directory,
|
||||
trustKey: trustKey, logs: this.context.cacheDir, certificates: '/etc/security/certificates', directory: this.store.directory,
|
||||
previewDirectory: this.store.previewDirectory, version: this.state.version });
|
||||
} catch { this.state.signalText = tr("安全存储不可用,请解锁设备后重试"); this.state.signal = -1; }
|
||||
finally { if (session === this.session) this.starting = false; }
|
||||
@@ -228,6 +233,20 @@ export class MobileController {
|
||||
this.navigate('settings');
|
||||
}
|
||||
navigate(page: string): void { this.state.page = page; this.state.modal = ''; }
|
||||
async pairingInfo(copy: boolean): Promise<void> {
|
||||
try {
|
||||
const seed = await this.store.trustKey();
|
||||
const info = JSON.parse(bridge.mobileTrustInfo(seed, this.state.identity)) as TrustInfo;
|
||||
if (!this.foreground || this.state.page !== 'settings') return;
|
||||
this.state.trustFingerprint = info.fingerprint.replace(/(.{4})(?!$)/g, '$1 ');
|
||||
if (copy) {
|
||||
try {
|
||||
await pasteboard.getSystemPasteboard().setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, info.pairing));
|
||||
this.toast(tr("配对信息已复制。"));
|
||||
} catch { this.toast(tr("无法复制配对信息,请重试。")); }
|
||||
}
|
||||
} catch { this.toast(tr("安全存储不可用,请解锁设备后重试")); }
|
||||
}
|
||||
history(): void {
|
||||
const values = this.store.recent(this.scope()), now = this.now(); this.presence?.watch(values.map((d: RecentDevice) => d.id), now);
|
||||
this.state.recent = values.filter((d: RecentDevice) => this.presence?.online(d.id, now))
|
||||
@@ -290,8 +309,8 @@ export class MobileController {
|
||||
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));
|
||||
if (!this.state.ready || !this.state.consent || this.state.modal === 'privacyUnlock') return;
|
||||
this.send({ op: 'chord', code, modifiers: [17, 16, 18, 91].filter((key: number) => modifiers.includes(key)), connection: this.connection });
|
||||
}
|
||||
shortcut(code: number): void { this.chord(code, [this.state.platform === 'macos' ? 91 : 17]); }
|
||||
text(text: string): void {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RemoteShortcut, RemoteShortcuts } from './RemoteShortcuts';
|
||||
import { RemoteShortcutPanel, RemoteShortcutManager } from './RemoteShortcutPanel';
|
||||
import { tr } from './Localization';
|
||||
import { ResolutionMode, resolutionLabel } from './RemoteDisplayResolutions';
|
||||
import { MobileController, MobileState } from './MobileController';
|
||||
@@ -26,6 +28,12 @@ export struct MobileRemoteView {
|
||||
private orbPositioned: boolean = false;
|
||||
@State private keyboard: boolean = false;
|
||||
@State private computerKeyboard: boolean = false;
|
||||
@State private shortcutPanel: boolean = false;
|
||||
@State private shortcutManager: boolean = false;
|
||||
@State private pinnedShortcuts: RemoteShortcut[] = [];
|
||||
@State private shortcutRevision: number = 0;
|
||||
private shortcutStore?: RemoteShortcuts;
|
||||
private shortcutPlatform: string = '';
|
||||
@State private shift: boolean = false;
|
||||
@State private modifiers: number[] = [];
|
||||
@State private keyboardX: number = 8;
|
||||
@@ -56,9 +64,23 @@ export struct MobileRemoteView {
|
||||
private wheelLast: number = 0;
|
||||
private keyboardOriginX: number = 0;
|
||||
private keyboardOriginY: number = 0;
|
||||
aboutToAppear(): void { if (this.controller) this.controller.releaseInput = () => this.release(); this.stateChanged(); }
|
||||
aboutToAppear(): void {
|
||||
if (this.controller) { this.controller.releaseInput = () => this.release(); this.shortcutStore = new RemoteShortcuts(this.controller.store); }
|
||||
this.refreshShortcuts(); this.stateChanged();
|
||||
}
|
||||
private refreshShortcuts(): void { this.pinnedShortcuts = this.shortcutStore?.pinned(this.state.platform) ?? []; this.shortcutRevision++; }
|
||||
private sendShortcut(item: RemoteShortcut): void {
|
||||
if (!this.state.ready || item.platform !== RemoteShortcuts.platform(this.state.platform)) return;
|
||||
this.modifiers = []; this.shift = false;
|
||||
if (item.service) this.controller?.sas(); else this.controller?.chord(item.key, item.modifiers);
|
||||
}
|
||||
private manageShortcuts(): void { this.controller?.release(); this.menu = ''; this.shortcutManager = true; }
|
||||
aboutToDisappear(): void { this.release(); if (this.controller) this.controller.releaseInput = () => {}; this.controller?.surface('0'); }
|
||||
private stateChanged(): void {
|
||||
if (!this.state.ready || this.state.modal === 'privacyUnlock') this.shortcutManager = false;
|
||||
if (this.shortcutPlatform !== this.state.platform) {
|
||||
this.shortcutPlatform = this.state.platform; this.shortcutManager = false; this.refreshShortcuts();
|
||||
}
|
||||
if (this.lastReset !== this.state.viewportReset || this.lastRelative !== this.state.relative) {
|
||||
this.release();
|
||||
if (this.lastReset !== this.state.viewportReset) { this.viewport.reset(); this.applyViewport(); }
|
||||
@@ -165,6 +187,8 @@ export struct MobileRemoteView {
|
||||
case 'space': return tr("空格");
|
||||
case 'computer': return tr("电脑");
|
||||
case 'clipboard': return tr("剪贴板");
|
||||
case 'Win': return RemoteShortcuts.modifierName(91, this.state.platform);
|
||||
case 'Alt': return RemoteShortcuts.modifierName(18, this.state.platform);
|
||||
default: return this.shift && key.length === 1 ? key.toUpperCase() : key;
|
||||
}
|
||||
}
|
||||
@@ -182,12 +206,13 @@ export struct MobileRemoteView {
|
||||
if (value === 'clipboard') { this.state.modal = 'clipboard'; return; }
|
||||
const modifier = value === 'Ctrl' ? 17 : value === 'Alt' ? 18 : value === 'Win' ? 91 : 0;
|
||||
if (modifier) { this.modifiers = this.modifiers.includes(modifier) ? this.modifiers.filter((m: number) => m !== modifier) : [...this.modifiers, modifier]; return; }
|
||||
if (value === 'copy' || value === 'paste' || value === 'select_all') { this.controller?.shortcut(value === 'copy' ? 67 : value === 'paste' ? 86 : 65); return; }
|
||||
if (value === 'copy' || value === 'paste' || value === 'select_all') { this.controller?.shortcut(value === 'copy' ? 67 : value === 'paste' ? 86 : 65); this.modifiers = []; this.shift = false; return; }
|
||||
const names = ['space','⌫','Enter','Esc','Tab','↑','↓','←','→']; const codes = [32,8,13,27,9,38,40,37,39];
|
||||
let code = names.includes(value) ? codes[names.indexOf(value)] : /^F[1-6]$/.test(value) ? 111 + Number(value.substring(1)) : 0;
|
||||
if (!code && this.modifiers.length) { const key = asciiKey(this.shift ? value.toUpperCase() : value); code = key & 255; if (key & 256) this.modifiers = [...this.modifiers, 16]; }
|
||||
if (code) { this.controller?.chord(code, this.modifiers); this.modifiers = []; }
|
||||
else this.controller?.text(this.shift ? value.toUpperCase() : value);
|
||||
this.shift = false;
|
||||
}
|
||||
private keyboardDrag(event: GestureEvent): void {
|
||||
this.keyboardX = clamp(this.keyboardOriginX + event.offsetX, 8, this.viewWidth - this.keyboardWidth() - 8);
|
||||
@@ -314,12 +339,38 @@ export struct MobileRemoteView {
|
||||
@Builder
|
||||
Keyboard() {
|
||||
Column({ space: 3 }) {
|
||||
Row() {
|
||||
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }.layoutWeight(1).height(28).accessibilityText(tr("拖动键盘"))
|
||||
.gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; })
|
||||
.onActionUpdate((e: GestureEvent) => this.keyboardDrag(e)))
|
||||
MobileIconButton({ icon: 'close', label: tr("收起键盘"), iconExtent: 28, iconSize: 18, action: () => { this.keyboard = false; this.modifiers = []; } })
|
||||
}.width('100%')
|
||||
Stack() { Row() {}.width(32).height(4).backgroundColor(MobileTheme.secondary).borderRadius(2) }
|
||||
.width('100%').height(14).accessibilityText(tr('拖动键盘'))
|
||||
.gesture(PanGesture().onActionStart(() => { this.keyboardOriginX = this.keyboardX; this.keyboardOriginY = this.keyboardY; })
|
||||
.onActionUpdate((e: GestureEvent) => this.keyboardDrag(e)))
|
||||
Row({ space: 2 }) {
|
||||
Button(this.shortcutPanel ? 'ABC' : this.computerKeyboard ? 'ABC' : '⌘', { type: ButtonType.Normal })
|
||||
.width(44).height(44).fontSize(15).padding(0).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
|
||||
.accessibilityText(tr(this.computerKeyboard || this.shortcutPanel ? '键盘' : '电脑键盘'))
|
||||
.onClick(() => { if (this.shortcutPanel) { this.shortcutPanel = false; this.computerKeyboard = false; } else this.computerKeyboard = !this.computerKeyboard; this.modifiers = []; })
|
||||
if (this.shortcutPanel) {
|
||||
Text(tr('快捷键')).layoutWeight(1).fontSize(14).textAlign(TextAlign.Center)
|
||||
} else {
|
||||
Scroll() {
|
||||
Row({ space: 4 }) {
|
||||
ForEach(this.pinnedShortcuts, (item: RemoteShortcut) => {
|
||||
Button(item.title(), { type: ButtonType.Normal }).height(44).constraintSize({ minWidth: 44 }).fontSize(13)
|
||||
.padding({ left: 10, right: 10 }).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
|
||||
.accessibilityText(`${item.title()}, ${item.chord()}`).onClick(() => this.sendShortcut(item))
|
||||
}, (item: RemoteShortcut) => item.id)
|
||||
}
|
||||
}.scrollable(ScrollDirection.Horizontal).scrollBar(BarState.Off).layoutWeight(1)
|
||||
}
|
||||
Button('•••', { type: ButtonType.Normal }).width(44).height(44).padding(0).fontColor(MobileTheme.ink).backgroundColor(Color.Transparent)
|
||||
.accessibilityText(tr('更多快捷键')).onClick(() => { this.shortcutPanel = !this.shortcutPanel; this.modifiers = []; })
|
||||
MobileIconButton({ icon: 'close', label: tr('收起键盘'), iconExtent: 44, iconSize: 20, buttonFill: '#D1D6DE', action: () => { this.keyboard = false; this.modifiers = []; } })
|
||||
}.width('100%').height(44)
|
||||
if (this.shortcutPanel) {
|
||||
Column() {
|
||||
RemoteShortcutPanel({ store: this.shortcutStore, platform: RemoteShortcuts.platform(this.state.platform), revision: this.shortcutRevision,
|
||||
send: (item: RemoteShortcut) => this.sendShortcut(item), manage: () => this.manageShortcuts(), changed: () => this.refreshShortcuts() })
|
||||
}.width('100%').height(Math.min(230, Math.max(100, this.viewHeight - 88)))
|
||||
} else {
|
||||
Scroll() {
|
||||
Column({ space: 3 }) {
|
||||
ForEach(this.keyboardRows(), (row: KeyRow) => {
|
||||
@@ -334,7 +385,8 @@ export struct MobileRemoteView {
|
||||
}.width('100%')
|
||||
})
|
||||
}.width('100%')
|
||||
}.width('100%').constraintSize({ maxHeight: Math.max(31, this.viewHeight - 60) }).scrollBar(BarState.Off)
|
||||
}.width('100%').constraintSize({ maxHeight: Math.max(31, this.viewHeight - 88) }).scrollBar(BarState.Off)
|
||||
}
|
||||
}.padding(5).width(this.keyboardWidth()).backgroundColor('#D1D6DE').borderRadius(12)
|
||||
.shadow({ radius: 20, color: '#33000000', offsetY: 5 }).onAreaChange((_old: Area, next: Area) => this.keyboardHeight = Number(next.height))
|
||||
.position({ x: clamp(this.keyboardX, 8, this.viewWidth - this.keyboardWidth() - 8), y: clamp(this.keyboardY, 8, Math.max(8, this.viewHeight - this.keyboardHeight - 8)) })
|
||||
@@ -372,10 +424,10 @@ export struct MobileRemoteView {
|
||||
}.padding(20).constraintSize({ minWidth: 200 }).backgroundColor('#B0202024').borderRadius(14)
|
||||
.position({ x: (this.viewWidth - 200) / 2, y: (this.viewHeight - 104) / 2 }).hitTestBehavior(HitTestMode.None)
|
||||
}
|
||||
Column() {}.width('100%').height('100%').focusable(true).defaultFocus(true)
|
||||
Column() {}.width('100%').height('100%').focusable(!this.shortcutManager).enabled(!this.shortcutManager).defaultFocus(true)
|
||||
.onTouch((e: TouchEvent) => this.touch(e)).onMouse((e: MouseEvent) => this.physicalMouse(e))
|
||||
.onAxisEvent((event: AxisEvent) => { const delta = event.getVerticalAxisValue(); if (delta) this.mouse.scroll(-delta); })
|
||||
.onKeyEvent((e: KeyEvent) => { if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; })
|
||||
.onKeyEvent((e: KeyEvent) => { if (this.shortcutManager) return false; if (e.type === KeyType.Down || e.type === KeyType.Up) { this.controller?.physicalKey(e.keyCode, e.type === KeyType.Down); return true; } return false; })
|
||||
.onBlur(() => this.controller?.release())
|
||||
.accessibilityText(tr("远程桌面:轻触点击、长按右键、双指捏合缩放、放大后双指拖动画面,原始比例下双指滚动"))
|
||||
MobileIcon({ name: 'cursor', iconExtent: 24, color: '#000000' })
|
||||
@@ -388,6 +440,10 @@ export struct MobileRemoteView {
|
||||
}.width(54).height(54).backgroundColor('#F5F5F7').borderRadius(27).shadow({ radius: 14, color: '#33000000', offsetY: 4 })
|
||||
.position({ x: this.orbX, y: this.orbY }).accessibilityText(tr("远程控制菜单,拖动可移动")).accessibilityLevel('yes')
|
||||
.onTouch((e: TouchEvent) => this.orbTouch(e))
|
||||
if (this.shortcutManager) {
|
||||
RemoteShortcutManager({ store: this.shortcutStore, platform: RemoteShortcuts.platform(this.state.platform),
|
||||
changed: () => this.refreshShortcuts(), close: () => { this.shortcutManager = false; this.refreshShortcuts(); } })
|
||||
}
|
||||
}.width('100%').height('100%').backgroundColor(Color.Black).clip(true)
|
||||
.onAreaChange((_old: Area, next: Area) => {
|
||||
this.release(); this.viewWidth = Number(next.width); this.viewHeight = Number(next.height); this.fit();
|
||||
|
||||
@@ -6,9 +6,11 @@ import { cryptoFramework } from '@kit.CryptoArchitectureKit';
|
||||
import { util } from '@kit.ArkTS';
|
||||
import { fileIo as fs } from '@kit.CoreFileKit';
|
||||
import { image } from '@kit.ImageKit';
|
||||
import bridge from 'libcrossdesk.so';
|
||||
|
||||
export interface RecentDevice { id: string; name: string; platform: string; remember: boolean; preview: string; }
|
||||
export class MobileStore {
|
||||
private static trustLoading?: Promise<string>;
|
||||
private data: preferences.Preferences;
|
||||
readonly directory: string;
|
||||
readonly previewDirectory: string;
|
||||
@@ -59,6 +61,25 @@ export class MobileStore {
|
||||
try { await asset.remove(await this.query(key)); }
|
||||
catch (error) { if ((error as ErrorWithCode).code !== asset.ErrorCode.NOT_FOUND) throw new Error(tr("无法删除已保存的凭据")); }
|
||||
}
|
||||
async trustKey(): Promise<string> {
|
||||
// Serializes pairing export and reconnects, including overlapping page lifetimes.
|
||||
if (MobileStore.trustLoading) return MobileStore.trustLoading;
|
||||
const task = this.loadTrustKey(); MobileStore.trustLoading = task;
|
||||
try { return await task; }
|
||||
finally { if (MobileStore.trustLoading === task) MobileStore.trustLoading = undefined; }
|
||||
}
|
||||
private async loadTrustKey(): Promise<string> {
|
||||
let seed = await this.secret('device-trust:v1');
|
||||
if (!seed) {
|
||||
if (this.flag('deviceTrustInitialized')) throw new Error('Device signing identity missing');
|
||||
seed = bridge.mobileGenerateTrustKey();
|
||||
await this.saveSecret('device-trust:v1', seed);
|
||||
}
|
||||
// Validate persisted data without replacing a corrupt/unavailable identity.
|
||||
bridge.mobileTrustInfo(seed, '');
|
||||
this.put('deviceTrustInitialized', true);
|
||||
return seed;
|
||||
}
|
||||
recent(scope: string): RecentDevice[] {
|
||||
try {
|
||||
const entries = JSON.parse(this.text(`recent:${scope}`, '[]')) as RecentDevice[];
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
import { tr } from './Localization';
|
||||
import { MobileTheme, MobileSegment } from './MobileWidgets';
|
||||
import { RemoteShortcut, RemoteShortcuts } from './RemoteShortcuts';
|
||||
|
||||
@Component
|
||||
export struct RemoteShortcutPanel {
|
||||
store?: RemoteShortcuts;
|
||||
@Prop @Watch('reload') platform: string = 'unknown';
|
||||
@Prop @Watch('reload') revision: number = 0;
|
||||
send: (item: RemoteShortcut) => void = () => {};
|
||||
manage: () => void = () => {};
|
||||
changed: () => void = () => {};
|
||||
@State private group: number = 0;
|
||||
@State private page: number = 0;
|
||||
@State private desktop: string = '';
|
||||
@State private items: RemoteShortcut[] = [];
|
||||
aboutToAppear(): void { this.reload(); }
|
||||
private reload(): void { this.desktop = this.store?.desktop() ?? ''; this.items = this.store?.items(this.platform) ?? []; this.page = 0; }
|
||||
private filtered(): RemoteShortcut[] { return this.items.filter((item: RemoteShortcut) => item.group === ['common', 'keys', 'system', 'mine'][this.group]); }
|
||||
build() {
|
||||
Column({ space: 6 }) {
|
||||
MobileSegment({ labels: [tr('常用'), tr('按键'), tr('系统'), tr('我的')], selection: this.group,
|
||||
changed: (index: number) => { this.group = index; this.page = 0; } })
|
||||
if (this.group === 2 && this.platform === 'linux') {
|
||||
Select([{ value: tr('选择桌面方案') }, { value: 'GNOME' }, { value: 'KDE Plasma' }])
|
||||
.selected(this.desktop === 'gnome' ? 1 : this.desktop === 'kde' ? 2 : 0)
|
||||
.value(this.desktop === 'gnome' ? 'GNOME' : this.desktop === 'kde' ? 'KDE Plasma' : tr('选择桌面方案'))
|
||||
.font({ size: 12 }).height(32).onSelect((index: number) => { this.store?.setDesktop(['', 'gnome', 'kde'][index]); this.reload(); this.changed(); })
|
||||
}
|
||||
Scroll() {
|
||||
Column({ space: 4 }) {
|
||||
if (!this.filtered().length) {
|
||||
Text(tr(this.platform === 'unknown' ? '等待远端平台信息' : this.group === 2 && this.platform === 'linux' ? '选择桌面方案以加载系统预设' : '还没有自定义快捷键'))
|
||||
.fontSize(12).fontColor(MobileTheme.secondary).padding(12)
|
||||
}
|
||||
GridRow({ columns: 3, gutter: 4 }) {
|
||||
ForEach(this.filtered().slice(this.page * 6, this.page * 6 + 6), (item: RemoteShortcut) => {
|
||||
GridCol() {
|
||||
Column({ space: 3 }) {
|
||||
Text(item.title()).fontSize(12).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
|
||||
Text(item.chord()).fontSize(10).fontColor(MobileTheme.secondary).maxLines(1)
|
||||
}.width('100%').height(48).justifyContent(FlexAlign.Center).borderRadius(7).backgroundColor(Color.White)
|
||||
.accessibilityLevel('yes').accessibilityText(`${item.title()}, ${item.chord()}`).onClick(() => this.send(item))
|
||||
}
|
||||
}, (item: RemoteShortcut) => item.id)
|
||||
}
|
||||
}.width('100%')
|
||||
}.width('100%').layoutWeight(1)
|
||||
Row() {
|
||||
Button('‹').width(40).height(36).enabled(this.page > 0).accessibilityText(tr('上一页')).onClick(() => this.page--)
|
||||
Text(`${this.page + 1}/${Math.max(1, Math.ceil(this.filtered().length / 6))}`).fontSize(11).width(38).textAlign(TextAlign.Center)
|
||||
Button('›').width(40).height(36).enabled((this.page + 1) * 6 < this.filtered().length).accessibilityText(tr('下一页')).onClick(() => this.page++)
|
||||
Blank()
|
||||
Button(tr('管理与自定义')).fontSize(12).height(36).enabled(this.platform !== 'unknown').onClick(this.manage)
|
||||
}.width('100%')
|
||||
}.width('100%').height('100%').padding(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Component
|
||||
export struct RemoteShortcutManager {
|
||||
store?: RemoteShortcuts;
|
||||
platform: string = 'unknown';
|
||||
close: () => void = () => {};
|
||||
changed: () => void = () => {};
|
||||
@State private items: RemoteShortcut[] = [];
|
||||
@State private pinned: RemoteShortcut[] = [];
|
||||
@State private editing: boolean = false;
|
||||
@State private itemId: string = '';
|
||||
@State private name: string = '';
|
||||
@State private target: string = 'windows';
|
||||
@State private mainKey: number = 67;
|
||||
@State private modifiers: number[] = [];
|
||||
@State private pin: boolean = true;
|
||||
@State private error: string = '';
|
||||
aboutToAppear(): void { this.reload(); }
|
||||
private reload(): void { this.items = this.store?.items(this.platform) ?? []; this.pinned = this.store?.pinned(this.platform) ?? []; this.changed(); }
|
||||
private edit(item?: RemoteShortcut): void {
|
||||
this.itemId = item?.id ?? ''; this.name = item?.name ?? ''; this.target = item?.platform ?? this.platform;
|
||||
this.mainKey = item?.key ?? 67; this.modifiers = item?.modifiers.slice() ?? [this.target === 'macos' ? 91 : 17];
|
||||
this.pin = item ? (this.store?.pins(item.platform).includes(item.id) ?? false) : (this.store?.pins(this.target).length ?? 0) < 8;
|
||||
this.error = ''; this.editing = true;
|
||||
}
|
||||
private setPinned(item: RemoteShortcut, add: boolean): void {
|
||||
const values = this.store?.pins(this.platform) ?? [];
|
||||
if (add && values.length >= 8) { this.error = tr('最多固定 8 个快捷键'); return; }
|
||||
this.store?.setPins(this.platform, add ? values.concat([item.id]) : values.filter((id: string) => id !== item.id)); this.error = ''; this.reload();
|
||||
}
|
||||
private moveUp(item: RemoteShortcut): void {
|
||||
const values = this.store?.pins(this.platform) ?? [], index = values.indexOf(item.id);
|
||||
if (index > 0) { values.splice(index, 1); values.splice(index - 1, 0, item.id); this.store?.setPins(this.platform, values); this.reload(); }
|
||||
}
|
||||
private save(): void {
|
||||
this.error = this.store?.save(this.itemId, this.name, this.target, this.mainKey, this.modifiers, this.pin) ?? '';
|
||||
if (!this.error) { this.editing = false; this.reload(); }
|
||||
}
|
||||
@Builder
|
||||
Item(item: RemoteShortcut, pinned: boolean) {
|
||||
Column({ space: 4 }) {
|
||||
Row() {
|
||||
Text(item.title()).fontSize(15).fontColor(MobileTheme.ink).layoutWeight(1).maxLines(2)
|
||||
Text(item.chord()).fontSize(12).fontColor(MobileTheme.secondary).maxLines(2)
|
||||
}.width('100%')
|
||||
Row({ space: 6 }) {
|
||||
if (pinned) {
|
||||
Button(tr('向上移动')).fontSize(12).height(36).enabled(this.pinned[0]?.id !== item.id).onClick(() => this.moveUp(item))
|
||||
Button(tr('移除')).fontSize(12).height(36).onClick(() => this.setPinned(item, false))
|
||||
} else {
|
||||
Button(tr('添加到常用栏')).fontSize(12).height(36).enabled(!this.pinned.some((value: RemoteShortcut) => value.id === item.id)).onClick(() => this.setPinned(item, true))
|
||||
if (item.custom) {
|
||||
Button(tr('编辑')).fontSize(12).height(36).onClick(() => this.edit(item))
|
||||
Button(tr('删除')).fontSize(12).height(36).backgroundColor(MobileTheme.red).onClick(() => { this.store?.remove(item.id); this.reload(); })
|
||||
}
|
||||
}
|
||||
}.width('100%')
|
||||
}.padding(12).width('100%').borderRadius(10).backgroundColor(Color.White)
|
||||
}
|
||||
build() {
|
||||
Column({ space: 12 }) {
|
||||
Row() {
|
||||
Button(tr(this.editing ? '取消' : '完成')).fontSize(14).height(44).onClick(() => { if (this.editing) this.editing = false; else this.close(); })
|
||||
Text(tr(this.editing ? this.itemId ? '编辑快捷键' : '新建快捷键' : '管理快捷键')).fontSize(17).fontWeight(600).layoutWeight(1).textAlign(TextAlign.Center)
|
||||
Button(tr(this.editing ? '保存' : '新建')).fontSize(14).height(44).onClick(() => { if (this.editing) this.save(); else this.edit(); })
|
||||
}.width('100%')
|
||||
Text(tr('本地编辑,输入不会发送到远端')).fontSize(12).fontColor(MobileTheme.secondary)
|
||||
if (this.error) { Text(this.error).fontSize(13).fontColor(MobileTheme.red) }
|
||||
Scroll() {
|
||||
Column({ space: 12 }) {
|
||||
if (this.editing) {
|
||||
TextInput({ placeholder: tr('名称'), text: this.name }).maxLength(64).onChange((value: string) => this.name = value)
|
||||
Text(tr('适用远端')).width('100%').fontSize(13)
|
||||
Select(RemoteShortcuts.platforms.map((os: string): SelectOption => ({ value: RemoteShortcuts.platformName(os) })))
|
||||
.selected(RemoteShortcuts.platforms.indexOf(this.target)).value(RemoteShortcuts.platformName(this.target))
|
||||
.width('100%').onSelect((index: number) => this.target = RemoteShortcuts.platforms[index])
|
||||
Text(tr('主按键')).width('100%').fontSize(13)
|
||||
Select(RemoteShortcuts.keys().map((key: number): SelectOption => ({ value: RemoteShortcuts.keyName(key, this.target) })))
|
||||
.selected(RemoteShortcuts.keys().indexOf(this.mainKey)).value(RemoteShortcuts.keyName(this.mainKey, this.target))
|
||||
.width('100%').onSelect((index: number) => this.mainKey = RemoteShortcuts.keys()[index])
|
||||
Text(tr('修饰键')).width('100%').fontSize(13)
|
||||
Row({ space: 6 }) {
|
||||
ForEach(RemoteShortcuts.modifiers, (key: number) => {
|
||||
Button(RemoteShortcuts.modifierName(key, this.target)).layoutWeight(1).height(44)
|
||||
.backgroundColor(this.modifiers.includes(key) ? MobileTheme.blue : MobileTheme.secondary)
|
||||
.accessibilitySelected(this.modifiers.includes(key)).onClick(() => this.modifiers = this.modifiers.includes(key) ? this.modifiers.filter((value: number) => value !== key) : RemoteShortcuts.normalize(this.modifiers.concat([key])))
|
||||
}, (key: number) => `${key}-${this.target}`)
|
||||
}.width('100%')
|
||||
Text(`${tr('发送组合')}: ${new RemoteShortcut('', '', this.target, 'mine', this.mainKey, this.modifiers).chord()}`).fontSize(16).padding(12)
|
||||
Row() {
|
||||
Text(tr('固定到键盘上方')).layoutWeight(1).fontSize(14)
|
||||
Toggle({ type: ToggleType.Switch, isOn: this.pin }).onChange((value: boolean) => this.pin = value)
|
||||
}.width('100%')
|
||||
} else {
|
||||
Text(`${RemoteShortcuts.platformName(this.platform)} · ${tr('键盘上方常用栏')} (${this.pinned.length}/8)`).fontSize(14).width('100%')
|
||||
ForEach(this.pinned, (item: RemoteShortcut) => { this.Item(item, true) }, (item: RemoteShortcut) => item.id)
|
||||
Text(tr('快捷键')).fontSize(14).width('100%')
|
||||
ForEach(this.items, (item: RemoteShortcut) => { this.Item(item, false) }, (item: RemoteShortcut) => item.id)
|
||||
}
|
||||
}.width('100%').padding({ bottom: 24 })
|
||||
}.layoutWeight(1).width('100%')
|
||||
}.padding(16).width('100%').height('100%').backgroundColor(MobileTheme.background)
|
||||
.onKeyEvent((event: KeyEvent) => { if (event.keyCode === 2 && event.type === KeyType.Down) { if (this.editing) this.editing = false; else this.close(); return true; } return false; })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { MobileStore } from './MobileStore';
|
||||
import { tr } from './Localization';
|
||||
|
||||
export class RemoteShortcut {
|
||||
id: string; name: string; platform: string; group: string; key: number; modifiers: number[];
|
||||
service: boolean; custom: boolean;
|
||||
constructor(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = [], service: boolean = false, custom: boolean = false) {
|
||||
this.id = id; this.name = name; this.platform = platform; this.group = group; this.key = key;
|
||||
this.modifiers = modifiers.slice(); this.service = service; this.custom = custom;
|
||||
}
|
||||
title(): string { return this.custom ? this.name : tr(this.name); }
|
||||
chord(): string { return this.modifiers.map((key: number) => RemoteShortcuts.modifierName(key, this.platform)).concat([RemoteShortcuts.keyName(this.key, this.platform)]).join(' + '); }
|
||||
}
|
||||
|
||||
export class RemoteShortcuts {
|
||||
static readonly platforms: string[] = ['windows', 'macos', 'linux'];
|
||||
static readonly modifiers: number[] = [17, 16, 18, 91];
|
||||
private store: MobileStore;
|
||||
private prefix: string = 'remote_shortcuts_v1:';
|
||||
constructor(store: MobileStore) { this.store = store; }
|
||||
static platform(value: string): string { return RemoteShortcuts.platforms.includes(value) ? value : 'unknown'; }
|
||||
static platformName(value: string): string { return value === 'windows' ? 'Windows' : value === 'macos' ? 'macOS' : value === 'linux' ? 'Linux' : tr('未知'); }
|
||||
static modifierName(key: number, platform: string): string {
|
||||
return key === 17 ? 'Ctrl' : key === 16 ? 'Shift' : key === 18 ? (platform === 'macos' ? 'Option' : 'Alt') : key === 91 ? (platform === 'macos' ? 'Cmd' : platform === 'linux' ? 'Super' : 'Win') : '';
|
||||
}
|
||||
static keyName(key: number, platform: string): string {
|
||||
if (key >= 65 && key <= 90 || key >= 48 && key <= 57) return String.fromCharCode(key);
|
||||
if (key >= 112 && key <= 123) return `F${key - 111}`;
|
||||
switch (key) {
|
||||
case 8: return platform === 'macos' ? 'Delete ⌫' : 'Backspace'; case 9: return 'Tab'; case 13: return 'Enter';
|
||||
case 27: return 'Esc'; case 32: return 'Space'; case 33: return 'PageUp'; case 34: return 'PageDown';
|
||||
case 35: return 'End'; case 36: return 'Home'; case 37: return '←'; case 38: return '↑'; case 39: return '→';
|
||||
case 40: return '↓'; case 45: return 'Insert'; case 46: return platform === 'macos' ? 'Delete ⌦' : 'Delete'; default: return '';
|
||||
}
|
||||
}
|
||||
static keys(): number[] {
|
||||
const keys: number[] = []; for (let i = 65; i <= 90; i++) keys.push(i); for (let i = 48; i <= 57; i++) keys.push(i);
|
||||
for (let i = 112; i <= 123; i++) keys.push(i);
|
||||
return keys.concat([27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]);
|
||||
}
|
||||
static normalize(values: number[]): number[] { return RemoteShortcuts.modifiers.filter((key: number) => values.includes(key)); }
|
||||
desktop(): string { const value = this.store.text(this.prefix + 'linux_desktop'); return ['gnome', 'kde'].includes(value) ? value : ''; }
|
||||
setDesktop(value: string): void { this.store.put(this.prefix + 'linux_desktop', ['gnome', 'kde'].includes(value) ? value : ''); }
|
||||
private preset(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = []): RemoteShortcut { return new RemoteShortcut(id, name, platform, group, key, modifiers); }
|
||||
items(remote: string): RemoteShortcut[] {
|
||||
const os = RemoteShortcuts.platform(remote); let result: RemoteShortcut[] = [];
|
||||
if (os !== 'unknown') {
|
||||
const ids = ['copy', 'paste', 'cut', 'all', 'undo', 'save'], names = ['复制', '粘贴', '剪切', '全选', '撤销', '保存'], keys = [67, 86, 88, 65, 90, 83];
|
||||
for (let i = 0; i < ids.length; i++) result.push(this.preset(ids[i], names[i], os, 'common', keys[i], [os === 'macos' ? 91 : 17]));
|
||||
if (os === 'linux') result = result.concat([this.preset('term-copy', '终端复制', os, 'common', 67, [17, 16]), this.preset('term-paste', '终端粘贴', os, 'common', 86, [17, 16])]);
|
||||
}
|
||||
const keys = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40]; for (let i = 112; i <= 123; i++) keys.push(i);
|
||||
keys.forEach((key: number) => result.push(this.preset(key === 27 ? 'esc' : key === 9 ? 'tab' : `key-${key}`, RemoteShortcuts.keyName(key, os), os, 'keys', key)));
|
||||
if (os === 'windows') {
|
||||
result = result.concat([this.preset('switch', '切换窗口', os, 'system', 9, [18]), this.preset('desktop', '显示桌面', os, 'system', 68, [91]), this.preset('task', '任务管理器', os, 'system', 27, [17, 16]), new RemoteShortcut('sas', '安全选项', os, 'system', 46, [17, 18], true)]);
|
||||
} else if (os === 'macos') {
|
||||
result = result.concat([this.preset('switch', '切换应用', os, 'system', 9, [91]), this.preset('search', '聚焦搜索', os, 'system', 32, [91]), this.preset('forcequit', '强制退出面板', os, 'system', 27, [18, 91])]);
|
||||
} else if (os === 'linux' && this.desktop()) {
|
||||
result.push(this.preset('switch', this.desktop() === 'gnome' ? '切换应用' : '切换窗口', os, 'system', 9, [this.desktop() === 'gnome' ? 91 : 18]));
|
||||
result.push(this.preset('run', '运行窗口', os, 'system', 113, [18]));
|
||||
result.push(this.desktop() === 'gnome' ? this.preset('apps', '应用列表', os, 'system', 65, [91]) : this.preset('window-menu', '窗口菜单', os, 'system', 114, [18]));
|
||||
}
|
||||
return result.concat(this.custom().filter((item: RemoteShortcut) => item.platform === os));
|
||||
}
|
||||
custom(): RemoteShortcut[] {
|
||||
try {
|
||||
const raw = JSON.parse(this.store.text(this.prefix + 'custom', '[]')) as RemoteShortcut[];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const seen: string[] = [], result: RemoteShortcut[] = [];
|
||||
raw.forEach((item: RemoteShortcut) => {
|
||||
if (!item || typeof item.id !== 'string' || !item.id.startsWith('custom-') || seen.includes(item.id) || typeof item.name !== 'string' || !item.name.trim() || item.name.length > 64 || !RemoteShortcuts.platforms.includes(item.platform) || !RemoteShortcuts.keys().includes(item.key) || !Array.isArray(item.modifiers) || item.modifiers.length > 4 || !item.modifiers.every((key: number) => RemoteShortcuts.modifiers.includes(key))) return;
|
||||
if (item.platform === 'windows' && item.key === 46 && item.modifiers.includes(17) && item.modifiers.includes(18)) return;
|
||||
seen.push(item.id); result.push(new RemoteShortcut(item.id, item.name, item.platform, 'mine', item.key, RemoteShortcuts.normalize(item.modifiers), false, true));
|
||||
});
|
||||
return result.slice(0, 16);
|
||||
} catch { return []; }
|
||||
}
|
||||
pins(platform: string): string[] {
|
||||
const raw = this.store.text(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), '');
|
||||
const available = this.items(platform).map((item: RemoteShortcut) => item.id);
|
||||
let values: string[] = ['esc', 'tab', 'copy', 'paste'];
|
||||
try { const parsed = JSON.parse(raw) as string[]; if (Array.isArray(parsed)) values = parsed; } catch {}
|
||||
return values.filter((id: string, index: number) => typeof id === 'string' && available.includes(id) && values.indexOf(id) === index).slice(0, 8);
|
||||
}
|
||||
setPins(platform: string, values: string[]): void { this.store.put(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), JSON.stringify(values.slice(0, 8))); }
|
||||
pinned(platform: string): RemoteShortcut[] {
|
||||
const items = this.items(platform), result: RemoteShortcut[] = [];
|
||||
this.pins(platform).forEach((id: string) => { const item = items.find((value: RemoteShortcut) => value.id === id); if (item) result.push(item); });
|
||||
return result;
|
||||
}
|
||||
remove(id: string): void {
|
||||
this.store.put(this.prefix + 'custom', JSON.stringify(this.custom().filter((item: RemoteShortcut) => item.id !== id)));
|
||||
RemoteShortcuts.platforms.forEach((platform: string) => this.setPins(platform, this.pins(platform).filter((value: string) => value !== id)));
|
||||
}
|
||||
save(id: string, name: string, platform: string, key: number, modifiers: number[], pin: boolean): string {
|
||||
name = name.trim(); if (!name || name.length > 64) return tr('请输入有效的快捷键名称');
|
||||
if (!RemoteShortcuts.platforms.includes(platform) || !RemoteShortcuts.keys().includes(key)) return tr('请选择有效的按键');
|
||||
const normalized = RemoteShortcuts.normalize(modifiers);
|
||||
if (platform === 'windows' && key === 46 && normalized.includes(17) && normalized.includes(18)) return tr('请使用系统预设中的安全选项');
|
||||
const old = this.custom();
|
||||
if (old.some((item: RemoteShortcut) => item.id !== id && item.platform === platform && item.key === key && item.modifiers.join(',') === normalized.join(','))) return tr('已有相同组合的快捷键');
|
||||
if (!old.some((item: RemoteShortcut) => item.id === id) && old.length >= 16) return tr('最多保存 16 个自定义快捷键');
|
||||
if (pin && !this.pins(platform).includes(id) && this.pins(platform).length >= 8) return tr('最多固定 8 个快捷键');
|
||||
const itemId = id || `custom-${Date.now()}-${Math.floor(Math.random() * 1000000000)}`;
|
||||
const values = old.filter((item: RemoteShortcut) => item.id !== itemId); values.push(new RemoteShortcut(itemId, name, platform, 'mine', key, normalized, false, true));
|
||||
this.store.put(this.prefix + 'custom', JSON.stringify(values));
|
||||
RemoteShortcuts.platforms.forEach((target: string) => { const list = this.pins(target), index = list.indexOf(itemId), selected = list.filter((value: string) => value !== itemId); if (target === platform && pin) selected.splice(index >= 0 ? Math.min(index, selected.length) : selected.length, 0, itemId); this.setPins(target, selected); });
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,18 @@ struct Mobile {
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
|
||||
Text(this.state.identity ? tr("本机 ID {0}", [String(this.formatId(this.state.identity))]) : this.state.signalText).fontSize(12).fontColor(MobileTheme.secondary)
|
||||
.textAlign(TextAlign.Center).padding({ left: 16, right: 16, top: 12, bottom: 16 }).width('100%').copyOption(CopyOptions.LocalDevice)
|
||||
this.SectionTitle(tr("本机配对信息"))
|
||||
Column() {
|
||||
this.SettingRow(tr("查看指纹"), '', () => this.controller?.pairingInfo(false))
|
||||
if (this.state.trustFingerprint) {
|
||||
Text(this.state.trustFingerprint).fontSize(14).fontFamily('monospace').fontColor(MobileTheme.secondary)
|
||||
.padding(16).width('100%').copyOption(CopyOptions.LocalDevice)
|
||||
}
|
||||
this.Rule()
|
||||
this.SettingRow(tr("复制配对信息"), '', () => this.controller?.pairingInfo(true))
|
||||
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line })
|
||||
Text(tr("将配对信息添加到被控端的可信设备列表,并核对指纹。"))
|
||||
.fontSize(12).fontColor(MobileTheme.secondary).width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
|
||||
this.SectionTitle(tr("隐私"))
|
||||
Column() {
|
||||
this.SettingRow(tr("隐私与授权"), this.state.consent ? tr("已授权") : tr("未授权"), () => this.navigate('privacy'))
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "key_mapping.h"
|
||||
#include <app_version.h>
|
||||
#include <mobile_controller_protocol.h>
|
||||
#include <mobile_device_trust.h>
|
||||
#include <mobile_file_transfer_session.h>
|
||||
#include <mobile_video_timing.h>
|
||||
#include <display_stream_id.h>
|
||||
@@ -146,7 +147,9 @@ class MobileSession {
|
||||
auto info = MakeHostInformation("CrossDesk HarmonyOS Mobile", {}, true, s->version_, HostPlatform::HarmonyOS);
|
||||
info.i.supports_video_codec_status = true;
|
||||
info.preferred_display_resolutions = s->preferred_display_resolutions_;
|
||||
s->Send(info); FreeRemoteAction(info);
|
||||
const auto message = mobile_controller::DeviceTrustIdentity::Advertise(info.to_json());
|
||||
MiniRtcSendReliableDataFrame(s->controller_, message.data(), message.size(), kControlStream);
|
||||
FreeRemoteAction(info);
|
||||
RemoteAction audio{}; audio.type = audio_capture; audio.a = !s->muted_; s->Send(audio);
|
||||
s->audio_.Start();
|
||||
} else if (status >= MiniRtcConnectionDisconnected) s->Disconnect("");
|
||||
@@ -183,6 +186,13 @@ class MobileSession {
|
||||
else if (name == kClipboardStream && bytes.size() <= kMaxClipboardBytes)
|
||||
s->Event({{"type", "clipboard"}, {"text", bytes}}, true);
|
||||
else if (name == kControlStream) {
|
||||
if (s->trust_) {
|
||||
if (auto proof = s->trust_->ChallengeResponse(bytes, s->login_.substr(0, s->login_.find('@')),
|
||||
"CrossDesk HarmonyOS Mobile")) {
|
||||
if (!proof->empty()) MiniRtcSendReliableDataFrame(s->controller_, proof->data(), proof->size(), kControlStream);
|
||||
return;
|
||||
}
|
||||
}
|
||||
RemoteAction action{};
|
||||
if (action.from_json(bytes)) {
|
||||
const auto value = json::parse(action.to_json()); FreeRemoteAction(action);
|
||||
@@ -248,11 +258,13 @@ class MobileSession {
|
||||
Disconnect(""); online_=false;
|
||||
if (identity_) MiniRtcDestroyPeer(&identity_);
|
||||
++session_; login_.clear(); signal_status_=-1;
|
||||
trust_.reset();
|
||||
}
|
||||
void Execute(const json& v) {
|
||||
const auto op=v.at("op").get<std::string>();
|
||||
if (op=="start") {
|
||||
Stop(); session_=v.at("session").get<uint64_t>();
|
||||
trust_=std::make_unique<mobile_controller::DeviceTrustIdentity>(v.at("trustKey").get_ref<const std::string&>());
|
||||
server_=v.at("server").get<std::string>(); port_=v.at("port").get<int>();
|
||||
logs_=v.at("logs").get<std::string>(); login_=v.value("identity",""); version_=v.value("version","0.1.0");
|
||||
const auto roots=v.at("certificates").get<std::string>(); directory_=v.at("directory").get<std::string>();
|
||||
@@ -315,6 +327,21 @@ class MobileSession {
|
||||
if (key<=0||key>255) return;
|
||||
const bool down=v.at("down").get<bool>(); if (down) keys_.insert(key); else keys_.erase(key);
|
||||
RemoteAction a{}; a.type=keyboard; a.k={size_t(key),0,false,down?key_down:key_up}; Send(a,kKeyboardStream);
|
||||
} else if (op=="chord") {
|
||||
if (v.at("connection").get<uint64_t>() != connection_) return;
|
||||
const int key=v.at("code").get<int>();
|
||||
const auto modifiers=v.at("modifiers").get<std::vector<int>>();
|
||||
if (key<=0||key>255||modifiers.size()>4) return;
|
||||
std::vector<int> ordered;
|
||||
for (const int modifier : {17,16,18,91})
|
||||
if (std::find(modifiers.begin(),modifiers.end(),modifier)!=modifiers.end()) ordered.push_back(modifier);
|
||||
const auto stroke=[this](int code,bool down) {
|
||||
if (down) keys_.insert(code); else keys_.erase(code);
|
||||
RemoteAction a{}; a.type=keyboard; a.k={size_t(code),0,false,down?key_down:key_up}; Send(a,kKeyboardStream);
|
||||
};
|
||||
for (const int modifier : ordered) stroke(modifier,true);
|
||||
stroke(key,true); stroke(key,false);
|
||||
for (auto it=ordered.rbegin();it!=ordered.rend();++it) stroke(*it,false);
|
||||
} else if (op=="display") {
|
||||
const int index=v.at("index").get<int>(); if(index<0||index>=32)return;
|
||||
Release(); display_=index; ++video_generation_; renderer_.Clear();
|
||||
@@ -408,6 +435,7 @@ class MobileSession {
|
||||
MiniRtcPeerPtr* controller_=nullptr;
|
||||
std::unique_ptr<Context> identity_context_,control_context_;
|
||||
std::unique_ptr<mobile_controller::FileTransferSession> files_;
|
||||
std::unique_ptr<mobile_controller::DeviceTrustIdentity> trust_;
|
||||
std::string server_,logs_,login_,controller_login_,remote_,directory_,preview_directory_,preview_path_,version_;
|
||||
std::vector<DisplayResolutionPreference> preferred_display_resolutions_;
|
||||
int port_=9099,signal_status_=-1;
|
||||
@@ -432,4 +460,10 @@ void MobileCommand(const std::string& text) {
|
||||
}
|
||||
std::string MobilePoll() { std::lock_guard lock(mobile_mutex); return mobile?mobile->Poll():"[]"; }
|
||||
void MobileShutdown() { std::lock_guard lock(mobile_mutex); mobile.reset(); }
|
||||
std::string MobileGenerateTrustKey() { return mobile_controller::DeviceTrustIdentity::GeneratePrivateKey(); }
|
||||
std::string MobileTrustInfo(const std::string& seed, const std::string& device_id) {
|
||||
mobile_controller::DeviceTrustIdentity trust(seed);
|
||||
return json{{"pairing", trust.PairingInfo(device_id.substr(0, device_id.find('@')), "CrossDesk HarmonyOS Mobile")},
|
||||
{"fingerprint", trust.Fingerprint()}}.dump();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ namespace crossdesk::harmony {
|
||||
__attribute__((visibility("default"))) void MobileCommand(const std::string& json);
|
||||
__attribute__((visibility("default"))) std::string MobilePoll();
|
||||
__attribute__((visibility("default"))) void MobileShutdown();
|
||||
__attribute__((visibility("default"))) std::string MobileGenerateTrustKey();
|
||||
__attribute__((visibility("default"))) std::string MobileTrustInfo(const std::string& seed, const std::string& device_id);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -5,13 +5,15 @@ add_rules("mode.release", "mode.debug")
|
||||
set_policy("package.precompiled", false)
|
||||
add_cxflags("-fPIC")
|
||||
includes("../../../deps/submodules/minirtc")
|
||||
add_requires("openssl3 3.3.2", {system = false, configs = {shared = false}})
|
||||
|
||||
target("crossdesk_harmony_mobile")
|
||||
set_kind("shared")
|
||||
set_default(false)
|
||||
add_deps("minirtc")
|
||||
add_packages("nlohmann_json", "libyuv")
|
||||
add_packages("nlohmann_json", "libyuv", "openssl3")
|
||||
add_files("mobile_session.cpp", "media.cpp", "audio.cpp", "../../../libs/wire/src/remote_action.cpp", "../../../libs/wire/src/file_transfer.cpp", "../../../libs/wire/src/app_version.cpp")
|
||||
add_files("../../../libs/controller/src/mobile_device_trust.cpp")
|
||||
-- Export only the bridge API; bundled static dependencies are internal.
|
||||
add_cxflags("-fvisibility=hidden", "-fvisibility-inlines-hidden")
|
||||
add_shflags("-Wl,--exclude-libs,ALL", {force = true})
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
10000000000000000000001D /* RemoteMouseInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001D /* RemoteMouseInput.swift */; };
|
||||
10000000000000000000001E /* VirtualMouseBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20000000000000000000001E /* VirtualMouseBar.swift */; };
|
||||
100000000000000000000030 /* RemoteSystemKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000030 /* RemoteSystemKeyboard.swift */; };
|
||||
100000000000000000000031 /* RemoteShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000031 /* RemoteShortcuts.swift */; };
|
||||
100000000000000000000032 /* RemoteShortcutPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200000000000000000000032 /* RemoteShortcutPanel.swift */; };
|
||||
3C550ED141D4AEA30950BB7A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5BD6CC3A43B86A2E2267994F /* Localizable.strings */; };
|
||||
4870B14C46F4C4B8F10FEF08 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = A0A3C0DA09EE562AE21DF986 /* InfoPlist.strings */; };
|
||||
C10F10000000000000000001 /* RemotePrivacyScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10F20000000000000000001 /* RemotePrivacyScreen.swift */; };
|
||||
@@ -92,6 +94,8 @@
|
||||
20000000000000000000001D /* RemoteMouseInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMouseInput.swift; sourceTree = "<group>"; };
|
||||
20000000000000000000001E /* VirtualMouseBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMouseBar.swift; sourceTree = "<group>"; };
|
||||
200000000000000000000030 /* RemoteSystemKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSystemKeyboard.swift; sourceTree = "<group>"; };
|
||||
200000000000000000000031 /* RemoteShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcuts.swift; sourceTree = "<group>"; };
|
||||
200000000000000000000032 /* RemoteShortcutPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcutPanel.swift; sourceTree = "<group>"; };
|
||||
23D5AD87A9BF0CBF3A6AADDF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
2E83770B40B2D299796EB0CD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Resources/zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
2EA0435DFAEE685F04391B3B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Resources/es.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
@@ -187,6 +191,8 @@
|
||||
children = (
|
||||
20000000000000000000000A /* RemoteTouchInputView.swift */,
|
||||
200000000000000000000030 /* RemoteSystemKeyboard.swift */,
|
||||
200000000000000000000031 /* RemoteShortcuts.swift */,
|
||||
200000000000000000000032 /* RemoteShortcutPanel.swift */,
|
||||
20000000000000000000001D /* RemoteMouseInput.swift */,
|
||||
20000000000000000000001E /* VirtualMouseBar.swift */,
|
||||
);
|
||||
@@ -361,6 +367,8 @@
|
||||
100000000000000000000006 /* NativeMetalVideoView.swift in Sources */,
|
||||
100000000000000000000007 /* RemoteTouchInputView.swift in Sources */,
|
||||
100000000000000000000030 /* RemoteSystemKeyboard.swift in Sources */,
|
||||
100000000000000000000031 /* RemoteShortcuts.swift in Sources */,
|
||||
100000000000000000000032 /* RemoteShortcutPanel.swift in Sources */,
|
||||
10000000000000000000001D /* RemoteMouseInput.swift in Sources */,
|
||||
10000000000000000000001E /* VirtualMouseBar.swift in Sources */,
|
||||
100000000000000000000012 /* PrivacyPreferences.swift in Sources */,
|
||||
|
||||
@@ -151,6 +151,7 @@ typedef struct {
|
||||
/// All MiniRTC ownership and calls are serialized internally. Delegate methods
|
||||
/// are delivered on the main queue so SwiftUI never touches callback threads.
|
||||
@interface CrossDeskRTCBridge : NSObject
|
||||
+ (nullable NSDictionary<NSString *, NSString *> *)deviceTrustInfoForDeviceID:(NSString *)deviceID;
|
||||
|
||||
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
|
||||
|
||||
@@ -194,6 +195,7 @@ typedef struct {
|
||||
- (void)sendScrollX:(float)x y:(float)y deltaX:(NSInteger)deltaX
|
||||
deltaY:(NSInteger)deltaY;
|
||||
- (void)sendWindowsKeyCode:(NSUInteger)keyCode isDown:(BOOL)isDown;
|
||||
- (void)sendKeyChord:(NSUInteger)keyCode modifiers:(NSArray<NSNumber *> *)modifiers;
|
||||
- (void)sendText:(NSString *)text;
|
||||
- (void)sendCommittedText:(NSString *)text;
|
||||
- (void)switchToDisplay:(NSInteger)displayIndex;
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
#include <remote_action.h>
|
||||
#include <stream_names.h>
|
||||
#include "minirtc.h"
|
||||
#include "../../../../libs/controller/include/mobile_device_trust.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using crossdesk::mobile_controller::DeviceTrustIdentity;
|
||||
|
||||
using crossdesk::ControlType;
|
||||
using crossdesk::FileTransferAck;
|
||||
using crossdesk::KeyFlag;
|
||||
@@ -71,6 +74,7 @@ struct PresenceRequest {
|
||||
};
|
||||
|
||||
struct RTCState {
|
||||
std::unique_ptr<DeviceTrustIdentity> trust;
|
||||
MiniRtcPeerPtr *identity_peer = nullptr;
|
||||
MiniRtcPeerPtr *controller_peer = nullptr;
|
||||
CallbackContext identity_context;
|
||||
@@ -211,6 +215,47 @@ OSStatus RemoveIdentityCredential(NSString *account) {
|
||||
return status;
|
||||
}
|
||||
|
||||
std::unique_ptr<DeviceTrustIdentity> LoadDeviceTrustIdentity() {
|
||||
// Pairing export and all bridges share one installation-wide Keychain item.
|
||||
static std::mutex mutex;
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
NSDictionary *query = @{
|
||||
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: @"cn.crossdesk.mobile.device-trust",
|
||||
(__bridge id)kSecAttrAccount: @"v1",
|
||||
(__bridge id)kSecAttrSynchronizable: @NO,
|
||||
};
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
NSMutableDictionary *read = [query mutableCopy];
|
||||
read[(__bridge id)kSecReturnData] = @YES;
|
||||
read[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
|
||||
CFTypeRef result = nullptr;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)read, &result);
|
||||
id object = CFBridgingRelease(result);
|
||||
std::string seed;
|
||||
struct Clean { std::string& value; ~Clean() { DeviceTrustIdentity::ClearSecret(value); } } clean{seed};
|
||||
if (status == errSecSuccess && [object isKindOfClass:NSData.class]) {
|
||||
NSData *data = object;
|
||||
seed.assign(static_cast<const char *>(data.bytes), data.length);
|
||||
} else if (status == errSecItemNotFound &&
|
||||
![NSUserDefaults.standardUserDefaults boolForKey:@"CrossDeskDeviceTrustInitialized.v1"]) {
|
||||
seed = DeviceTrustIdentity::GeneratePrivateKey();
|
||||
NSMutableDictionary *item = [query mutableCopy];
|
||||
item[(__bridge id)kSecValueData] = [NSData dataWithBytes:seed.data() length:seed.size()];
|
||||
item[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
||||
status = SecItemAdd((__bridge CFDictionaryRef)item, nullptr);
|
||||
if (status == errSecDuplicateItem) continue;
|
||||
if (status != errSecSuccess) throw std::runtime_error("Device signing identity save failed");
|
||||
} else {
|
||||
throw std::runtime_error("Device signing identity unavailable");
|
||||
}
|
||||
auto identity = std::make_unique<DeviceTrustIdentity>(seed);
|
||||
[NSUserDefaults.standardUserDefaults setBool:YES forKey:@"CrossDeskDeviceTrustInitialized.v1"];
|
||||
return identity;
|
||||
}
|
||||
throw std::runtime_error("Device signing identity unavailable");
|
||||
}
|
||||
|
||||
void CopyCString(char *destination, size_t capacity,
|
||||
const std::string &source) {
|
||||
if (!destination || capacity == 0) return;
|
||||
@@ -499,6 +544,15 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
std::atomic<uint64_t> _networkingGeneration;
|
||||
}
|
||||
|
||||
+ (nullable NSDictionary<NSString *, NSString *> *)deviceTrustInfoForDeviceID:(NSString *)deviceID {
|
||||
try {
|
||||
auto trust = LoadDeviceTrustIdentity();
|
||||
auto pairing = trust->PairingInfo(BaseIdentity(deviceID.UTF8String ?: ""), "CrossDesk iOS");
|
||||
return @{@"pairing": [NSString stringWithUTF8String:pairing.c_str()],
|
||||
@"fingerprint": [NSString stringWithUTF8String:trust->Fingerprint().c_str()]};
|
||||
} catch (const std::exception&) { return nil; }
|
||||
}
|
||||
|
||||
+ (nullable NSString *)availableUpdateForAppVersion:(NSString *)appVersion
|
||||
releaseJSON:(NSData *)releaseJSON {
|
||||
const auto latest = crossdesk::ParseVersionInfoJSON(std::string_view(
|
||||
@@ -610,6 +664,15 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
self->_state->identity_with_password.clear();
|
||||
self->_state->identity_base.clear();
|
||||
|
||||
try {
|
||||
self->_state->trust = LoadDeviceTrustIdentity();
|
||||
} catch (const std::exception&) {
|
||||
[self reportIdentityStorageStatus:errSecDecode];
|
||||
[self handleSignalState:CrossDeskSignalStateCredentialUnavailable
|
||||
role:PeerRole::Identity generation:self->_identityGeneration.load()];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *cached = nil;
|
||||
OSStatus load_status = LoadIdentityCredential(
|
||||
IdentityCredentialAccount(trimmed, signalPort), &cached);
|
||||
@@ -911,6 +974,30 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
stream:kKeyboardStream];
|
||||
}
|
||||
|
||||
- (void)sendKeyChord:(NSUInteger)keyCode modifiers:(NSArray<NSNumber *> *)modifiers {
|
||||
if (!keyCode || keyCode > 255 || modifiers.count > 4) return;
|
||||
std::vector<NSUInteger> keys;
|
||||
for (NSUInteger code : {17u, 16u, 18u, 91u}) {
|
||||
if ([modifiers containsObject:@(code)]) keys.push_back(code);
|
||||
}
|
||||
std::vector<std::string> messages;
|
||||
for (NSUInteger code : keys) messages.push_back(KeyJSON(code, true));
|
||||
messages.push_back(KeyJSON(keyCode, true));
|
||||
messages.push_back(KeyJSON(keyCode, false));
|
||||
for (auto it = keys.rbegin(); it != keys.rend(); ++it)
|
||||
messages.push_back(KeyJSON(*it, false));
|
||||
const uint64_t generation = _activeControllerGeneration.load();
|
||||
// A whole chord shares one RTC task and can never spill into a new session.
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (![self isControllerGenerationActive:generation] ||
|
||||
!self->_state->controller_peer) return;
|
||||
for (const auto& message : messages) {
|
||||
MiniRtcSendReliableDataFrame(self->_state->controller_peer,
|
||||
message.data(), message.size(), kKeyboardStream);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)sendText:(NSString *)text {
|
||||
NSData *utf8 = [text dataUsingEncoding:NSUTF8StringEncoding];
|
||||
if (!utf8.length) return;
|
||||
@@ -1484,7 +1571,7 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
action.i.top = nullptr;
|
||||
action.i.right = nullptr;
|
||||
action.i.bottom = nullptr;
|
||||
const std::string host_info = action.to_json();
|
||||
const std::string host_info = DeviceTrustIdentity::Advertise(action.to_json());
|
||||
MiniRtcSendReliableDataFrame(self->_state->controller_peer, host_info.data(),
|
||||
host_info.size(), kControlStream);
|
||||
const std::string stream = crossdesk::MakeDisplayStreamId(
|
||||
@@ -1754,6 +1841,18 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id,
|
||||
if (!data || size == 0 || !sourceID) return;
|
||||
const std::string source(sourceID, sourceIDSize);
|
||||
if (source == kControlStream) {
|
||||
if (DeviceTrustIdentity::IsChallenge(std::string_view(data, size))) {
|
||||
const std::string challenge(data, size);
|
||||
dispatch_async(_rtcQueue, ^{
|
||||
if (![self isControllerGenerationActive:generation] || !self->_state->controller_peer ||
|
||||
!self->_state->trust) return;
|
||||
auto proof = self->_state->trust->ChallengeResponse(challenge,
|
||||
self->_state->identity_base, "CrossDesk iOS");
|
||||
if (proof && !proof->empty())
|
||||
MiniRtcSendReliableDataFrame(self->_state->controller_peer, proof->data(), proof->size(), kControlStream);
|
||||
});
|
||||
return;
|
||||
}
|
||||
RemoteAction action{};
|
||||
if (!action.from_json(std::string(data, size))) return;
|
||||
if (action.type == ControlType::video_codec_status) {
|
||||
|
||||
@@ -804,6 +804,37 @@ private struct PasswordPromptView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct DevicePairingView: View {
|
||||
let deviceID: String
|
||||
@State private var info: [String: String]?
|
||||
@State private var copied = false
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
Text(NSLocalizedString("将配对信息添加到被控端的可信设备列表,并核对指纹。", comment: ""))
|
||||
if let info, let fingerprint = info["fingerprint"], let pairing = info["pairing"] {
|
||||
Text(fingerprint.enumerated().map { index, character in
|
||||
(index > 0 && index % 4 == 0 ? " " : "") + String(character)
|
||||
}.joined()).font(.footnote.monospaced()).textSelection(.enabled)
|
||||
Button(NSLocalizedString("复制配对信息", comment: "")) {
|
||||
UIPasteboard.general.setItems([["public.utf8-plain-text": pairing]], options: [.localOnly: true])
|
||||
copied = true
|
||||
}
|
||||
if copied { Text(NSLocalizedString("配对信息已复制。", comment: "")).foregroundStyle(.secondary) }
|
||||
} else {
|
||||
Text(NSLocalizedString("安全存储不可用,请解锁设备后重试", comment: "")).foregroundStyle(.orange)
|
||||
Button(NSLocalizedString("重试", comment: "")) { load() }
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("本机配对信息", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.task { load() }
|
||||
}
|
||||
private func load() { info = CrossDeskRTCBridge.deviceTrustInfo(forDeviceID: deviceID); copied = false }
|
||||
}
|
||||
|
||||
private struct ServerSettingsView: View {
|
||||
@EnvironmentObject private var appUpdates: AppUpdateChecker
|
||||
@ObservedObject var session: RemoteSessionModel
|
||||
@@ -901,6 +932,11 @@ private struct ServerSettingsView: View {
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
PrivacySettingsSection(session: session)
|
||||
Section {
|
||||
NavigationLink(NSLocalizedString("本机配对信息", comment: "")) {
|
||||
DevicePairingView(deviceID: session.localIdentity)
|
||||
}
|
||||
}
|
||||
Section {
|
||||
NavigationLink {
|
||||
ThirdPartyLicensesView(session: session)
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
import SwiftUI
|
||||
|
||||
struct RemoteShortcutPanel: View {
|
||||
@ObservedObject var store: RemoteShortcutStore
|
||||
@ObservedObject var session: RemoteSessionModel
|
||||
let systemKeyboard: () -> Void
|
||||
let dismiss: () -> Void
|
||||
let manage: () -> Void
|
||||
@State private var category = "common"
|
||||
@State private var page = 0
|
||||
private let groups = ["common", "keys", "system", "mine"]
|
||||
private let labels = ["常用", "按键", "系统", "我的"]
|
||||
private var platform: String { session.remotePlatform.rawValue }
|
||||
private var items: [RemoteShortcut] { store.items(platform: platform).filter { $0.group == category } }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
RemoteKeyboardToolbar(
|
||||
switchKeyboard: systemKeyboard,
|
||||
more: systemKeyboard,
|
||||
moreAccessibilityLabel: NSLocalizedString("系统键盘", comment: ""),
|
||||
dismiss: dismiss
|
||||
) {
|
||||
Text(NSLocalizedString("快捷键", comment: ""))
|
||||
.font(.subheadline).foregroundStyle(.secondary)
|
||||
}
|
||||
VStack(spacing: 4) {
|
||||
Picker(NSLocalizedString("快捷键", comment: ""), selection: $category) {
|
||||
ForEach(Array(groups.enumerated()), id: \.element) { index, group in
|
||||
Text(NSLocalizedString(labels[index], comment: "")).tag(group)
|
||||
}
|
||||
}.pickerStyle(.segmented)
|
||||
if category == "system" && platform == "linux" {
|
||||
Picker(NSLocalizedString("选择桌面方案", comment: ""), selection: Binding(get: { store.desktop }, set: { store.desktop = $0 })) {
|
||||
Text(NSLocalizedString("选择桌面方案", comment: "")).tag("")
|
||||
Text("GNOME").tag("gnome")
|
||||
Text("KDE Plasma").tag("kde")
|
||||
}.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
GeometryReader { geometry in
|
||||
ScrollView {
|
||||
if items.isEmpty {
|
||||
Text(NSLocalizedString(!RemoteShortcut.platforms.contains(platform) ? "等待远端平台信息" : category == "system" ? "选择桌面方案以加载系统预设" : "还没有自定义快捷键", comment: ""))
|
||||
.font(.footnote).foregroundStyle(.secondary).frame(maxWidth: .infinity).padding(.vertical, 16)
|
||||
} else {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 6), count: 3), spacing: 6) {
|
||||
ForEach(Array(items.dropFirst(page * 6).prefix(6))) { item in
|
||||
Button {
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
session.sendShortcut(item)
|
||||
} label: {
|
||||
VStack(spacing: 3) {
|
||||
Text(item.title).font(.subheadline)
|
||||
Text(item.chord).font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.frame(maxWidth: .infinity, minHeight: max(44, (geometry.size.height - (session.keyboardInputStatus.isEmpty ? 6 : 34)) / 2))
|
||||
.background(Color(uiColor: .secondarySystemBackground), in: RoundedRectangle(cornerRadius: 7))
|
||||
}.disabled(!session.isConnected)
|
||||
.accessibilityLabel(item.title + ", " + item.chord)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !session.keyboardInputStatus.isEmpty {
|
||||
Text(session.keyboardInputStatus).font(.caption).foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack(spacing: 6) {
|
||||
if items.count > 6 {
|
||||
Button { page = max(0, page - 1) } label: { Image(systemName: "chevron.left").frame(width: 44, height: 44) }
|
||||
.disabled(page == 0).accessibilityLabel(NSLocalizedString("上一页", comment: ""))
|
||||
Text("\(page + 1) / \((items.count + 5) / 6)").font(.caption)
|
||||
Button { page += 1 } label: { Image(systemName: "chevron.right").frame(width: 44, height: 44) }
|
||||
.disabled((page + 1) * 6 >= items.count).accessibilityLabel(NSLocalizedString("下一页", comment: ""))
|
||||
} else {
|
||||
Text(RemoteShortcut.platformName(platform)).font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
Button(NSLocalizedString("管理与自定义", comment: ""), action: manage)
|
||||
.font(.subheadline).frame(minHeight: 44)
|
||||
.disabled(!RemoteShortcut.platforms.contains(platform))
|
||||
}.frame(height: 44)
|
||||
}
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.background(Color(uiColor: RemoteKeyboardInputBar.keyboardBackground))
|
||||
.onValueChange(of: category) { _ in page = 0 }
|
||||
.onValueChange(of: platform) { _ in page = 0 }
|
||||
.onValueChange(of: store.revision) { _ in page = min(page, max(0, (items.count - 1) / 6)) }
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoteShortcutManager: View {
|
||||
@ObservedObject var store: RemoteShortcutStore
|
||||
let platform: String
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var editor: RemoteShortcut?
|
||||
@State private var error = ""
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
Section {
|
||||
Text(NSLocalizedString("本地编辑,输入不会发送到远端", comment: "")).font(.footnote).foregroundStyle(.secondary)
|
||||
}
|
||||
Section(NSLocalizedString("键盘上方常用栏", comment: "")) {
|
||||
ForEach(store.pinned(platform: platform)) { item in
|
||||
HStack {
|
||||
label(item)
|
||||
Spacer(minLength: 2)
|
||||
Button {
|
||||
var values = store.pins(platform: platform)
|
||||
if let index = values.firstIndex(of: item.id), index > 0 { values.swapAt(index - 1, index); store.setPins(values, platform: platform) }
|
||||
} label: { Image(systemName: "arrow.up").frame(width: 44, height: 44) }
|
||||
.disabled(store.pins(platform: platform).first == item.id)
|
||||
.accessibilityLabel(NSLocalizedString("向上移动", comment: ""))
|
||||
Button(NSLocalizedString("移除", comment: "")) { store.setPins(store.pins(platform: platform).filter { $0 != item.id }, platform: platform) }
|
||||
.frame(minHeight: 44)
|
||||
}.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("添加到常用栏", comment: "")) {
|
||||
ForEach(store.items(platform: platform)) { item in
|
||||
HStack {
|
||||
label(item)
|
||||
Spacer(minLength: 2)
|
||||
if !store.pins(platform: platform).contains(item.id) {
|
||||
Button {
|
||||
var values = store.pins(platform: platform)
|
||||
guard values.count < 8 else { error = NSLocalizedString("最多固定 8 个快捷键", comment: ""); return }
|
||||
values.append(item.id); store.setPins(values, platform: platform)
|
||||
} label: { Image(systemName: "plus").frame(width: 44, height: 44) }
|
||||
.accessibilityLabel(NSLocalizedString("添加到常用栏", comment: ""))
|
||||
}
|
||||
if item.custom {
|
||||
Button(NSLocalizedString("编辑", comment: "")) { editor = item }.frame(minHeight: 44)
|
||||
Button(role: .destructive) { store.remove(item.id) } label: { Image(systemName: "trash").frame(width: 44, height: 44) }
|
||||
.accessibilityLabel(NSLocalizedString("删除", comment: ""))
|
||||
}
|
||||
}.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
if !error.isEmpty { Text(error).foregroundStyle(.red) }
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("管理快捷键", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { Button(NSLocalizedString("完成", comment: "")) { dismiss() } }
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(NSLocalizedString("新建", comment: "")) {
|
||||
editor = RemoteShortcut(id: UUID().uuidString, name: "", platform: platform, group: "mine", key: 83, modifiers: [platform == "macos" ? 91 : 17])
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(item: $editor) { item in RemoteShortcutEditor(store: store, item: item) }
|
||||
}
|
||||
}
|
||||
private func label(_ item: RemoteShortcut) -> some View {
|
||||
VStack(alignment: .leading, spacing: 3) { Text(item.title); Text(item.chord).font(.caption).foregroundStyle(.secondary) }
|
||||
}
|
||||
}
|
||||
|
||||
private struct RemoteShortcutEditor: View {
|
||||
@ObservedObject var store: RemoteShortcutStore
|
||||
let item: RemoteShortcut
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var name: String
|
||||
@State private var platform: String
|
||||
@State private var key: UInt
|
||||
@State private var modifiers: Set<UInt>
|
||||
@State private var pin: Bool
|
||||
@State private var error = ""
|
||||
init(store: RemoteShortcutStore, item: RemoteShortcut) {
|
||||
self.store = store; self.item = item
|
||||
_name = State(initialValue: item.name); _platform = State(initialValue: item.platform)
|
||||
_key = State(initialValue: item.key); _modifiers = State(initialValue: Set(item.modifiers))
|
||||
_pin = State(initialValue: item.custom ? store.pins(platform: item.platform).contains(item.id) : store.pins(platform: item.platform).count < 8)
|
||||
}
|
||||
private var preview: RemoteShortcut { RemoteShortcut(id: "", name: name, platform: platform, group: "mine", key: key, modifiers: RemoteShortcut.modifierKeys.filter(modifiers.contains)) }
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section { Text(NSLocalizedString("本地编辑,输入不会发送到远端", comment: "")).font(.footnote).foregroundStyle(.secondary) }
|
||||
TextField(NSLocalizedString("名称", comment: ""), text: $name)
|
||||
Picker(NSLocalizedString("适用远端", comment: ""), selection: $platform) {
|
||||
ForEach(RemoteShortcut.platforms, id: \.self) { Text(RemoteShortcut.platformName($0)).tag($0) }
|
||||
}
|
||||
Picker(NSLocalizedString("主按键", comment: ""), selection: $key) {
|
||||
ForEach(RemoteShortcut.keys, id: \.self) { Text(RemoteShortcut.keyName($0, platform: platform)).tag($0) }
|
||||
}
|
||||
Section(NSLocalizedString("修饰键", comment: "")) {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(RemoteShortcut.modifierKeys, id: \.self) { code in
|
||||
Button {
|
||||
if modifiers.contains(code) { modifiers.remove(code) } else { modifiers.insert(code) }
|
||||
} label: {
|
||||
Text(RemoteShortcut.modifierName(code, platform: platform)).font(.subheadline)
|
||||
.frame(maxWidth: .infinity, minHeight: 44)
|
||||
.background(modifiers.contains(code) ? Color.accentColor.opacity(0.18) : Color(uiColor: .tertiarySystemFill), in: RoundedRectangle(cornerRadius: 7))
|
||||
}.buttonStyle(.borderless)
|
||||
.accessibilityAddTraits(modifiers.contains(code) ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
}
|
||||
Section(NSLocalizedString("发送组合", comment: "")) { Text(preview.chord) }
|
||||
Toggle(NSLocalizedString("固定到键盘上方", comment: ""), isOn: $pin)
|
||||
if !error.isEmpty { Text(error).foregroundStyle(.red) }
|
||||
}
|
||||
.navigationTitle(NSLocalizedString(item.custom ? "编辑快捷键" : "新建快捷键", comment: ""))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) { Button(NSLocalizedString("取消", comment: "")) { dismiss() } }
|
||||
ToolbarItem(placement: .confirmationAction) {
|
||||
Button(NSLocalizedString("保存", comment: "")) {
|
||||
if let message = store.save(id: item.custom ? item.id : nil, name: name, platform: platform, key: key, modifiers: Array(modifiers), pin: pin) { error = message } else { dismiss() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
struct RemoteShortcut: Codable, Identifiable, Equatable {
|
||||
let id: String
|
||||
let name: String
|
||||
let platform: String
|
||||
let group: String
|
||||
let key: UInt
|
||||
let modifiers: [UInt]
|
||||
var service = false
|
||||
var custom = false
|
||||
|
||||
var title: String { custom ? name : NSLocalizedString(name, comment: "") }
|
||||
var chord: String {
|
||||
(modifiers.map { Self.modifierName($0, platform: platform) } +
|
||||
[Self.keyName(key, platform: platform)]).joined(separator: " + ")
|
||||
}
|
||||
static let platforms = ["windows", "macos", "linux"]
|
||||
static let modifierKeys: [UInt] = [17, 16, 18, 91]
|
||||
static let keys: [UInt] = Array(65...90) + Array(48...57) + Array(112...123) +
|
||||
[27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]
|
||||
static func modifierName(_ key: UInt, platform: String) -> String {
|
||||
switch key {
|
||||
case 17: return "Ctrl"
|
||||
case 16: return "Shift"
|
||||
case 18: return platform == "macos" ? "Option" : "Alt"
|
||||
case 91: return platform == "macos" ? "Cmd" : platform == "linux" ? "Super" : "Win"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
static func keyName(_ key: UInt, platform: String) -> String {
|
||||
if (65...90).contains(key) || (48...57).contains(key) { return String(UnicodeScalar(UInt32(key))!) }
|
||||
if (112...123).contains(key) { return "F\(key - 111)" }
|
||||
switch key {
|
||||
case 8: return platform == "macos" ? "Delete ⌫" : "Backspace"
|
||||
case 9: return "Tab"
|
||||
case 13: return "Enter"
|
||||
case 27: return "Esc"
|
||||
case 32: return "Space"
|
||||
case 33: return "PageUp"
|
||||
case 34: return "PageDown"
|
||||
case 35: return "End"
|
||||
case 36: return "Home"
|
||||
case 37: return "←"
|
||||
case 38: return "↑"
|
||||
case 39: return "→"
|
||||
case 40: return "↓"
|
||||
case 45: return "Insert"
|
||||
case 46: return platform == "macos" ? "Delete ⌦" : "Delete"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
static func platformName(_ platform: String) -> String {
|
||||
switch platform {
|
||||
case "windows": return "Windows"
|
||||
case "macos": return "macOS"
|
||||
case "linux": return "Linux"
|
||||
default: return NSLocalizedString("未知", comment: "")
|
||||
}
|
||||
}
|
||||
var valid: Bool {
|
||||
Self.platforms.contains(platform) && id.hasPrefix("custom-") &&
|
||||
!name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && name.count <= 64 &&
|
||||
Self.keys.contains(key) && modifiers.count <= 4 &&
|
||||
modifiers.allSatisfy(Self.modifierKeys.contains) &&
|
||||
!(platform == "windows" && key == 46 && modifiers.contains(17) && modifiers.contains(18))
|
||||
}
|
||||
}
|
||||
|
||||
final class RemoteShortcutStore: ObservableObject {
|
||||
@Published private(set) var revision = 0
|
||||
private let defaults: UserDefaults
|
||||
private let prefix = "remote_shortcuts_v1."
|
||||
init(defaults: UserDefaults = .standard) { self.defaults = defaults }
|
||||
var desktop: String {
|
||||
get { let value = defaults.string(forKey: prefix + "linux_desktop") ?? ""; return ["gnome", "kde"].contains(value) ? value : "" }
|
||||
set { defaults.set(["gnome", "kde"].contains(newValue) ? newValue : "", forKey: prefix + "linux_desktop"); revision &+= 1 }
|
||||
}
|
||||
var custom: [RemoteShortcut] {
|
||||
guard let data = defaults.data(forKey: prefix + "custom"),
|
||||
let decoded = try? JSONDecoder().decode([RemoteShortcut].self, from: data) else { return [] }
|
||||
var seen = Set<String>()
|
||||
return Array(decoded.filter { $0.valid && $0.custom && !$0.service && seen.insert($0.id).inserted }.prefix(16))
|
||||
}
|
||||
func items(platform: String) -> [RemoteShortcut] {
|
||||
func item(_ id: String, _ name: String, _ group: String, _ key: UInt, _ modifiers: UInt...) -> RemoteShortcut {
|
||||
RemoteShortcut(id: id, name: name, platform: platform, group: group, key: key, modifiers: modifiers)
|
||||
}
|
||||
var result: [RemoteShortcut] = []
|
||||
if RemoteShortcut.platforms.contains(platform) {
|
||||
let primary: UInt = platform == "macos" ? 91 : 17
|
||||
let names = ["复制", "粘贴", "剪切", "全选", "撤销", "保存"]
|
||||
let ids = ["copy", "paste", "cut", "all", "undo", "save"]
|
||||
let keys: [UInt] = [67, 86, 88, 65, 90, 83]
|
||||
for index in ids.indices { result.append(item(ids[index], names[index], "common", keys[index], primary)) }
|
||||
if platform == "linux" {
|
||||
result += [item("term-copy", "终端复制", "common", 67, 17, 16), item("term-paste", "终端粘贴", "common", 86, 17, 16)]
|
||||
}
|
||||
}
|
||||
let keys: [UInt] = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40] + Array(112...123)
|
||||
for key in keys {
|
||||
result.append(item(key == 27 ? "esc" : key == 9 ? "tab" : "key-\(key)", RemoteShortcut.keyName(key, platform: platform), "keys", key))
|
||||
}
|
||||
if platform == "windows" {
|
||||
result += [item("switch", "切换窗口", "system", 9, 18), item("desktop", "显示桌面", "system", 68, 91), item("task", "任务管理器", "system", 27, 17, 16)]
|
||||
result.append(RemoteShortcut(id: "sas", name: "安全选项", platform: platform, group: "system", key: 46, modifiers: [17, 18], service: true))
|
||||
} else if platform == "macos" {
|
||||
result += [item("switch", "切换应用", "system", 9, 91), item("search", "聚焦搜索", "system", 32, 91), item("forcequit", "强制退出面板", "system", 27, 18, 91)]
|
||||
} else if platform == "linux" && !desktop.isEmpty {
|
||||
result += [item("switch", desktop == "gnome" ? "切换应用" : "切换窗口", "system", 9, desktop == "gnome" ? 91 : 18), item("run", "运行窗口", "system", 113, 18)]
|
||||
result.append(desktop == "gnome" ? item("apps", "应用列表", "system", 65, 91) : item("window-menu", "窗口菜单", "system", 114, 18))
|
||||
}
|
||||
return result + custom.filter { $0.platform == platform }
|
||||
}
|
||||
func pins(platform: String) -> [String] {
|
||||
let values = defaults.stringArray(forKey: prefix + "pins_" + platform) ?? ["esc", "tab", "copy", "paste"]
|
||||
let available = Set(items(platform: platform).map(\.id))
|
||||
var seen = Set<String>()
|
||||
return Array(values.filter { available.contains($0) && seen.insert($0).inserted }.prefix(8))
|
||||
}
|
||||
func pinned(platform: String) -> [RemoteShortcut] {
|
||||
let all = items(platform: platform)
|
||||
return pins(platform: platform).compactMap { id in all.first { $0.id == id } }
|
||||
}
|
||||
func setPins(_ values: [String], platform: String) {
|
||||
defaults.set(Array(values.prefix(8)), forKey: prefix + "pins_" + platform); revision &+= 1
|
||||
}
|
||||
func remove(_ id: String) {
|
||||
write(custom.filter { $0.id != id })
|
||||
for platform in RemoteShortcut.platforms { setPins(pins(platform: platform).filter { $0 != id }, platform: platform) }
|
||||
}
|
||||
func save(id: String?, name: String, platform: String, key: UInt, modifiers: [UInt], pin: Bool) -> String? {
|
||||
let name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let normalized = RemoteShortcut.modifierKeys.filter(modifiers.contains)
|
||||
let item = RemoteShortcut(id: id ?? "custom-" + UUID().uuidString, name: name, platform: platform, group: "mine", key: key, modifiers: normalized, custom: true)
|
||||
if platform == "windows" && key == 46 && normalized.contains(17) && normalized.contains(18) { return NSLocalizedString("请使用系统预设中的安全选项", comment: "") }
|
||||
guard item.valid else { return NSLocalizedString("请输入有效的快捷键名称", comment: "") }
|
||||
var values = custom
|
||||
if values.contains(where: { $0.id != id && $0.platform == platform && $0.key == key && Set($0.modifiers) == Set(normalized) }) { return NSLocalizedString("已有相同组合的快捷键", comment: "") }
|
||||
if !values.contains(where: { $0.id == id }) && values.count >= 16 { return NSLocalizedString("最多保存 16 个自定义快捷键", comment: "") }
|
||||
if pin && !pins(platform: platform).contains(item.id) && pins(platform: platform).count >= 8 { return NSLocalizedString("最多固定 8 个快捷键", comment: "") }
|
||||
values.removeAll { $0.id == id }; values.append(item); write(values)
|
||||
for target in RemoteShortcut.platforms {
|
||||
var selected = pins(platform: target); let previous = selected.firstIndex(of: item.id)
|
||||
selected.removeAll { $0 == item.id }
|
||||
if target == platform && pin { selected.insert(item.id, at: min(previous ?? selected.count, selected.count)) }
|
||||
setPins(selected, platform: target)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
private func write(_ values: [RemoteShortcut]) {
|
||||
guard let data = try? JSONEncoder().encode(values) else { return }
|
||||
defaults.set(data, forKey: prefix + "custom"); revision &+= 1
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ struct RemoteSystemKeyboard: View {
|
||||
var geometry: RemoteKeyboardGeometry? = nil
|
||||
var animatePresentation = true
|
||||
var didPresent: () -> Void = {}
|
||||
var shortcuts: [RemoteShortcut] = []
|
||||
var sendShortcut: (RemoteShortcut) -> Void = { _ in }
|
||||
var showShortcuts: () -> Void = {}
|
||||
let switchKeyboard: () -> Void
|
||||
let dismiss: () -> Void
|
||||
|
||||
@@ -23,6 +26,11 @@ struct RemoteSystemKeyboard: View {
|
||||
didPresent: didPresent,
|
||||
key: session.sendKeyStroke,
|
||||
status: session.keyboardInputStatus,
|
||||
shortcuts: shortcuts,
|
||||
sendShortcut: sendShortcut,
|
||||
showShortcuts: showShortcuts,
|
||||
inputBlocked: { session.showKeyboardCompositionHint() },
|
||||
inputReady: session.clearKeyboardInputStatus,
|
||||
switchKeyboard: switchKeyboard,
|
||||
dismiss: dismiss
|
||||
)
|
||||
@@ -43,6 +51,11 @@ private struct RemoteKeyboardHost: UIViewControllerRepresentable {
|
||||
let didPresent: () -> Void
|
||||
let key: (UInt) -> Void
|
||||
let status: String
|
||||
let shortcuts: [RemoteShortcut]
|
||||
let sendShortcut: (RemoteShortcut) -> Void
|
||||
let showShortcuts: () -> Void
|
||||
let inputBlocked: () -> Void
|
||||
let inputReady: () -> Void
|
||||
let switchKeyboard: () -> Void
|
||||
let dismiss: () -> Void
|
||||
|
||||
@@ -63,13 +76,32 @@ private struct RemoteKeyboardHost: UIViewControllerRepresentable {
|
||||
controller.animatePresentation = animatePresentation
|
||||
controller.didPresent = didPresent
|
||||
controller.editor.key = key
|
||||
let prepareShortcut: () -> Bool = { [weak controller] in
|
||||
guard let controller else { return false }
|
||||
if controller.editor.markedTextRange != nil { inputBlocked(); return false }
|
||||
guard controller.editor.prepareShortcut() else { return false }
|
||||
inputReady()
|
||||
return true
|
||||
}
|
||||
controller.inputBar.switchKeyboard = { [weak controller] in
|
||||
controller?.rememberKeyboardHeight()
|
||||
guard let controller, prepareShortcut() else { return }
|
||||
controller.rememberKeyboardHeight()
|
||||
// Let SwiftUI install the replacement panel before teardown hides
|
||||
// the native keyboard, avoiding an empty frame between the two.
|
||||
controller?.animateDismissal = false
|
||||
controller.animateDismissal = false
|
||||
switchKeyboard()
|
||||
}
|
||||
controller.inputBar.showShortcuts = { [weak controller] in
|
||||
guard let controller, prepareShortcut() else { return }
|
||||
controller.rememberKeyboardHeight()
|
||||
controller.animateDismissal = false
|
||||
showShortcuts()
|
||||
}
|
||||
controller.inputBar.sendShortcut = { item in
|
||||
guard prepareShortcut() else { return }
|
||||
sendShortcut(item)
|
||||
}
|
||||
controller.inputBar.setShortcuts(shortcuts)
|
||||
controller.inputBar.dismiss = { [weak controller] in
|
||||
controller?.stop()
|
||||
dismiss()
|
||||
@@ -204,6 +236,8 @@ final class RemoteKeyboardController: UIViewController, ObservableObject {
|
||||
finishKeyboardPresentation()
|
||||
editor.stop()
|
||||
inputBar.switchKeyboard = nil
|
||||
inputBar.showShortcuts = nil
|
||||
inputBar.sendShortcut = nil
|
||||
inputBar.dismiss = nil
|
||||
let dismiss = {
|
||||
self.editor.resignFirstResponder()
|
||||
@@ -258,6 +292,41 @@ private final class RemoteKeyboardHostView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Both replacement panels use the same edge controls and layout. Only the
|
||||
/// center content changes, so switching panels cannot move the return label.
|
||||
struct RemoteKeyboardToolbar<Center: View>: View {
|
||||
let switchKeyboard: () -> Void
|
||||
let more: () -> Void
|
||||
var moreAccessibilityLabel = NSLocalizedString("更多快捷键", comment: "")
|
||||
let dismiss: () -> Void
|
||||
var dismissAccessibilityLabel = NSLocalizedString("收起键盘", comment: "")
|
||||
@ViewBuilder var center: () -> Center
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
Button(NSLocalizedString("系统键盘", comment: ""), action: switchKeyboard)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.frame(minWidth: 80, minHeight: 44)
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.accessibilityLabel(NSLocalizedString("切换到系统键盘", comment: ""))
|
||||
center()
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
Button(action: more) {
|
||||
Image(systemName: "ellipsis")
|
||||
.font(.system(size: 17))
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.accessibilityLabel(moreAccessibilityLabel)
|
||||
RemoteKeyboardDismissButton(accessibilityLabel: dismissAccessibilityLabel, action: dismiss)
|
||||
.frame(width: 44, height: 44)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
.frame(height: 44)
|
||||
.foregroundStyle(Color(uiColor: .label))
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the same UIKit symbol layout in both the native and SwiftUI toolbars.
|
||||
/// Matching font sizes alone still gives their symbols different baselines.
|
||||
struct RemoteKeyboardDismissButton: UIViewRepresentable {
|
||||
@@ -306,7 +375,11 @@ final class RemoteKeyboardInputBar: UIView {
|
||||
traits.userInterfaceStyle == .dark ? .secondarySystemBackground : .systemGray4
|
||||
}
|
||||
var switchKeyboard: (() -> Void)?
|
||||
var showShortcuts: (() -> Void)?
|
||||
var sendShortcut: ((RemoteShortcut) -> Void)?
|
||||
var dismiss: (() -> Void)?
|
||||
private let shortcutRow = UIStackView()
|
||||
private var shortcuts: [RemoteShortcut] = []
|
||||
private let statusLabel = UILabel()
|
||||
private let row = UIStackView()
|
||||
private var status = ""
|
||||
@@ -331,10 +404,29 @@ final class RemoteKeyboardInputBar: UIView {
|
||||
dismissButton.accessibilityLabel = NSLocalizedString("收起系统键盘", comment: "")
|
||||
dismissButton.addTarget(self, action: #selector(hideKeyboard), for: .touchUpInside)
|
||||
|
||||
let moreButton = UIButton(type: .system)
|
||||
moreButton.setImage(UIImage(systemName: "ellipsis"), for: .normal)
|
||||
moreButton.accessibilityLabel = NSLocalizedString("更多快捷键", comment: "")
|
||||
moreButton.addTarget(self, action: #selector(openShortcuts), for: .touchUpInside)
|
||||
let scroll = UIScrollView()
|
||||
scroll.showsHorizontalScrollIndicator = false
|
||||
shortcutRow.axis = .horizontal
|
||||
shortcutRow.spacing = 2
|
||||
shortcutRow.translatesAutoresizingMaskIntoConstraints = false
|
||||
scroll.addSubview(shortcutRow)
|
||||
NSLayoutConstraint.activate([
|
||||
shortcutRow.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor),
|
||||
shortcutRow.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor),
|
||||
shortcutRow.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
|
||||
shortcutRow.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
|
||||
shortcutRow.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor),
|
||||
moreButton.widthAnchor.constraint(equalToConstant: 44)
|
||||
])
|
||||
|
||||
row.axis = .horizontal
|
||||
row.alignment = .fill
|
||||
row.spacing = 4
|
||||
for item in [computerButton, UIView(), dismissButton] { row.addArrangedSubview(item) }
|
||||
for item in [computerButton, scroll, moreButton, dismissButton] { row.addArrangedSubview(item) }
|
||||
row.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(row)
|
||||
NSLayoutConstraint.activate([
|
||||
@@ -368,6 +460,24 @@ final class RemoteKeyboardInputBar: UIView {
|
||||
setNeedsLayout()
|
||||
}
|
||||
|
||||
func setShortcuts(_ items: [RemoteShortcut]) {
|
||||
guard items != shortcuts else { return }
|
||||
shortcuts = items
|
||||
for child in shortcutRow.arrangedSubviews { shortcutRow.removeArrangedSubview(child); child.removeFromSuperview() }
|
||||
for item in items {
|
||||
let button = UIButton(type: .system)
|
||||
button.setTitle(item.title, for: .normal)
|
||||
button.titleLabel?.font = .systemFont(ofSize: 13)
|
||||
button.accessibilityLabel = item.title + ", " + item.chord
|
||||
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
|
||||
button.addAction(UIAction { [weak self] _ in
|
||||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||||
self?.sendShortcut?(item)
|
||||
}, for: .touchUpInside)
|
||||
shortcutRow.addArrangedSubview(button)
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let inset = max(8, safeAreaInsets.left)
|
||||
@@ -383,6 +493,7 @@ final class RemoteKeyboardInputBar: UIView {
|
||||
}
|
||||
|
||||
@objc private func switchToComputer() { switchKeyboard?() }
|
||||
@objc private func openShortcuts() { showShortcuts?() }
|
||||
@objc private func hideKeyboard() { dismiss?() }
|
||||
}
|
||||
|
||||
@@ -523,6 +634,13 @@ final class RemoteTextInputView: UITextField, UITextFieldDelegate {
|
||||
resetContext()
|
||||
}
|
||||
|
||||
func prepareShortcut() -> Bool {
|
||||
guard markedTextRange == nil, synchronizeText() else { return false }
|
||||
// Future IME corrections must not delete text at a caret moved by a shortcut.
|
||||
resetContext()
|
||||
return true
|
||||
}
|
||||
|
||||
func stop() {
|
||||
acceptingInput = false
|
||||
applyEdit = nil
|
||||
|
||||
@@ -1224,11 +1224,34 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
}
|
||||
|
||||
func sendKeyStroke(_ keyCode: UInt) {
|
||||
bridge.sendWindowsKeyCode(keyCode, isDown: true)
|
||||
bridge.sendWindowsKeyCode(keyCode, isDown: false)
|
||||
sendKeyChord(keyCode, modifiers: [])
|
||||
}
|
||||
|
||||
func sendKeyChord(_ keyCode: UInt, modifiers: [UInt]) {
|
||||
guard hasNetworkConsent, isConnected, !privacyScreen.unlockNotice else { return }
|
||||
bridge.sendKeyChord(keyCode, modifiers: RemoteShortcut.modifierKeys.filter(modifiers.contains).map { NSNumber(value: $0) })
|
||||
}
|
||||
|
||||
func showKeyboardCompositionHint() {
|
||||
keyboardInputStatus = NSLocalizedString("请先选词或取消组词", comment: "")
|
||||
}
|
||||
|
||||
func clearKeyboardInputStatus() { keyboardInputStatus = "" }
|
||||
|
||||
func sendShortcut(_ item: RemoteShortcut) {
|
||||
guard hasNetworkConsent, isConnected, !privacyScreen.unlockNotice,
|
||||
item.platform == remotePlatform.rawValue else { return }
|
||||
keyboardInputStatus = ""
|
||||
if item.service {
|
||||
sendSecureAttention()
|
||||
keyboardInputStatus = controlStatus
|
||||
} else {
|
||||
sendKeyChord(item.key, modifiers: item.modifiers)
|
||||
}
|
||||
}
|
||||
|
||||
func sendKeyState(_ keyCode: UInt, isDown: Bool) {
|
||||
guard hasNetworkConsent, isConnected else { return }
|
||||
bridge.sendWindowsKeyCode(keyCode, isDown: isDown)
|
||||
}
|
||||
|
||||
@@ -1504,6 +1527,7 @@ final class RemoteSessionModel: NSObject, ObservableObject, CrossDeskRTCBridgeDe
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge, didSendSecureAttention success: Bool) {
|
||||
guard hasNetworkConsent, isConnected else { return }
|
||||
controlStatus = success ? NSLocalizedString("已发送 Ctrl+Alt+Del", comment: "") : NSLocalizedString("指令发送失败,请重试", comment: "")
|
||||
keyboardInputStatus = controlStatus
|
||||
}
|
||||
|
||||
func rtcBridge(_ bridge: CrossDeskRTCBridge,
|
||||
|
||||
@@ -31,6 +31,7 @@ private enum RemoteKeyboardMetrics {
|
||||
private enum RemoteKeyboardMode: String {
|
||||
case system
|
||||
case computer
|
||||
case shortcuts
|
||||
}
|
||||
|
||||
private struct RemoteViewportState: Equatable {
|
||||
@@ -45,6 +46,10 @@ struct RemoteSessionView: View {
|
||||
@State private var virtualMouseVisible = false
|
||||
@StateObject private var systemKeyboardContext = RemoteKeyboardInputContext()
|
||||
@StateObject private var keyboardGeometry = RemoteKeyboardGeometry()
|
||||
@StateObject private var shortcutStore = RemoteShortcutStore()
|
||||
@State private var shortcutManagerVisible = false
|
||||
@State private var shortcutManagerPlatform = "unknown"
|
||||
@State private var previousKeyboardMode: RemoteKeyboardMode = .computer
|
||||
@State private var keyboardInputVisible = false
|
||||
@State private var keyboardMode: RemoteKeyboardMode = .system
|
||||
@State private var animateSystemKeyboardPresentation = true
|
||||
@@ -171,10 +176,13 @@ struct RemoteSessionView: View {
|
||||
geometry: keyboardGeometry,
|
||||
animatePresentation: animateSystemKeyboardPresentation,
|
||||
didPresent: { waitingForSystemKeyboard = false },
|
||||
shortcuts: shortcutStore.pinned(platform: session.remotePlatform.rawValue),
|
||||
sendShortcut: session.sendShortcut,
|
||||
showShortcuts: { previousKeyboardMode = .shortcuts; keyboardMode = .shortcuts },
|
||||
switchKeyboard: { keyboardMode = .computer },
|
||||
dismiss: { keyboardInputVisible = false })
|
||||
}
|
||||
if keyboardMode == .computer || waitingForSystemKeyboard {
|
||||
if keyboardMode != .system || waitingForSystemKeyboard {
|
||||
dockedKeyboard
|
||||
}
|
||||
}
|
||||
@@ -204,6 +212,7 @@ struct RemoteSessionView: View {
|
||||
if visible {
|
||||
mouseInput.releaseAll()
|
||||
keyboardInputVisible = false
|
||||
shortcutManagerVisible = false
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
@@ -262,6 +271,7 @@ struct RemoteSessionView: View {
|
||||
if !connected {
|
||||
mouseInput.releaseAll()
|
||||
keyboardInputVisible = false
|
||||
shortcutManagerVisible = false
|
||||
}
|
||||
}
|
||||
.onValueChange(of: session.remoteCursorPositionRevision) { _ in
|
||||
@@ -278,6 +288,14 @@ struct RemoteSessionView: View {
|
||||
session.fileImportFailed()
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $shortcutManagerVisible, onDismiss: {
|
||||
if session.isConnected && scenePhase == .active && !session.privacyScreen.unlockNotice {
|
||||
keyboardMode = .shortcuts
|
||||
keyboardInputVisible = true
|
||||
}
|
||||
}) {
|
||||
RemoteShortcutManager(store: shortcutStore, platform: shortcutManagerPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
private var privacyUnlockOverlay: some View {
|
||||
@@ -505,7 +523,7 @@ struct RemoteSessionView: View {
|
||||
}
|
||||
}
|
||||
x = min(max(x, halfWidth + 8), max(halfWidth + 8, size.width - halfWidth - 8))
|
||||
if keyboardInputVisible && keyboardMode == .computer {
|
||||
if keyboardInputVisible && keyboardMode != .system {
|
||||
let keyboardSize = keyboardPanelSize(in: size)
|
||||
let center = keyboardCenter(in: size, panelSize: keyboardSize)
|
||||
let keyboardRect = CGRect(x: center.x - keyboardSize.width / 2, y: center.y - keyboardSize.height / 2, width: keyboardSize.width, height: keyboardSize.height)
|
||||
@@ -537,16 +555,33 @@ struct RemoteSessionView: View {
|
||||
.ignoresSafeArea(.keyboard)
|
||||
}
|
||||
|
||||
private func returnToSystemKeyboard() {
|
||||
previousKeyboardMode = keyboardMode
|
||||
animateSystemKeyboardPresentation = false
|
||||
waitingForSystemKeyboard = true
|
||||
keyboardMode = .system
|
||||
}
|
||||
|
||||
private func remoteKeyboardPanel(bottomInset: CGFloat) -> some View {
|
||||
RemoteComputerKeyboard(
|
||||
session: session,
|
||||
switchKeyboard: {
|
||||
animateSystemKeyboardPresentation = false
|
||||
waitingForSystemKeyboard = true
|
||||
keyboardMode = .system
|
||||
},
|
||||
dismiss: { keyboardInputVisible = false }
|
||||
)
|
||||
Group {
|
||||
if keyboardMode == .shortcuts || (waitingForSystemKeyboard && previousKeyboardMode == .shortcuts) {
|
||||
RemoteShortcutPanel(store: shortcutStore, session: session,
|
||||
systemKeyboard: returnToSystemKeyboard,
|
||||
dismiss: { keyboardInputVisible = false },
|
||||
manage: {
|
||||
mouseInput.releaseAll()
|
||||
keyboardInputVisible = false
|
||||
statusMenuVisible = false
|
||||
shortcutManagerPlatform = session.remotePlatform.rawValue
|
||||
shortcutManagerVisible = true
|
||||
})
|
||||
} else {
|
||||
RemoteComputerKeyboard(session: session,
|
||||
switchKeyboard: returnToSystemKeyboard,
|
||||
showShortcuts: { previousKeyboardMode = .shortcuts; keyboardMode = .shortcuts },
|
||||
dismiss: { keyboardInputVisible = false })
|
||||
}
|
||||
}
|
||||
.padding(.bottom, bottomInset)
|
||||
.background(RemoteKeyboardPalette.background)
|
||||
}
|
||||
@@ -837,45 +872,44 @@ private struct RemoteComputerKeyboard: View {
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
@StateObject private var feedback = RemoteKeyboardFeedback()
|
||||
let switchKeyboard: () -> Void
|
||||
let showShortcuts: () -> Void
|
||||
let dismiss: () -> Void
|
||||
@State private var activeModifiers: Set<UInt> = []
|
||||
@State private var page: RemoteComputerKeyboardPage = .input
|
||||
|
||||
private static let commonRow: [RemoteComputerKey] = [
|
||||
private var commonRow: [RemoteComputerKey] { [
|
||||
.init("Ctrl", 0x11, isModifier: true),
|
||||
.init("Win", 0x5B, isModifier: true),
|
||||
.init("Alt", 0x12, isModifier: true),
|
||||
.init(RemoteShortcut.modifierName(91, platform: session.remotePlatform.rawValue), 0x5B, isModifier: true),
|
||||
.init(RemoteShortcut.modifierName(18, platform: session.remotePlatform.rawValue), 0x12, isModifier: true),
|
||||
.init(NSLocalizedString("空格", comment: ""), 0x20, width: 2.6),
|
||||
.init(NSLocalizedString("回车", comment: ""), 0x0D, width: 1.4, style: .special)
|
||||
]
|
||||
] }
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 4) {
|
||||
Button(NSLocalizedString("系统键盘", comment: "")) {
|
||||
RemoteKeyboardToolbar(
|
||||
switchKeyboard: {
|
||||
feedback.keyPressed()
|
||||
releaseModifiers()
|
||||
switchKeyboard()
|
||||
}
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
.frame(minWidth: 80, minHeight: 44)
|
||||
.accessibilityLabel(NSLocalizedString("切换到系统键盘", comment: ""))
|
||||
},
|
||||
more: {
|
||||
releaseModifiers()
|
||||
showShortcuts()
|
||||
},
|
||||
dismiss: {
|
||||
feedback.keyPressed()
|
||||
releaseModifiers()
|
||||
dismiss()
|
||||
},
|
||||
dismissAccessibilityLabel: NSLocalizedString("收起电脑键盘", comment: "")
|
||||
) {
|
||||
if verticalSizeClass == .compact {
|
||||
pageTabs
|
||||
} else {
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
RemoteKeyboardDismissButton(accessibilityLabel: NSLocalizedString("收起电脑键盘", comment: "")) {
|
||||
feedback.keyPressed()
|
||||
releaseModifiers()
|
||||
dismiss()
|
||||
}
|
||||
.frame(width: RemoteKeyboardDismissButton.sideLength,
|
||||
height: RemoteKeyboardDismissButton.sideLength)
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
.frame(height: 44)
|
||||
|
||||
if verticalSizeClass != .compact {
|
||||
pageTabs
|
||||
@@ -919,7 +953,7 @@ private struct RemoteComputerKeyboard: View {
|
||||
}
|
||||
|
||||
RemoteComputerKeyboardRow(
|
||||
keys: Self.commonRow,
|
||||
keys: commonRow,
|
||||
activeModifiers: activeModifiers,
|
||||
action: handleKey
|
||||
)
|
||||
@@ -966,14 +1000,13 @@ private struct RemoteComputerKeyboard: View {
|
||||
switch key.action {
|
||||
case let .modifier(keyCode):
|
||||
if activeModifiers.contains(keyCode) {
|
||||
session.sendKeyState(keyCode, isDown: false)
|
||||
activeModifiers.remove(keyCode)
|
||||
} else {
|
||||
session.sendKeyState(keyCode, isDown: true)
|
||||
activeModifiers.insert(keyCode)
|
||||
}
|
||||
case let .stroke(keyCode):
|
||||
session.sendKeyStroke(keyCode)
|
||||
session.sendKeyChord(keyCode, modifiers: Array(activeModifiers))
|
||||
activeModifiers.removeAll()
|
||||
case .switchKeyboard:
|
||||
releaseModifiers()
|
||||
switchKeyboard()
|
||||
@@ -984,9 +1017,6 @@ private struct RemoteComputerKeyboard: View {
|
||||
}
|
||||
|
||||
private func releaseModifiers() {
|
||||
for keyCode in activeModifiers {
|
||||
session.sendKeyState(keyCode, isDown: false)
|
||||
}
|
||||
activeModifiers.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Auflösung des entfernten Bildschirms";
|
||||
"设备不支持" = "Nicht unterstützt";
|
||||
"等待电脑端确认…" = "Warten auf Bestätigung vom Computer...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Fügen Sie diese Kopplungsdaten auf dem gesteuerten Gerät zur Vertrauensliste hinzu und prüfen Sie den Fingerabdruck.";
|
||||
"本机配对信息" = "Kopplungsdaten dieses Geräts";
|
||||
"查看指纹" = "Fingerabdruck anzeigen";
|
||||
"复制配对信息" = "Kopplungsdaten kopieren";
|
||||
"配对信息已复制。" = "Kopplungsdaten kopiert.";
|
||||
"无法复制配对信息,请重试。" = "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.";
|
||||
"安全存储不可用,请解锁设备后重试" = "Der sichere Speicher ist nicht verfügbar. Bitte entsperren Sie das Gerät und versuchen Sie es erneut";
|
||||
"上一页" = "Vorherige Seite";
|
||||
"下一页" = "Nächste Seite";
|
||||
"主按键" = "Haupttaste";
|
||||
"任务管理器" = "Task-Manager";
|
||||
"保存" = "Speichern";
|
||||
"修饰键" = "Modifikatortasten";
|
||||
"全选" = "Alles auswählen";
|
||||
"切换应用" = "Apps wechseln";
|
||||
"切换窗口" = "Fenster wechseln";
|
||||
"剪切" = "Ausschneiden";
|
||||
"发送组合" = "Tastenkombination";
|
||||
"名称" = "Name";
|
||||
"向上移动" = "Nach oben";
|
||||
"固定到键盘上方" = "Über der Tastatur anheften";
|
||||
"复制" = "Kopieren";
|
||||
"安全选项" = "Sicherheitsoptionen";
|
||||
"已有相同组合的快捷键" = "Diese Kombination ist bereits vorhanden";
|
||||
"常用" = "Favoriten";
|
||||
"应用列表" = "App-Liste";
|
||||
"强制退出面板" = "Dialog zum sofortigen Beenden";
|
||||
"快捷键" = "Tastenkürzel";
|
||||
"我的" = "Eigene";
|
||||
"按键" = "Tasten";
|
||||
"撤销" = "Rückgängig";
|
||||
"收起键盘" = "Tastatur ausblenden";
|
||||
"新建" = "Neu";
|
||||
"新建快捷键" = "Neues Tastenkürzel";
|
||||
"显示桌面" = "Desktop anzeigen";
|
||||
"更多快捷键" = "Weitere Tastenkürzel";
|
||||
"最多保存 16 个自定义快捷键" = "Bis zu 16 eigene Tastenkürzel speichern";
|
||||
"最多固定 8 个快捷键" = "Bis zu 8 Tastenkürzel anheften";
|
||||
"本地编辑,输入不会发送到远端" = "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.";
|
||||
"添加到常用栏" = "Zur Favoritenleiste hinzufügen";
|
||||
"移除" = "Entfernen";
|
||||
"窗口菜单" = "Fenstermenü";
|
||||
"等待远端平台信息" = "Warten auf Informationen zur Remote-Plattform";
|
||||
"管理与自定义" = "Verwalten und anpassen";
|
||||
"管理快捷键" = "Tastenkürzel verwalten";
|
||||
"粘贴" = "Einfügen";
|
||||
"系统" = "System";
|
||||
"终端复制" = "Im Terminal kopieren";
|
||||
"终端粘贴" = "Im Terminal einfügen";
|
||||
"编辑" = "Bearbeiten";
|
||||
"编辑快捷键" = "Tastenkürzel bearbeiten";
|
||||
"聚焦搜索" = "Spotlight-Suche";
|
||||
"请使用系统预设中的安全选项" = "Sicherheitsoptionen in den Systemvorgaben verwenden";
|
||||
"请先选词或取消组词" = "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen";
|
||||
"请输入有效的快捷键名称" = "Namen mit 1–64 Zeichen eingeben";
|
||||
"请选择有效的按键" = "Eine gültige Taste auswählen";
|
||||
"运行窗口" = "Ausführen-Dialog";
|
||||
"返回" = "Zurück";
|
||||
"还没有自定义快捷键" = "Noch keine eigenen Tastenkürzel";
|
||||
"适用远端" = "Remote-Plattform";
|
||||
"选择桌面方案" = "Desktop-Umgebung auswählen";
|
||||
"选择桌面方案以加载系统预设" = "Desktop-Umgebung wählen, um Systemvorgaben zu laden";
|
||||
"键盘上方常用栏" = "Favoriten über der Tastatur";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Remote display resolution";
|
||||
"设备不支持" = "Unsupported";
|
||||
"等待电脑端确认…" = "Waiting for confirmation from the computer...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Add this pairing information to the controlled device’s trusted list and verify the fingerprint.";
|
||||
"本机配对信息" = "This device’s pairing info";
|
||||
"查看指纹" = "View fingerprint";
|
||||
"复制配对信息" = "Copy pairing info";
|
||||
"配对信息已复制。" = "Pairing information copied.";
|
||||
"无法复制配对信息,请重试。" = "Unable to copy pairing information. Please try again.";
|
||||
"安全存储不可用,请解锁设备后重试" = "Secure storage is unavailable, please unlock the device and try again";
|
||||
"上一页" = "Previous page";
|
||||
"下一页" = "Next page";
|
||||
"主按键" = "Main key";
|
||||
"任务管理器" = "Task Manager";
|
||||
"保存" = "Save";
|
||||
"修饰键" = "Modifiers";
|
||||
"全选" = "Select all";
|
||||
"切换应用" = "Switch apps";
|
||||
"切换窗口" = "Switch windows";
|
||||
"剪切" = "Cut";
|
||||
"发送组合" = "Key combination";
|
||||
"名称" = "Name";
|
||||
"向上移动" = "Move up";
|
||||
"固定到键盘上方" = "Pin above keyboard";
|
||||
"复制" = "Copy";
|
||||
"安全选项" = "Security options";
|
||||
"已有相同组合的快捷键" = "This combination already exists";
|
||||
"常用" = "Favorites";
|
||||
"应用列表" = "App list";
|
||||
"强制退出面板" = "Force Quit dialog";
|
||||
"快捷键" = "Shortcuts";
|
||||
"我的" = "Mine";
|
||||
"按键" = "Keys";
|
||||
"撤销" = "Undo";
|
||||
"收起键盘" = "Hide keyboard";
|
||||
"新建" = "New";
|
||||
"新建快捷键" = "New shortcut";
|
||||
"显示桌面" = "Show desktop";
|
||||
"更多快捷键" = "More shortcuts";
|
||||
"最多保存 16 个自定义快捷键" = "Save up to 16 custom shortcuts";
|
||||
"最多固定 8 个快捷键" = "Pin up to 8 shortcuts";
|
||||
"本地编辑,输入不会发送到远端" = "Local editing. Input stays on this device.";
|
||||
"添加到常用栏" = "Add to favorites bar";
|
||||
"移除" = "Remove";
|
||||
"窗口菜单" = "Window menu";
|
||||
"等待远端平台信息" = "Waiting for remote platform information";
|
||||
"管理与自定义" = "Manage & customize";
|
||||
"管理快捷键" = "Manage shortcuts";
|
||||
"粘贴" = "Paste";
|
||||
"系统" = "System";
|
||||
"终端复制" = "Terminal copy";
|
||||
"终端粘贴" = "Terminal paste";
|
||||
"编辑" = "Edit";
|
||||
"编辑快捷键" = "Edit shortcut";
|
||||
"聚焦搜索" = "Spotlight search";
|
||||
"请使用系统预设中的安全选项" = "Use Security options in the system presets";
|
||||
"请先选词或取消组词" = "Confirm or cancel the current composition first";
|
||||
"请输入有效的快捷键名称" = "Enter a shortcut name (1–64 characters)";
|
||||
"请选择有效的按键" = "Select a valid key";
|
||||
"运行窗口" = "Run dialog";
|
||||
"返回" = "Back";
|
||||
"还没有自定义快捷键" = "No custom shortcuts yet";
|
||||
"适用远端" = "Remote platform";
|
||||
"选择桌面方案" = "Choose desktop environment";
|
||||
"选择桌面方案以加载系统预设" = "Choose a desktop environment to load system presets";
|
||||
"键盘上方常用栏" = "Favorites above keyboard";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Resolución de la pantalla remota";
|
||||
"设备不支持" = "No compatible";
|
||||
"等待电脑端确认…" = "Esperando confirmación del ordenador...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Añada esta información de emparejamiento a la lista de confianza del dispositivo controlado y verifique la huella.";
|
||||
"本机配对信息" = "Información de este dispositivo";
|
||||
"查看指纹" = "Ver huella";
|
||||
"复制配对信息" = "Copiar información";
|
||||
"配对信息已复制。" = "Información de emparejamiento copiada.";
|
||||
"无法复制配对信息,请重试。" = "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.";
|
||||
"安全存储不可用,请解锁设备后重试" = "El almacenamiento seguro no está disponible. Desbloquee el dispositivo e inténtelo nuevamente.";
|
||||
"上一页" = "Página anterior";
|
||||
"下一页" = "Página siguiente";
|
||||
"主按键" = "Tecla principal";
|
||||
"任务管理器" = "Administrador de tareas";
|
||||
"保存" = "Guardar";
|
||||
"修饰键" = "Modificadores";
|
||||
"全选" = "Seleccionar todo";
|
||||
"切换应用" = "Cambiar de aplicación";
|
||||
"切换窗口" = "Cambiar de ventana";
|
||||
"剪切" = "Cortar";
|
||||
"发送组合" = "Combinación de teclas";
|
||||
"名称" = "Nombre";
|
||||
"向上移动" = "Subir";
|
||||
"固定到键盘上方" = "Fijar sobre el teclado";
|
||||
"复制" = "Copiar";
|
||||
"安全选项" = "Opciones de seguridad";
|
||||
"已有相同组合的快捷键" = "Esta combinación ya existe";
|
||||
"常用" = "Favoritos";
|
||||
"应用列表" = "Lista de aplicaciones";
|
||||
"强制退出面板" = "Diálogo Forzar salida";
|
||||
"快捷键" = "Atajos";
|
||||
"我的" = "Mis atajos";
|
||||
"按键" = "Teclas";
|
||||
"撤销" = "Deshacer";
|
||||
"收起键盘" = "Ocultar teclado";
|
||||
"新建" = "Nuevo";
|
||||
"新建快捷键" = "Nuevo atajo";
|
||||
"显示桌面" = "Mostrar escritorio";
|
||||
"更多快捷键" = "Más atajos";
|
||||
"最多保存 16 个自定义快捷键" = "Guarda hasta 16 atajos personalizados";
|
||||
"最多固定 8 个快捷键" = "Fija hasta 8 atajos";
|
||||
"本地编辑,输入不会发送到远端" = "Edición local. Lo escrito permanece en este dispositivo.";
|
||||
"添加到常用栏" = "Añadir a la barra de favoritos";
|
||||
"移除" = "Quitar";
|
||||
"窗口菜单" = "Menú de ventana";
|
||||
"等待远端平台信息" = "Esperando información del sistema remoto";
|
||||
"管理与自定义" = "Gestionar y personalizar";
|
||||
"管理快捷键" = "Gestionar atajos";
|
||||
"粘贴" = "Pegar";
|
||||
"系统" = "Sistema";
|
||||
"终端复制" = "Copiar en terminal";
|
||||
"终端粘贴" = "Pegar en terminal";
|
||||
"编辑" = "Editar";
|
||||
"编辑快捷键" = "Editar atajo";
|
||||
"聚焦搜索" = "Búsqueda de Spotlight";
|
||||
"请使用系统预设中的安全选项" = "Usa Opciones de seguridad en los ajustes del sistema";
|
||||
"请先选词或取消组词" = "Confirma o cancela primero la composición actual";
|
||||
"请输入有效的快捷键名称" = "Introduce un nombre de 1 a 64 caracteres";
|
||||
"请选择有效的按键" = "Selecciona una tecla válida";
|
||||
"运行窗口" = "Diálogo Ejecutar";
|
||||
"返回" = "Volver";
|
||||
"还没有自定义快捷键" = "Aún no hay atajos personalizados";
|
||||
"适用远端" = "Sistema remoto";
|
||||
"选择桌面方案" = "Elegir entorno de escritorio";
|
||||
"选择桌面方案以加载系统预设" = "Elige un entorno para cargar los ajustes del sistema";
|
||||
"键盘上方常用栏" = "Favoritos sobre el teclado";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Résolution de l’écran distant";
|
||||
"设备不支持" = "Non pris en charge";
|
||||
"等待电脑端确认…" = "En attente de confirmation de l'ordinateur...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Ajoutez ces informations à la liste des appareils de confiance de l’appareil contrôlé et vérifiez l’empreinte.";
|
||||
"本机配对信息" = "Informations de cet appareil";
|
||||
"查看指纹" = "Afficher l’empreinte";
|
||||
"复制配对信息" = "Copier les informations";
|
||||
"配对信息已复制。" = "Informations d’association copiées.";
|
||||
"无法复制配对信息,请重试。" = "Impossible de copier les informations d’association. Réessayez.";
|
||||
"安全存储不可用,请解锁设备后重试" = "Le stockage sécurisé n'est pas disponible, veuillez déverrouiller l'appareil et réessayer";
|
||||
"上一页" = "Page précédente";
|
||||
"下一页" = "Page suivante";
|
||||
"主按键" = "Touche principale";
|
||||
"任务管理器" = "Gestionnaire des tâches";
|
||||
"保存" = "Enregistrer";
|
||||
"修饰键" = "Touches de modification";
|
||||
"全选" = "Sélectionner tout";
|
||||
"切换应用" = "Changer d’application";
|
||||
"切换窗口" = "Changer de fenêtre";
|
||||
"剪切" = "Couper";
|
||||
"发送组合" = "Combinaison à envoyer";
|
||||
"名称" = "Nom";
|
||||
"向上移动" = "Monter";
|
||||
"固定到键盘上方" = "Épingler au-dessus du clavier";
|
||||
"复制" = "Copier";
|
||||
"安全选项" = "Options de sécurité";
|
||||
"已有相同组合的快捷键" = "Cette combinaison existe déjà";
|
||||
"常用" = "Favoris";
|
||||
"应用列表" = "Liste des applications";
|
||||
"强制退出面板" = "Fenêtre Forcer à quitter";
|
||||
"快捷键" = "Raccourcis";
|
||||
"我的" = "Mes raccourcis";
|
||||
"按键" = "Touches";
|
||||
"撤销" = "Annuler l’action";
|
||||
"收起键盘" = "Masquer le clavier";
|
||||
"新建" = "Nouveau";
|
||||
"新建快捷键" = "Nouveau raccourci";
|
||||
"显示桌面" = "Afficher le bureau";
|
||||
"更多快捷键" = "Autres raccourcis";
|
||||
"最多保存 16 个自定义快捷键" = "Enregistrez jusqu’à 16 raccourcis personnalisés";
|
||||
"最多固定 8 个快捷键" = "Épinglez jusqu’à 8 raccourcis";
|
||||
"本地编辑,输入不会发送到远端" = "Édition locale. La saisie reste sur cet appareil.";
|
||||
"添加到常用栏" = "Ajouter à la barre des favoris";
|
||||
"移除" = "Retirer";
|
||||
"窗口菜单" = "Menu de la fenêtre";
|
||||
"等待远端平台信息" = "En attente des informations du système distant";
|
||||
"管理与自定义" = "Gérer et personnaliser";
|
||||
"管理快捷键" = "Gérer les raccourcis";
|
||||
"粘贴" = "Coller";
|
||||
"系统" = "Système";
|
||||
"终端复制" = "Copier dans le terminal";
|
||||
"终端粘贴" = "Coller dans le terminal";
|
||||
"编辑" = "Modifier";
|
||||
"编辑快捷键" = "Modifier le raccourci";
|
||||
"聚焦搜索" = "Recherche Spotlight";
|
||||
"请使用系统预设中的安全选项" = "Utilisez les options de sécurité des préréglages système";
|
||||
"请先选词或取消组词" = "Validez ou annulez d’abord la composition en cours";
|
||||
"请输入有效的快捷键名称" = "Saisissez un nom de 1 à 64 caractères";
|
||||
"请选择有效的按键" = "Sélectionnez une touche valide";
|
||||
"运行窗口" = "Fenêtre Exécuter";
|
||||
"返回" = "Retour";
|
||||
"还没有自定义快捷键" = "Aucun raccourci personnalisé";
|
||||
"适用远端" = "Système distant";
|
||||
"选择桌面方案" = "Choisir l’environnement de bureau";
|
||||
"选择桌面方案以加载系统预设" = "Choisissez un environnement pour charger les préréglages système";
|
||||
"键盘上方常用栏" = "Favoris au-dessus du clavier";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "接続先の画面解像度";
|
||||
"设备不支持" = "サポートされていません";
|
||||
"等待电脑端确认…" = "コンピューターからの確認を待っています...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "操作されるデバイスの信頼済みリストにこのペアリング情報を追加し、フィンガープリントを確認してください。";
|
||||
"本机配对信息" = "このデバイスのペアリング情報";
|
||||
"查看指纹" = "フィンガープリントを表示";
|
||||
"复制配对信息" = "ペアリング情報をコピー";
|
||||
"配对信息已复制。" = "ペアリング情報をコピーしました。";
|
||||
"无法复制配对信息,请重试。" = "ペアリング情報をコピーできません。もう一度お試しください。";
|
||||
"安全存储不可用,请解锁设备后重试" = "安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。";
|
||||
"上一页" = "前のページ";
|
||||
"下一页" = "次のページ";
|
||||
"主按键" = "メインキー";
|
||||
"任务管理器" = "タスクマネージャー";
|
||||
"保存" = "保存";
|
||||
"修饰键" = "修飾キー";
|
||||
"全选" = "全て選択";
|
||||
"切换应用" = "アプリを切り替え";
|
||||
"切换窗口" = "ウインドウを切り替え";
|
||||
"剪切" = "切り取り";
|
||||
"发送组合" = "送信するキーの組み合わせ";
|
||||
"名称" = "名前";
|
||||
"向上移动" = "上へ移動";
|
||||
"固定到键盘上方" = "キーボードの上に固定";
|
||||
"复制" = "コピー";
|
||||
"安全选项" = "セキュリティオプション";
|
||||
"已有相同组合的快捷键" = "この組み合わせは登録済みです";
|
||||
"常用" = "よく使う項目";
|
||||
"应用列表" = "アプリ一覧";
|
||||
"强制退出面板" = "強制終了ダイアログ";
|
||||
"快捷键" = "ショートカット";
|
||||
"我的" = "マイ項目";
|
||||
"按键" = "キー";
|
||||
"撤销" = "取り消す";
|
||||
"收起键盘" = "キーボードを隠す";
|
||||
"新建" = "新規";
|
||||
"新建快捷键" = "ショートカットを作成";
|
||||
"显示桌面" = "デスクトップを表示";
|
||||
"更多快捷键" = "その他のショートカット";
|
||||
"最多保存 16 个自定义快捷键" = "カスタムショートカットは16個まで保存できます";
|
||||
"最多固定 8 个快捷键" = "固定できるショートカットは8個までです";
|
||||
"本地编辑,输入不会发送到远端" = "ローカル編集。入力はリモートに送信されません。";
|
||||
"添加到常用栏" = "お気に入りバーに追加";
|
||||
"移除" = "取り除く";
|
||||
"窗口菜单" = "ウインドウメニュー";
|
||||
"等待远端平台信息" = "リモートのOS情報を待っています";
|
||||
"管理与自定义" = "管理とカスタマイズ";
|
||||
"管理快捷键" = "ショートカットを管理";
|
||||
"粘贴" = "貼り付け";
|
||||
"系统" = "システム";
|
||||
"终端复制" = "端末でコピー";
|
||||
"终端粘贴" = "端末に貼り付け";
|
||||
"编辑" = "編集";
|
||||
"编辑快捷键" = "ショートカットを編集";
|
||||
"聚焦搜索" = "Spotlight検索";
|
||||
"请使用系统预设中的安全选项" = "システムプリセットのセキュリティオプションを使用してください";
|
||||
"请先选词或取消组词" = "先に変換を確定またはキャンセルしてください";
|
||||
"请输入有效的快捷键名称" = "ショートカット名を1〜64文字で入力してください";
|
||||
"请选择有效的按键" = "有効なキーを選択してください";
|
||||
"运行窗口" = "実行ダイアログ";
|
||||
"返回" = "戻る";
|
||||
"还没有自定义快捷键" = "カスタムショートカットはまだありません";
|
||||
"适用远端" = "対象リモートOS";
|
||||
"选择桌面方案" = "デスクトップ環境を選択";
|
||||
"选择桌面方案以加载系统预设" = "デスクトップ環境を選択してプリセットを読み込みます";
|
||||
"键盘上方常用栏" = "キーボード上のお気に入り";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "원격 화면 해상도";
|
||||
"设备不支持" = "지원되지 않음";
|
||||
"等待电脑端确认…" = "컴퓨터의 확인을 기다리는 중...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "제어 대상 장치의 신뢰 목록에 이 페어링 정보를 추가하고 지문을 확인하세요.";
|
||||
"本机配对信息" = "이 장치의 페어링 정보";
|
||||
"查看指纹" = "지문 보기";
|
||||
"复制配对信息" = "페어링 정보 복사";
|
||||
"配对信息已复制。" = "페어링 정보를 복사했습니다.";
|
||||
"无法复制配对信息,请重试。" = "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.";
|
||||
"安全存储不可用,请解锁设备后重试" = "보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요.";
|
||||
"上一页" = "이전 페이지";
|
||||
"下一页" = "다음 페이지";
|
||||
"主按键" = "기본 키";
|
||||
"任务管理器" = "작업 관리자";
|
||||
"保存" = "저장";
|
||||
"修饰键" = "보조 키";
|
||||
"全选" = "모두 선택";
|
||||
"切换应用" = "앱 전환";
|
||||
"切换窗口" = "창 전환";
|
||||
"剪切" = "잘라내기";
|
||||
"发送组合" = "전송할 키 조합";
|
||||
"名称" = "이름";
|
||||
"向上移动" = "위로 이동";
|
||||
"固定到键盘上方" = "키보드 위에 고정";
|
||||
"复制" = "복사";
|
||||
"安全选项" = "보안 옵션";
|
||||
"已有相同组合的快捷键" = "이미 등록된 키 조합입니다";
|
||||
"常用" = "자주 사용";
|
||||
"应用列表" = "앱 목록";
|
||||
"强制退出面板" = "강제 종료 대화상자";
|
||||
"快捷键" = "단축키";
|
||||
"我的" = "내 단축키";
|
||||
"按键" = "키";
|
||||
"撤销" = "실행 취소";
|
||||
"收起键盘" = "키보드 숨기기";
|
||||
"新建" = "새로 만들기";
|
||||
"新建快捷键" = "새 단축키";
|
||||
"显示桌面" = "바탕 화면 표시";
|
||||
"更多快捷键" = "더 많은 단축키";
|
||||
"最多保存 16 个自定义快捷键" = "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다";
|
||||
"最多固定 8 个快捷键" = "단축키는 최대 8개까지 고정할 수 있습니다";
|
||||
"本地编辑,输入不会发送到远端" = "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.";
|
||||
"添加到常用栏" = "즐겨찾기 모음에 추가";
|
||||
"移除" = "제거";
|
||||
"窗口菜单" = "창 메뉴";
|
||||
"等待远端平台信息" = "원격 플랫폼 정보를 기다리는 중";
|
||||
"管理与自定义" = "관리 및 사용자 지정";
|
||||
"管理快捷键" = "단축키 관리";
|
||||
"粘贴" = "붙여넣기";
|
||||
"系统" = "시스템";
|
||||
"终端复制" = "터미널 복사";
|
||||
"终端粘贴" = "터미널 붙여넣기";
|
||||
"编辑" = "편집";
|
||||
"编辑快捷键" = "단축키 편집";
|
||||
"聚焦搜索" = "Spotlight 검색";
|
||||
"请使用系统预设中的安全选项" = "시스템 프리셋의 보안 옵션을 사용하세요";
|
||||
"请先选词或取消组词" = "먼저 조합 중인 문자를 확정하거나 취소하세요";
|
||||
"请输入有效的快捷键名称" = "단축키 이름을 1~64자로 입력하세요";
|
||||
"请选择有效的按键" = "유효한 키를 선택하세요";
|
||||
"运行窗口" = "실행 대화상자";
|
||||
"返回" = "뒤로";
|
||||
"还没有自定义快捷键" = "아직 사용자 지정 단축키가 없습니다";
|
||||
"适用远端" = "대상 원격 플랫폼";
|
||||
"选择桌面方案" = "데스크톱 환경 선택";
|
||||
"选择桌面方案以加载系统预设" = "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요";
|
||||
"键盘上方常用栏" = "키보드 위 즐겨찾기 모음";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Resolução da tela remota";
|
||||
"设备不支持" = "Não suportado";
|
||||
"等待电脑端确认…" = "Aguardando confirmação do computador...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Adicione estas informações de pareamento à lista de confiança do dispositivo controlado e confira a impressão digital.";
|
||||
"本机配对信息" = "Informações deste dispositivo";
|
||||
"查看指纹" = "Ver impressão digital";
|
||||
"复制配对信息" = "Copiar informações";
|
||||
"配对信息已复制。" = "Informações de pareamento copiadas.";
|
||||
"无法复制配对信息,请重试。" = "Não foi possível copiar as informações de pareamento. Tente novamente.";
|
||||
"安全存储不可用,请解锁设备后重试" = "O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente";
|
||||
"上一页" = "Página anterior";
|
||||
"下一页" = "Próxima página";
|
||||
"主按键" = "Tecla principal";
|
||||
"任务管理器" = "Gerenciador de Tarefas";
|
||||
"保存" = "Salvar";
|
||||
"修饰键" = "Modificadores";
|
||||
"全选" = "Selecionar tudo";
|
||||
"切换应用" = "Alternar aplicativos";
|
||||
"切换窗口" = "Alternar janelas";
|
||||
"剪切" = "Recortar";
|
||||
"发送组合" = "Combinação de teclas";
|
||||
"名称" = "Nome";
|
||||
"向上移动" = "Mover para cima";
|
||||
"固定到键盘上方" = "Fixar acima do teclado";
|
||||
"复制" = "Copiar";
|
||||
"安全选项" = "Opções de segurança";
|
||||
"已有相同组合的快捷键" = "Esta combinação já existe";
|
||||
"常用" = "Favoritos";
|
||||
"应用列表" = "Lista de aplicativos";
|
||||
"强制退出面板" = "Janela Forçar Encerrar";
|
||||
"快捷键" = "Atalhos";
|
||||
"我的" = "Meus atalhos";
|
||||
"按键" = "Teclas";
|
||||
"撤销" = "Desfazer";
|
||||
"收起键盘" = "Ocultar teclado";
|
||||
"新建" = "Novo";
|
||||
"新建快捷键" = "Novo atalho";
|
||||
"显示桌面" = "Mostrar área de trabalho";
|
||||
"更多快捷键" = "Mais atalhos";
|
||||
"最多保存 16 个自定义快捷键" = "Salve até 16 atalhos personalizados";
|
||||
"最多固定 8 个快捷键" = "Fixe até 8 atalhos";
|
||||
"本地编辑,输入不会发送到远端" = "Edição local. A entrada fica neste dispositivo.";
|
||||
"添加到常用栏" = "Adicionar à barra de favoritos";
|
||||
"移除" = "Remover";
|
||||
"窗口菜单" = "Menu da janela";
|
||||
"等待远端平台信息" = "Aguardando informações do sistema remoto";
|
||||
"管理与自定义" = "Gerenciar e personalizar";
|
||||
"管理快捷键" = "Gerenciar atalhos";
|
||||
"粘贴" = "Colar";
|
||||
"系统" = "Sistema";
|
||||
"终端复制" = "Copiar no terminal";
|
||||
"终端粘贴" = "Colar no terminal";
|
||||
"编辑" = "Editar";
|
||||
"编辑快捷键" = "Editar atalho";
|
||||
"聚焦搜索" = "Busca do Spotlight";
|
||||
"请使用系统预设中的安全选项" = "Use Opções de segurança nas predefinições do sistema";
|
||||
"请先选词或取消组词" = "Confirme ou cancele primeiro a composição atual";
|
||||
"请输入有效的快捷键名称" = "Insira um nome de 1 a 64 caracteres";
|
||||
"请选择有效的按键" = "Selecione uma tecla válida";
|
||||
"运行窗口" = "Janela Executar";
|
||||
"返回" = "Voltar";
|
||||
"还没有自定义快捷键" = "Ainda não há atalhos personalizados";
|
||||
"适用远端" = "Sistema remoto";
|
||||
"选择桌面方案" = "Escolher ambiente de trabalho";
|
||||
"选择桌面方案以加载系统预设" = "Escolha um ambiente para carregar as predefinições do sistema";
|
||||
"键盘上方常用栏" = "Favoritos acima do teclado";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Resolução da tela remota";
|
||||
"设备不支持" = "Não suportado";
|
||||
"等待电脑端确认…" = "Aguardando confirmação do computador...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Adicione estas informações de pareamento à lista de confiança do dispositivo controlado e confira a impressão digital.";
|
||||
"本机配对信息" = "Informações deste dispositivo";
|
||||
"查看指纹" = "Ver impressão digital";
|
||||
"复制配对信息" = "Copiar informações";
|
||||
"配对信息已复制。" = "Informações de pareamento copiadas.";
|
||||
"无法复制配对信息,请重试。" = "Não foi possível copiar as informações de pareamento. Tente novamente.";
|
||||
"安全存储不可用,请解锁设备后重试" = "O armazenamento seguro não está disponível. Desbloqueie o dispositivo e tente novamente";
|
||||
"上一页" = "Página anterior";
|
||||
"下一页" = "Próxima página";
|
||||
"主按键" = "Tecla principal";
|
||||
"任务管理器" = "Gerenciador de Tarefas";
|
||||
"保存" = "Salvar";
|
||||
"修饰键" = "Modificadores";
|
||||
"全选" = "Selecionar tudo";
|
||||
"切换应用" = "Alternar aplicativos";
|
||||
"切换窗口" = "Alternar janelas";
|
||||
"剪切" = "Recortar";
|
||||
"发送组合" = "Combinação de teclas";
|
||||
"名称" = "Nome";
|
||||
"向上移动" = "Mover para cima";
|
||||
"固定到键盘上方" = "Fixar acima do teclado";
|
||||
"复制" = "Copiar";
|
||||
"安全选项" = "Opções de segurança";
|
||||
"已有相同组合的快捷键" = "Esta combinação já existe";
|
||||
"常用" = "Favoritos";
|
||||
"应用列表" = "Lista de aplicativos";
|
||||
"强制退出面板" = "Janela Forçar Encerrar";
|
||||
"快捷键" = "Atalhos";
|
||||
"我的" = "Meus atalhos";
|
||||
"按键" = "Teclas";
|
||||
"撤销" = "Desfazer";
|
||||
"收起键盘" = "Ocultar teclado";
|
||||
"新建" = "Novo";
|
||||
"新建快捷键" = "Novo atalho";
|
||||
"显示桌面" = "Mostrar área de trabalho";
|
||||
"更多快捷键" = "Mais atalhos";
|
||||
"最多保存 16 个自定义快捷键" = "Salve até 16 atalhos personalizados";
|
||||
"最多固定 8 个快捷键" = "Fixe até 8 atalhos";
|
||||
"本地编辑,输入不会发送到远端" = "Edição local. A entrada fica neste dispositivo.";
|
||||
"添加到常用栏" = "Adicionar à barra de favoritos";
|
||||
"移除" = "Remover";
|
||||
"窗口菜单" = "Menu da janela";
|
||||
"等待远端平台信息" = "Aguardando informações do sistema remoto";
|
||||
"管理与自定义" = "Gerenciar e personalizar";
|
||||
"管理快捷键" = "Gerenciar atalhos";
|
||||
"粘贴" = "Colar";
|
||||
"系统" = "Sistema";
|
||||
"终端复制" = "Copiar no terminal";
|
||||
"终端粘贴" = "Colar no terminal";
|
||||
"编辑" = "Editar";
|
||||
"编辑快捷键" = "Editar atalho";
|
||||
"聚焦搜索" = "Busca do Spotlight";
|
||||
"请使用系统预设中的安全选项" = "Use Opções de segurança nas predefinições do sistema";
|
||||
"请先选词或取消组词" = "Confirme ou cancele primeiro a composição atual";
|
||||
"请输入有效的快捷键名称" = "Insira um nome de 1 a 64 caracteres";
|
||||
"请选择有效的按键" = "Selecione uma tecla válida";
|
||||
"运行窗口" = "Janela Executar";
|
||||
"返回" = "Voltar";
|
||||
"还没有自定义快捷键" = "Ainda não há atalhos personalizados";
|
||||
"适用远端" = "Sistema remoto";
|
||||
"选择桌面方案" = "Escolher ambiente de trabalho";
|
||||
"选择桌面方案以加载系统预设" = "Escolha um ambiente para carregar as predefinições do sistema";
|
||||
"键盘上方常用栏" = "Favoritos acima do teclado";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "Разрешение удалённого экрана";
|
||||
"设备不支持" = "Не поддерживается";
|
||||
"等待电脑端确认…" = "Ожидание подтверждения от компьютера...";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "Добавьте эти данные сопряжения в список доверенных устройств на управляемом компьютере и сверьте отпечаток.";
|
||||
"本机配对信息" = "Данные этого устройства";
|
||||
"查看指纹" = "Показать отпечаток";
|
||||
"复制配对信息" = "Копировать данные";
|
||||
"配对信息已复制。" = "Данные сопряжения скопированы.";
|
||||
"无法复制配对信息,请重试。" = "Не удалось скопировать данные сопряжения. Повторите попытку.";
|
||||
"安全存储不可用,请解锁设备后重试" = "Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку.";
|
||||
"上一页" = "Предыдущая страница";
|
||||
"下一页" = "Следующая страница";
|
||||
"主按键" = "Основная клавиша";
|
||||
"任务管理器" = "Диспетчер задач";
|
||||
"保存" = "Сохранить";
|
||||
"修饰键" = "Клавиши-модификаторы";
|
||||
"全选" = "Выбрать все";
|
||||
"切换应用" = "Переключить приложение";
|
||||
"切换窗口" = "Переключить окно";
|
||||
"剪切" = "Вырезать";
|
||||
"发送组合" = "Сочетание клавиш";
|
||||
"名称" = "Название";
|
||||
"向上移动" = "Переместить вверх";
|
||||
"固定到键盘上方" = "Закрепить над клавиатурой";
|
||||
"复制" = "Копировать";
|
||||
"安全选项" = "Параметры безопасности";
|
||||
"已有相同组合的快捷键" = "Такое сочетание уже существует";
|
||||
"常用" = "Избранное";
|
||||
"应用列表" = "Список приложений";
|
||||
"强制退出面板" = "Окно принудительного завершения";
|
||||
"快捷键" = "Сочетания клавиш";
|
||||
"我的" = "Мои";
|
||||
"按键" = "Клавиши";
|
||||
"撤销" = "Отменить";
|
||||
"收起键盘" = "Скрыть клавиатуру";
|
||||
"新建" = "Создать";
|
||||
"新建快捷键" = "Новое сочетание";
|
||||
"显示桌面" = "Показать рабочий стол";
|
||||
"更多快捷键" = "Другие сочетания";
|
||||
"最多保存 16 个自定义快捷键" = "Можно сохранить до 16 своих сочетаний";
|
||||
"最多固定 8 个快捷键" = "Можно закрепить до 8 сочетаний";
|
||||
"本地编辑,输入不会发送到远端" = "Локальное редактирование. Ввод не отправляется на удалённое устройство.";
|
||||
"添加到常用栏" = "Добавить на панель избранного";
|
||||
"移除" = "Убрать";
|
||||
"窗口菜单" = "Меню окна";
|
||||
"等待远端平台信息" = "Ожидание сведений об удалённой системе";
|
||||
"管理与自定义" = "Управление и настройка";
|
||||
"管理快捷键" = "Управление сочетаниями";
|
||||
"粘贴" = "Вставить";
|
||||
"系统" = "Система";
|
||||
"终端复制" = "Копировать в терминале";
|
||||
"终端粘贴" = "Вставить в терминале";
|
||||
"编辑" = "Изменить";
|
||||
"编辑快捷键" = "Изменить сочетание";
|
||||
"聚焦搜索" = "Поиск Spotlight";
|
||||
"请使用系统预设中的安全选项" = "Используйте параметры безопасности из системных наборов";
|
||||
"请先选词或取消组词" = "Сначала подтвердите или отмените текущий ввод";
|
||||
"请输入有效的快捷键名称" = "Введите название от 1 до 64 символов";
|
||||
"请选择有效的按键" = "Выберите допустимую клавишу";
|
||||
"运行窗口" = "Окно запуска";
|
||||
"返回" = "Назад";
|
||||
"还没有自定义快捷键" = "Своих сочетаний пока нет";
|
||||
"适用远端" = "Удалённая система";
|
||||
"选择桌面方案" = "Выберите окружение рабочего стола";
|
||||
"选择桌面方案以加载系统预设" = "Выберите окружение, чтобы загрузить системные наборы";
|
||||
"键盘上方常用栏" = "Избранное над клавиатурой";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "被控端分辨率";
|
||||
"设备不支持" = "设备不支持";
|
||||
"等待电脑端确认…" = "等待电脑端确认…";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "将配对信息添加到被控端的可信设备列表,并核对指纹。";
|
||||
"本机配对信息" = "本机配对信息";
|
||||
"查看指纹" = "查看指纹";
|
||||
"复制配对信息" = "复制配对信息";
|
||||
"配对信息已复制。" = "配对信息已复制。";
|
||||
"无法复制配对信息,请重试。" = "无法复制配对信息,请重试。";
|
||||
"安全存储不可用,请解锁设备后重试" = "安全存储不可用,请解锁设备后重试";
|
||||
"上一页" = "上一页";
|
||||
"下一页" = "下一页";
|
||||
"主按键" = "主按键";
|
||||
"任务管理器" = "任务管理器";
|
||||
"保存" = "保存";
|
||||
"修饰键" = "修饰键";
|
||||
"全选" = "全选";
|
||||
"切换应用" = "切换应用";
|
||||
"切换窗口" = "切换窗口";
|
||||
"剪切" = "剪切";
|
||||
"发送组合" = "发送组合";
|
||||
"名称" = "名称";
|
||||
"向上移动" = "向上移动";
|
||||
"固定到键盘上方" = "固定到键盘上方";
|
||||
"复制" = "复制";
|
||||
"安全选项" = "安全选项";
|
||||
"已有相同组合的快捷键" = "已有相同组合的快捷键";
|
||||
"常用" = "常用";
|
||||
"应用列表" = "应用列表";
|
||||
"强制退出面板" = "强制退出面板";
|
||||
"快捷键" = "快捷键";
|
||||
"我的" = "我的";
|
||||
"按键" = "按键";
|
||||
"撤销" = "撤销";
|
||||
"收起键盘" = "收起键盘";
|
||||
"新建" = "新建";
|
||||
"新建快捷键" = "新建快捷键";
|
||||
"显示桌面" = "显示桌面";
|
||||
"更多快捷键" = "更多快捷键";
|
||||
"最多保存 16 个自定义快捷键" = "最多保存 16 个自定义快捷键";
|
||||
"最多固定 8 个快捷键" = "最多固定 8 个快捷键";
|
||||
"本地编辑,输入不会发送到远端" = "本地编辑,输入不会发送到远端";
|
||||
"添加到常用栏" = "添加到常用栏";
|
||||
"移除" = "移除";
|
||||
"窗口菜单" = "窗口菜单";
|
||||
"等待远端平台信息" = "等待远端平台信息";
|
||||
"管理与自定义" = "管理与自定义";
|
||||
"管理快捷键" = "管理快捷键";
|
||||
"粘贴" = "粘贴";
|
||||
"系统" = "系统";
|
||||
"终端复制" = "终端复制";
|
||||
"终端粘贴" = "终端粘贴";
|
||||
"编辑" = "编辑";
|
||||
"编辑快捷键" = "编辑快捷键";
|
||||
"聚焦搜索" = "聚焦搜索";
|
||||
"请使用系统预设中的安全选项" = "请使用系统预设中的安全选项";
|
||||
"请先选词或取消组词" = "请先选词或取消组词";
|
||||
"请输入有效的快捷键名称" = "请输入有效的快捷键名称";
|
||||
"请选择有效的按键" = "请选择有效的按键";
|
||||
"运行窗口" = "运行窗口";
|
||||
"返回" = "返回";
|
||||
"还没有自定义快捷键" = "还没有自定义快捷键";
|
||||
"适用远端" = "适用远端";
|
||||
"选择桌面方案" = "选择桌面方案";
|
||||
"选择桌面方案以加载系统预设" = "选择桌面方案以加载系统预设";
|
||||
"键盘上方常用栏" = "键盘上方常用栏";
|
||||
@@ -288,3 +288,66 @@
|
||||
"被控端分辨率" = "被控端解析度";
|
||||
"设备不支持" = "裝置不支援";
|
||||
"等待电脑端确认…" = "等待電腦端確認…";
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。" = "將配對資訊新增至被控端的可信裝置清單,並核對指紋。";
|
||||
"本机配对信息" = "本機配對資訊";
|
||||
"查看指纹" = "查看指紋";
|
||||
"复制配对信息" = "複製配對資訊";
|
||||
"配对信息已复制。" = "配對資訊已複製。";
|
||||
"无法复制配对信息,请重试。" = "無法複製配對資訊,請重試。";
|
||||
"安全存储不可用,请解锁设备后重试" = "安全儲存不可用,請解鎖裝置後重試";
|
||||
"上一页" = "上一頁";
|
||||
"下一页" = "下一頁";
|
||||
"主按键" = "主按鍵";
|
||||
"任务管理器" = "工作管理員";
|
||||
"保存" = "儲存";
|
||||
"修饰键" = "修飾鍵";
|
||||
"全选" = "全選";
|
||||
"切换应用" = "切換應用程式";
|
||||
"切换窗口" = "切換視窗";
|
||||
"剪切" = "剪下";
|
||||
"发送组合" = "傳送組合";
|
||||
"名称" = "名稱";
|
||||
"向上移动" = "向上移動";
|
||||
"固定到键盘上方" = "固定到鍵盤上方";
|
||||
"复制" = "複製";
|
||||
"安全选项" = "安全性選項";
|
||||
"已有相同组合的快捷键" = "已有相同組合的快捷鍵";
|
||||
"常用" = "常用";
|
||||
"应用列表" = "應用程式列表";
|
||||
"强制退出面板" = "強制結束面板";
|
||||
"快捷键" = "快速鍵";
|
||||
"我的" = "我的";
|
||||
"按键" = "按鍵";
|
||||
"撤销" = "復原";
|
||||
"收起键盘" = "收起鍵盤";
|
||||
"新建" = "新增";
|
||||
"新建快捷键" = "新增快速鍵";
|
||||
"显示桌面" = "顯示桌面";
|
||||
"更多快捷键" = "更多快速鍵";
|
||||
"最多保存 16 个自定义快捷键" = "最多儲存 16 個自訂快速鍵";
|
||||
"最多固定 8 个快捷键" = "最多固定 8 個快速鍵";
|
||||
"本地编辑,输入不会发送到远端" = "本機編輯,輸入不會傳送到遠端";
|
||||
"添加到常用栏" = "加入常用列";
|
||||
"移除" = "移除";
|
||||
"窗口菜单" = "視窗選單";
|
||||
"等待远端平台信息" = "等待遠端平台資訊";
|
||||
"管理与自定义" = "管理與自訂";
|
||||
"管理快捷键" = "管理快速鍵";
|
||||
"粘贴" = "貼上";
|
||||
"系统" = "系統";
|
||||
"终端复制" = "終端機複製";
|
||||
"终端粘贴" = "終端機貼上";
|
||||
"编辑" = "編輯";
|
||||
"编辑快捷键" = "編輯快速鍵";
|
||||
"聚焦搜索" = "Spotlight 搜尋";
|
||||
"请使用系统预设中的安全选项" = "請使用系統預設中的安全性選項";
|
||||
"请先选词或取消组词" = "請先選字或取消組字";
|
||||
"请输入有效的快捷键名称" = "請輸入有效的快速鍵名稱";
|
||||
"请选择有效的按键" = "請選擇有效的按鍵";
|
||||
"运行窗口" = "執行視窗";
|
||||
"返回" = "返回";
|
||||
"还没有自定义快捷键" = "尚無自訂快速鍵";
|
||||
"适用远端" = "適用遠端";
|
||||
"选择桌面方案" = "選擇桌面環境";
|
||||
"选择桌面方案以加载系统预设" = "選擇桌面環境以載入系統預設";
|
||||
"键盘上方常用栏" = "鍵盤上方常用列";
|
||||
+26
-4
@@ -8,6 +8,12 @@ UPnP discovery and router port mapping are disabled in iOS builds, matching
|
||||
Android. Local connections, ICE/STUN hole punching, and TURN UDP/TCP relay
|
||||
remain available. The native archive excludes GUPnP and its HTTP/XML dependencies.
|
||||
|
||||
## Trusted device authentication
|
||||
|
||||
Open **Settings → This device’s pairing info** to view the fingerprint and copy the public pairing information. Import it under **Manage trusted devices → Add a trusted device** on the controlled desktop and compare fingerprints. Once trusted, this iPhone or iPad can connect with **Only allow trusted devices** enabled. The connection password is still required.
|
||||
|
||||
The controller responds to the shared Ed25519 challenge protocol. Its signing seed is stored in a separate, non-synchronizing, device-only Keychain item, never in caches, logs or the exported pairing information. Restarting, clearing caches and changing signaling servers or login IDs preserve the same key. An unavailable, corrupt or unexpectedly missing identity fails initialization instead of silently generating a replacement. A new installation on another device needs its own pairing.
|
||||
|
||||
## Remote privacy screen
|
||||
|
||||
The floating menu includes a **Privacy screen** button that toggles the host's
|
||||
@@ -276,8 +282,9 @@ the insertion point, and Return starts a new context. Text edited directly on
|
||||
the host is not mirrored back; automatic corrections assume the remote caret
|
||||
still follows the text entered from the phone.
|
||||
|
||||
Only the **电脑键盘** switch and keyboard hide button are visible in the 44-point
|
||||
native input row, anchored to `UIKeyboardLayoutGuide`. Its background continues
|
||||
The 44-point native input row contains the **电脑键盘** switch, a horizontally
|
||||
scrollable favorites bar (Escape, Tab, Copy and Paste by default), More and Hide.
|
||||
It is anchored to `UIKeyboardLayoutGuide`. Its background continues
|
||||
behind the system keyboard; no input accessory slot is reserved. The IME's
|
||||
candidate bar remains native. Chinese nine-key Pinyin reserves its own
|
||||
spelling-choice row above the candidates (for example `m`, `n`, `o`); this may
|
||||
@@ -304,8 +311,8 @@ iOS does not expose the native keyboard's intensity for custom controls to read.
|
||||
One feedback generator is reused
|
||||
while the keyboard is visible and released when it closes. Haptics require
|
||||
supported hardware; the simulator can only verify the feedback trigger.
|
||||
Latched modifiers survive page changes and are released when the
|
||||
keyboard closes or switches back to the system keyboard. The panel reuses the
|
||||
Modifiers are selected locally and applied to the next key, then cleared. A
|
||||
whole chord is queued as one RTC task and releases modifiers in reverse order. The panel reuses the
|
||||
last measured native keyboard height, including the 44-point control row, for
|
||||
the current window width and orientation. Before landscape has been measured,
|
||||
the iPhone fallback uses a 161-point compact keybed plus the actual bottom safe
|
||||
@@ -699,3 +706,18 @@ the applicable [export-compliance determination](https://developer.apple.com/hel
|
||||
After that determination, supply the appropriate declaration and any required
|
||||
approval code in the distribution build. The repository deliberately does not
|
||||
preselect an exemption.
|
||||
|
||||
## Remote shortcuts
|
||||
|
||||
More replaces the keybed with Common, Keys, System and Mine groups, using the
|
||||
last measured native keyboard height. Copy/Paste and other semantic presets
|
||||
follow the remote OS: Ctrl on Windows/Linux and Cmd on macOS. Linux system
|
||||
presets require an explicit GNOME or KDE Plasma choice. Windows security
|
||||
options use the existing Ctrl+Alt+Del service command. Unknown remote platforms
|
||||
expose generic keys until host information arrives.
|
||||
|
||||
Manage & Customize opens a separate local screen. Save up to 16 named custom
|
||||
chords and pin up to 8 shortcuts per remote platform; reorder, unpin, edit and
|
||||
delete them locally. Custom chords preserve their selected modifiers when the
|
||||
target platform changes. Shortcut taps flush committed text and reset the IME
|
||||
correction context; an active composition must be confirmed or cancelled first.
|
||||
@@ -155,6 +155,7 @@ WIRE_BUILD_DIR="${IOS_DIR}/.xmake/wire-build${CACHE_SUFFIX}"
|
||||
WIRE_WORK_DIR="${IOS_DIR}/.xmake/wire-work${CACHE_SUFFIX}"
|
||||
WIRE_MANIFEST_FILE="${WIRE_WORK_DIR}/xmake.lua"
|
||||
WIRE_LIBRARY="${WIRE_BUILD_DIR}/iphoneos/arm64/${MODE}/libcrossdesk_wire.a"
|
||||
TRUST_LIBRARY="${WIRE_BUILD_DIR}/iphoneos/arm64/${MODE}/libcrossdesk_mobile_trust.a"
|
||||
|
||||
mkdir -p "${OUTPUT_DIR}" "${WIRE_WORK_DIR}"
|
||||
if ! cmp -s "${REPO_DIR}/xmake.lua" "${WIRE_MANIFEST_FILE}"; then
|
||||
@@ -228,6 +229,7 @@ fi
|
||||
cd "${WIRE_WORK_DIR}"
|
||||
print "Building CrossDesk wire for ${SDK_NAME} (${MODE})"
|
||||
run_xmake b -P "${WIRE_WORK_DIR}" "${XMAKE_LOG_FLAGS[@]}" crossdesk_wire
|
||||
run_xmake b -P "${WIRE_WORK_DIR}" "${XMAKE_LOG_FLAGS[@]}" crossdesk_mobile_trust
|
||||
)
|
||||
|
||||
if [[ ! -f "${WIRE_LIBRARY}" ]]; then
|
||||
@@ -304,7 +306,7 @@ verify_native_symbols() {
|
||||
# timestamp tick cannot leave a stale merged library. Preserve the output mtime
|
||||
# on no-op builds to avoid relinking the Xcode app.
|
||||
shasum -a 256 "${SCRIPT_DIR}/build_minirtc_ios.sh" "${MINIRTC_LIBRARY}" \
|
||||
"${DEPENDENCY_ARCHIVES[@]}" "${WIRE_LIBRARY}" > "${TEMP_MANIFEST}"
|
||||
"${DEPENDENCY_ARCHIVES[@]}" "${WIRE_LIBRARY}" "${TRUST_LIBRARY}" > "${TEMP_MANIFEST}"
|
||||
if [[ -f "${OUTPUT_LIBRARY}" ]] && cmp -s "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"; then
|
||||
verify_native_symbols "${OUTPUT_LIBRARY}"
|
||||
print "Up to date: ${OUTPUT_LIBRARY}"
|
||||
@@ -315,7 +317,7 @@ fi
|
||||
# Suppress only that diagnostic; retain the members and other warnings.
|
||||
/usr/bin/libtool -static -no_warning_for_no_symbols -o "${TEMP_LIBRARY}" \
|
||||
"${MINIRTC_LIBRARY}" "${MERGE_ARCHIVES[@]}" \
|
||||
"${WIRE_LIBRARY}"
|
||||
"${WIRE_LIBRARY}" "${TRUST_LIBRARY}"
|
||||
verify_native_symbols "${TEMP_LIBRARY}"
|
||||
mv -f "${TEMP_LIBRARY}" "${OUTPUT_LIBRARY}"
|
||||
mv -f "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 9b1acc206b...17f53c7168.
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MOBILE_DEVICE_TRUST_H_
|
||||
#define _MOBILE_DEVICE_TRUST_H_
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk::mobile_controller {
|
||||
// The platform must durably save the seed in its secure store before using it.
|
||||
// Existing invalid/unavailable identities must never be replaced automatically.
|
||||
class DeviceTrustIdentity {
|
||||
public:
|
||||
explicit DeviceTrustIdentity(std::string_view seed);
|
||||
~DeviceTrustIdentity();
|
||||
static std::string GeneratePrivateKey();
|
||||
static void ClearSecret(std::string& value);
|
||||
static std::string Advertise(std::string_view host_information);
|
||||
static bool IsChallenge(std::string_view data) noexcept;
|
||||
std::string PairingInfo(const std::string& device_id, const std::string& name) const;
|
||||
const std::string& Fingerprint() const;
|
||||
// nullopt: ordinary control message; empty: unsupported/malformed challenge.
|
||||
std::optional<std::string> ChallengeResponse(std::string_view data,
|
||||
const std::string& device_id, const std::string& name) const noexcept;
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
} // namespace crossdesk::mobile_controller
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "mobile_device_trust.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/sha.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace crossdesk::mobile_controller {
|
||||
// The platform must durably save the seed in its secure store before using it.
|
||||
// A missing seed may be generated on first use; a malformed one must never be
|
||||
// replaced automatically, since that would invalidate existing pairings.
|
||||
class DeviceTrustIdentity::Impl {
|
||||
public:
|
||||
explicit Impl(std::string_view seed) {
|
||||
Require(IsHex(seed, 32));
|
||||
Secret bytes;
|
||||
for (size_t i = 0; i < bytes.value.size(); ++i)
|
||||
bytes.value[i] = (Digit(seed[2 * i]) << 4) | Digit(seed[2 * i + 1]);
|
||||
key_.reset(EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr,
|
||||
bytes.value.data(), bytes.value.size()));
|
||||
Require(bool(key_));
|
||||
std::array<unsigned char, 32> public_key{};
|
||||
size_t size = public_key.size();
|
||||
Require(EVP_PKEY_get_raw_public_key(key_.get(), public_key.data(), &size) == 1 && size == 32);
|
||||
public_key_ = Hex(public_key.data(), public_key.size());
|
||||
std::array<unsigned char, SHA256_DIGEST_LENGTH> hash{};
|
||||
Require(SHA256(public_key.data(), public_key.size(), hash.data()) != nullptr);
|
||||
fingerprint_ = Hex(hash.data(), hash.size());
|
||||
}
|
||||
|
||||
static std::string GeneratePrivateKey() {
|
||||
Key key(EVP_PKEY_Q_keygen(nullptr, nullptr, "ED25519"), EVP_PKEY_free);
|
||||
Require(bool(key));
|
||||
Secret bytes;
|
||||
size_t size = bytes.value.size();
|
||||
Require(EVP_PKEY_get_raw_private_key(key.get(), bytes.value.data(), &size) == 1 && size == 32);
|
||||
return Hex(bytes.value.data(), bytes.value.size());
|
||||
}
|
||||
|
||||
std::string PairingInfo(const std::string& device_id, const std::string& name) const {
|
||||
Require(Label(device_id) && Label(name));
|
||||
return nlohmann::json{{"type", "crossdesk-pairing"}, {"version", 1},
|
||||
{"public_key", public_key_}, {"device_id", device_id}, {"name", name}}.dump();
|
||||
}
|
||||
|
||||
const std::string& Fingerprint() const { return fingerprint_; }
|
||||
|
||||
// nullopt: ordinary control message; empty: unsupported/malformed challenge.
|
||||
// No peer-controlled parse or crypto exception may escape a data callback.
|
||||
std::optional<std::string> ChallengeResponse(std::string_view data,
|
||||
const std::string& device_id, const std::string& name) const noexcept {
|
||||
if (data.empty() || data.size() > 2048) return std::nullopt;
|
||||
try {
|
||||
auto message = nlohmann::json::parse(data.begin(), data.end(), nullptr, false);
|
||||
if (!message.is_object() || !message.contains("type") ||
|
||||
message["type"] != "device_trust_challenge") return std::nullopt;
|
||||
if (!message.at("version").is_number_integer() || message.at("version") != 1 ||
|
||||
!message.at("nonce").is_string()) return std::string{};
|
||||
const auto nonce = message.at("nonce").get<std::string>();
|
||||
if (!IsHex(nonce, 32) || !Label(device_id) || !Label(name)) return std::string{};
|
||||
const auto transcript = nlohmann::json::array({"crossdesk.device-trust.v1", nonce,
|
||||
public_key_, device_id, name}).dump();
|
||||
std::unique_ptr<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
|
||||
std::array<unsigned char, 64> signature{};
|
||||
size_t size = signature.size();
|
||||
Require(context && EVP_DigestSignInit(context.get(), nullptr, nullptr, nullptr, key_.get()) == 1 &&
|
||||
EVP_DigestSign(context.get(), signature.data(), &size,
|
||||
reinterpret_cast<const unsigned char*>(transcript.data()), transcript.size()) == 1 && size == 64);
|
||||
return nlohmann::json{{"type", "device_trust_proof"}, {"version", 1}, {"nonce", nonce},
|
||||
{"public_key", public_key_}, {"device_id", device_id}, {"name", name},
|
||||
{"signature", Hex(signature.data(), signature.size())}}.dump();
|
||||
} catch (const std::exception&) { return std::string{}; }
|
||||
}
|
||||
|
||||
private:
|
||||
using Key = std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)>;
|
||||
struct Secret {
|
||||
std::array<unsigned char, 32> value{};
|
||||
~Secret() { OPENSSL_cleanse(value.data(), value.size()); }
|
||||
};
|
||||
static void Require(bool ok) {
|
||||
if (!ok) throw std::runtime_error("Device signing identity unavailable");
|
||||
}
|
||||
static int Digit(char c) { return c <= '9' ? c - '0' : c - 'a' + 10; }
|
||||
static bool IsHex(std::string_view value, size_t bytes) {
|
||||
return value.size() == bytes * 2 && std::all_of(value.begin(), value.end(),
|
||||
[](char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); });
|
||||
}
|
||||
static std::string Hex(const unsigned char* bytes, size_t size) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
result.reserve(size * 2);
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
result += digits[bytes[i] >> 4]; result += digits[bytes[i] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
static bool Label(const std::string& value) {
|
||||
return value.size() <= 128 && std::none_of(value.begin(), value.end(),
|
||||
[](unsigned char c) { return c < 32 || c == 127; });
|
||||
}
|
||||
Key key_{nullptr, EVP_PKEY_free};
|
||||
std::string public_key_, fingerprint_;
|
||||
};
|
||||
DeviceTrustIdentity::DeviceTrustIdentity(std::string_view seed) : impl_(std::make_unique<Impl>(seed)) {}
|
||||
DeviceTrustIdentity::~DeviceTrustIdentity() = default;
|
||||
std::string DeviceTrustIdentity::GeneratePrivateKey() { return Impl::GeneratePrivateKey(); }
|
||||
std::string DeviceTrustIdentity::PairingInfo(const std::string& id, const std::string& name) const {
|
||||
return impl_->PairingInfo(id, name);
|
||||
}
|
||||
const std::string& DeviceTrustIdentity::Fingerprint() const { return impl_->Fingerprint(); }
|
||||
std::optional<std::string> DeviceTrustIdentity::ChallengeResponse(std::string_view data,
|
||||
const std::string& id, const std::string& name) const noexcept {
|
||||
return impl_->ChallengeResponse(data, id, name);
|
||||
}
|
||||
std::string DeviceTrustIdentity::Advertise(std::string_view host_information) {
|
||||
auto info = nlohmann::json::parse(host_information.begin(), host_information.end());
|
||||
info["device_trust_version"] = 1;
|
||||
return info.dump();
|
||||
}
|
||||
void DeviceTrustIdentity::ClearSecret(std::string& value) {
|
||||
OPENSSL_cleanse(value.data(), value.size());
|
||||
value.clear();
|
||||
}
|
||||
bool DeviceTrustIdentity::IsChallenge(std::string_view data) noexcept {
|
||||
if (data.empty() || data.size() > 2048) return false;
|
||||
try {
|
||||
const auto message = nlohmann::json::parse(data.begin(), data.end(), nullptr, false);
|
||||
return message.is_object() && message.contains("type") && message["type"] == "device_trust_challenge";
|
||||
} catch (const std::exception&) { return false; }
|
||||
}
|
||||
} // namespace crossdesk::mobile_controller
|
||||
+788
-3
@@ -11051,6 +11051,606 @@
|
||||
"fr": "Résolution de l’écran distant",
|
||||
"es": "Resolución de la pantalla remota",
|
||||
"pt-BR": "Resolução da tela remota"
|
||||
},
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。": {
|
||||
"zh-Hans": "将配对信息添加到被控端的可信设备列表,并核对指纹。",
|
||||
"en": "Add this pairing information to the controlled device’s trusted list and verify the fingerprint.",
|
||||
"ru": "Добавьте эти данные сопряжения в список доверенных устройств на управляемом компьютере и сверьте отпечаток.",
|
||||
"zh-Hant": "將配對資訊新增至被控端的可信裝置清單,並核對指紋。",
|
||||
"ja": "操作されるデバイスの信頼済みリストにこのペアリング情報を追加し、フィンガープリントを確認してください。",
|
||||
"ko": "제어 대상 장치의 신뢰 목록에 이 페어링 정보를 추가하고 지문을 확인하세요.",
|
||||
"de": "Fügen Sie diese Kopplungsdaten auf dem gesteuerten Gerät zur Vertrauensliste hinzu und prüfen Sie den Fingerabdruck.",
|
||||
"fr": "Ajoutez ces informations à la liste des appareils de confiance de l’appareil contrôlé et vérifiez l’empreinte.",
|
||||
"es": "Añada esta información de emparejamiento a la lista de confianza del dispositivo controlado y verifique la huella.",
|
||||
"pt-BR": "Adicione estas informações de pareamento à lista de confiança do dispositivo controlado e confira a impressão digital."
|
||||
},
|
||||
"上一页": {
|
||||
"zh-Hans": "上一页",
|
||||
"en": "Previous page",
|
||||
"zh-Hant": "上一頁",
|
||||
"ja": "前のページ",
|
||||
"ko": "이전 페이지",
|
||||
"de": "Vorherige Seite",
|
||||
"fr": "Page précédente",
|
||||
"es": "Página anterior",
|
||||
"pt-BR": "Página anterior",
|
||||
"ru": "Предыдущая страница"
|
||||
},
|
||||
"下一页": {
|
||||
"zh-Hans": "下一页",
|
||||
"en": "Next page",
|
||||
"zh-Hant": "下一頁",
|
||||
"ja": "次のページ",
|
||||
"ko": "다음 페이지",
|
||||
"de": "Nächste Seite",
|
||||
"fr": "Page suivante",
|
||||
"es": "Página siguiente",
|
||||
"pt-BR": "Próxima página",
|
||||
"ru": "Следующая страница"
|
||||
},
|
||||
"主按键": {
|
||||
"zh-Hans": "主按键",
|
||||
"en": "Main key",
|
||||
"zh-Hant": "主按鍵",
|
||||
"ja": "メインキー",
|
||||
"ko": "기본 키",
|
||||
"de": "Haupttaste",
|
||||
"fr": "Touche principale",
|
||||
"es": "Tecla principal",
|
||||
"pt-BR": "Tecla principal",
|
||||
"ru": "Основная клавиша"
|
||||
},
|
||||
"任务管理器": {
|
||||
"zh-Hans": "任务管理器",
|
||||
"en": "Task Manager",
|
||||
"zh-Hant": "工作管理員",
|
||||
"ja": "タスクマネージャー",
|
||||
"ko": "작업 관리자",
|
||||
"de": "Task-Manager",
|
||||
"fr": "Gestionnaire des tâches",
|
||||
"es": "Administrador de tareas",
|
||||
"pt-BR": "Gerenciador de Tarefas",
|
||||
"ru": "Диспетчер задач"
|
||||
},
|
||||
"修饰键": {
|
||||
"zh-Hans": "修饰键",
|
||||
"en": "Modifiers",
|
||||
"zh-Hant": "修飾鍵",
|
||||
"ja": "修飾キー",
|
||||
"ko": "보조 키",
|
||||
"de": "Modifikatortasten",
|
||||
"fr": "Touches de modification",
|
||||
"es": "Modificadores",
|
||||
"pt-BR": "Modificadores",
|
||||
"ru": "Клавиши-модификаторы"
|
||||
},
|
||||
"切换应用": {
|
||||
"zh-Hans": "切换应用",
|
||||
"en": "Switch apps",
|
||||
"zh-Hant": "切換應用程式",
|
||||
"ja": "アプリを切り替え",
|
||||
"ko": "앱 전환",
|
||||
"de": "Apps wechseln",
|
||||
"fr": "Changer d’application",
|
||||
"es": "Cambiar de aplicación",
|
||||
"pt-BR": "Alternar aplicativos",
|
||||
"ru": "Переключить приложение"
|
||||
},
|
||||
"切换窗口": {
|
||||
"zh-Hans": "切换窗口",
|
||||
"en": "Switch windows",
|
||||
"zh-Hant": "切換視窗",
|
||||
"ja": "ウインドウを切り替え",
|
||||
"ko": "창 전환",
|
||||
"de": "Fenster wechseln",
|
||||
"fr": "Changer de fenêtre",
|
||||
"es": "Cambiar de ventana",
|
||||
"pt-BR": "Alternar janelas",
|
||||
"ru": "Переключить окно"
|
||||
},
|
||||
"剪切": {
|
||||
"zh-Hans": "剪切",
|
||||
"en": "Cut",
|
||||
"zh-Hant": "剪下",
|
||||
"ja": "切り取り",
|
||||
"ko": "잘라내기",
|
||||
"de": "Ausschneiden",
|
||||
"fr": "Couper",
|
||||
"es": "Cortar",
|
||||
"pt-BR": "Recortar",
|
||||
"ru": "Вырезать"
|
||||
},
|
||||
"发送组合": {
|
||||
"zh-Hans": "发送组合",
|
||||
"en": "Key combination",
|
||||
"zh-Hant": "傳送組合",
|
||||
"ja": "送信するキーの組み合わせ",
|
||||
"ko": "전송할 키 조합",
|
||||
"de": "Tastenkombination",
|
||||
"fr": "Combinaison à envoyer",
|
||||
"es": "Combinación de teclas",
|
||||
"pt-BR": "Combinação de teclas",
|
||||
"ru": "Сочетание клавиш"
|
||||
},
|
||||
"名称": {
|
||||
"zh-Hans": "名称",
|
||||
"en": "Name",
|
||||
"zh-Hant": "名稱",
|
||||
"ja": "名前",
|
||||
"ko": "이름",
|
||||
"de": "Name",
|
||||
"fr": "Nom",
|
||||
"es": "Nombre",
|
||||
"pt-BR": "Nome",
|
||||
"ru": "Название"
|
||||
},
|
||||
"向上移动": {
|
||||
"zh-Hans": "向上移动",
|
||||
"en": "Move up",
|
||||
"zh-Hant": "向上移動",
|
||||
"ja": "上へ移動",
|
||||
"ko": "위로 이동",
|
||||
"de": "Nach oben",
|
||||
"fr": "Monter",
|
||||
"es": "Subir",
|
||||
"pt-BR": "Mover para cima",
|
||||
"ru": "Переместить вверх"
|
||||
},
|
||||
"固定到键盘上方": {
|
||||
"zh-Hans": "固定到键盘上方",
|
||||
"en": "Pin above keyboard",
|
||||
"zh-Hant": "固定到鍵盤上方",
|
||||
"ja": "キーボードの上に固定",
|
||||
"ko": "키보드 위에 고정",
|
||||
"de": "Über der Tastatur anheften",
|
||||
"fr": "Épingler au-dessus du clavier",
|
||||
"es": "Fijar sobre el teclado",
|
||||
"pt-BR": "Fixar acima do teclado",
|
||||
"ru": "Закрепить над клавиатурой"
|
||||
},
|
||||
"安全选项": {
|
||||
"zh-Hans": "安全选项",
|
||||
"en": "Security options",
|
||||
"zh-Hant": "安全性選項",
|
||||
"ja": "セキュリティオプション",
|
||||
"ko": "보안 옵션",
|
||||
"de": "Sicherheitsoptionen",
|
||||
"fr": "Options de sécurité",
|
||||
"es": "Opciones de seguridad",
|
||||
"pt-BR": "Opções de segurança",
|
||||
"ru": "Параметры безопасности"
|
||||
},
|
||||
"已有相同组合的快捷键": {
|
||||
"zh-Hans": "已有相同组合的快捷键",
|
||||
"en": "This combination already exists",
|
||||
"zh-Hant": "已有相同組合的快捷鍵",
|
||||
"ja": "この組み合わせは登録済みです",
|
||||
"ko": "이미 등록된 키 조합입니다",
|
||||
"de": "Diese Kombination ist bereits vorhanden",
|
||||
"fr": "Cette combinaison existe déjà",
|
||||
"es": "Esta combinación ya existe",
|
||||
"pt-BR": "Esta combinação já existe",
|
||||
"ru": "Такое сочетание уже существует"
|
||||
},
|
||||
"常用": {
|
||||
"zh-Hans": "常用",
|
||||
"en": "Favorites",
|
||||
"zh-Hant": "常用",
|
||||
"ja": "よく使う項目",
|
||||
"ko": "자주 사용",
|
||||
"de": "Favoriten",
|
||||
"fr": "Favoris",
|
||||
"es": "Favoritos",
|
||||
"pt-BR": "Favoritos",
|
||||
"ru": "Избранное"
|
||||
},
|
||||
"应用列表": {
|
||||
"zh-Hans": "应用列表",
|
||||
"en": "App list",
|
||||
"zh-Hant": "應用程式列表",
|
||||
"ja": "アプリ一覧",
|
||||
"ko": "앱 목록",
|
||||
"de": "App-Liste",
|
||||
"fr": "Liste des applications",
|
||||
"es": "Lista de aplicaciones",
|
||||
"pt-BR": "Lista de aplicativos",
|
||||
"ru": "Список приложений"
|
||||
},
|
||||
"强制退出面板": {
|
||||
"zh-Hans": "强制退出面板",
|
||||
"en": "Force Quit dialog",
|
||||
"zh-Hant": "強制結束面板",
|
||||
"ja": "強制終了ダイアログ",
|
||||
"ko": "강제 종료 대화상자",
|
||||
"de": "Dialog zum sofortigen Beenden",
|
||||
"fr": "Fenêtre Forcer à quitter",
|
||||
"es": "Diálogo Forzar salida",
|
||||
"pt-BR": "Janela Forçar Encerrar",
|
||||
"ru": "Окно принудительного завершения"
|
||||
},
|
||||
"快捷键": {
|
||||
"zh-Hans": "快捷键",
|
||||
"en": "Shortcuts",
|
||||
"zh-Hant": "快速鍵",
|
||||
"ja": "ショートカット",
|
||||
"ko": "단축키",
|
||||
"de": "Tastenkürzel",
|
||||
"fr": "Raccourcis",
|
||||
"es": "Atajos",
|
||||
"pt-BR": "Atalhos",
|
||||
"ru": "Сочетания клавиш"
|
||||
},
|
||||
"我的": {
|
||||
"zh-Hans": "我的",
|
||||
"en": "Mine",
|
||||
"zh-Hant": "我的",
|
||||
"ja": "マイ項目",
|
||||
"ko": "내 단축키",
|
||||
"de": "Eigene",
|
||||
"fr": "Mes raccourcis",
|
||||
"es": "Mis atajos",
|
||||
"pt-BR": "Meus atalhos",
|
||||
"ru": "Мои"
|
||||
},
|
||||
"按键": {
|
||||
"zh-Hans": "按键",
|
||||
"en": "Keys",
|
||||
"zh-Hant": "按鍵",
|
||||
"ja": "キー",
|
||||
"ko": "키",
|
||||
"de": "Tasten",
|
||||
"fr": "Touches",
|
||||
"es": "Teclas",
|
||||
"pt-BR": "Teclas",
|
||||
"ru": "Клавиши"
|
||||
},
|
||||
"撤销": {
|
||||
"zh-Hans": "撤销",
|
||||
"en": "Undo",
|
||||
"zh-Hant": "復原",
|
||||
"ja": "取り消す",
|
||||
"ko": "실행 취소",
|
||||
"de": "Rückgängig",
|
||||
"fr": "Annuler l’action",
|
||||
"es": "Deshacer",
|
||||
"pt-BR": "Desfazer",
|
||||
"ru": "Отменить"
|
||||
},
|
||||
"新建": {
|
||||
"zh-Hans": "新建",
|
||||
"en": "New",
|
||||
"zh-Hant": "新增",
|
||||
"ja": "新規",
|
||||
"ko": "새로 만들기",
|
||||
"de": "Neu",
|
||||
"fr": "Nouveau",
|
||||
"es": "Nuevo",
|
||||
"pt-BR": "Novo",
|
||||
"ru": "Создать"
|
||||
},
|
||||
"新建快捷键": {
|
||||
"zh-Hans": "新建快捷键",
|
||||
"en": "New shortcut",
|
||||
"zh-Hant": "新增快速鍵",
|
||||
"ja": "ショートカットを作成",
|
||||
"ko": "새 단축키",
|
||||
"de": "Neues Tastenkürzel",
|
||||
"fr": "Nouveau raccourci",
|
||||
"es": "Nuevo atajo",
|
||||
"pt-BR": "Novo atalho",
|
||||
"ru": "Новое сочетание"
|
||||
},
|
||||
"显示桌面": {
|
||||
"zh-Hans": "显示桌面",
|
||||
"en": "Show desktop",
|
||||
"zh-Hant": "顯示桌面",
|
||||
"ja": "デスクトップを表示",
|
||||
"ko": "바탕 화면 표시",
|
||||
"de": "Desktop anzeigen",
|
||||
"fr": "Afficher le bureau",
|
||||
"es": "Mostrar escritorio",
|
||||
"pt-BR": "Mostrar área de trabalho",
|
||||
"ru": "Показать рабочий стол"
|
||||
},
|
||||
"更多快捷键": {
|
||||
"zh-Hans": "更多快捷键",
|
||||
"en": "More shortcuts",
|
||||
"zh-Hant": "更多快速鍵",
|
||||
"ja": "その他のショートカット",
|
||||
"ko": "더 많은 단축키",
|
||||
"de": "Weitere Tastenkürzel",
|
||||
"fr": "Autres raccourcis",
|
||||
"es": "Más atajos",
|
||||
"pt-BR": "Mais atalhos",
|
||||
"ru": "Другие сочетания"
|
||||
},
|
||||
"最多保存 16 个自定义快捷键": {
|
||||
"zh-Hans": "最多保存 16 个自定义快捷键",
|
||||
"en": "Save up to 16 custom shortcuts",
|
||||
"zh-Hant": "最多儲存 16 個自訂快速鍵",
|
||||
"ja": "カスタムショートカットは16個まで保存できます",
|
||||
"ko": "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다",
|
||||
"de": "Bis zu 16 eigene Tastenkürzel speichern",
|
||||
"fr": "Enregistrez jusqu’à 16 raccourcis personnalisés",
|
||||
"es": "Guarda hasta 16 atajos personalizados",
|
||||
"pt-BR": "Salve até 16 atalhos personalizados",
|
||||
"ru": "Можно сохранить до 16 своих сочетаний"
|
||||
},
|
||||
"最多固定 8 个快捷键": {
|
||||
"zh-Hans": "最多固定 8 个快捷键",
|
||||
"en": "Pin up to 8 shortcuts",
|
||||
"zh-Hant": "最多固定 8 個快速鍵",
|
||||
"ja": "固定できるショートカットは8個までです",
|
||||
"ko": "단축키는 최대 8개까지 고정할 수 있습니다",
|
||||
"de": "Bis zu 8 Tastenkürzel anheften",
|
||||
"fr": "Épinglez jusqu’à 8 raccourcis",
|
||||
"es": "Fija hasta 8 atajos",
|
||||
"pt-BR": "Fixe até 8 atalhos",
|
||||
"ru": "Можно закрепить до 8 сочетаний"
|
||||
},
|
||||
"本地编辑,输入不会发送到远端": {
|
||||
"zh-Hans": "本地编辑,输入不会发送到远端",
|
||||
"en": "Local editing. Input stays on this device.",
|
||||
"zh-Hant": "本機編輯,輸入不會傳送到遠端",
|
||||
"ja": "ローカル編集。入力はリモートに送信されません。",
|
||||
"ko": "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.",
|
||||
"de": "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.",
|
||||
"fr": "Édition locale. La saisie reste sur cet appareil.",
|
||||
"es": "Edición local. Lo escrito permanece en este dispositivo.",
|
||||
"pt-BR": "Edição local. A entrada fica neste dispositivo.",
|
||||
"ru": "Локальное редактирование. Ввод не отправляется на удалённое устройство."
|
||||
},
|
||||
"添加到常用栏": {
|
||||
"zh-Hans": "添加到常用栏",
|
||||
"en": "Add to favorites bar",
|
||||
"zh-Hant": "加入常用列",
|
||||
"ja": "お気に入りバーに追加",
|
||||
"ko": "즐겨찾기 모음에 추가",
|
||||
"de": "Zur Favoritenleiste hinzufügen",
|
||||
"fr": "Ajouter à la barre des favoris",
|
||||
"es": "Añadir a la barra de favoritos",
|
||||
"pt-BR": "Adicionar à barra de favoritos",
|
||||
"ru": "Добавить на панель избранного"
|
||||
},
|
||||
"移除": {
|
||||
"zh-Hans": "移除",
|
||||
"en": "Remove",
|
||||
"zh-Hant": "移除",
|
||||
"ja": "取り除く",
|
||||
"ko": "제거",
|
||||
"de": "Entfernen",
|
||||
"fr": "Retirer",
|
||||
"es": "Quitar",
|
||||
"pt-BR": "Remover",
|
||||
"ru": "Убрать"
|
||||
},
|
||||
"窗口菜单": {
|
||||
"zh-Hans": "窗口菜单",
|
||||
"en": "Window menu",
|
||||
"zh-Hant": "視窗選單",
|
||||
"ja": "ウインドウメニュー",
|
||||
"ko": "창 메뉴",
|
||||
"de": "Fenstermenü",
|
||||
"fr": "Menu de la fenêtre",
|
||||
"es": "Menú de ventana",
|
||||
"pt-BR": "Menu da janela",
|
||||
"ru": "Меню окна"
|
||||
},
|
||||
"等待远端平台信息": {
|
||||
"zh-Hans": "等待远端平台信息",
|
||||
"en": "Waiting for remote platform information",
|
||||
"zh-Hant": "等待遠端平台資訊",
|
||||
"ja": "リモートのOS情報を待っています",
|
||||
"ko": "원격 플랫폼 정보를 기다리는 중",
|
||||
"de": "Warten auf Informationen zur Remote-Plattform",
|
||||
"fr": "En attente des informations du système distant",
|
||||
"es": "Esperando información del sistema remoto",
|
||||
"pt-BR": "Aguardando informações do sistema remoto",
|
||||
"ru": "Ожидание сведений об удалённой системе"
|
||||
},
|
||||
"管理与自定义": {
|
||||
"zh-Hans": "管理与自定义",
|
||||
"en": "Manage & customize",
|
||||
"zh-Hant": "管理與自訂",
|
||||
"ja": "管理とカスタマイズ",
|
||||
"ko": "관리 및 사용자 지정",
|
||||
"de": "Verwalten und anpassen",
|
||||
"fr": "Gérer et personnaliser",
|
||||
"es": "Gestionar y personalizar",
|
||||
"pt-BR": "Gerenciar e personalizar",
|
||||
"ru": "Управление и настройка"
|
||||
},
|
||||
"管理快捷键": {
|
||||
"zh-Hans": "管理快捷键",
|
||||
"en": "Manage shortcuts",
|
||||
"zh-Hant": "管理快速鍵",
|
||||
"ja": "ショートカットを管理",
|
||||
"ko": "단축키 관리",
|
||||
"de": "Tastenkürzel verwalten",
|
||||
"fr": "Gérer les raccourcis",
|
||||
"es": "Gestionar atajos",
|
||||
"pt-BR": "Gerenciar atalhos",
|
||||
"ru": "Управление сочетаниями"
|
||||
},
|
||||
"系统": {
|
||||
"zh-Hans": "系统",
|
||||
"en": "System",
|
||||
"zh-Hant": "系統",
|
||||
"ja": "システム",
|
||||
"ko": "시스템",
|
||||
"de": "System",
|
||||
"fr": "Système",
|
||||
"es": "Sistema",
|
||||
"pt-BR": "Sistema",
|
||||
"ru": "Система"
|
||||
},
|
||||
"终端复制": {
|
||||
"zh-Hans": "终端复制",
|
||||
"en": "Terminal copy",
|
||||
"zh-Hant": "終端機複製",
|
||||
"ja": "端末でコピー",
|
||||
"ko": "터미널 복사",
|
||||
"de": "Im Terminal kopieren",
|
||||
"fr": "Copier dans le terminal",
|
||||
"es": "Copiar en terminal",
|
||||
"pt-BR": "Copiar no terminal",
|
||||
"ru": "Копировать в терминале"
|
||||
},
|
||||
"终端粘贴": {
|
||||
"zh-Hans": "终端粘贴",
|
||||
"en": "Terminal paste",
|
||||
"zh-Hant": "終端機貼上",
|
||||
"ja": "端末に貼り付け",
|
||||
"ko": "터미널 붙여넣기",
|
||||
"de": "Im Terminal einfügen",
|
||||
"fr": "Coller dans le terminal",
|
||||
"es": "Pegar en terminal",
|
||||
"pt-BR": "Colar no terminal",
|
||||
"ru": "Вставить в терминале"
|
||||
},
|
||||
"编辑快捷键": {
|
||||
"zh-Hans": "编辑快捷键",
|
||||
"en": "Edit shortcut",
|
||||
"zh-Hant": "編輯快速鍵",
|
||||
"ja": "ショートカットを編集",
|
||||
"ko": "단축키 편집",
|
||||
"de": "Tastenkürzel bearbeiten",
|
||||
"fr": "Modifier le raccourci",
|
||||
"es": "Editar atajo",
|
||||
"pt-BR": "Editar atalho",
|
||||
"ru": "Изменить сочетание"
|
||||
},
|
||||
"聚焦搜索": {
|
||||
"zh-Hans": "聚焦搜索",
|
||||
"en": "Spotlight search",
|
||||
"zh-Hant": "Spotlight 搜尋",
|
||||
"ja": "Spotlight検索",
|
||||
"ko": "Spotlight 검색",
|
||||
"de": "Spotlight-Suche",
|
||||
"fr": "Recherche Spotlight",
|
||||
"es": "Búsqueda de Spotlight",
|
||||
"pt-BR": "Busca do Spotlight",
|
||||
"ru": "Поиск Spotlight"
|
||||
},
|
||||
"请使用系统预设中的安全选项": {
|
||||
"zh-Hans": "请使用系统预设中的安全选项",
|
||||
"en": "Use Security options in the system presets",
|
||||
"zh-Hant": "請使用系統預設中的安全性選項",
|
||||
"ja": "システムプリセットのセキュリティオプションを使用してください",
|
||||
"ko": "시스템 프리셋의 보안 옵션을 사용하세요",
|
||||
"de": "Sicherheitsoptionen in den Systemvorgaben verwenden",
|
||||
"fr": "Utilisez les options de sécurité des préréglages système",
|
||||
"es": "Usa Opciones de seguridad en los ajustes del sistema",
|
||||
"pt-BR": "Use Opções de segurança nas predefinições do sistema",
|
||||
"ru": "Используйте параметры безопасности из системных наборов"
|
||||
},
|
||||
"请先选词或取消组词": {
|
||||
"zh-Hans": "请先选词或取消组词",
|
||||
"en": "Confirm or cancel the current composition first",
|
||||
"zh-Hant": "請先選字或取消組字",
|
||||
"ja": "先に変換を確定またはキャンセルしてください",
|
||||
"ko": "먼저 조합 중인 문자를 확정하거나 취소하세요",
|
||||
"de": "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen",
|
||||
"fr": "Validez ou annulez d’abord la composition en cours",
|
||||
"es": "Confirma o cancela primero la composición actual",
|
||||
"pt-BR": "Confirme ou cancele primeiro a composição atual",
|
||||
"ru": "Сначала подтвердите или отмените текущий ввод"
|
||||
},
|
||||
"请输入有效的快捷键名称": {
|
||||
"zh-Hans": "请输入有效的快捷键名称",
|
||||
"en": "Enter a shortcut name (1–64 characters)",
|
||||
"zh-Hant": "請輸入有效的快速鍵名稱",
|
||||
"ja": "ショートカット名を1〜64文字で入力してください",
|
||||
"ko": "단축키 이름을 1~64자로 입력하세요",
|
||||
"de": "Namen mit 1–64 Zeichen eingeben",
|
||||
"fr": "Saisissez un nom de 1 à 64 caractères",
|
||||
"es": "Introduce un nombre de 1 a 64 caracteres",
|
||||
"pt-BR": "Insira um nome de 1 a 64 caracteres",
|
||||
"ru": "Введите название от 1 до 64 символов"
|
||||
},
|
||||
"请选择有效的按键": {
|
||||
"zh-Hans": "请选择有效的按键",
|
||||
"en": "Select a valid key",
|
||||
"zh-Hant": "請選擇有效的按鍵",
|
||||
"ja": "有効なキーを選択してください",
|
||||
"ko": "유효한 키를 선택하세요",
|
||||
"de": "Eine gültige Taste auswählen",
|
||||
"fr": "Sélectionnez une touche valide",
|
||||
"es": "Selecciona una tecla válida",
|
||||
"pt-BR": "Selecione uma tecla válida",
|
||||
"ru": "Выберите допустимую клавишу"
|
||||
},
|
||||
"运行窗口": {
|
||||
"zh-Hans": "运行窗口",
|
||||
"en": "Run dialog",
|
||||
"zh-Hant": "執行視窗",
|
||||
"ja": "実行ダイアログ",
|
||||
"ko": "실행 대화상자",
|
||||
"de": "Ausführen-Dialog",
|
||||
"fr": "Fenêtre Exécuter",
|
||||
"es": "Diálogo Ejecutar",
|
||||
"pt-BR": "Janela Executar",
|
||||
"ru": "Окно запуска"
|
||||
},
|
||||
"还没有自定义快捷键": {
|
||||
"zh-Hans": "还没有自定义快捷键",
|
||||
"en": "No custom shortcuts yet",
|
||||
"zh-Hant": "尚無自訂快速鍵",
|
||||
"ja": "カスタムショートカットはまだありません",
|
||||
"ko": "아직 사용자 지정 단축키가 없습니다",
|
||||
"de": "Noch keine eigenen Tastenkürzel",
|
||||
"fr": "Aucun raccourci personnalisé",
|
||||
"es": "Aún no hay atajos personalizados",
|
||||
"pt-BR": "Ainda não há atalhos personalizados",
|
||||
"ru": "Своих сочетаний пока нет"
|
||||
},
|
||||
"适用远端": {
|
||||
"zh-Hans": "适用远端",
|
||||
"en": "Remote platform",
|
||||
"zh-Hant": "適用遠端",
|
||||
"ja": "対象リモートOS",
|
||||
"ko": "대상 원격 플랫폼",
|
||||
"de": "Remote-Plattform",
|
||||
"fr": "Système distant",
|
||||
"es": "Sistema remoto",
|
||||
"pt-BR": "Sistema remoto",
|
||||
"ru": "Удалённая система"
|
||||
},
|
||||
"选择桌面方案": {
|
||||
"zh-Hans": "选择桌面方案",
|
||||
"en": "Choose desktop environment",
|
||||
"zh-Hant": "選擇桌面環境",
|
||||
"ja": "デスクトップ環境を選択",
|
||||
"ko": "데스크톱 환경 선택",
|
||||
"de": "Desktop-Umgebung auswählen",
|
||||
"fr": "Choisir l’environnement de bureau",
|
||||
"es": "Elegir entorno de escritorio",
|
||||
"pt-BR": "Escolher ambiente de trabalho",
|
||||
"ru": "Выберите окружение рабочего стола"
|
||||
},
|
||||
"选择桌面方案以加载系统预设": {
|
||||
"zh-Hans": "选择桌面方案以加载系统预设",
|
||||
"en": "Choose a desktop environment to load system presets",
|
||||
"zh-Hant": "選擇桌面環境以載入系統預設",
|
||||
"ja": "デスクトップ環境を選択してプリセットを読み込みます",
|
||||
"ko": "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요",
|
||||
"de": "Desktop-Umgebung wählen, um Systemvorgaben zu laden",
|
||||
"fr": "Choisissez un environnement pour charger les préréglages système",
|
||||
"es": "Elige un entorno para cargar los ajustes del sistema",
|
||||
"pt-BR": "Escolha um ambiente para carregar as predefinições do sistema",
|
||||
"ru": "Выберите окружение, чтобы загрузить системные наборы"
|
||||
},
|
||||
"键盘上方常用栏": {
|
||||
"zh-Hans": "键盘上方常用栏",
|
||||
"en": "Favorites above keyboard",
|
||||
"zh-Hant": "鍵盤上方常用列",
|
||||
"ja": "キーボード上のお気に入り",
|
||||
"ko": "키보드 위 즐겨찾기 모음",
|
||||
"de": "Favoriten über der Tastatur",
|
||||
"fr": "Favoris au-dessus du clavier",
|
||||
"es": "Favoritos sobre el teclado",
|
||||
"pt-BR": "Favoritos acima do teclado",
|
||||
"ru": "Избранное над клавиатурой"
|
||||
}
|
||||
},
|
||||
"desktop": {
|
||||
@@ -12413,7 +13013,69 @@
|
||||
",点击重试",
|
||||
"被控端分辨率",
|
||||
"设备不支持",
|
||||
"等待电脑端确认…"
|
||||
"等待电脑端确认…",
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。",
|
||||
"本机配对信息",
|
||||
"查看指纹",
|
||||
"复制配对信息",
|
||||
"配对信息已复制。",
|
||||
"无法复制配对信息,请重试。",
|
||||
"安全存储不可用,请解锁设备后重试",
|
||||
"上一页",
|
||||
"下一页",
|
||||
"主按键",
|
||||
"任务管理器",
|
||||
"保存",
|
||||
"修饰键",
|
||||
"全选",
|
||||
"切换应用",
|
||||
"切换窗口",
|
||||
"剪切",
|
||||
"发送组合",
|
||||
"名称",
|
||||
"向上移动",
|
||||
"固定到键盘上方",
|
||||
"复制",
|
||||
"安全选项",
|
||||
"已有相同组合的快捷键",
|
||||
"常用",
|
||||
"应用列表",
|
||||
"强制退出面板",
|
||||
"快捷键",
|
||||
"我的",
|
||||
"按键",
|
||||
"撤销",
|
||||
"新建",
|
||||
"新建快捷键",
|
||||
"显示桌面",
|
||||
"更多快捷键",
|
||||
"最多保存 16 个自定义快捷键",
|
||||
"最多固定 8 个快捷键",
|
||||
"未知",
|
||||
"本地编辑,输入不会发送到远端",
|
||||
"添加到常用栏",
|
||||
"移除",
|
||||
"窗口菜单",
|
||||
"等待远端平台信息",
|
||||
"管理与自定义",
|
||||
"管理快捷键",
|
||||
"粘贴",
|
||||
"系统",
|
||||
"终端复制",
|
||||
"终端粘贴",
|
||||
"编辑",
|
||||
"编辑快捷键",
|
||||
"聚焦搜索",
|
||||
"请使用系统预设中的安全选项",
|
||||
"请先选词或取消组词",
|
||||
"请输入有效的快捷键名称",
|
||||
"请选择有效的按键",
|
||||
"运行窗口",
|
||||
"还没有自定义快捷键",
|
||||
"适用远端",
|
||||
"选择桌面方案",
|
||||
"选择桌面方案以加载系统预设",
|
||||
"键盘上方常用栏"
|
||||
],
|
||||
"ios": [
|
||||
"%1$@ 传输失败",
|
||||
@@ -12704,7 +13366,70 @@
|
||||
"新版本需要 iOS %1$@ 或更高版本",
|
||||
"被控端分辨率",
|
||||
"设备不支持",
|
||||
"等待电脑端确认…"
|
||||
"等待电脑端确认…",
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。",
|
||||
"本机配对信息",
|
||||
"查看指纹",
|
||||
"复制配对信息",
|
||||
"配对信息已复制。",
|
||||
"无法复制配对信息,请重试。",
|
||||
"安全存储不可用,请解锁设备后重试",
|
||||
"上一页",
|
||||
"下一页",
|
||||
"主按键",
|
||||
"任务管理器",
|
||||
"保存",
|
||||
"修饰键",
|
||||
"全选",
|
||||
"切换应用",
|
||||
"切换窗口",
|
||||
"剪切",
|
||||
"发送组合",
|
||||
"名称",
|
||||
"向上移动",
|
||||
"固定到键盘上方",
|
||||
"复制",
|
||||
"安全选项",
|
||||
"已有相同组合的快捷键",
|
||||
"常用",
|
||||
"应用列表",
|
||||
"强制退出面板",
|
||||
"快捷键",
|
||||
"我的",
|
||||
"按键",
|
||||
"撤销",
|
||||
"收起键盘",
|
||||
"新建",
|
||||
"新建快捷键",
|
||||
"显示桌面",
|
||||
"更多快捷键",
|
||||
"最多保存 16 个自定义快捷键",
|
||||
"最多固定 8 个快捷键",
|
||||
"本地编辑,输入不会发送到远端",
|
||||
"添加到常用栏",
|
||||
"移除",
|
||||
"窗口菜单",
|
||||
"等待远端平台信息",
|
||||
"管理与自定义",
|
||||
"管理快捷键",
|
||||
"粘贴",
|
||||
"系统",
|
||||
"终端复制",
|
||||
"终端粘贴",
|
||||
"编辑",
|
||||
"编辑快捷键",
|
||||
"聚焦搜索",
|
||||
"请使用系统预设中的安全选项",
|
||||
"请先选词或取消组词",
|
||||
"请输入有效的快捷键名称",
|
||||
"请选择有效的按键",
|
||||
"运行窗口",
|
||||
"返回",
|
||||
"还没有自定义快捷键",
|
||||
"适用远端",
|
||||
"选择桌面方案",
|
||||
"选择桌面方案以加载系统预设",
|
||||
"键盘上方常用栏"
|
||||
],
|
||||
"harmony": [
|
||||
"OpenFEC 告知",
|
||||
@@ -12948,7 +13673,67 @@
|
||||
"鼠标左键,按住拖动",
|
||||
"鼠标控制",
|
||||
"被控端分辨率",
|
||||
"设备不支持"
|
||||
"设备不支持",
|
||||
"将配对信息添加到被控端的可信设备列表,并核对指纹。",
|
||||
"本机配对信息",
|
||||
"查看指纹",
|
||||
"复制配对信息",
|
||||
"配对信息已复制。",
|
||||
"无法复制配对信息,请重试。",
|
||||
"上一页",
|
||||
"下一页",
|
||||
"主按键",
|
||||
"任务管理器",
|
||||
"保存",
|
||||
"修饰键",
|
||||
"切换应用",
|
||||
"切换窗口",
|
||||
"剪切",
|
||||
"发送组合",
|
||||
"名称",
|
||||
"向上移动",
|
||||
"固定到键盘上方",
|
||||
"安全选项",
|
||||
"已有相同组合的快捷键",
|
||||
"常用",
|
||||
"应用列表",
|
||||
"强制退出面板",
|
||||
"快捷键",
|
||||
"我的",
|
||||
"按键",
|
||||
"撤销",
|
||||
"新建",
|
||||
"新建快捷键",
|
||||
"显示桌面",
|
||||
"更多快捷键",
|
||||
"最多保存 16 个自定义快捷键",
|
||||
"最多固定 8 个快捷键",
|
||||
"未知",
|
||||
"本地编辑,输入不会发送到远端",
|
||||
"添加到常用栏",
|
||||
"电脑键盘",
|
||||
"移除",
|
||||
"窗口菜单",
|
||||
"等待远端平台信息",
|
||||
"管理与自定义",
|
||||
"管理快捷键",
|
||||
"系统",
|
||||
"系统键盘",
|
||||
"终端复制",
|
||||
"终端粘贴",
|
||||
"编辑",
|
||||
"编辑快捷键",
|
||||
"聚焦搜索",
|
||||
"请使用系统预设中的安全选项",
|
||||
"请先选词或取消组词",
|
||||
"请输入有效的快捷键名称",
|
||||
"请选择有效的按键",
|
||||
"运行窗口",
|
||||
"还没有自定义快捷键",
|
||||
"适用远端",
|
||||
"选择桌面方案",
|
||||
"选择桌面方案以加载系统预设",
|
||||
"键盘上方常用栏"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,19 @@ target("crossdesk_wire")
|
||||
add_includedirs(path.join(wire_dir, "include"), {public = true})
|
||||
target_end()
|
||||
|
||||
-- iOS packages this controller helper alongside the wire and MiniRTC archives.
|
||||
-- Keep crypto headers out of the Objective-C++ bridge's public include paths.
|
||||
if is_plat("iphoneos") then
|
||||
includes(path.join(source_dir, "deps/submodules/minirtc/thirdparty/openssl"))
|
||||
add_requires("openssl3 3.3.2", {system = false, configs = {shared = false}})
|
||||
target("crossdesk_mobile_trust")
|
||||
set_kind("static")
|
||||
add_packages("nlohmann_json", "openssl3")
|
||||
add_includedirs(path.join(source_dir, "libs/controller/include"))
|
||||
add_files(path.join(source_dir, "libs/controller/src/mobile_device_trust.cpp"))
|
||||
target_end()
|
||||
end
|
||||
|
||||
target("crossdesk_file_browser")
|
||||
set_kind("static")
|
||||
set_languages("c++20")
|
||||
|
||||
Reference in new issue
Block a user