[feat] add customizable mobile shortcuts for Windows, macOS and Linux, refs #153

This commit is contained in:
dijunkun committed 2026-11-13 16:37:55 +08:00
1 parent 72dda9bb90
commit bad4569117
47 files changed
+3314 -70

No files matched your search

@@ -248,4 +248,58 @@ export const translations: Record<string, string[]> = {
"复制配对信息": ["复制配对信息", "Copy pairing info", "Копировать данные", "複製配對資訊", "ペアリング情報をコピー", "페어링 정보 복사", "Kopplungsdaten kopieren", "Copier les informations", "Copiar información", "Copiar informações"],
"配对信息已复制。": ["配对信息已复制。", "Pairing information copied.", "Данные сопряжения скопированы.", "配對資訊已複製。", "ペアリング情報をコピーしました。", "페어링 정보를 복사했습니다.", "Kopplungsdaten kopiert.", "Informations d’association copiées.", "Información de emparejamiento copiada.", "Informações de pareamento copiadas."],
"无法复制配对信息,请重试。": ["无法复制配对信息,请重试。", "Unable to copy pairing information. Please try again.", "Не удалось скопировать данные сопряжения. Повторите попытку.", "無法複製配對資訊,請重試。", "ペアリング情報をコピーできません。もう一度お試しください。", "페어링 정보를 복사할 수 없습니다. 다시 시도하세요.", "Kopplungsdaten konnten nicht kopiert werden. Bitte erneut versuchen.", "Impossible de copier les informations d’association. Réessayez.", "No se pudo copiar la información de emparejamiento. Inténtelo de nuevo.", "Não foi possível copiar as informações de pareamento. Tente novamente."],
"上一页": ["上一页", "Previous page", "Предыдущая страница", "上一頁", "前のページ", "이전 페이지", "Vorherige Seite", "Page précédente", "Página anterior", "Página anterior"],
"下一页": ["下一页", "Next page", "Следующая страница", "下一頁", "次のページ", "다음 페이지", "Nächste Seite", "Page suivante", "Página siguiente", "Próxima página"],
"主按键": ["主按键", "Main key", "Основная клавиша", "主按鍵", "メインキー", "기본 키", "Haupttaste", "Touche principale", "Tecla principal", "Tecla principal"],
"任务管理器": ["任务管理器", "Task Manager", "Диспетчер задач", "工作管理員", "タスクマネージャー", "작업 관리자", "Task-Manager", "Gestionnaire des tâches", "Administrador de tareas", "Gerenciador de Tarefas"],
"保存": ["保存", "Save", "Сохранить", "儲存", "保存", "저장", "Speichern", "Enregistrer", "Guardar", "Salvar"],
"修饰键": ["修饰键", "Modifiers", "Клавиши-модификаторы", "修飾鍵", "修飾キー", "보조 키", "Modifikatortasten", "Touches de modification", "Modificadores", "Modificadores"],
"切换应用": ["切换应用", "Switch apps", "Переключить приложение", "切換應用程式", "アプリを切り替え", "앱 전환", "Apps wechseln", "Changer d’application", "Cambiar de aplicación", "Alternar aplicativos"],
"切换窗口": ["切换窗口", "Switch windows", "Переключить окно", "切換視窗", "ウインドウを切り替え", "창 전환", "Fenster wechseln", "Changer de fenêtre", "Cambiar de ventana", "Alternar janelas"],
"剪切": ["剪切", "Cut", "Вырезать", "剪下", "切り取り", "잘라내기", "Ausschneiden", "Couper", "Cortar", "Recortar"],
"发送组合": ["发送组合", "Key combination", "Сочетание клавиш", "傳送組合", "送信するキーの組み合わせ", "전송할 키 조합", "Tastenkombination", "Combinaison à envoyer", "Combinación de teclas", "Combinação de teclas"],
"名称": ["名称", "Name", "Название", "名稱", "名前", "이름", "Name", "Nom", "Nombre", "Nome"],
"向上移动": ["向上移动", "Move up", "Переместить вверх", "向上移動", "上へ移動", "위로 이동", "Nach oben", "Monter", "Subir", "Mover para cima"],
"固定到键盘上方": ["固定到键盘上方", "Pin above keyboard", "Закрепить над клавиатурой", "固定到鍵盤上方", "キーボードの上に固定", "키보드 위에 고정", "Über der Tastatur anheften", "Épingler au-dessus du clavier", "Fijar sobre el teclado", "Fixar acima do teclado"],
"安全选项": ["安全选项", "Security options", "Параметры безопасности", "安全性選項", "セキュリティオプション", "보안 옵션", "Sicherheitsoptionen", "Options de sécurité", "Opciones de seguridad", "Opções de segurança"],
"已有相同组合的快捷键": ["已有相同组合的快捷键", "This combination already exists", "Такое сочетание уже существует", "已有相同組合的快捷鍵", "この組み合わせは登録済みです", "이미 등록된 키 조합입니다", "Diese Kombination ist bereits vorhanden", "Cette combinaison existe déjà", "Esta combinación ya existe", "Esta combinação já existe"],
"常用": ["常用", "Favorites", "Избранное", "常用", "よく使う項目", "자주 사용", "Favoriten", "Favoris", "Favoritos", "Favoritos"],
"应用列表": ["应用列表", "App list", "Список приложений", "應用程式列表", "アプリ一覧", "앱 목록", "App-Liste", "Liste des applications", "Lista de aplicaciones", "Lista de aplicativos"],
"强制退出面板": ["强制退出面板", "Force Quit dialog", "Окно принудительного завершения", "強制結束面板", "強制終了ダイアログ", "강제 종료 대화상자", "Dialog zum sofortigen Beenden", "Fenêtre Forcer à quitter", "Diálogo Forzar salida", "Janela Forçar Encerrar"],
"快捷键": ["快捷键", "Shortcuts", "Сочетания клавиш", "快速鍵", "ショートカット", "단축키", "Tastenkürzel", "Raccourcis", "Atajos", "Atalhos"],
"我的": ["我的", "Mine", "Мои", "我的", "マイ項目", "내 단축키", "Eigene", "Mes raccourcis", "Mis atajos", "Meus atalhos"],
"按键": ["按键", "Keys", "Клавиши", "按鍵", "キー", "키", "Tasten", "Touches", "Teclas", "Teclas"],
"撤销": ["撤销", "Undo", "Отменить", "復原", "取り消す", "실행 취소", "Rückgängig", "Annuler l’action", "Deshacer", "Desfazer"],
"新建": ["新建", "New", "Создать", "新增", "新規", "새로 만들기", "Neu", "Nouveau", "Nuevo", "Novo"],
"新建快捷键": ["新建快捷键", "New shortcut", "Новое сочетание", "新增快速鍵", "ショートカットを作成", "새 단축키", "Neues Tastenkürzel", "Nouveau raccourci", "Nuevo atajo", "Novo atalho"],
"显示桌面": ["显示桌面", "Show desktop", "Показать рабочий стол", "顯示桌面", "デスクトップを表示", "바탕 화면 표시", "Desktop anzeigen", "Afficher le bureau", "Mostrar escritorio", "Mostrar área de trabalho"],
"更多快捷键": ["更多快捷键", "More shortcuts", "Другие сочетания", "更多快速鍵", "その他のショートカット", "더 많은 단축키", "Weitere Tastenkürzel", "Autres raccourcis", "Más atajos", "Mais atalhos"],
"最多保存 16 个自定义快捷键": ["最多保存 16 个自定义快捷键", "Save up to 16 custom shortcuts", "Можно сохранить до 16 своих сочетаний", "最多儲存 16 個自訂快速鍵", "カスタムショートカットは16個まで保存できます", "사용자 지정 단축키는 최대 16개까지 저장할 수 있습니다", "Bis zu 16 eigene Tastenkürzel speichern", "Enregistrez jusqu’à 16 raccourcis personnalisés", "Guarda hasta 16 atajos personalizados", "Salve até 16 atalhos personalizados"],
"最多固定 8 个快捷键": ["最多固定 8 个快捷键", "Pin up to 8 shortcuts", "Можно закрепить до 8 сочетаний", "最多固定 8 個快速鍵", "固定できるショートカットは8個までです", "단축키는 최대 8개까지 고정할 수 있습니다", "Bis zu 8 Tastenkürzel anheften", "Épinglez jusqu’à 8 raccourcis", "Fija hasta 8 atajos", "Fixe até 8 atalhos"],
"未知": ["未知", "Unknown", "Неизвестно", "未知", "不明", "알 수 없음", "Unbekannt", "Inconnu", "Desconocido", "Desconhecido"],
"本地编辑,输入不会发送到远端": ["本地编辑,输入不会发送到远端", "Local editing. Input stays on this device.", "Локальное редактирование. Ввод не отправляется на удалённое устройство.", "本機編輯,輸入不會傳送到遠端", "ローカル編集。入力はリモートに送信されません。", "로컬 편집 중입니다. 입력은 원격 기기로 전송되지 않습니다.", "Lokale Bearbeitung. Eingaben bleiben auf diesem Gerät.", "Édition locale. La saisie reste sur cet appareil.", "Edición local. Lo escrito permanece en este dispositivo.", "Edição local. A entrada fica neste dispositivo."],
"添加到常用栏": ["添加到常用栏", "Add to favorites bar", "Добавить на панель избранного", "加入常用列", "お気に入りバーに追加", "즐겨찾기 모음에 추가", "Zur Favoritenleiste hinzufügen", "Ajouter à la barre des favoris", "Añadir a la barra de favoritos", "Adicionar à barra de favoritos"],
"电脑键盘": ["电脑键盘", "Computer keyboard", "Компьютерная клавиатура", "電腦鍵盤", "コンピューターのキーボード", "컴퓨터 키보드", "Computertastatur", "Clavier d'ordinateur", "Teclado de computadora", "Teclado de computador"],
"移除": ["移除", "Remove", "Убрать", "移除", "取り除く", "제거", "Entfernen", "Retirer", "Quitar", "Remover"],
"窗口菜单": ["窗口菜单", "Window menu", "Меню окна", "視窗選單", "ウインドウメニュー", "창 메뉴", "Fenstermenü", "Menu de la fenêtre", "Menú de ventana", "Menu da janela"],
"等待远端平台信息": ["等待远端平台信息", "Waiting for remote platform information", "Ожидание сведений об удалённой системе", "等待遠端平台資訊", "リモートのOS情報を待っています", "원격 플랫폼 정보를 기다리는 중", "Warten auf Informationen zur Remote-Plattform", "En attente des informations du système distant", "Esperando información del sistema remoto", "Aguardando informações do sistema remoto"],
"管理与自定义": ["管理与自定义", "Manage & customize", "Управление и настройка", "管理與自訂", "管理とカスタマイズ", "관리 및 사용자 지정", "Verwalten und anpassen", "Gérer et personnaliser", "Gestionar y personalizar", "Gerenciar e personalizar"],
"管理快捷键": ["管理快捷键", "Manage shortcuts", "Управление сочетаниями", "管理快速鍵", "ショートカットを管理", "단축키 관리", "Tastenkürzel verwalten", "Gérer les raccourcis", "Gestionar atajos", "Gerenciar atalhos"],
"系统": ["系统", "System", "Система", "系統", "システム", "시스템", "System", "Système", "Sistema", "Sistema"],
"系统键盘": ["系统键盘", "System keyboard", "Системная клавиатура", "系統鍵盤", "システムキーボード", "시스템 키보드", "Systemtastatur", "Clavier système", "Teclado del sistema", "Teclado do sistema"],
"终端复制": ["终端复制", "Terminal copy", "Копировать в терминале", "終端機複製", "端末でコピー", "터미널 복사", "Im Terminal kopieren", "Copier dans le terminal", "Copiar en terminal", "Copiar no terminal"],
"终端粘贴": ["终端粘贴", "Terminal paste", "Вставить в терминале", "終端機貼上", "端末に貼り付け", "터미널 붙여넣기", "Im Terminal einfügen", "Coller dans le terminal", "Pegar en terminal", "Colar no terminal"],
"编辑": ["编辑", "Edit", "Изменить", "編輯", "編集", "편집", "Bearbeiten", "Modifier", "Editar", "Editar"],
"编辑快捷键": ["编辑快捷键", "Edit shortcut", "Изменить сочетание", "編輯快速鍵", "ショートカットを編集", "단축키 편집", "Tastenkürzel bearbeiten", "Modifier le raccourci", "Editar atajo", "Editar atalho"],
"聚焦搜索": ["聚焦搜索", "Spotlight search", "Поиск Spotlight", "Spotlight 搜尋", "Spotlight検索", "Spotlight 검색", "Spotlight-Suche", "Recherche Spotlight", "Búsqueda de Spotlight", "Busca do Spotlight"],
"请使用系统预设中的安全选项": ["请使用系统预设中的安全选项", "Use Security options in the system presets", "Используйте параметры безопасности из системных наборов", "請使用系統預設中的安全性選項", "システムプリセットのセキュリティオプションを使用してください", "시스템 프리셋의 보안 옵션을 사용하세요", "Sicherheitsoptionen in den Systemvorgaben verwenden", "Utilisez les options de sécurité des préréglages système", "Usa Opciones de seguridad en los ajustes del sistema", "Use Opções de segurança nas predefinições do sistema"],
"请先选词或取消组词": ["请先选词或取消组词", "Confirm or cancel the current composition first", "Сначала подтвердите или отмените текущий ввод", "請先選字或取消組字", "先に変換を確定またはキャンセルしてください", "먼저 조합 중인 문자를 확정하거나 취소하세요", "Zuerst die aktuelle Texteingabe bestätigen oder abbrechen", "Validez ou annulez d’abord la composition en cours", "Confirma o cancela primero la composición actual", "Confirme ou cancele primeiro a composição atual"],
"请输入有效的快捷键名称": ["请输入有效的快捷键名称", "Enter a shortcut name (1–64 characters)", "Введите название от 1 до 64 символов", "請輸入有效的快速鍵名稱", "ショートカット名を1〜64文字で入力してください", "단축키 이름을 1~64자로 입력하세요", "Namen mit 1–64 Zeichen eingeben", "Saisissez un nom de 1 à 64 caractères", "Introduce un nombre de 1 a 64 caracteres", "Insira um nome de 1 a 64 caracteres"],
"请选择有效的按键": ["请选择有效的按键", "Select a valid key", "Выберите допустимую клавишу", "請選擇有效的按鍵", "有効なキーを選択してください", "유효한 키를 선택하세요", "Eine gültige Taste auswählen", "Sélectionnez une touche valide", "Selecciona una tecla válida", "Selecione uma tecla válida"],
"运行窗口": ["运行窗口", "Run dialog", "Окно запуска", "執行視窗", "実行ダイアログ", "실행 대화상자", "Ausführen-Dialog", "Fenêtre Exécuter", "Diálogo Ejecutar", "Janela Executar"],
"还没有自定义快捷键": ["还没有自定义快捷键", "No custom shortcuts yet", "Своих сочетаний пока нет", "尚無自訂快速鍵", "カスタムショートカットはまだありません", "아직 사용자 지정 단축키가 없습니다", "Noch keine eigenen Tastenkürzel", "Aucun raccourci personnalisé", "Aún no hay atajos personalizados", "Ainda não há atalhos personalizados"],
"适用远端": ["适用远端", "Remote platform", "Удалённая система", "適用遠端", "対象リモートOS", "대상 원격 플랫폼", "Remote-Plattform", "Système distant", "Sistema remoto", "Sistema remoto"],
"选择桌面方案": ["选择桌面方案", "Choose desktop environment", "Выберите окружение рабочего стола", "選擇桌面環境", "デスクトップ環境を選択", "데스크톱 환경 선택", "Desktop-Umgebung auswählen", "Choisir l’environnement de bureau", "Elegir entorno de escritorio", "Escolher ambiente de trabalho"],
"选择桌面方案以加载系统预设": ["选择桌面方案以加载系统预设", "Choose a desktop environment to load system presets", "Выберите окружение, чтобы загрузить системные наборы", "選擇桌面環境以載入系統預設", "デスクトップ環境を選択してプリセットを読み込みます", "데스크톱 환경을 선택하여 시스템 프리셋을 불러오세요", "Desktop-Umgebung wählen, um Systemvorgaben zu laden", "Choisissez un environnement pour charger les préréglages système", "Elige un entorno para cargar los ajustes del sistema", "Escolha um ambiente para carregar as predefinições do sistema"],
"键盘上方常用栏": ["键盘上方常用栏", "Favorites above keyboard", "Избранное над клавиатурой", "鍵盤上方常用列", "キーボード上のお気に入り", "키보드 위 즐겨찾기 모음", "Favoriten über der Tastatur", "Favoris au-dessus du clavier", "Favoritos sobre el teclado", "Favoritos acima do teclado"],
};
@@ -12,6 +12,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 {
@@ -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();
@@ -0,0 +1,163 @@
import { tr } from './Localization';
import { MobileTheme, MobileSegment } from './MobileWidgets';
import { RemoteShortcut, RemoteShortcuts } from './RemoteShortcuts';
@Component
export struct RemoteShortcutPanel {
store?: RemoteShortcuts;
@Prop @Watch('reload') platform: string = 'unknown';
@Prop @Watch('reload') revision: number = 0;
send: (item: RemoteShortcut) => void = () => {};
manage: () => void = () => {};
changed: () => void = () => {};
@State private group: number = 0;
@State private page: number = 0;
@State private desktop: string = '';
@State private items: RemoteShortcut[] = [];
aboutToAppear(): void { this.reload(); }
private reload(): void { this.desktop = this.store?.desktop() ?? ''; this.items = this.store?.items(this.platform) ?? []; this.page = 0; }
private filtered(): RemoteShortcut[] { return this.items.filter((item: RemoteShortcut) => item.group === ['common', 'keys', 'system', 'mine'][this.group]); }
build() {
Column({ space: 6 }) {
MobileSegment({ labels: [tr('常用'), tr('按键'), tr('系统'), tr('我的')], selection: this.group,
changed: (index: number) => { this.group = index; this.page = 0; } })
if (this.group === 2 && this.platform === 'linux') {
Select([{ value: tr('选择桌面方案') }, { value: 'GNOME' }, { value: 'KDE Plasma' }])
.selected(this.desktop === 'gnome' ? 1 : this.desktop === 'kde' ? 2 : 0)
.value(this.desktop === 'gnome' ? 'GNOME' : this.desktop === 'kde' ? 'KDE Plasma' : tr('选择桌面方案'))
.font({ size: 12 }).height(32).onSelect((index: number) => { this.store?.setDesktop(['', 'gnome', 'kde'][index]); this.reload(); this.changed(); })
}
Scroll() {
Column({ space: 4 }) {
if (!this.filtered().length) {
Text(tr(this.platform === 'unknown' ? '等待远端平台信息' : this.group === 2 && this.platform === 'linux' ? '选择桌面方案以加载系统预设' : '还没有自定义快捷键'))
.fontSize(12).fontColor(MobileTheme.secondary).padding(12)
}
GridRow({ columns: 3, gutter: 4 }) {
ForEach(this.filtered().slice(this.page * 6, this.page * 6 + 6), (item: RemoteShortcut) => {
GridCol() {
Column({ space: 3 }) {
Text(item.title()).fontSize(12).maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis })
Text(item.chord()).fontSize(10).fontColor(MobileTheme.secondary).maxLines(1)
}.width('100%').height(48).justifyContent(FlexAlign.Center).borderRadius(7).backgroundColor(Color.White)
.accessibilityLevel('yes').accessibilityText(`${item.title()}, ${item.chord()}`).onClick(() => this.send(item))
}
}, (item: RemoteShortcut) => item.id)
}
}.width('100%')
}.width('100%').layoutWeight(1)
Row() {
Button('‹').width(40).height(36).enabled(this.page > 0).accessibilityText(tr('上一页')).onClick(() => this.page--)
Text(`${this.page + 1}/${Math.max(1, Math.ceil(this.filtered().length / 6))}`).fontSize(11).width(38).textAlign(TextAlign.Center)
Button('›').width(40).height(36).enabled((this.page + 1) * 6 < this.filtered().length).accessibilityText(tr('下一页')).onClick(() => this.page++)
Blank()
Button(tr('管理与自定义')).fontSize(12).height(36).enabled(this.platform !== 'unknown').onClick(this.manage)
}.width('100%')
}.width('100%').height('100%').padding(3)
}
}
@Component
export struct RemoteShortcutManager {
store?: RemoteShortcuts;
platform: string = 'unknown';
close: () => void = () => {};
changed: () => void = () => {};
@State private items: RemoteShortcut[] = [];
@State private pinned: RemoteShortcut[] = [];
@State private editing: boolean = false;
@State private itemId: string = '';
@State private name: string = '';
@State private target: string = 'windows';
@State private mainKey: number = 67;
@State private modifiers: number[] = [];
@State private pin: boolean = true;
@State private error: string = '';
aboutToAppear(): void { this.reload(); }
private reload(): void { this.items = this.store?.items(this.platform) ?? []; this.pinned = this.store?.pinned(this.platform) ?? []; this.changed(); }
private edit(item?: RemoteShortcut): void {
this.itemId = item?.id ?? ''; this.name = item?.name ?? ''; this.target = item?.platform ?? this.platform;
this.mainKey = item?.key ?? 67; this.modifiers = item?.modifiers.slice() ?? [this.target === 'macos' ? 91 : 17];
this.pin = item ? (this.store?.pins(item.platform).includes(item.id) ?? false) : (this.store?.pins(this.target).length ?? 0) < 8;
this.error = ''; this.editing = true;
}
private setPinned(item: RemoteShortcut, add: boolean): void {
const values = this.store?.pins(this.platform) ?? [];
if (add && values.length >= 8) { this.error = tr('最多固定 8 个快捷键'); return; }
this.store?.setPins(this.platform, add ? values.concat([item.id]) : values.filter((id: string) => id !== item.id)); this.error = ''; this.reload();
}
private moveUp(item: RemoteShortcut): void {
const values = this.store?.pins(this.platform) ?? [], index = values.indexOf(item.id);
if (index > 0) { values.splice(index, 1); values.splice(index - 1, 0, item.id); this.store?.setPins(this.platform, values); this.reload(); }
}
private save(): void {
this.error = this.store?.save(this.itemId, this.name, this.target, this.mainKey, this.modifiers, this.pin) ?? '';
if (!this.error) { this.editing = false; this.reload(); }
}
@Builder
Item(item: RemoteShortcut, pinned: boolean) {
Column({ space: 4 }) {
Row() {
Text(item.title()).fontSize(15).fontColor(MobileTheme.ink).layoutWeight(1).maxLines(2)
Text(item.chord()).fontSize(12).fontColor(MobileTheme.secondary).maxLines(2)
}.width('100%')
Row({ space: 6 }) {
if (pinned) {
Button(tr('向上移动')).fontSize(12).height(36).enabled(this.pinned[0]?.id !== item.id).onClick(() => this.moveUp(item))
Button(tr('移除')).fontSize(12).height(36).onClick(() => this.setPinned(item, false))
} else {
Button(tr('添加到常用栏')).fontSize(12).height(36).enabled(!this.pinned.some((value: RemoteShortcut) => value.id === item.id)).onClick(() => this.setPinned(item, true))
if (item.custom) {
Button(tr('编辑')).fontSize(12).height(36).onClick(() => this.edit(item))
Button(tr('删除')).fontSize(12).height(36).backgroundColor(MobileTheme.red).onClick(() => { this.store?.remove(item.id); this.reload(); })
}
}
}.width('100%')
}.padding(12).width('100%').borderRadius(10).backgroundColor(Color.White)
}
build() {
Column({ space: 12 }) {
Row() {
Button(tr(this.editing ? '取消' : '完成')).fontSize(14).height(44).onClick(() => { if (this.editing) this.editing = false; else this.close(); })
Text(tr(this.editing ? this.itemId ? '编辑快捷键' : '新建快捷键' : '管理快捷键')).fontSize(17).fontWeight(600).layoutWeight(1).textAlign(TextAlign.Center)
Button(tr(this.editing ? '保存' : '新建')).fontSize(14).height(44).onClick(() => { if (this.editing) this.save(); else this.edit(); })
}.width('100%')
Text(tr('本地编辑,输入不会发送到远端')).fontSize(12).fontColor(MobileTheme.secondary)
if (this.error) { Text(this.error).fontSize(13).fontColor(MobileTheme.red) }
Scroll() {
Column({ space: 12 }) {
if (this.editing) {
TextInput({ placeholder: tr('名称'), text: this.name }).maxLength(64).onChange((value: string) => this.name = value)
Text(tr('适用远端')).width('100%').fontSize(13)
Select(RemoteShortcuts.platforms.map((os: string): SelectOption => ({ value: RemoteShortcuts.platformName(os) })))
.selected(RemoteShortcuts.platforms.indexOf(this.target)).value(RemoteShortcuts.platformName(this.target))
.width('100%').onSelect((index: number) => this.target = RemoteShortcuts.platforms[index])
Text(tr('主按键')).width('100%').fontSize(13)
Select(RemoteShortcuts.keys().map((key: number): SelectOption => ({ value: RemoteShortcuts.keyName(key, this.target) })))
.selected(RemoteShortcuts.keys().indexOf(this.mainKey)).value(RemoteShortcuts.keyName(this.mainKey, this.target))
.width('100%').onSelect((index: number) => this.mainKey = RemoteShortcuts.keys()[index])
Text(tr('修饰键')).width('100%').fontSize(13)
Row({ space: 6 }) {
ForEach(RemoteShortcuts.modifiers, (key: number) => {
Button(RemoteShortcuts.modifierName(key, this.target)).layoutWeight(1).height(44)
.backgroundColor(this.modifiers.includes(key) ? MobileTheme.blue : MobileTheme.secondary)
.accessibilitySelected(this.modifiers.includes(key)).onClick(() => this.modifiers = this.modifiers.includes(key) ? this.modifiers.filter((value: number) => value !== key) : RemoteShortcuts.normalize(this.modifiers.concat([key])))
}, (key: number) => `${key}-${this.target}`)
}.width('100%')
Text(`${tr('发送组合')}: ${new RemoteShortcut('', '', this.target, 'mine', this.mainKey, this.modifiers).chord()}`).fontSize(16).padding(12)
Row() {
Text(tr('固定到键盘上方')).layoutWeight(1).fontSize(14)
Toggle({ type: ToggleType.Switch, isOn: this.pin }).onChange((value: boolean) => this.pin = value)
}.width('100%')
} else {
Text(`${RemoteShortcuts.platformName(this.platform)} · ${tr('键盘上方常用栏')} (${this.pinned.length}/8)`).fontSize(14).width('100%')
ForEach(this.pinned, (item: RemoteShortcut) => { this.Item(item, true) }, (item: RemoteShortcut) => item.id)
Text(tr('快捷键')).fontSize(14).width('100%')
ForEach(this.items, (item: RemoteShortcut) => { this.Item(item, false) }, (item: RemoteShortcut) => item.id)
}
}.width('100%').padding({ bottom: 24 })
}.layoutWeight(1).width('100%')
}.padding(16).width('100%').height('100%').backgroundColor(MobileTheme.background)
.onKeyEvent((event: KeyEvent) => { if (event.keyCode === 2 && event.type === KeyType.Down) { if (this.editing) this.editing = false; else this.close(); return true; } return false; })
}
}
@@ -0,0 +1,110 @@
import { MobileStore } from './MobileStore';
import { tr } from './Localization';
export class RemoteShortcut {
id: string; name: string; platform: string; group: string; key: number; modifiers: number[];
service: boolean; custom: boolean;
constructor(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = [], service: boolean = false, custom: boolean = false) {
this.id = id; this.name = name; this.platform = platform; this.group = group; this.key = key;
this.modifiers = modifiers.slice(); this.service = service; this.custom = custom;
}
title(): string { return this.custom ? this.name : tr(this.name); }
chord(): string { return this.modifiers.map((key: number) => RemoteShortcuts.modifierName(key, this.platform)).concat([RemoteShortcuts.keyName(this.key, this.platform)]).join(' + '); }
}
export class RemoteShortcuts {
static readonly platforms: string[] = ['windows', 'macos', 'linux'];
static readonly modifiers: number[] = [17, 16, 18, 91];
private store: MobileStore;
private prefix: string = 'remote_shortcuts_v1:';
constructor(store: MobileStore) { this.store = store; }
static platform(value: string): string { return RemoteShortcuts.platforms.includes(value) ? value : 'unknown'; }
static platformName(value: string): string { return value === 'windows' ? 'Windows' : value === 'macos' ? 'macOS' : value === 'linux' ? 'Linux' : tr('未知'); }
static modifierName(key: number, platform: string): string {
return key === 17 ? 'Ctrl' : key === 16 ? 'Shift' : key === 18 ? (platform === 'macos' ? 'Option' : 'Alt') : key === 91 ? (platform === 'macos' ? 'Cmd' : platform === 'linux' ? 'Super' : 'Win') : '';
}
static keyName(key: number, platform: string): string {
if (key >= 65 && key <= 90 || key >= 48 && key <= 57) return String.fromCharCode(key);
if (key >= 112 && key <= 123) return `F${key - 111}`;
switch (key) {
case 8: return platform === 'macos' ? 'Delete ⌫' : 'Backspace'; case 9: return 'Tab'; case 13: return 'Enter';
case 27: return 'Esc'; case 32: return 'Space'; case 33: return 'PageUp'; case 34: return 'PageDown';
case 35: return 'End'; case 36: return 'Home'; case 37: return '←'; case 38: return '↑'; case 39: return '→';
case 40: return '↓'; case 45: return 'Insert'; case 46: return platform === 'macos' ? 'Delete ⌦' : 'Delete'; default: return '';
}
}
static keys(): number[] {
const keys: number[] = []; for (let i = 65; i <= 90; i++) keys.push(i); for (let i = 48; i <= 57; i++) keys.push(i);
for (let i = 112; i <= 123; i++) keys.push(i);
return keys.concat([27, 9, 13, 32, 8, 46, 45, 36, 35, 33, 34, 37, 38, 39, 40]);
}
static normalize(values: number[]): number[] { return RemoteShortcuts.modifiers.filter((key: number) => values.includes(key)); }
desktop(): string { const value = this.store.text(this.prefix + 'linux_desktop'); return ['gnome', 'kde'].includes(value) ? value : ''; }
setDesktop(value: string): void { this.store.put(this.prefix + 'linux_desktop', ['gnome', 'kde'].includes(value) ? value : ''); }
private preset(id: string, name: string, platform: string, group: string, key: number, modifiers: number[] = []): RemoteShortcut { return new RemoteShortcut(id, name, platform, group, key, modifiers); }
items(remote: string): RemoteShortcut[] {
const os = RemoteShortcuts.platform(remote); let result: RemoteShortcut[] = [];
if (os !== 'unknown') {
const ids = ['copy', 'paste', 'cut', 'all', 'undo', 'save'], names = ['复制', '粘贴', '剪切', '全选', '撤销', '保存'], keys = [67, 86, 88, 65, 90, 83];
for (let i = 0; i < ids.length; i++) result.push(this.preset(ids[i], names[i], os, 'common', keys[i], [os === 'macos' ? 91 : 17]));
if (os === 'linux') result = result.concat([this.preset('term-copy', '终端复制', os, 'common', 67, [17, 16]), this.preset('term-paste', '终端粘贴', os, 'common', 86, [17, 16])]);
}
const keys = [27, 9, 13, 8, 46, 36, 35, 33, 34, 37, 38, 39, 40]; for (let i = 112; i <= 123; i++) keys.push(i);
keys.forEach((key: number) => result.push(this.preset(key === 27 ? 'esc' : key === 9 ? 'tab' : `key-${key}`, RemoteShortcuts.keyName(key, os), os, 'keys', key)));
if (os === 'windows') {
result = result.concat([this.preset('switch', '切换窗口', os, 'system', 9, [18]), this.preset('desktop', '显示桌面', os, 'system', 68, [91]), this.preset('task', '任务管理器', os, 'system', 27, [17, 16]), new RemoteShortcut('sas', '安全选项', os, 'system', 46, [17, 18], true)]);
} else if (os === 'macos') {
result = result.concat([this.preset('switch', '切换应用', os, 'system', 9, [91]), this.preset('search', '聚焦搜索', os, 'system', 32, [91]), this.preset('forcequit', '强制退出面板', os, 'system', 27, [18, 91])]);
} else if (os === 'linux' && this.desktop()) {
result.push(this.preset('switch', this.desktop() === 'gnome' ? '切换应用' : '切换窗口', os, 'system', 9, [this.desktop() === 'gnome' ? 91 : 18]));
result.push(this.preset('run', '运行窗口', os, 'system', 113, [18]));
result.push(this.desktop() === 'gnome' ? this.preset('apps', '应用列表', os, 'system', 65, [91]) : this.preset('window-menu', '窗口菜单', os, 'system', 114, [18]));
}
return result.concat(this.custom().filter((item: RemoteShortcut) => item.platform === os));
}
custom(): RemoteShortcut[] {
try {
const raw = JSON.parse(this.store.text(this.prefix + 'custom', '[]')) as RemoteShortcut[];
if (!Array.isArray(raw)) return [];
const seen: string[] = [], result: RemoteShortcut[] = [];
raw.forEach((item: RemoteShortcut) => {
if (!item || typeof item.id !== 'string' || !item.id.startsWith('custom-') || seen.includes(item.id) || typeof item.name !== 'string' || !item.name.trim() || item.name.length > 64 || !RemoteShortcuts.platforms.includes(item.platform) || !RemoteShortcuts.keys().includes(item.key) || !Array.isArray(item.modifiers) || item.modifiers.length > 4 || !item.modifiers.every((key: number) => RemoteShortcuts.modifiers.includes(key))) return;
if (item.platform === 'windows' && item.key === 46 && item.modifiers.includes(17) && item.modifiers.includes(18)) return;
seen.push(item.id); result.push(new RemoteShortcut(item.id, item.name, item.platform, 'mine', item.key, RemoteShortcuts.normalize(item.modifiers), false, true));
});
return result.slice(0, 16);
} catch { return []; }
}
pins(platform: string): string[] {
const raw = this.store.text(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), '');
const available = this.items(platform).map((item: RemoteShortcut) => item.id);
let values: string[] = ['esc', 'tab', 'copy', 'paste'];
try { const parsed = JSON.parse(raw) as string[]; if (Array.isArray(parsed)) values = parsed; } catch {}
return values.filter((id: string, index: number) => typeof id === 'string' && available.includes(id) && values.indexOf(id) === index).slice(0, 8);
}
setPins(platform: string, values: string[]): void { this.store.put(this.prefix + 'pins_' + RemoteShortcuts.platform(platform), JSON.stringify(values.slice(0, 8))); }
pinned(platform: string): RemoteShortcut[] {
const items = this.items(platform), result: RemoteShortcut[] = [];
this.pins(platform).forEach((id: string) => { const item = items.find((value: RemoteShortcut) => value.id === id); if (item) result.push(item); });
return result;
}
remove(id: string): void {
this.store.put(this.prefix + 'custom', JSON.stringify(this.custom().filter((item: RemoteShortcut) => item.id !== id)));
RemoteShortcuts.platforms.forEach((platform: string) => this.setPins(platform, this.pins(platform).filter((value: string) => value !== id)));
}
save(id: string, name: string, platform: string, key: number, modifiers: number[], pin: boolean): string {
name = name.trim(); if (!name || name.length > 64) return tr('请输入有效的快捷键名称');
if (!RemoteShortcuts.platforms.includes(platform) || !RemoteShortcuts.keys().includes(key)) return tr('请选择有效的按键');
const normalized = RemoteShortcuts.normalize(modifiers);
if (platform === 'windows' && key === 46 && normalized.includes(17) && normalized.includes(18)) return tr('请使用系统预设中的安全选项');
const old = this.custom();
if (old.some((item: RemoteShortcut) => item.id !== id && item.platform === platform && item.key === key && item.modifiers.join(',') === normalized.join(','))) return tr('已有相同组合的快捷键');
if (!old.some((item: RemoteShortcut) => item.id === id) && old.length >= 16) return tr('最多保存 16 个自定义快捷键');
if (pin && !this.pins(platform).includes(id) && this.pins(platform).length >= 8) return tr('最多固定 8 个快捷键');
const itemId = id || `custom-${Date.now()}-${Math.floor(Math.random() * 1000000000)}`;
const values = old.filter((item: RemoteShortcut) => item.id !== itemId); values.push(new RemoteShortcut(itemId, name, platform, 'mine', key, normalized, false, true));
this.store.put(this.prefix + 'custom', JSON.stringify(values));
RemoteShortcuts.platforms.forEach((target: string) => { const list = this.pins(target), index = list.indexOf(itemId), selected = list.filter((value: string) => value !== itemId); if (target === platform && pin) selected.splice(index >= 0 ? Math.min(index, selected.length) : selected.length, 0, itemId); this.setPins(target, selected); });
return '';
}
}