diff --git a/apps/android/README.md b/apps/android/README.md index 7e0d344f..0b2fb490 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -159,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 环境验证。 @@ -256,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 个,支持排序、取消固定、编辑与删除。自定义组合保留用户选择的实际修饰键。组合键整体排队,按反序释放修饰键;快捷键执行前先发送已确认的文本并重置纠错上下文,尚在组词时提示先选词或取消。 diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/LocalizationKeys.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/LocalizationKeys.java index 5fd39427..b39e4d05 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/LocalizationKeys.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/LocalizationKeys.java @@ -321,6 +321,61 @@ final class LocalizationKeys { 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() {} } diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/MainActivity.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/MainActivity.java index 5941bd36..768c22e8 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/MainActivity.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/MainActivity.java @@ -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);} @@ -524,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{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(); } diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java index 2bfce7a2..f66f6d26 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/NativeSession.java @@ -299,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; diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboard.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboard.java index 93758522..7a4075d9 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboard.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboard.java @@ -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 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); diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboardInput.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboardInput.java index bbb3ab45..b2a5bde1 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboardInput.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteKeyboardInput.java @@ -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, diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcutPanel.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcutPanel.java new file mode 100644 index 00000000..ec249dbc --- /dev/null +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcutPanel.java @@ -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 send; + private final Runnable changed; + private String platform="unknown"; + private int category,page; + private Dialog manager; + RemoteShortcutPanel(Context context,MobileUi ui,Consumer 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 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 changed){Spinner spinner=new Spinner(context);ArrayAdapter 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 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 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 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 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 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()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 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); + } +} diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcuts.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcuts.java new file mode 100644 index 00000000..a9f86500 --- /dev/null +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/RemoteShortcuts.java @@ -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 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 keys(){List 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 items(String remote){ + String os=platform(remote);List 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 custom(){ + List result=new ArrayList<>(); + try{JSONArray entries=new JSONArray(preferences.getString("custom","[]"));for(int i=0;i64||!keys().contains(key)||modifiers==null||modifiers.length()>4)continue; + int[] values=new int[modifiers.length()];boolean valid=true;for(int j=0;jcode==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 pins(String os){ + List 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 available=items(os);result.removeIf(id->available.stream().noneMatch(item->item.id.equals(id))); + return result; + } + List pinned(String os){List 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 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 values=custom();values.removeIf(item->item.id.equals(id));write(values);for(String os:PLATFORMS){List 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 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 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 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 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();} +} diff --git a/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java b/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java index 51f9019f..6f30fdf5 100644 --- a/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java +++ b/apps/android/app/src/main/java/cn/crossdesk/mobile/SessionControls.java @@ -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; diff --git a/apps/android/app/src/main/res/values-b+zh+Hans/strings.xml b/apps/android/app/src/main/res/values-b+zh+Hans/strings.xml index 5c9a55ed..9b5f798d 100644 --- a/apps/android/app/src/main/res/values-b+zh+Hans/strings.xml +++ b/apps/android/app/src/main/res/values-b+zh+Hans/strings.xml @@ -315,4 +315,59 @@ "配对信息已复制。" "无法复制配对信息,请重试。" "安全存储不可用,请解锁设备后重试" + "上一页" + "下一页" + "主按键" + "任务管理器" + "保存" + "修饰键" + "全选" + "切换应用" + "切换窗口" + "剪切" + "发送组合" + "名称" + "向上移动" + "固定到键盘上方" + "复制" + "安全选项" + "已有相同组合的快捷键" + "常用" + "应用列表" + "强制退出面板" + "快捷键" + "我的" + "按键" + "撤销" + "新建" + "新建快捷键" + "显示桌面" + "更多快捷键" + "最多保存 16 个自定义快捷键" + "最多固定 8 个快捷键" + "未知" + "本地编辑,输入不会发送到远端" + "添加到常用栏" + "移除" + "窗口菜单" + "等待远端平台信息" + "管理与自定义" + "管理快捷键" + "粘贴" + "系统" + "终端复制" + "终端粘贴" + "编辑" + "编辑快捷键" + "聚焦搜索" + "请使用系统预设中的安全选项" + "请先选词或取消组词" + "请输入有效的快捷键名称" + "请选择有效的按键" + "运行窗口" + "还没有自定义快捷键" + "适用远端" + "选择桌面方案" + "选择桌面方案以加载系统预设" + "键盘上方常用栏" diff --git a/apps/android/app/src/main/res/values-b+zh+Hant/strings.xml b/apps/android/app/src/main/res/values-b+zh+Hant/strings.xml index a31ae8bd..8cdfc8b7 100644 --- a/apps/android/app/src/main/res/values-b+zh+Hant/strings.xml +++ b/apps/android/app/src/main/res/values-b+zh+Hant/strings.xml @@ -315,4 +315,59 @@ "配對資訊已複製。" "無法複製配對資訊,請重試。" "安全儲存不可用,請解鎖裝置後重試" + "上一頁" + "下一頁" + "主按鍵" + "工作管理員" + "儲存" + "修飾鍵" + "全選" + "切換應用程式" + "切換視窗" + "剪下" + "傳送組合" + "名稱" + "向上移動" + "固定到鍵盤上方" + "複製" + "安全性選項" + "已有相同組合的快捷鍵" + "常用" + "應用程式列表" + "強制結束面板" + "快速鍵" + "我的" + "按鍵" + "復原" + "新增" + "新增快速鍵" + "顯示桌面" + "更多快速鍵" + "最多儲存 16 個自訂快速鍵" + "最多固定 8 個快速鍵" + "未知" + "本機編輯,輸入不會傳送到遠端" + "加入常用列" + "移除" + "視窗選單" + "等待遠端平台資訊" + "管理與自訂" + "管理快速鍵" + "貼上" + "系統" + "終端機複製" + "終端機貼上" + "編輯" + "編輯快速鍵" + "Spotlight 搜尋" + "請使用系統預設中的安全性選項" + "請先選字或取消組字" + "請輸入有效的快速鍵名稱" + "請選擇有效的按鍵" + "執行視窗" + "尚無自訂快速鍵" + "適用遠端" + "選擇桌面環境" + "選擇桌面環境以載入系統預設" + "鍵盤上方常用列" diff --git a/apps/android/app/src/main/res/values-de/strings.xml b/apps/android/app/src/main/res/values-de/strings.xml index 71a8ab10..2cb90907 100644 --- a/apps/android/app/src/main/res/values-de/strings.xml +++ b/apps/android/app/src/main/res/values-de/strings.xml @@ -315,4 +315,59 @@ "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" + "Neu" + "Neues Tastenkürzel" + "Desktop anzeigen" + "Weitere Tastenkürzel" + "Bis zu 16 eigene Tastenkürzel speichern" + "Bis zu 8 Tastenkürzel anheften" + "Unbekannt" + "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" + "Noch keine eigenen Tastenkürzel" + "Remote-Plattform" + "Desktop-Umgebung auswählen" + "Desktop-Umgebung wählen, um Systemvorgaben zu laden" + "Favoriten über der Tastatur" diff --git a/apps/android/app/src/main/res/values-es/strings.xml b/apps/android/app/src/main/res/values-es/strings.xml index 67526de1..56e10133 100644 --- a/apps/android/app/src/main/res/values-es/strings.xml +++ b/apps/android/app/src/main/res/values-es/strings.xml @@ -315,4 +315,59 @@ "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" + "Nuevo" + "Nuevo atajo" + "Mostrar escritorio" + "Más atajos" + "Guarda hasta 16 atajos personalizados" + "Fija hasta 8 atajos" + "Desconocido" + "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" + "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" diff --git a/apps/android/app/src/main/res/values-fr/strings.xml b/apps/android/app/src/main/res/values-fr/strings.xml index 5fb96ea2..eee44660 100644 --- a/apps/android/app/src/main/res/values-fr/strings.xml +++ b/apps/android/app/src/main/res/values-fr/strings.xml @@ -315,4 +315,59 @@ "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" + "Nouveau" + "Nouveau raccourci" + "Afficher le bureau" + "Autres raccourcis" + "Enregistrez jusqu’à 16 raccourcis personnalisés" + "Épinglez jusqu’à 8 raccourcis" + "Inconnu" + "É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" + "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" diff --git a/apps/android/app/src/main/res/values-ja/strings.xml b/apps/android/app/src/main/res/values-ja/strings.xml index 5b15eed3..debaad1b 100644 --- a/apps/android/app/src/main/res/values-ja/strings.xml +++ b/apps/android/app/src/main/res/values-ja/strings.xml @@ -315,4 +315,59 @@ "ペアリング情報をコピーしました。" "ペアリング情報をコピーできません。もう一度お試しください。" "安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。" + "前のページ" + "次のページ" + "メインキー" + "タスクマネージャー" + "保存" + "修飾キー" + "全て選択" + "アプリを切り替え" + "ウインドウを切り替え" + "切り取り" + "送信するキーの組み合わせ" + "名前" + "上へ移動" + "キーボードの上に固定" + "コピー" + "セキュリティオプション" + "この組み合わせは登録済みです" + "よく使う項目" + "アプリ一覧" + "強制終了ダイアログ" + "ショートカット" + "マイ項目" + "キー" + "取り消す" + "新規" + "ショートカットを作成" + "デスクトップを表示" + "その他のショートカット" + "カスタムショートカットは16個まで保存できます" + "固定できるショートカットは8個までです" + "不明" + "ローカル編集。入力はリモートに送信されません。" + "お気に入りバーに追加" + "取り除く" + "ウインドウメニュー" + "リモートのOS情報を待っています" + "管理とカスタマイズ" + "ショートカットを管理" + "貼り付け" + "システム" + "端末でコピー" + "端末に貼り付け" + "編集" + "ショートカットを編集" + "Spotlight検索" + "システムプリセットのセキュリティオプションを使用してください" + "先に変換を確定またはキャンセルしてください" + "ショートカット名を1〜64文字で入力してください" + "有効なキーを選択してください" + "実行ダイアログ" + "カスタムショートカットはまだありません" + "対象リモートOS" + "デスクトップ環境を選択" + "デスクトップ環境を選択してプリセットを読み込みます" + "キーボード上のお気に入り" diff --git a/apps/android/app/src/main/res/values-ko/strings.xml b/apps/android/app/src/main/res/values-ko/strings.xml index ede8b133..1be3f94e 100644 --- a/apps/android/app/src/main/res/values-ko/strings.xml +++ b/apps/android/app/src/main/res/values-ko/strings.xml @@ -315,4 +315,59 @@ "페어링 정보를 복사했습니다." "페어링 정보를 복사할 수 없습니다. 다시 시도하세요." "보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요." + "이전 페이지" + "다음 페이지" + "기본 키" + "작업 관리자" + "저장" + "보조 키" + "모두 선택" + "앱 전환" + "창 전환" + "잘라내기" + "전송할 키 조합" + "이름" + "위로 이동" + "키보드 위에 고정" + "복사" + "보안 옵션" + "이미 등록된 키 조합입니다" + "자주 사용" + "앱 목록" + "강제 종료 대화상자" + "단축키" + "내 단축키" + "키" + "실행 취소" + "새로 만들기" + "새 단축키" + "바탕 화면 표시" + "더 많은 단축키" + "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다" + "단축키는 최대 8개까지 고정할 수 있습니다" + "알 수 없음" + "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다." + "즐겨찾기 모음에 추가" + "제거" + "창 메뉴" + "원격 플랫폼 정보를 기다리는 중" + "관리 및 사용자 지정" + "단축키 관리" + "붙여넣기" + "시스템" + "터미널 복사" + "터미널 붙여넣기" + "편집" + "단축키 편집" + "Spotlight 검색" + "시스템 프리셋의 보안 옵션을 사용하세요" + "먼저 조합 중인 문자를 확정하거나 취소하세요" + "단축키 이름을 1~64자로 입력하세요" + "유효한 키를 선택하세요" + "실행 대화상자" + "아직 사용자 지정 단축키가 없습니다" + "대상 원격 플랫폼" + "데스크톱 환경 선택" + "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요" + "키보드 위 즐겨찾기 모음" diff --git a/apps/android/app/src/main/res/values-pt/strings.xml b/apps/android/app/src/main/res/values-pt/strings.xml index 018200ed..39df05b2 100644 --- a/apps/android/app/src/main/res/values-pt/strings.xml +++ b/apps/android/app/src/main/res/values-pt/strings.xml @@ -315,4 +315,59 @@ "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" + "Novo" + "Novo atalho" + "Mostrar área de trabalho" + "Mais atalhos" + "Salve até 16 atalhos personalizados" + "Fixe até 8 atalhos" + "Desconhecido" + "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" + "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" diff --git a/apps/android/app/src/main/res/values-ru/strings.xml b/apps/android/app/src/main/res/values-ru/strings.xml index 086ce7d6..ad9c07d0 100644 --- a/apps/android/app/src/main/res/values-ru/strings.xml +++ b/apps/android/app/src/main/res/values-ru/strings.xml @@ -315,4 +315,59 @@ "Данные сопряжения скопированы." "Не удалось скопировать данные сопряжения. Повторите попытку." "Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку." + "Предыдущая страница" + "Следующая страница" + "Основная клавиша" + "Диспетчер задач" + "Сохранить" + "Клавиши-модификаторы" + "Выбрать все" + "Переключить приложение" + "Переключить окно" + "Вырезать" + "Сочетание клавиш" + "Название" + "Переместить вверх" + "Закрепить над клавиатурой" + "Копировать" + "Параметры безопасности" + "Такое сочетание уже существует" + "Избранное" + "Список приложений" + "Окно принудительного завершения" + "Сочетания клавиш" + "Мои" + "Клавиши" + "Отменить" + "Создать" + "Новое сочетание" + "Показать рабочий стол" + "Другие сочетания" + "Можно сохранить до 16 своих сочетаний" + "Можно закрепить до 8 сочетаний" + "Неизвестно" + "Локальное редактирование. Ввод не отправляется на удалённое устройство." + "Добавить на панель избранного" + "Убрать" + "Меню окна" + "Ожидание сведений об удалённой системе" + "Управление и настройка" + "Управление сочетаниями" + "Вставить" + "Система" + "Копировать в терминале" + "Вставить в терминале" + "Изменить" + "Изменить сочетание" + "Поиск Spotlight" + "Используйте параметры безопасности из системных наборов" + "Сначала подтвердите или отмените текущий ввод" + "Введите название от 1 до 64 символов" + "Выберите допустимую клавишу" + "Окно запуска" + "Своих сочетаний пока нет" + "Удалённая система" + "Выберите окружение рабочего стола" + "Выберите окружение, чтобы загрузить системные наборы" + "Избранное над клавиатурой" diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml index eb1f0272..718b7f8f 100644 --- a/apps/android/app/src/main/res/values/strings.xml +++ b/apps/android/app/src/main/res/values/strings.xml @@ -315,4 +315,59 @@ "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" + "New" + "New shortcut" + "Show desktop" + "More shortcuts" + "Save up to 16 custom shortcuts" + "Pin up to 8 shortcuts" + "Unknown" + "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" + "No custom shortcuts yet" + "Remote platform" + "Choose desktop environment" + "Choose a desktop environment to load system presets" + "Favorites above keyboard" diff --git a/apps/harmony/README.md b/apps/harmony/README.md index 755823c5..e0e1e1cc 100644 --- a/apps/harmony/README.md +++ b/apps/harmony/README.md @@ -11,6 +11,12 @@ 手机控制端的“设置 → 屏幕方向 → 连接建立后锁定横屏”默认关闭并保存在本机。开启后,手机连接时进入并保持横屏;关闭时跟随设备方向。平板和多窗口不受此选项影响。退出会话恢复首页方向,返回后台会话沿用该会话的方向设置。已移除检测手机姿态后解除初始横屏的逻辑。 +## 移动端快捷键 + +手机和平板在现有悬浮键盘上提供常用快捷键栏和“更多”入口;更多面板替换按键区域,包含“常用 / 按键 / 系统 / 我的”。预设根据远端 Windows、macOS、Linux 适配,Linux 系统预设需选择 GNOME 或 KDE Plasma。当前输入入口仍是 ArkUI 悬浮键盘,未改为系统输入法。 + +管理页独立处理本地输入,支持 16 个自定义组合、每个远端平台 8 个固定项,以及排序、编辑和删除。配置保存在本机,组合键使用同一条原生队列任务发送,按反序释放修饰键,并校验连接代次。 + ## 移动端可信设备鉴权 手机和平板的“设置 → 本机配对信息”支持查看指纹和复制配对信息。在被控电脑的“管理可信设备 → 添加可信设备”中粘贴并核对指纹后,可以连接开启“仅允许可信设备控制”的电脑;连接密码仍需验证。 diff --git a/apps/harmony/entry/src/main/ets/mobile/LocalizationData.ets b/apps/harmony/entry/src/main/ets/mobile/LocalizationData.ets index a3a34dd8..fdd8dba7 100644 --- a/apps/harmony/entry/src/main/ets/mobile/LocalizationData.ets +++ b/apps/harmony/entry/src/main/ets/mobile/LocalizationData.ets @@ -248,4 +248,58 @@ export const translations: Record = { "复制配对信息": ["复制配对信息", "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"], }; diff --git a/apps/harmony/entry/src/main/ets/mobile/MobileController.ets b/apps/harmony/entry/src/main/ets/mobile/MobileController.ets index 6470e384..432943e1 100644 --- a/apps/harmony/entry/src/main/ets/mobile/MobileController.ets +++ b/apps/harmony/entry/src/main/ets/mobile/MobileController.ets @@ -12,6 +12,7 @@ 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; @@ -308,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 { diff --git a/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets b/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets index 489bf918..6be9463f 100644 --- a/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets +++ b/apps/harmony/entry/src/main/ets/mobile/MobileRemoteView.ets @@ -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(); diff --git a/apps/harmony/entry/src/main/ets/mobile/RemoteShortcutPanel.ets b/apps/harmony/entry/src/main/ets/mobile/RemoteShortcutPanel.ets new file mode 100644 index 00000000..d225b28b --- /dev/null +++ b/apps/harmony/entry/src/main/ets/mobile/RemoteShortcutPanel.ets @@ -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; }) + } +} diff --git a/apps/harmony/entry/src/main/ets/mobile/RemoteShortcuts.ets b/apps/harmony/entry/src/main/ets/mobile/RemoteShortcuts.ets new file mode 100644 index 00000000..7443e30e --- /dev/null +++ b/apps/harmony/entry/src/main/ets/mobile/RemoteShortcuts.ets @@ -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 ''; + } +} diff --git a/apps/harmony/native/mobile_session.cpp b/apps/harmony/native/mobile_session.cpp index 0f07514c..ee17d5d2 100644 --- a/apps/harmony/native/mobile_session.cpp +++ b/apps/harmony/native/mobile_session.cpp @@ -327,6 +327,21 @@ class MobileSession { if (key<=0||key>255) return; const bool down=v.at("down").get(); 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() != connection_) return; + const int key=v.at("code").get(); + const auto modifiers=v.at("modifiers").get>(); + if (key<=0||key>255||modifiers.size()>4) return; + std::vector 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(); if(index<0||index>=32)return; Release(); display_=index; ++video_generation_; renderer_.Clear(); diff --git a/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj b/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj index 90ba393a..232da099 100644 --- a/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj +++ b/apps/ios/CrossDeskMobile.xcodeproj/project.pbxproj @@ -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 = ""; }; 20000000000000000000001E /* VirtualMouseBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VirtualMouseBar.swift; sourceTree = ""; }; 200000000000000000000030 /* RemoteSystemKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteSystemKeyboard.swift; sourceTree = ""; }; + 200000000000000000000031 /* RemoteShortcuts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcuts.swift; sourceTree = ""; }; + 200000000000000000000032 /* RemoteShortcutPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShortcutPanel.swift; sourceTree = ""; }; 23D5AD87A9BF0CBF3A6AADDF /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Resources/en.lproj/InfoPlist.strings; sourceTree = ""; }; 2E83770B40B2D299796EB0CD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Resources/zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; 2EA0435DFAEE685F04391B3B /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Resources/es.lproj/Localizable.strings; sourceTree = ""; }; @@ -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 */, diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h index f9507e05..92a1c4f1 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.h @@ -195,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 *)modifiers; - (void)sendText:(NSString *)text; - (void)sendCommittedText:(NSString *)text; - (void)switchToDisplay:(NSInteger)displayIndex; diff --git a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm index bc757bd2..be501a78 100644 --- a/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm +++ b/apps/ios/CrossDeskMobile/Bridge/CrossDeskRTCBridge.mm @@ -974,6 +974,30 @@ MiniRtcParams MakeParams(const RTCState &state, const std::string &user_id, stream:kKeyboardStream]; } +- (void)sendKeyChord:(NSUInteger)keyCode modifiers:(NSArray *)modifiers { + if (!keyCode || keyCode > 255 || modifiers.count > 4) return; + std::vector keys; + for (NSUInteger code : {17u, 16u, 18u, 91u}) { + if ([modifiers containsObject:@(code)]) keys.push_back(code); + } + std::vector 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; diff --git a/apps/ios/CrossDeskMobile/Input/RemoteShortcutPanel.swift b/apps/ios/CrossDeskMobile/Input/RemoteShortcutPanel.swift new file mode 100644 index 00000000..8dccd80f --- /dev/null +++ b/apps/ios/CrossDeskMobile/Input/RemoteShortcutPanel.swift @@ -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 + @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() } + } + } + } + } + } +} diff --git a/apps/ios/CrossDeskMobile/Input/RemoteShortcuts.swift b/apps/ios/CrossDeskMobile/Input/RemoteShortcuts.swift new file mode 100644 index 00000000..c6b175bb --- /dev/null +++ b/apps/ios/CrossDeskMobile/Input/RemoteShortcuts.swift @@ -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() + 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() + 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 + } +} diff --git a/apps/ios/CrossDeskMobile/Input/RemoteSystemKeyboard.swift b/apps/ios/CrossDeskMobile/Input/RemoteSystemKeyboard.swift index a1093b56..e0861198 100644 --- a/apps/ios/CrossDeskMobile/Input/RemoteSystemKeyboard.swift +++ b/apps/ios/CrossDeskMobile/Input/RemoteSystemKeyboard.swift @@ -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: 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 diff --git a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift index 744fdf32..457258e7 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionModel.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionModel.swift @@ -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, diff --git a/apps/ios/CrossDeskMobile/RemoteSessionView.swift b/apps/ios/CrossDeskMobile/RemoteSessionView.swift index 04fe8c4d..b911165d 100644 --- a/apps/ios/CrossDeskMobile/RemoteSessionView.swift +++ b/apps/ios/CrossDeskMobile/RemoteSessionView.swift @@ -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 = [] @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() } } diff --git a/apps/ios/CrossDeskMobile/Resources/de.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/de.lproj/Localizable.strings index 1cc60210..06cf845d 100644 --- a/apps/ios/CrossDeskMobile/Resources/de.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/de.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/en.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/en.lproj/Localizable.strings index fe996cba..d6c59e51 100644 --- a/apps/ios/CrossDeskMobile/Resources/en.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/en.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/es.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/es.lproj/Localizable.strings index 1958424d..ed067b0c 100644 --- a/apps/ios/CrossDeskMobile/Resources/es.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/es.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/fr.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/fr.lproj/Localizable.strings index 2293e41e..799f7370 100644 --- a/apps/ios/CrossDeskMobile/Resources/fr.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/fr.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/ja.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/ja.lproj/Localizable.strings index 8e31d547..9d289b2b 100644 --- a/apps/ios/CrossDeskMobile/Resources/ja.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/ja.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "ペアリング情報をコピーしました。"; "无法复制配对信息,请重试。" = "ペアリング情報をコピーできません。もう一度お試しください。"; "安全存储不可用,请解锁设备后重试" = "安全なストレージが利用できません。デバイスのロックを解除して、もう一度お試しください。"; +"上一页" = "前のページ"; +"下一页" = "次のページ"; +"主按键" = "メインキー"; +"任务管理器" = "タスクマネージャー"; +"保存" = "保存"; +"修饰键" = "修飾キー"; +"全选" = "全て選択"; +"切换应用" = "アプリを切り替え"; +"切换窗口" = "ウインドウを切り替え"; +"剪切" = "切り取り"; +"发送组合" = "送信するキーの組み合わせ"; +"名称" = "名前"; +"向上移动" = "上へ移動"; +"固定到键盘上方" = "キーボードの上に固定"; +"复制" = "コピー"; +"安全选项" = "セキュリティオプション"; +"已有相同组合的快捷键" = "この組み合わせは登録済みです"; +"常用" = "よく使う項目"; +"应用列表" = "アプリ一覧"; +"强制退出面板" = "強制終了ダイアログ"; +"快捷键" = "ショートカット"; +"我的" = "マイ項目"; +"按键" = "キー"; +"撤销" = "取り消す"; +"收起键盘" = "キーボードを隠す"; +"新建" = "新規"; +"新建快捷键" = "ショートカットを作成"; +"显示桌面" = "デスクトップを表示"; +"更多快捷键" = "その他のショートカット"; +"最多保存 16 个自定义快捷键" = "カスタムショートカットは16個まで保存できます"; +"最多固定 8 个快捷键" = "固定できるショートカットは8個までです"; +"本地编辑,输入不会发送到远端" = "ローカル編集。入力はリモートに送信されません。"; +"添加到常用栏" = "お気に入りバーに追加"; +"移除" = "取り除く"; +"窗口菜单" = "ウインドウメニュー"; +"等待远端平台信息" = "リモートのOS情報を待っています"; +"管理与自定义" = "管理とカスタマイズ"; +"管理快捷键" = "ショートカットを管理"; +"粘贴" = "貼り付け"; +"系统" = "システム"; +"终端复制" = "端末でコピー"; +"终端粘贴" = "端末に貼り付け"; +"编辑" = "編集"; +"编辑快捷键" = "ショートカットを編集"; +"聚焦搜索" = "Spotlight検索"; +"请使用系统预设中的安全选项" = "システムプリセットのセキュリティオプションを使用してください"; +"请先选词或取消组词" = "先に変換を確定またはキャンセルしてください"; +"请输入有效的快捷键名称" = "ショートカット名を1〜64文字で入力してください"; +"请选择有效的按键" = "有効なキーを選択してください"; +"运行窗口" = "実行ダイアログ"; +"返回" = "戻る"; +"还没有自定义快捷键" = "カスタムショートカットはまだありません"; +"适用远端" = "対象リモートOS"; +"选择桌面方案" = "デスクトップ環境を選択"; +"选择桌面方案以加载系统预设" = "デスクトップ環境を選択してプリセットを読み込みます"; +"键盘上方常用栏" = "キーボード上のお気に入り"; diff --git a/apps/ios/CrossDeskMobile/Resources/ko.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/ko.lproj/Localizable.strings index 96e607ce..59bd3c47 100644 --- a/apps/ios/CrossDeskMobile/Resources/ko.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/ko.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "페어링 정보를 복사했습니다."; "无法复制配对信息,请重试。" = "페어링 정보를 복사할 수 없습니다. 다시 시도하세요."; "安全存储不可用,请解锁设备后重试" = "보안 저장소를 사용할 수 없습니다. 장치를 잠금 해제한 후 다시 시도해 주세요."; +"上一页" = "이전 페이지"; +"下一页" = "다음 페이지"; +"主按键" = "기본 키"; +"任务管理器" = "작업 관리자"; +"保存" = "저장"; +"修饰键" = "보조 키"; +"全选" = "모두 선택"; +"切换应用" = "앱 전환"; +"切换窗口" = "창 전환"; +"剪切" = "잘라내기"; +"发送组合" = "전송할 키 조합"; +"名称" = "이름"; +"向上移动" = "위로 이동"; +"固定到键盘上方" = "키보드 위에 고정"; +"复制" = "복사"; +"安全选项" = "보안 옵션"; +"已有相同组合的快捷键" = "이미 등록된 키 조합입니다"; +"常用" = "자주 사용"; +"应用列表" = "앱 목록"; +"强制退出面板" = "강제 종료 대화상자"; +"快捷键" = "단축키"; +"我的" = "내 단축키"; +"按键" = "키"; +"撤销" = "실행 취소"; +"收起键盘" = "키보드 숨기기"; +"新建" = "새로 만들기"; +"新建快捷键" = "새 단축키"; +"显示桌面" = "바탕 화면 표시"; +"更多快捷键" = "더 많은 단축키"; +"最多保存 16 个自定义快捷键" = "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다"; +"最多固定 8 个快捷键" = "단축키는 최대 8개까지 고정할 수 있습니다"; +"本地编辑,输入不会发送到远端" = "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다."; +"添加到常用栏" = "즐겨찾기 모음에 추가"; +"移除" = "제거"; +"窗口菜单" = "창 메뉴"; +"等待远端平台信息" = "원격 플랫폼 정보를 기다리는 중"; +"管理与自定义" = "관리 및 사용자 지정"; +"管理快捷键" = "단축키 관리"; +"粘贴" = "붙여넣기"; +"系统" = "시스템"; +"终端复制" = "터미널 복사"; +"终端粘贴" = "터미널 붙여넣기"; +"编辑" = "편집"; +"编辑快捷键" = "단축키 편집"; +"聚焦搜索" = "Spotlight 검색"; +"请使用系统预设中的安全选项" = "시스템 프리셋의 보안 옵션을 사용하세요"; +"请先选词或取消组词" = "먼저 조합 중인 문자를 확정하거나 취소하세요"; +"请输入有效的快捷键名称" = "단축키 이름을 1~64자로 입력하세요"; +"请选择有效的按键" = "유효한 키를 선택하세요"; +"运行窗口" = "실행 대화상자"; +"返回" = "뒤로"; +"还没有自定义快捷键" = "아직 사용자 지정 단축키가 없습니다"; +"适用远端" = "대상 원격 플랫폼"; +"选择桌面方案" = "데스크톱 환경 선택"; +"选择桌面方案以加载系统预设" = "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요"; +"键盘上方常用栏" = "키보드 위 즐겨찾기 모음"; diff --git a/apps/ios/CrossDeskMobile/Resources/pt-BR.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/pt-BR.lproj/Localizable.strings index 1fd0ed72..b39defbb 100644 --- a/apps/ios/CrossDeskMobile/Resources/pt-BR.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/pt-BR.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/pt.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/pt.lproj/Localizable.strings index 1fd0ed72..b39defbb 100644 --- a/apps/ios/CrossDeskMobile/Resources/pt.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/pt.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "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"; diff --git a/apps/ios/CrossDeskMobile/Resources/ru.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/ru.lproj/Localizable.strings index 1547cd28..d74323f8 100644 --- a/apps/ios/CrossDeskMobile/Resources/ru.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/ru.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "Данные сопряжения скопированы."; "无法复制配对信息,请重试。" = "Не удалось скопировать данные сопряжения. Повторите попытку."; "安全存储不可用,请解锁设备后重试" = "Безопасное хранилище недоступно. Разблокируйте устройство и повторите попытку."; +"上一页" = "Предыдущая страница"; +"下一页" = "Следующая страница"; +"主按键" = "Основная клавиша"; +"任务管理器" = "Диспетчер задач"; +"保存" = "Сохранить"; +"修饰键" = "Клавиши-модификаторы"; +"全选" = "Выбрать все"; +"切换应用" = "Переключить приложение"; +"切换窗口" = "Переключить окно"; +"剪切" = "Вырезать"; +"发送组合" = "Сочетание клавиш"; +"名称" = "Название"; +"向上移动" = "Переместить вверх"; +"固定到键盘上方" = "Закрепить над клавиатурой"; +"复制" = "Копировать"; +"安全选项" = "Параметры безопасности"; +"已有相同组合的快捷键" = "Такое сочетание уже существует"; +"常用" = "Избранное"; +"应用列表" = "Список приложений"; +"强制退出面板" = "Окно принудительного завершения"; +"快捷键" = "Сочетания клавиш"; +"我的" = "Мои"; +"按键" = "Клавиши"; +"撤销" = "Отменить"; +"收起键盘" = "Скрыть клавиатуру"; +"新建" = "Создать"; +"新建快捷键" = "Новое сочетание"; +"显示桌面" = "Показать рабочий стол"; +"更多快捷键" = "Другие сочетания"; +"最多保存 16 个自定义快捷键" = "Можно сохранить до 16 своих сочетаний"; +"最多固定 8 个快捷键" = "Можно закрепить до 8 сочетаний"; +"本地编辑,输入不会发送到远端" = "Локальное редактирование. Ввод не отправляется на удалённое устройство."; +"添加到常用栏" = "Добавить на панель избранного"; +"移除" = "Убрать"; +"窗口菜单" = "Меню окна"; +"等待远端平台信息" = "Ожидание сведений об удалённой системе"; +"管理与自定义" = "Управление и настройка"; +"管理快捷键" = "Управление сочетаниями"; +"粘贴" = "Вставить"; +"系统" = "Система"; +"终端复制" = "Копировать в терминале"; +"终端粘贴" = "Вставить в терминале"; +"编辑" = "Изменить"; +"编辑快捷键" = "Изменить сочетание"; +"聚焦搜索" = "Поиск Spotlight"; +"请使用系统预设中的安全选项" = "Используйте параметры безопасности из системных наборов"; +"请先选词或取消组词" = "Сначала подтвердите или отмените текущий ввод"; +"请输入有效的快捷键名称" = "Введите название от 1 до 64 символов"; +"请选择有效的按键" = "Выберите допустимую клавишу"; +"运行窗口" = "Окно запуска"; +"返回" = "Назад"; +"还没有自定义快捷键" = "Своих сочетаний пока нет"; +"适用远端" = "Удалённая система"; +"选择桌面方案" = "Выберите окружение рабочего стола"; +"选择桌面方案以加载系统预设" = "Выберите окружение, чтобы загрузить системные наборы"; +"键盘上方常用栏" = "Избранное над клавиатурой"; diff --git a/apps/ios/CrossDeskMobile/Resources/zh-Hans.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/zh-Hans.lproj/Localizable.strings index 106e6a98..afef7952 100644 --- a/apps/ios/CrossDeskMobile/Resources/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/zh-Hans.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "配对信息已复制。"; "无法复制配对信息,请重试。" = "无法复制配对信息,请重试。"; "安全存储不可用,请解锁设备后重试" = "安全存储不可用,请解锁设备后重试"; +"上一页" = "上一页"; +"下一页" = "下一页"; +"主按键" = "主按键"; +"任务管理器" = "任务管理器"; +"保存" = "保存"; +"修饰键" = "修饰键"; +"全选" = "全选"; +"切换应用" = "切换应用"; +"切换窗口" = "切换窗口"; +"剪切" = "剪切"; +"发送组合" = "发送组合"; +"名称" = "名称"; +"向上移动" = "向上移动"; +"固定到键盘上方" = "固定到键盘上方"; +"复制" = "复制"; +"安全选项" = "安全选项"; +"已有相同组合的快捷键" = "已有相同组合的快捷键"; +"常用" = "常用"; +"应用列表" = "应用列表"; +"强制退出面板" = "强制退出面板"; +"快捷键" = "快捷键"; +"我的" = "我的"; +"按键" = "按键"; +"撤销" = "撤销"; +"收起键盘" = "收起键盘"; +"新建" = "新建"; +"新建快捷键" = "新建快捷键"; +"显示桌面" = "显示桌面"; +"更多快捷键" = "更多快捷键"; +"最多保存 16 个自定义快捷键" = "最多保存 16 个自定义快捷键"; +"最多固定 8 个快捷键" = "最多固定 8 个快捷键"; +"本地编辑,输入不会发送到远端" = "本地编辑,输入不会发送到远端"; +"添加到常用栏" = "添加到常用栏"; +"移除" = "移除"; +"窗口菜单" = "窗口菜单"; +"等待远端平台信息" = "等待远端平台信息"; +"管理与自定义" = "管理与自定义"; +"管理快捷键" = "管理快捷键"; +"粘贴" = "粘贴"; +"系统" = "系统"; +"终端复制" = "终端复制"; +"终端粘贴" = "终端粘贴"; +"编辑" = "编辑"; +"编辑快捷键" = "编辑快捷键"; +"聚焦搜索" = "聚焦搜索"; +"请使用系统预设中的安全选项" = "请使用系统预设中的安全选项"; +"请先选词或取消组词" = "请先选词或取消组词"; +"请输入有效的快捷键名称" = "请输入有效的快捷键名称"; +"请选择有效的按键" = "请选择有效的按键"; +"运行窗口" = "运行窗口"; +"返回" = "返回"; +"还没有自定义快捷键" = "还没有自定义快捷键"; +"适用远端" = "适用远端"; +"选择桌面方案" = "选择桌面方案"; +"选择桌面方案以加载系统预设" = "选择桌面方案以加载系统预设"; +"键盘上方常用栏" = "键盘上方常用栏"; diff --git a/apps/ios/CrossDeskMobile/Resources/zh-Hant.lproj/Localizable.strings b/apps/ios/CrossDeskMobile/Resources/zh-Hant.lproj/Localizable.strings index 77b4f5af..6b5a157a 100644 --- a/apps/ios/CrossDeskMobile/Resources/zh-Hant.lproj/Localizable.strings +++ b/apps/ios/CrossDeskMobile/Resources/zh-Hant.lproj/Localizable.strings @@ -295,3 +295,59 @@ "配对信息已复制。" = "配對資訊已複製。"; "无法复制配对信息,请重试。" = "無法複製配對資訊,請重試。"; "安全存储不可用,请解锁设备后重试" = "安全儲存不可用,請解鎖裝置後重試"; +"上一页" = "上一頁"; +"下一页" = "下一頁"; +"主按键" = "主按鍵"; +"任务管理器" = "工作管理員"; +"保存" = "儲存"; +"修饰键" = "修飾鍵"; +"全选" = "全選"; +"切换应用" = "切換應用程式"; +"切换窗口" = "切換視窗"; +"剪切" = "剪下"; +"发送组合" = "傳送組合"; +"名称" = "名稱"; +"向上移动" = "向上移動"; +"固定到键盘上方" = "固定到鍵盤上方"; +"复制" = "複製"; +"安全选项" = "安全性選項"; +"已有相同组合的快捷键" = "已有相同組合的快捷鍵"; +"常用" = "常用"; +"应用列表" = "應用程式列表"; +"强制退出面板" = "強制結束面板"; +"快捷键" = "快速鍵"; +"我的" = "我的"; +"按键" = "按鍵"; +"撤销" = "復原"; +"收起键盘" = "收起鍵盤"; +"新建" = "新增"; +"新建快捷键" = "新增快速鍵"; +"显示桌面" = "顯示桌面"; +"更多快捷键" = "更多快速鍵"; +"最多保存 16 个自定义快捷键" = "最多儲存 16 個自訂快速鍵"; +"最多固定 8 个快捷键" = "最多固定 8 個快速鍵"; +"本地编辑,输入不会发送到远端" = "本機編輯,輸入不會傳送到遠端"; +"添加到常用栏" = "加入常用列"; +"移除" = "移除"; +"窗口菜单" = "視窗選單"; +"等待远端平台信息" = "等待遠端平台資訊"; +"管理与自定义" = "管理與自訂"; +"管理快捷键" = "管理快速鍵"; +"粘贴" = "貼上"; +"系统" = "系統"; +"终端复制" = "終端機複製"; +"终端粘贴" = "終端機貼上"; +"编辑" = "編輯"; +"编辑快捷键" = "編輯快速鍵"; +"聚焦搜索" = "Spotlight 搜尋"; +"请使用系统预设中的安全选项" = "請使用系統預設中的安全性選項"; +"请先选词或取消组词" = "請先選字或取消組字"; +"请输入有效的快捷键名称" = "請輸入有效的快速鍵名稱"; +"请选择有效的按键" = "請選擇有效的按鍵"; +"运行窗口" = "執行視窗"; +"返回" = "返回"; +"还没有自定义快捷键" = "尚無自訂快速鍵"; +"适用远端" = "適用遠端"; +"选择桌面方案" = "選擇桌面環境"; +"选择桌面方案以加载系统预设" = "選擇桌面環境以載入系統預設"; +"键盘上方常用栏" = "鍵盤上方常用列"; diff --git a/apps/ios/README.md b/apps/ios/README.md index 5cfe5b07..1381d51b 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -282,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 @@ -310,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 @@ -705,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. diff --git a/localization/catalog.json b/localization/catalog.json index fa892a76..38fd9a3b 100644 --- a/localization/catalog.json +++ b/localization/catalog.json @@ -11063,6 +11063,594 @@ "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": { @@ -12432,7 +13020,62 @@ "复制配对信息", "配对信息已复制。", "无法复制配对信息,请重试。", - "安全存储不可用,请解锁设备后重试" + "安全存储不可用,请解锁设备后重试", + "上一页", + "下一页", + "主按键", + "任务管理器", + "保存", + "修饰键", + "全选", + "切换应用", + "切换窗口", + "剪切", + "发送组合", + "名称", + "向上移动", + "固定到键盘上方", + "复制", + "安全选项", + "已有相同组合的快捷键", + "常用", + "应用列表", + "强制退出面板", + "快捷键", + "我的", + "按键", + "撤销", + "新建", + "新建快捷键", + "显示桌面", + "更多快捷键", + "最多保存 16 个自定义快捷键", + "最多固定 8 个快捷键", + "未知", + "本地编辑,输入不会发送到远端", + "添加到常用栏", + "移除", + "窗口菜单", + "等待远端平台信息", + "管理与自定义", + "管理快捷键", + "粘贴", + "系统", + "终端复制", + "终端粘贴", + "编辑", + "编辑快捷键", + "聚焦搜索", + "请使用系统预设中的安全选项", + "请先选词或取消组词", + "请输入有效的快捷键名称", + "请选择有效的按键", + "运行窗口", + "还没有自定义快捷键", + "适用远端", + "选择桌面方案", + "选择桌面方案以加载系统预设", + "键盘上方常用栏" ], "ios": [ "%1$@ 传输失败", @@ -12730,7 +13373,63 @@ "复制配对信息", "配对信息已复制。", "无法复制配对信息,请重试。", - "安全存储不可用,请解锁设备后重试" + "安全存储不可用,请解锁设备后重试", + "上一页", + "下一页", + "主按键", + "任务管理器", + "保存", + "修饰键", + "全选", + "切换应用", + "切换窗口", + "剪切", + "发送组合", + "名称", + "向上移动", + "固定到键盘上方", + "复制", + "安全选项", + "已有相同组合的快捷键", + "常用", + "应用列表", + "强制退出面板", + "快捷键", + "我的", + "按键", + "撤销", + "收起键盘", + "新建", + "新建快捷键", + "显示桌面", + "更多快捷键", + "最多保存 16 个自定义快捷键", + "最多固定 8 个快捷键", + "本地编辑,输入不会发送到远端", + "添加到常用栏", + "移除", + "窗口菜单", + "等待远端平台信息", + "管理与自定义", + "管理快捷键", + "粘贴", + "系统", + "终端复制", + "终端粘贴", + "编辑", + "编辑快捷键", + "聚焦搜索", + "请使用系统预设中的安全选项", + "请先选词或取消组词", + "请输入有效的快捷键名称", + "请选择有效的按键", + "运行窗口", + "返回", + "还没有自定义快捷键", + "适用远端", + "选择桌面方案", + "选择桌面方案以加载系统预设", + "键盘上方常用栏" ], "harmony": [ "OpenFEC 告知", @@ -12980,7 +13679,61 @@ "查看指纹", "复制配对信息", "配对信息已复制。", - "无法复制配对信息,请重试。" + "无法复制配对信息,请重试。", + "上一页", + "下一页", + "主按键", + "任务管理器", + "保存", + "修饰键", + "切换应用", + "切换窗口", + "剪切", + "发送组合", + "名称", + "向上移动", + "固定到键盘上方", + "安全选项", + "已有相同组合的快捷键", + "常用", + "应用列表", + "强制退出面板", + "快捷键", + "我的", + "按键", + "撤销", + "新建", + "新建快捷键", + "显示桌面", + "更多快捷键", + "最多保存 16 个自定义快捷键", + "最多固定 8 个快捷键", + "未知", + "本地编辑,输入不会发送到远端", + "添加到常用栏", + "电脑键盘", + "移除", + "窗口菜单", + "等待远端平台信息", + "管理与自定义", + "管理快捷键", + "系统", + "系统键盘", + "终端复制", + "终端粘贴", + "编辑", + "编辑快捷键", + "聚焦搜索", + "请使用系统预设中的安全选项", + "请先选词或取消组词", + "请输入有效的快捷键名称", + "请选择有效的按键", + "运行窗口", + "还没有自定义快捷键", + "适用远端", + "选择桌面方案", + "选择桌面方案以加载系统预设", + "键盘上方常用栏" ] } }