dsh-plugin-mobile-link 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -12
- package/cordis.patch.yml +5 -0
- package/lib/cli.js +11 -13
- package/lib/client.js +1453 -91
- package/lib/dlp.js +15 -0
- package/lib/enroll.js +249 -35
- package/lib/handoff.js +1 -1
- package/lib/hello.js +1 -1
- package/lib/history/fold-summary.js +85 -32
- package/lib/history/fold.js +11 -1
- package/lib/history/question-record.js +186 -0
- package/lib/i18n.js +177 -0
- package/lib/image-originals.js +121 -0
- package/lib/image-variants.js +315 -0
- package/lib/index.js +59 -2
- package/lib/journal.js +2 -0
- package/lib/link.js +274 -17
- package/lib/router.js +70 -8
- package/lib/routes.js +147 -0
- package/lib/session-digest.js +24 -10
- package/lib/session-scope.js +89 -0
- package/lib/state.js +3 -3
- package/lib/status.js +42 -11
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
;(() => {
|
|
11
11
|
/** 路由用相对路径:宿主把插件路由挂在同一个源上。 */
|
|
12
12
|
const STATUS_ROUTE = 'mobile-link/status'
|
|
13
|
+
const ENROLL_ROUTE = 'mobile-link/enroll'
|
|
14
|
+
const DEVICES_ROUTE = 'mobile-link/devices'
|
|
15
|
+
const REVOKE_ROUTE = 'mobile-link/devices/revoke'
|
|
13
16
|
const PAIR_ROUTE = 'mobile-link/pair-code'
|
|
14
17
|
const REGENERATE_ROUTE = 'mobile-link/pair-code/regenerate'
|
|
15
18
|
const SLOT_ENTRY = 'sidebar.footer.action'
|
|
@@ -17,11 +20,871 @@ const SLOT_OVERLAY = 'shell.overlay'
|
|
|
17
20
|
const ENTRY_ID = 'mobile-link-entry'
|
|
18
21
|
const PANEL_ID = 'mobile-link-panel'
|
|
19
22
|
const STYLE_ID = 'dsh-mobile-link-style'
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
/**
|
|
24
|
+
* UI language: Chinese is the source language; English and Spanish come from the tables
|
|
25
|
+
* below, picked from `navigator.language`, and an untranslated sentence falls back to the
|
|
26
|
+
* source text. Inlined here because the client half is a hand-written bundle that only
|
|
27
|
+
* requires platform seeds.
|
|
28
|
+
*/
|
|
29
|
+
const MESSAGES = {
|
|
30
|
+
en: {
|
|
31
|
+
'(已配对 ${paired}/${max})': '(paired ${paired}/${max})',
|
|
32
|
+
'移动端连接': 'Mobile link',
|
|
33
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'Mobile link: QR code, paired phones and link status',
|
|
34
|
+
'此电脑尚未登记': 'This computer is not registered',
|
|
35
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'This computer is not registered. Register it from the command line:',
|
|
36
|
+
'登记此电脑以连接中转服务器。': 'Register this computer to connect to the relay.',
|
|
37
|
+
'此电脑已登记。': 'This computer is registered.',
|
|
38
|
+
'中转地址': 'Relay address',
|
|
39
|
+
'中转服务器': 'Relay',
|
|
40
|
+
'已预填,可修改。': 'Pre-filled; editable.',
|
|
41
|
+
'由配置指定,不可修改。': 'Set by the configuration; not editable.',
|
|
42
|
+
'请输入中转地址。': 'Enter the relay address.',
|
|
43
|
+
'中转地址无效。': 'The relay address is not valid.',
|
|
44
|
+
'请输入有效的中转地址。': 'Enter a valid relay address.',
|
|
45
|
+
'尝试次数过多,请稍后重试。': 'Too many attempts; try again later.',
|
|
46
|
+
'中转服务器未响应,请稍后重试。': 'The relay did not respond; try again later.',
|
|
47
|
+
'中转服务器登记失败,请稍后重试。': 'The relay refused the registration; try again later.',
|
|
48
|
+
'中转服务器返回了无法识别的响应。': 'The relay returned an unknown response.',
|
|
49
|
+
'连接器正在停止,请稍后重试。': 'The connector is stopping; try again later.',
|
|
50
|
+
'连接器内部错误,请稍后重试。': 'Internal connector error; try again later.',
|
|
51
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': 'The registration could not be saved; check the disk permissions.',
|
|
52
|
+
'登记后将自动连接。': 'It connects automatically once registered.',
|
|
53
|
+
'登记失败,请重试。': 'Registration failed; try again.',
|
|
54
|
+
'正在登记…': 'Registering…',
|
|
55
|
+
'正在获取…': 'Fetching…',
|
|
56
|
+
'正在读取状态…': 'Reading status…',
|
|
57
|
+
'正在连接…': 'Connecting…',
|
|
58
|
+
'登记': 'Register',
|
|
59
|
+
'重试': 'Retry',
|
|
60
|
+
'无法读取连接状态。': 'Could not read the link status.',
|
|
61
|
+
'无法连接中转服务器,请检查地址与网络。': 'Could not reach the relay; check the address and the network.',
|
|
62
|
+
'状态未知': 'Unknown state',
|
|
63
|
+
'已连接': 'Connected',
|
|
64
|
+
'在线': 'online',
|
|
65
|
+
'离线': 'offline',
|
|
66
|
+
'未连接,正在重新连接': 'Not connected, reconnecting',
|
|
67
|
+
'连接器已停用': 'Connector disabled',
|
|
68
|
+
'连接器已停用。': 'Connector disabled.',
|
|
69
|
+
'连接器已停用。请在插件配置中启用后重试。': 'Connector disabled. Enable it in the plugin configuration and retry.',
|
|
70
|
+
'配对二维码': 'Pairing QR code',
|
|
71
|
+
'获取配对码': 'Get pairing code',
|
|
72
|
+
'暂无配对码。': 'No pairing code yet.',
|
|
73
|
+
'复制配对码': 'Copy pairing code',
|
|
74
|
+
'已复制': 'Copied',
|
|
75
|
+
'重新生成': 'Regenerate',
|
|
76
|
+
'长期有效直到重新生成': 'Valid until regenerated',
|
|
77
|
+
'已配对设备': 'Paired devices',
|
|
78
|
+
'暂无已配对设备': 'No paired devices',
|
|
79
|
+
'暂无已配对设备。': 'No paired devices.',
|
|
80
|
+
'无法读取全部已配对设备。': 'Could not read the full device list.',
|
|
81
|
+
'撤销中…': 'Revoking…',
|
|
82
|
+
'撤销失败,请稍后重试。': 'Revocation failed; try again later.',
|
|
83
|
+
'撤销设备': 'Revoke device',
|
|
84
|
+
'撤销这台设备': 'Revoke this device',
|
|
85
|
+
'撤销这台设备?': 'Revoke this device?',
|
|
86
|
+
'这台设备不属于此电脑。': 'This device does not belong to this computer.',
|
|
87
|
+
'设备不存在或已被撤销。': 'The device does not exist or was already revoked.',
|
|
88
|
+
'撤销': 'Revoke',
|
|
89
|
+
'这台设备': 'this device',
|
|
90
|
+
'设备': 'Device',
|
|
91
|
+
'未命名设备': 'Unnamed device',
|
|
92
|
+
'关闭': 'Close',
|
|
93
|
+
'取消': 'Cancel',
|
|
94
|
+
'完成': 'Done',
|
|
95
|
+
'尚未安装 iOS App?': 'No iOS app yet?',
|
|
96
|
+
'安装 DSH Mobile': 'Install DSH Mobile',
|
|
97
|
+
'查看安装方式': 'See how to install',
|
|
98
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'Open DSH Mobile and scan the QR code on this computer.',
|
|
99
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'Install it on iPhone or iPad with TestFlight.',
|
|
100
|
+
'从 App Store 安装 TestFlight。': 'Install TestFlight from the App Store.',
|
|
101
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'Open the TestFlight invite link and install DSH Mobile.',
|
|
102
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'Requires iOS 17, iPadOS 17 or later.',
|
|
103
|
+
'命令行登记(适用于无界面环境)': 'Command-line registration (headless)',
|
|
104
|
+
'上次错误': 'Last error',
|
|
105
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
106
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
107
|
+
'GitHub 仓库': 'GitHub repository',
|
|
108
|
+
'欢迎在 GitHub 上为项目加星标。': 'If it helps you, star the project on GitHub.',
|
|
109
|
+
'官方原子组件不可用,移动端连接入口未注册': 'Official primitives unavailable; the entry was not registered',
|
|
110
|
+
'连不上本机连接器:${error}': 'Cannot reach the local connector: ${error}',
|
|
111
|
+
'面板状态层没起来:${error}': 'The status layer did not come up: ${error}',
|
|
112
|
+
'小窗没注册上:${error}': 'The panel was not registered: ${error}',
|
|
113
|
+
'侧栏入口没注册上:${error}': 'The sidebar entry was not registered: ${error}',
|
|
114
|
+
'连接器应答 HTTP ${status}': 'The connector answered HTTP ${status}',
|
|
115
|
+
'连接器应答无法解析(HTTP ${status})': 'Unreadable connector response (HTTP ${status})',
|
|
116
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '“${name}” will no longer connect. Scan the code again to pair it.',
|
|
117
|
+
'${online} 台在线': '${online} online',
|
|
118
|
+
'${total} 台在线': '${total} online',
|
|
119
|
+
'${online} 台在线,共 ${total} 台': '${online} of ${total} online',
|
|
120
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} of ${total} paired online',
|
|
121
|
+
'有效期至 ${hasta}(剩余 ${resto})': 'Valid until ${hasta} (${resto} left)',
|
|
122
|
+
},
|
|
123
|
+
es: {
|
|
124
|
+
'(已配对 ${paired}/${max})': '(emparejados ${paired}/${max})',
|
|
125
|
+
'移动端连接': 'Conexión móvil',
|
|
126
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'Conexión móvil: código QR, teléfonos emparejados y estado',
|
|
127
|
+
'此电脑尚未登记': 'Este equipo no está registrado',
|
|
128
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'Este equipo no está registrado. Regístralo desde la línea de comandos:',
|
|
129
|
+
'登记此电脑以连接中转服务器。': 'Registra este equipo para conectarlo al relay.',
|
|
130
|
+
'此电脑已登记。': 'Este equipo ya está registrado.',
|
|
131
|
+
'中转地址': 'Dirección del relay',
|
|
132
|
+
'中转服务器': 'Relay',
|
|
133
|
+
'已预填,可修改。': 'Viene rellenada; se puede editar.',
|
|
134
|
+
'由配置指定,不可修改。': 'Fijada por la configuración; no se puede editar.',
|
|
135
|
+
'请输入中转地址。': 'Introduce la dirección del relay.',
|
|
136
|
+
'中转地址无效。': 'La dirección del relay no es válida.',
|
|
137
|
+
'请输入有效的中转地址。': 'Introduce una dirección de relay válida.',
|
|
138
|
+
'尝试次数过多,请稍后重试。': 'Demasiados intentos; vuelve a intentarlo más tarde.',
|
|
139
|
+
'中转服务器未响应,请稍后重试。': 'El relay no respondió; vuelve a intentarlo más tarde.',
|
|
140
|
+
'中转服务器登记失败,请稍后重试。': 'El relay rechazó el registro; vuelve a intentarlo más tarde.',
|
|
141
|
+
'中转服务器返回了无法识别的响应。': 'El relay devolvió una respuesta desconocida.',
|
|
142
|
+
'连接器正在停止,请稍后重试。': 'El conector se está deteniendo; vuelve a intentarlo más tarde.',
|
|
143
|
+
'连接器内部错误,请稍后重试。': 'Error interno del conector; vuelve a intentarlo más tarde.',
|
|
144
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': 'No se pudo guardar el registro; revisa los permisos del disco.',
|
|
145
|
+
'登记后将自动连接。': 'Se conecta solo al registrarlo.',
|
|
146
|
+
'登记失败,请重试。': 'Falló el registro; vuelve a intentarlo.',
|
|
147
|
+
'正在登记…': 'Registrando…',
|
|
148
|
+
'正在获取…': 'Obteniendo…',
|
|
149
|
+
'正在读取状态…': 'Leyendo el estado…',
|
|
150
|
+
'正在连接…': 'Conectando…',
|
|
151
|
+
'登记': 'Registrar',
|
|
152
|
+
'重试': 'Reintentar',
|
|
153
|
+
'无法读取连接状态。': 'No se pudo leer el estado de la conexión.',
|
|
154
|
+
'无法连接中转服务器,请检查地址与网络。': 'No se pudo conectar al relay; revisa la dirección y la red.',
|
|
155
|
+
'状态未知': 'Estado desconocido',
|
|
156
|
+
'已连接': 'Conectado',
|
|
157
|
+
'在线': 'en línea',
|
|
158
|
+
'离线': 'sin conexión',
|
|
159
|
+
'未连接,正在重新连接': 'Sin conexión, reconectando',
|
|
160
|
+
'连接器已停用': 'Conector desactivado',
|
|
161
|
+
'连接器已停用。': 'Conector desactivado.',
|
|
162
|
+
'连接器已停用。请在插件配置中启用后重试。': 'Conector desactivado. Actívalo en la configuración del plugin y reintenta.',
|
|
163
|
+
'配对二维码': 'Código QR de emparejamiento',
|
|
164
|
+
'获取配对码': 'Obtener código de emparejamiento',
|
|
165
|
+
'暂无配对码。': 'Todavía no hay código de emparejamiento.',
|
|
166
|
+
'复制配对码': 'Copiar código de emparejamiento',
|
|
167
|
+
'已复制': 'Copiado',
|
|
168
|
+
'重新生成': 'Regenerar',
|
|
169
|
+
'长期有效直到重新生成': 'Válido hasta que se regenere',
|
|
170
|
+
'已配对设备': 'Dispositivos emparejados',
|
|
171
|
+
'暂无已配对设备': 'Sin dispositivos emparejados',
|
|
172
|
+
'暂无已配对设备。': 'Sin dispositivos emparejados.',
|
|
173
|
+
'无法读取全部已配对设备。': 'No se pudo leer la lista completa de dispositivos.',
|
|
174
|
+
'撤销中…': 'Revocando…',
|
|
175
|
+
'撤销失败,请稍后重试。': 'Falló la revocación; vuelve a intentarlo más tarde.',
|
|
176
|
+
'撤销设备': 'Revocar dispositivo',
|
|
177
|
+
'撤销这台设备': 'Revocar este dispositivo',
|
|
178
|
+
'撤销这台设备?': '¿Revocar este dispositivo?',
|
|
179
|
+
'这台设备不属于此电脑。': 'Este dispositivo no pertenece a este equipo.',
|
|
180
|
+
'设备不存在或已被撤销。': 'El dispositivo no existe o ya fue revocado.',
|
|
181
|
+
'撤销': 'Revocar',
|
|
182
|
+
'这台设备': 'este dispositivo',
|
|
183
|
+
'设备': 'Dispositivo',
|
|
184
|
+
'未命名设备': 'Dispositivo sin nombre',
|
|
185
|
+
'关闭': 'Cerrar',
|
|
186
|
+
'取消': 'Cancelar',
|
|
187
|
+
'完成': 'Listo',
|
|
188
|
+
'尚未安装 iOS App?': '¿Todavía no tienes la app de iOS?',
|
|
189
|
+
'安装 DSH Mobile': 'Instalar DSH Mobile',
|
|
190
|
+
'查看安装方式': 'Ver cómo instalar',
|
|
191
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'Abre DSH Mobile y escanea el código QR de este equipo.',
|
|
192
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'Instálala en iPhone o iPad con TestFlight.',
|
|
193
|
+
'从 App Store 安装 TestFlight。': 'Instala TestFlight desde el App Store.',
|
|
194
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'Abre el enlace de invitación de TestFlight e instala DSH Mobile.',
|
|
195
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'Requiere iOS 17, iPadOS 17 o superior.',
|
|
196
|
+
'命令行登记(适用于无界面环境)': 'Registro por línea de comandos (sin interfaz)',
|
|
197
|
+
'上次错误': 'Último error',
|
|
198
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <direccion-del-relay>',
|
|
199
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
200
|
+
'GitHub 仓库': 'Repositorio en GitHub',
|
|
201
|
+
'欢迎在 GitHub 上为项目加星标。': 'Si te sirve, deja una estrella en GitHub.',
|
|
202
|
+
'官方原子组件不可用,移动端连接入口未注册': 'Los componentes oficiales no están disponibles; la entrada no se registró',
|
|
203
|
+
'连不上本机连接器:${error}': 'No se pudo conectar al conector local: ${error}',
|
|
204
|
+
'面板状态层没起来:${error}': 'No se pudo iniciar la capa de estado: ${error}',
|
|
205
|
+
'小窗没注册上:${error}': 'No se pudo registrar la ventana: ${error}',
|
|
206
|
+
'侧栏入口没注册上:${error}': 'No se pudo registrar la entrada de la barra lateral: ${error}',
|
|
207
|
+
'连接器应答 HTTP ${status}': 'El conector respondió HTTP ${status}',
|
|
208
|
+
'连接器应答无法解析(HTTP ${status})': 'Respuesta del conector ilegible (HTTP ${status})',
|
|
209
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '“${name}” dejará de conectarse. Habrá que escanear el código otra vez.',
|
|
210
|
+
'${online} 台在线': '${online} en línea',
|
|
211
|
+
'${total} 台在线': '${total} en línea',
|
|
212
|
+
'${online} 台在线,共 ${total} 台': '${online} de ${total} en línea',
|
|
213
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} de ${total} emparejados en línea',
|
|
214
|
+
'有效期至 ${hasta}(剩余 ${resto})': 'Válido hasta ${hasta} (quedan ${resto})',
|
|
215
|
+
},
|
|
216
|
+
ja: {
|
|
217
|
+
'(已配对 ${paired}/${max})': '(ペアリング済み ${paired}/${max})',
|
|
218
|
+
'移动端连接': 'モバイルリンク',
|
|
219
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'モバイルリンク:QRコード、ペアリング済みスマートフォン、リンク状態',
|
|
220
|
+
'此电脑尚未登记': 'このコンピューターは登録されていません',
|
|
221
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'このコンピューターは登録されていません。コマンドラインから登録してください:',
|
|
222
|
+
'登记此电脑以连接中转服务器。': 'リレーに接続するには、このコンピューターを登録してください。',
|
|
223
|
+
'此电脑已登记。': 'このコンピューターは登録されています。',
|
|
224
|
+
'中转地址': 'リレーアドレス',
|
|
225
|
+
'中转服务器': 'リレー',
|
|
226
|
+
'已预填,可修改。': '事前入力済み。編集可能です。',
|
|
227
|
+
'由配置指定,不可修改。': '設定により指定されています。編集できません。',
|
|
228
|
+
'请输入中转地址。': 'リレーアドレスを入力してください。',
|
|
229
|
+
'中转地址无效。': 'リレーアドレスが無効です。',
|
|
230
|
+
'请输入有效的中转地址。': '有効なリレーアドレスを入力してください。',
|
|
231
|
+
'尝试次数过多,请稍后重试。': '試行回数が多すぎます。後でもう一度お試しください。',
|
|
232
|
+
'中转服务器未响应,请稍后重试。': 'リレーが応答しませんでした。後でもう一度お試しください。',
|
|
233
|
+
'中转服务器登记失败,请稍后重试。': 'リレーが登録を拒否しました。後でもう一度お試しください。',
|
|
234
|
+
'中转服务器返回了无法识别的响应。': 'リレーから不明な応答が返されました。',
|
|
235
|
+
'连接器正在停止,请稍后重试。': 'コネクターは停止中です。後でもう一度お試しください。',
|
|
236
|
+
'连接器内部错误,请稍后重试。': 'コネクターの内部エラーです。後でもう一度お試しください。',
|
|
237
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': '登録を保存できませんでした。ディスクの権限を確認してください。',
|
|
238
|
+
'登记后将自动连接。': '登録すると自動的に接続されます。',
|
|
239
|
+
'登记失败,请重试。': '登録に失敗しました。もう一度お試しください。',
|
|
240
|
+
'正在登记…': '登録中…',
|
|
241
|
+
'正在获取…': '取得中…',
|
|
242
|
+
'正在读取状态…': '状態を読み取り中…',
|
|
243
|
+
'正在连接…': '接続中…',
|
|
244
|
+
'登记': '登録',
|
|
245
|
+
'重试': '再試行',
|
|
246
|
+
'无法读取连接状态。': 'リンク状態を読み取れませんでした。',
|
|
247
|
+
'无法连接中转服务器,请检查地址与网络。': 'リレーに到達できませんでした。アドレスとネットワークを確認してください。',
|
|
248
|
+
'状态未知': '不明な状態',
|
|
249
|
+
'已连接': '接続済み',
|
|
250
|
+
'在线': 'オンライン',
|
|
251
|
+
'离线': 'オフライン',
|
|
252
|
+
'未连接,正在重新连接': '未接続、再接続中',
|
|
253
|
+
'连接器已停用': 'コネクターが無効です',
|
|
254
|
+
'连接器已停用。': 'コネクターが無効です。',
|
|
255
|
+
'连接器已停用。请在插件配置中启用后重试。': 'コネクターが無効です。プラグイン設定で有効にして再試行してください。',
|
|
256
|
+
'配对二维码': 'ペアリング用QRコード',
|
|
257
|
+
'获取配对码': 'ペアリングコードを取得',
|
|
258
|
+
'暂无配对码。': 'ペアリングコードはまだありません。',
|
|
259
|
+
'复制配对码': 'ペアリングコードをコピー',
|
|
260
|
+
'已复制': 'コピーしました',
|
|
261
|
+
'重新生成': '再生成',
|
|
262
|
+
'长期有效直到重新生成': '再生成するまで有効',
|
|
263
|
+
'已配对设备': 'ペアリング済みデバイス',
|
|
264
|
+
'暂无已配对设备': 'ペアリング済みデバイスなし',
|
|
265
|
+
'暂无已配对设备。': 'ペアリング済みデバイスはありません。',
|
|
266
|
+
'无法读取全部已配对设备。': 'デバイスリスト全体を読み取れませんでした。',
|
|
267
|
+
'撤销中…': '取り消し中…',
|
|
268
|
+
'撤销失败,请稍后重试。': '取り消しに失敗しました。後でもう一度お試しください。',
|
|
269
|
+
'撤销设备': 'デバイスを取り消す',
|
|
270
|
+
'撤销这台设备': 'このデバイスを取り消す',
|
|
271
|
+
'撤销这台设备?': 'このデバイスを取り消しますか?',
|
|
272
|
+
'这台设备不属于此电脑。': 'このデバイスはこのコンピューターに属していません。',
|
|
273
|
+
'设备不存在或已被撤销。': 'デバイスが存在しないか、すでに取り消されています。',
|
|
274
|
+
'撤销': '取り消す',
|
|
275
|
+
'这台设备': 'このデバイス',
|
|
276
|
+
'设备': 'デバイス',
|
|
277
|
+
'未命名设备': '名前のないデバイス',
|
|
278
|
+
'关闭': '閉じる',
|
|
279
|
+
'取消': 'キャンセル',
|
|
280
|
+
'完成': '完了',
|
|
281
|
+
'尚未安装 iOS App?': 'iOSアプリをまだお持ちでないですか?',
|
|
282
|
+
'安装 DSH Mobile': 'DSH Mobileをインストール',
|
|
283
|
+
'查看安装方式': 'インストール方法を見る',
|
|
284
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'DSH Mobileを開き、このコンピューターのQRコードをスキャンしてください。',
|
|
285
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'TestFlightを使用してiPhoneまたはiPadにインストールします。',
|
|
286
|
+
'从 App Store 安装 TestFlight。': 'App StoreからTestFlightをインストールしてください。',
|
|
287
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'TestFlightの招待リンクを開き、DSH Mobileをインストールしてください。',
|
|
288
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'iOS 17、iPadOS 17以降が必要です。',
|
|
289
|
+
'命令行登记(适用于无界面环境)': 'コマンドライン登録(ヘッドレス)',
|
|
290
|
+
'上次错误': '最後のエラー',
|
|
291
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
292
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
293
|
+
'GitHub 仓库': 'GitHubリポジトリ',
|
|
294
|
+
'欢迎在 GitHub 上为项目加星标。': 'お役に立てたら、GitHubでプロジェクトにスターを付けてください。',
|
|
295
|
+
'官方原子组件不可用,移动端连接入口未注册': '公式プリミティブが利用できないため、エントリは登録されませんでした',
|
|
296
|
+
'连不上本机连接器:${error}': 'ローカルコネクターに到達できません:${error}',
|
|
297
|
+
'面板状态层没起来:${error}': 'ステータスレイヤーが起動しませんでした:${error}',
|
|
298
|
+
'小窗没注册上:${error}': 'パネルは登録されませんでした:${error}',
|
|
299
|
+
'侧栏入口没注册上:${error}': 'サイドバーエントリは登録されませんでした:${error}',
|
|
300
|
+
'连接器应答 HTTP ${status}': 'コネクターがHTTP ${status}を返しました',
|
|
301
|
+
'连接器应答无法解析(HTTP ${status})': 'コネクターの応答を読み取れません(HTTP ${status})',
|
|
302
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '「${name}」は今後接続できなくなります。ペアリングし直すには、もう一度コードをスキャンしてください。',
|
|
303
|
+
'${online} 台在线': '${online} オンライン',
|
|
304
|
+
'${total} 台在线': '${total} オンライン',
|
|
305
|
+
'${online} 台在线,共 ${total} 台': '${total} 台中 ${online} がオンライン',
|
|
306
|
+
'${online} 台在线,共 ${total} 台已配对': 'ペアリング済み ${total} 台中 ${online} がオンライン',
|
|
307
|
+
'有效期至 ${hasta}(剩余 ${resto})': '${hasta} まで有効(残り ${resto})',
|
|
308
|
+
},
|
|
309
|
+
'zh-Hant': {
|
|
310
|
+
'(已配对 ${paired}/${max})': '(已配對 ${paired}/${max})',
|
|
311
|
+
'移动端连接': '手機連結',
|
|
312
|
+
'移动端连接:二维码、已配对的手机与连接状态': '手機連結:QR 碼、已配對手機和連結狀態',
|
|
313
|
+
'此电脑尚未登记': '這台電腦尚未註冊',
|
|
314
|
+
'此电脑尚未登记。请使用命令行完成登记:': '這台電腦尚未註冊。請從指令列註冊:',
|
|
315
|
+
'登记此电脑以连接中转服务器。': '註冊這台電腦以連線至中繼站。',
|
|
316
|
+
'此电脑已登记。': '這台電腦已註冊。',
|
|
317
|
+
'中转地址': '中繼站位址',
|
|
318
|
+
'中转服务器': '中繼站',
|
|
319
|
+
'已预填,可修改。': '已預填;可編輯。',
|
|
320
|
+
'由配置指定,不可修改。': '由設定檔設定;不可編輯。',
|
|
321
|
+
'请输入中转地址。': '輸入中繼站位址。',
|
|
322
|
+
'中转地址无效。': '中繼站位址無效。',
|
|
323
|
+
'请输入有效的中转地址。': '輸入有效的中繼站位址。',
|
|
324
|
+
'尝试次数过多,请稍后重试。': '嘗試次數過多;請稍後再試。',
|
|
325
|
+
'中转服务器未响应,请稍后重试。': '中繼站無回應;請稍後再試。',
|
|
326
|
+
'中转服务器登记失败,请稍后重试。': '中繼站拒絕註冊;請稍後再試。',
|
|
327
|
+
'中转服务器返回了无法识别的响应。': '中繼站回傳未知回應。',
|
|
328
|
+
'连接器正在停止,请稍后重试。': '連接器正在停止;請稍後再試。',
|
|
329
|
+
'连接器内部错误,请稍后重试。': '連接器內部錯誤;請稍後再試。',
|
|
330
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': '無法儲存註冊;請檢查磁碟權限。',
|
|
331
|
+
'登记后将自动连接。': '註冊後會自動連線。',
|
|
332
|
+
'登记失败,请重试。': '註冊失敗;請重試。',
|
|
333
|
+
'正在登记…': '註冊中…',
|
|
334
|
+
'正在获取…': '擷取中…',
|
|
335
|
+
'正在读取状态…': '讀取狀態中…',
|
|
336
|
+
'正在连接…': '連線中…',
|
|
337
|
+
'登记': '註冊',
|
|
338
|
+
'重试': '重試',
|
|
339
|
+
'无法读取连接状态。': '無法讀取連結狀態。',
|
|
340
|
+
'无法连接中转服务器,请检查地址与网络。': '無法連上中繼站;請檢查位址和網路。',
|
|
341
|
+
'状态未知': '未知狀態',
|
|
342
|
+
'已连接': '已連線',
|
|
343
|
+
'在线': '線上',
|
|
344
|
+
'离线': '離線',
|
|
345
|
+
'未连接,正在重新连接': '未連線,重新連線中',
|
|
346
|
+
'连接器已停用': '連接器已停用',
|
|
347
|
+
'连接器已停用。': '連接器已停用。',
|
|
348
|
+
'连接器已停用。请在插件配置中启用后重试。': '連接器已停用。請在外掛程式設定中啟用並重試。',
|
|
349
|
+
'配对二维码': '配對 QR 碼',
|
|
350
|
+
'获取配对码': '取得配對碼',
|
|
351
|
+
'暂无配对码。': '尚無配對碼。',
|
|
352
|
+
'复制配对码': '複製配對碼',
|
|
353
|
+
'已复制': '已複製',
|
|
354
|
+
'重新生成': '重新產生',
|
|
355
|
+
'长期有效直到重新生成': '在重新產生前有效',
|
|
356
|
+
'已配对设备': '已配對裝置',
|
|
357
|
+
'暂无已配对设备': '沒有已配對裝置',
|
|
358
|
+
'暂无已配对设备。': '沒有已配對裝置。',
|
|
359
|
+
'无法读取全部已配对设备。': '無法讀取完整裝置清單。',
|
|
360
|
+
'撤销中…': '撤銷中…',
|
|
361
|
+
'撤销失败,请稍后重试。': '撤銷失敗;請稍後再試。',
|
|
362
|
+
'撤销设备': '撤銷裝置',
|
|
363
|
+
'撤销这台设备': '撤銷此裝置',
|
|
364
|
+
'撤销这台设备?': '要撤銷此裝置嗎?',
|
|
365
|
+
'这台设备不属于此电脑。': '此裝置不屬於這台電腦。',
|
|
366
|
+
'设备不存在或已被撤销。': '此裝置不存在或已撤銷。',
|
|
367
|
+
'撤销': '撤銷',
|
|
368
|
+
'这台设备': '此裝置',
|
|
369
|
+
'设备': '裝置',
|
|
370
|
+
'未命名设备': '未命名裝置',
|
|
371
|
+
'关闭': '關閉',
|
|
372
|
+
'取消': '取消',
|
|
373
|
+
'完成': '完成',
|
|
374
|
+
'尚未安装 iOS App?': '還沒有 iOS App 嗎?',
|
|
375
|
+
'安装 DSH Mobile': '安裝 DSH Mobile',
|
|
376
|
+
'查看安装方式': '查看安裝方式',
|
|
377
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': '開啟 DSH Mobile 並掃描這台電腦上的 QR 碼。',
|
|
378
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': '使用 TestFlight 在 iPhone 或 iPad 上安裝。',
|
|
379
|
+
'从 App Store 安装 TestFlight。': '從 App Store 安裝 TestFlight。',
|
|
380
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': '開啟 TestFlight 邀請連結並安裝 DSH Mobile。',
|
|
381
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': '需要 iOS 17、iPadOS 17 或以上版本。',
|
|
382
|
+
'命令行登记(适用于无界面环境)': '指令列註冊(無頭模式)',
|
|
383
|
+
'上次错误': '最後錯誤',
|
|
384
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
385
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
386
|
+
'GitHub 仓库': 'GitHub 儲存庫',
|
|
387
|
+
'欢迎在 GitHub 上为项目加星标。': '如果有幫助,請在 GitHub 上為專案按星號。',
|
|
388
|
+
'官方原子组件不可用,移动端连接入口未注册': '官方基本功能不可用;條目未註冊',
|
|
389
|
+
'连不上本机连接器:${error}': '無法連線至本機連接器:${error}',
|
|
390
|
+
'面板状态层没起来:${error}': '狀態層未能啟動:${error}',
|
|
391
|
+
'小窗没注册上:${error}': '面板未註冊:${error}',
|
|
392
|
+
'侧栏入口没注册上:${error}': '側邊欄條目未註冊:${error}',
|
|
393
|
+
'连接器应答 HTTP ${status}': '連接器回應 HTTP ${status}',
|
|
394
|
+
'连接器应答无法解析(HTTP ${status})': '無法讀取的連接器回應(HTTP ${status})',
|
|
395
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '「${name}」將不再連線。請再次掃描代碼以進行配對。',
|
|
396
|
+
'${online} 台在线': '${online} 線上',
|
|
397
|
+
'${total} 台在线': '${total} 線上',
|
|
398
|
+
'${online} 台在线,共 ${total} 台': '${online} / ${total} 線上',
|
|
399
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} / ${total} 已配對線上',
|
|
400
|
+
'有效期至 ${hasta}(剩余 ${resto})': '有效至 ${hasta}(剩餘 ${resto})',
|
|
401
|
+
},
|
|
402
|
+
ko: {
|
|
403
|
+
'(已配对 ${paired}/${max})': '(페어링됨 ${paired}/${max})',
|
|
404
|
+
'移动端连接': '모바일 링크',
|
|
405
|
+
'移动端连接:二维码、已配对的手机与连接状态': '모바일 링크: QR 코드, 페어링된 휴대폰 및 링크 상태',
|
|
406
|
+
'此电脑尚未登记': '이 컴퓨터는 등록되지 않았습니다',
|
|
407
|
+
'此电脑尚未登记。请使用命令行完成登记:': '이 컴퓨터는 등록되지 않았습니다. 명령줄에서 등록하세요:',
|
|
408
|
+
'登记此电脑以连接中转服务器。': '이 컴퓨터를 등록하여 릴레이에 연결하세요.',
|
|
409
|
+
'此电脑已登记。': '이 컴퓨터는 등록되었습니다.',
|
|
410
|
+
'中转地址': '릴레이 주소',
|
|
411
|
+
'中转服务器': '릴레이',
|
|
412
|
+
'已预填,可修改。': '미리 채워짐; 편집 가능.',
|
|
413
|
+
'由配置指定,不可修改。': '구성에서 설정됨; 편집할 수 없습니다.',
|
|
414
|
+
'请输入中转地址。': '릴레이 주소를 입력하세요.',
|
|
415
|
+
'中转地址无效。': '릴레이 주소가 유효하지 않습니다.',
|
|
416
|
+
'请输入有效的中转地址。': '유효한 릴레이 주소를 입력하세요.',
|
|
417
|
+
'尝试次数过多,请稍后重试。': '시도 횟수가 너무 많습니다. 나중에 다시 시도하세요.',
|
|
418
|
+
'中转服务器未响应,请稍后重试。': '릴레이가 응답하지 않았습니다. 나중에 다시 시도하세요.',
|
|
419
|
+
'中转服务器登记失败,请稍后重试。': '릴레이가 등록을 거부했습니다. 나중에 다시 시도하세요.',
|
|
420
|
+
'中转服务器返回了无法识别的响应。': '릴레이가 알 수 없는 응답을 반환했습니다.',
|
|
421
|
+
'连接器正在停止,请稍后重试。': '커넥터가 중지 중입니다. 나중에 다시 시도하세요.',
|
|
422
|
+
'连接器内部错误,请稍后重试。': '내부 커넥터 오류입니다. 나중에 다시 시도하세요.',
|
|
423
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': '등록을 저장할 수 없습니다. 디스크 권한을 확인하세요.',
|
|
424
|
+
'登记后将自动连接。': '등록되면 자동으로 연결됩니다.',
|
|
425
|
+
'登记失败,请重试。': '등록에 실패했습니다. 다시 시도하세요.',
|
|
426
|
+
'正在登记…': '등록 중…',
|
|
427
|
+
'正在获取…': '가져오는 중…',
|
|
428
|
+
'正在读取状态…': '상태 읽는 중…',
|
|
429
|
+
'正在连接…': '연결 중…',
|
|
430
|
+
'登记': '등록',
|
|
431
|
+
'重试': '다시 시도',
|
|
432
|
+
'无法读取连接状态。': '링크 상태를 읽을 수 없습니다.',
|
|
433
|
+
'无法连接中转服务器,请检查地址与网络。': '릴레이에 연결할 수 없습니다. 주소와 네트워크를 확인하세요.',
|
|
434
|
+
'状态未知': '알 수 없는 상태',
|
|
435
|
+
'已连接': '연결됨',
|
|
436
|
+
'在线': '온라인',
|
|
437
|
+
'离线': '오프라인',
|
|
438
|
+
'未连接,正在重新连接': '연결되지 않음, 다시 연결 중',
|
|
439
|
+
'连接器已停用': '커넥터 비활성화됨',
|
|
440
|
+
'连接器已停用。': '커넥터가 비활성화되었습니다.',
|
|
441
|
+
'连接器已停用。请在插件配置中启用后重试。': '커넥터가 비활성화되었습니다. 플러그인 구성에서 활성화한 후 다시 시도하세요.',
|
|
442
|
+
'配对二维码': '페어링 QR 코드',
|
|
443
|
+
'获取配对码': '페어링 코드 받기',
|
|
444
|
+
'暂无配对码。': '아직 페어링 코드가 없습니다.',
|
|
445
|
+
'复制配对码': '페어링 코드 복사',
|
|
446
|
+
'已复制': '복사됨',
|
|
447
|
+
'重新生成': '재생성',
|
|
448
|
+
'长期有效直到重新生成': '재생성할 때까지 유효',
|
|
449
|
+
'已配对设备': '페어링된 기기',
|
|
450
|
+
'暂无已配对设备': '페어링된 기기 없음',
|
|
451
|
+
'暂无已配对设备。': '페어링된 기기가 없습니다.',
|
|
452
|
+
'无法读取全部已配对设备。': '전체 기기 목록을 읽을 수 없습니다.',
|
|
453
|
+
'撤销中…': '해지 중…',
|
|
454
|
+
'撤销失败,请稍后重试。': '해지에 실패했습니다. 나중에 다시 시도하세요.',
|
|
455
|
+
'撤销设备': '기기 해지',
|
|
456
|
+
'撤销这台设备': '이 기기 해지',
|
|
457
|
+
'撤销这台设备?': '이 기기를 해지하시겠습니까?',
|
|
458
|
+
'这台设备不属于此电脑。': '이 기기는 이 컴퓨터에 속하지 않습니다.',
|
|
459
|
+
'设备不存在或已被撤销。': '기기가 존재하지 않거나 이미 해지되었습니다.',
|
|
460
|
+
'撤销': '해지',
|
|
461
|
+
'这台设备': '이 기기',
|
|
462
|
+
'设备': '기기',
|
|
463
|
+
'未命名设备': '이름 없는 기기',
|
|
464
|
+
'关闭': '닫기',
|
|
465
|
+
'取消': '취소',
|
|
466
|
+
'完成': '완료',
|
|
467
|
+
'尚未安装 iOS App?': '아직 iOS 앱이 없나요?',
|
|
468
|
+
'安装 DSH Mobile': 'DSH Mobile 설치',
|
|
469
|
+
'查看安装方式': '설치 방법 보기',
|
|
470
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'DSH Mobile을 열고 이 컴퓨터의 QR 코드를 스캔하세요.',
|
|
471
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'TestFlight로 iPhone 또는 iPad에 설치하세요.',
|
|
472
|
+
'从 App Store 安装 TestFlight。': 'App Store에서 TestFlight를 설치하세요.',
|
|
473
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'TestFlight 초대 링크를 열고 DSH Mobile을 설치하세요.',
|
|
474
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'iOS 17, iPadOS 17 이상이 필요합니다.',
|
|
475
|
+
'命令行登记(适用于无界面环境)': '명령줄 등록(헤드리스)',
|
|
476
|
+
'上次错误': '마지막 오류',
|
|
477
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
478
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
479
|
+
'GitHub 仓库': 'GitHub 저장소',
|
|
480
|
+
'欢迎在 GitHub 上为项目加星标。': '도움이 되셨다면 GitHub에서 프로젝트에 별을 눌러 주세요.',
|
|
481
|
+
'官方原子组件不可用,移动端连接入口未注册': '공식 프리미티브를 사용할 수 없어 항목이 등록되지 않았습니다',
|
|
482
|
+
'连不上本机连接器:${error}': '로컬 커넥터에 연결할 수 없습니다: ${error}',
|
|
483
|
+
'面板状态层没起来:${error}': '상태 계층이 시작되지 않았습니다: ${error}',
|
|
484
|
+
'小窗没注册上:${error}': '패널이 등록되지 않았습니다: ${error}',
|
|
485
|
+
'侧栏入口没注册上:${error}': '사이드바 항목이 등록되지 않았습니다: ${error}',
|
|
486
|
+
'连接器应答 HTTP ${status}': '커넥터가 HTTP ${status}로 응답했습니다',
|
|
487
|
+
'连接器应答无法解析(HTTP ${status})': '커넥터 응답을 읽을 수 없습니다 (HTTP ${status})',
|
|
488
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '“${name}”은(는) 더 이상 연결되지 않습니다. 코드를 다시 스캔하여 페어링하세요.',
|
|
489
|
+
'${online} 台在线': '${online}개 온라인',
|
|
490
|
+
'${total} 台在线': '${total}개 온라인',
|
|
491
|
+
'${online} 台在线,共 ${total} 台': '${total}개 중 ${online}개 온라인',
|
|
492
|
+
'${online} 台在线,共 ${total} 台已配对': '페어링된 ${total}개 중 ${online}개 온라인',
|
|
493
|
+
'有效期至 ${hasta}(剩余 ${resto})': '${hasta}까지 유효 (${resto} 남음)',
|
|
494
|
+
},
|
|
495
|
+
'pt-BR': {
|
|
496
|
+
'(已配对 ${paired}/${max})': '(pareados ${paired}/${max})',
|
|
497
|
+
'移动端连接': 'Link móvel',
|
|
498
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'Link móvel: código QR, celulares pareados e status do link',
|
|
499
|
+
'此电脑尚未登记': 'Este computador não está registrado',
|
|
500
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'Este computador não está registrado. Registre-o pela linha de comando:',
|
|
501
|
+
'登记此电脑以连接中转服务器。': 'Registre este computador para conectar-se ao relay.',
|
|
502
|
+
'此电脑已登记。': 'Este computador está registrado.',
|
|
503
|
+
'中转地址': 'Endereço do relay',
|
|
504
|
+
'中转服务器': 'Relay',
|
|
505
|
+
'已预填,可修改。': 'Pré-preenchido; editável.',
|
|
506
|
+
'由配置指定,不可修改。': 'Definido pela configuração; não editável.',
|
|
507
|
+
'请输入中转地址。': 'Digite o endereço do relay.',
|
|
508
|
+
'中转地址无效。': 'O endereço do relay não é válido.',
|
|
509
|
+
'请输入有效的中转地址。': 'Digite um endereço de relay válido.',
|
|
510
|
+
'尝试次数过多,请稍后重试。': 'Muitas tentativas; tente novamente mais tarde.',
|
|
511
|
+
'中转服务器未响应,请稍后重试。': 'O relay não respondeu; tente novamente mais tarde.',
|
|
512
|
+
'中转服务器登记失败,请稍后重试。': 'O relay recusou o registro; tente novamente mais tarde.',
|
|
513
|
+
'中转服务器返回了无法识别的响应。': 'O relay retornou uma resposta desconhecida.',
|
|
514
|
+
'连接器正在停止,请稍后重试。': 'O conector está parando; tente novamente mais tarde.',
|
|
515
|
+
'连接器内部错误,请稍后重试。': 'Erro interno do conector; tente novamente mais tarde.',
|
|
516
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': 'Não foi possível salvar o registro; verifique as permissões do disco.',
|
|
517
|
+
'登记后将自动连接。': 'Ele se conecta automaticamente após o registro.',
|
|
518
|
+
'登记失败,请重试。': 'Falha no registro; tente novamente.',
|
|
519
|
+
'正在登记…': 'Registrando…',
|
|
520
|
+
'正在获取…': 'Buscando…',
|
|
521
|
+
'正在读取状态…': 'Lendo status…',
|
|
522
|
+
'正在连接…': 'Conectando…',
|
|
523
|
+
'登记': 'Registrar',
|
|
524
|
+
'重试': 'Tentar novamente',
|
|
525
|
+
'无法读取连接状态。': 'Não foi possível ler o status do link.',
|
|
526
|
+
'无法连接中转服务器,请检查地址与网络。': 'Não foi possível alcançar o relay; verifique o endereço e a rede.',
|
|
527
|
+
'状态未知': 'Estado desconhecido',
|
|
528
|
+
'已连接': 'Conectado',
|
|
529
|
+
'在线': 'online',
|
|
530
|
+
'离线': 'offline',
|
|
531
|
+
'未连接,正在重新连接': 'Desconectado, reconectando',
|
|
532
|
+
'连接器已停用': 'Conector desabilitado',
|
|
533
|
+
'连接器已停用。': 'Conector desabilitado.',
|
|
534
|
+
'连接器已停用。请在插件配置中启用后重试。': 'Conector desabilitado. Habilite-o na configuração do plugin e tente novamente.',
|
|
535
|
+
'配对二维码': 'Código QR de pareamento',
|
|
536
|
+
'获取配对码': 'Obter código de pareamento',
|
|
537
|
+
'暂无配对码。': 'Nenhum código de pareamento ainda.',
|
|
538
|
+
'复制配对码': 'Copiar código de pareamento',
|
|
539
|
+
'已复制': 'Copiado',
|
|
540
|
+
'重新生成': 'Regenerar',
|
|
541
|
+
'长期有效直到重新生成': 'Válido até ser regenerado',
|
|
542
|
+
'已配对设备': 'Dispositivos pareados',
|
|
543
|
+
'暂无已配对设备': 'Nenhum dispositivo pareado',
|
|
544
|
+
'暂无已配对设备。': 'Nenhum dispositivo pareado.',
|
|
545
|
+
'无法读取全部已配对设备。': 'Não foi possível ler a lista completa de dispositivos.',
|
|
546
|
+
'撤销中…': 'Revogando…',
|
|
547
|
+
'撤销失败,请稍后重试。': 'Falha na revogação; tente novamente mais tarde.',
|
|
548
|
+
'撤销设备': 'Revogar dispositivo',
|
|
549
|
+
'撤销这台设备': 'Revogar este dispositivo',
|
|
550
|
+
'撤销这台设备?': 'Revogar este dispositivo?',
|
|
551
|
+
'这台设备不属于此电脑。': 'Este dispositivo não pertence a este computador.',
|
|
552
|
+
'设备不存在或已被撤销。': 'O dispositivo não existe ou já foi revogado.',
|
|
553
|
+
'撤销': 'Revogar',
|
|
554
|
+
'这台设备': 'este dispositivo',
|
|
555
|
+
'设备': 'Dispositivo',
|
|
556
|
+
'未命名设备': 'Dispositivo sem nome',
|
|
557
|
+
'关闭': 'Fechar',
|
|
558
|
+
'取消': 'Cancelar',
|
|
559
|
+
'完成': 'Concluir',
|
|
560
|
+
'尚未安装 iOS App?': 'Ainda não tem o app para iOS?',
|
|
561
|
+
'安装 DSH Mobile': 'Instalar o DSH Mobile',
|
|
562
|
+
'查看安装方式': 'Veja como instalar',
|
|
563
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'Abra o DSH Mobile e escaneie o código QR neste computador.',
|
|
564
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'Instale-o no iPhone ou iPad com o TestFlight.',
|
|
565
|
+
'从 App Store 安装 TestFlight。': 'Instale o TestFlight pela App Store.',
|
|
566
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'Abra o link de convite do TestFlight e instale o DSH Mobile.',
|
|
567
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'Requer iOS 17, iPadOS 17 ou posterior.',
|
|
568
|
+
'命令行登记(适用于无界面环境)': 'Registro por linha de comando (headless)',
|
|
569
|
+
'上次错误': 'Último erro',
|
|
570
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
571
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
572
|
+
'GitHub 仓库': 'Repositório no GitHub',
|
|
573
|
+
'欢迎在 GitHub 上为项目加星标。': 'Se ajudar, dê uma estrela no projeto no GitHub.',
|
|
574
|
+
'官方原子组件不可用,移动端连接入口未注册': 'Primitivas oficiais indisponíveis; a entrada não foi registrada',
|
|
575
|
+
'连不上本机连接器:${error}': 'Não é possível alcançar o conector local: ${error}',
|
|
576
|
+
'面板状态层没起来:${error}': 'A camada de status não iniciou: ${error}',
|
|
577
|
+
'小窗没注册上:${error}': 'O painel não foi registrado: ${error}',
|
|
578
|
+
'侧栏入口没注册上:${error}': 'A entrada da barra lateral não foi registrada: ${error}',
|
|
579
|
+
'连接器应答 HTTP ${status}': 'O conector respondeu HTTP ${status}',
|
|
580
|
+
'连接器应答无法解析(HTTP ${status})': 'Resposta ilegível do conector (HTTP ${status})',
|
|
581
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '“${name}” não se conectará mais. Escaneie o código novamente para pareá-lo.',
|
|
582
|
+
'${online} 台在线': '${online} online',
|
|
583
|
+
'${total} 台在线': '${total} online',
|
|
584
|
+
'${online} 台在线,共 ${total} 台': '${online} de ${total} online',
|
|
585
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} de ${total} pareados online',
|
|
586
|
+
'有效期至 ${hasta}(剩余 ${resto})': 'Válido até ${hasta} (faltam ${resto})',
|
|
587
|
+
},
|
|
588
|
+
fr: {
|
|
589
|
+
'(已配对 ${paired}/${max})': '(appairés ${paired}/${max})',
|
|
590
|
+
'移动端连接': 'Lien mobile',
|
|
591
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'Lien mobile : code QR, téléphones appairés et état du lien',
|
|
592
|
+
'此电脑尚未登记': 'Cet ordinateur n\'est pas enregistré',
|
|
593
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'Cet ordinateur n\'est pas enregistré. Enregistrez-le depuis la ligne de commande :',
|
|
594
|
+
'登记此电脑以连接中转服务器。': 'Enregistrez cet ordinateur pour vous connecter au relais.',
|
|
595
|
+
'此电脑已登记。': 'Cet ordinateur est enregistré.',
|
|
596
|
+
'中转地址': 'Adresse du relais',
|
|
597
|
+
'中转服务器': 'Relais',
|
|
598
|
+
'已预填,可修改。': 'Préremplie ; modifiable.',
|
|
599
|
+
'由配置指定,不可修改。': 'Définie par la configuration ; non modifiable.',
|
|
600
|
+
'请输入中转地址。': 'Saisissez l\'adresse du relais.',
|
|
601
|
+
'中转地址无效。': 'L\'adresse du relais n\'est pas valide.',
|
|
602
|
+
'请输入有效的中转地址。': 'Saisissez une adresse de relais valide.',
|
|
603
|
+
'尝试次数过多,请稍后重试。': 'Trop de tentatives ; réessayez plus tard.',
|
|
604
|
+
'中转服务器未响应,请稍后重试。': 'Le relais n\'a pas répondu ; réessayez plus tard.',
|
|
605
|
+
'中转服务器登记失败,请稍后重试。': 'Le relais a refusé l\'enregistrement ; réessayez plus tard.',
|
|
606
|
+
'中转服务器返回了无法识别的响应。': 'Le relais a renvoyé une réponse inconnue.',
|
|
607
|
+
'连接器正在停止,请稍后重试。': 'Le connecteur s\'arrête ; réessayez plus tard.',
|
|
608
|
+
'连接器内部错误,请稍后重试。': 'Erreur interne du connecteur ; réessayez plus tard.',
|
|
609
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': 'L\'enregistrement n\'a pas pu être sauvegardé ; vérifiez les permissions du disque.',
|
|
610
|
+
'登记后将自动连接。': 'Il se connecte automatiquement une fois enregistré.',
|
|
611
|
+
'登记失败,请重试。': 'Échec de l\'enregistrement ; réessayez.',
|
|
612
|
+
'正在登记…': 'Enregistrement…',
|
|
613
|
+
'正在获取…': 'Récupération…',
|
|
614
|
+
'正在读取状态…': 'Lecture de l\'état…',
|
|
615
|
+
'正在连接…': 'Connexion…',
|
|
616
|
+
'登记': 'Enregistrer',
|
|
617
|
+
'重试': 'Réessayer',
|
|
618
|
+
'无法读取连接状态。': 'Impossible de lire l\'état du lien.',
|
|
619
|
+
'无法连接中转服务器,请检查地址与网络。': 'Impossible de joindre le relais ; vérifiez l\'adresse et le réseau.',
|
|
620
|
+
'状态未知': 'État inconnu',
|
|
621
|
+
'已连接': 'Connecté',
|
|
622
|
+
'在线': 'en ligne',
|
|
623
|
+
'离线': 'hors ligne',
|
|
624
|
+
'未连接,正在重新连接': 'Non connecté, reconnexion en cours',
|
|
625
|
+
'连接器已停用': 'Connecteur désactivé',
|
|
626
|
+
'连接器已停用。': 'Connecteur désactivé.',
|
|
627
|
+
'连接器已停用。请在插件配置中启用后重试。': 'Connecteur désactivé. Activez-le dans la configuration du plugin et réessayez.',
|
|
628
|
+
'配对二维码': 'Code QR d\'appairage',
|
|
629
|
+
'获取配对码': 'Obtenir le code d\'appairage',
|
|
630
|
+
'暂无配对码。': 'Aucun code d\'appairage pour l\'instant.',
|
|
631
|
+
'复制配对码': 'Copier le code d\'appairage',
|
|
632
|
+
'已复制': 'Copié',
|
|
633
|
+
'重新生成': 'Régénérer',
|
|
634
|
+
'长期有效直到重新生成': 'Valable jusqu\'à régénération',
|
|
635
|
+
'已配对设备': 'Appareils appairés',
|
|
636
|
+
'暂无已配对设备': 'Aucun appareil appairé',
|
|
637
|
+
'暂无已配对设备。': 'Aucun appareil appairé.',
|
|
638
|
+
'无法读取全部已配对设备。': 'Impossible de lire la liste complète des appareils.',
|
|
639
|
+
'撤销中…': 'Révocation…',
|
|
640
|
+
'撤销失败,请稍后重试。': 'Échec de la révocation ; réessayez plus tard.',
|
|
641
|
+
'撤销设备': 'Révoquer l\'appareil',
|
|
642
|
+
'撤销这台设备': 'Révoquer cet appareil',
|
|
643
|
+
'撤销这台设备?': 'Révoquer cet appareil ?',
|
|
644
|
+
'这台设备不属于此电脑。': 'Cet appareil n\'appartient pas à cet ordinateur.',
|
|
645
|
+
'设备不存在或已被撤销。': 'L\'appareil n\'existe pas ou a déjà été révoqué.',
|
|
646
|
+
'撤销': 'Révoquer',
|
|
647
|
+
'这台设备': 'cet appareil',
|
|
648
|
+
'设备': 'Appareil',
|
|
649
|
+
'未命名设备': 'Appareil sans nom',
|
|
650
|
+
'关闭': 'Fermer',
|
|
651
|
+
'取消': 'Annuler',
|
|
652
|
+
'完成': 'Terminé',
|
|
653
|
+
'尚未安装 iOS App?': 'Pas encore d\'application iOS ?',
|
|
654
|
+
'安装 DSH Mobile': 'Installer DSH Mobile',
|
|
655
|
+
'查看安装方式': 'Voir comment l\'installer',
|
|
656
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'Ouvrez DSH Mobile et scannez le code QR sur cet ordinateur.',
|
|
657
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'Installez-la sur iPhone ou iPad avec TestFlight.',
|
|
658
|
+
'从 App Store 安装 TestFlight。': 'Installez TestFlight depuis l\'App Store.',
|
|
659
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'Ouvrez le lien d\'invitation TestFlight et installez DSH Mobile.',
|
|
660
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'Nécessite iOS 17, iPadOS 17 ou une version ultérieure.',
|
|
661
|
+
'命令行登记(适用于无界面环境)': 'Enregistrement en ligne de commande (sans interface)',
|
|
662
|
+
'上次错误': 'Dernière erreur',
|
|
663
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
664
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
665
|
+
'GitHub 仓库': 'Dépôt GitHub',
|
|
666
|
+
'欢迎在 GitHub 上为项目加星标。': 'Si cela vous aide, ajoutez une étoile au projet sur GitHub.',
|
|
667
|
+
'官方原子组件不可用,移动端连接入口未注册': 'Primitives officielles indisponibles ; l\'entrée n\'a pas été enregistrée',
|
|
668
|
+
'连不上本机连接器:${error}': 'Impossible de joindre le connecteur local : ${error}',
|
|
669
|
+
'面板状态层没起来:${error}': 'La couche d\'état ne s\'est pas initialisée : ${error}',
|
|
670
|
+
'小窗没注册上:${error}': 'Le panneau n\'a pas été enregistré : ${error}',
|
|
671
|
+
'侧栏入口没注册上:${error}': 'L\'entrée de la barre latérale n\'a pas été enregistrée : ${error}',
|
|
672
|
+
'连接器应答 HTTP ${status}': 'Le connecteur a répondu HTTP ${status}',
|
|
673
|
+
'连接器应答无法解析(HTTP ${status})': 'Réponse illisible du connecteur (HTTP ${status})',
|
|
674
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '« ${name} » ne se connectera plus. Scannez à nouveau le code pour l\'appairer.',
|
|
675
|
+
'${online} 台在线': '${online} en ligne',
|
|
676
|
+
'${total} 台在线': '${total} en ligne',
|
|
677
|
+
'${online} 台在线,共 ${total} 台': '${online} sur ${total} en ligne',
|
|
678
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} sur ${total} appairés en ligne',
|
|
679
|
+
'有效期至 ${hasta}(剩余 ${resto})': 'Valable jusqu\'à ${hasta} (${resto} restant)',
|
|
680
|
+
},
|
|
681
|
+
de: {
|
|
682
|
+
'(已配对 ${paired}/${max})': '(gekoppelt: ${paired}/${max})',
|
|
683
|
+
'移动端连接': 'Mobile Verbindung',
|
|
684
|
+
'移动端连接:二维码、已配对的手机与连接状态': 'Mobile Verbindung: QR-Code, gekoppelte Telefone und Verbindungsstatus',
|
|
685
|
+
'此电脑尚未登记': 'Dieser Computer ist nicht registriert',
|
|
686
|
+
'此电脑尚未登记。请使用命令行完成登记:': 'Dieser Computer ist nicht registriert. Registrieren Sie ihn über die Befehlszeile:',
|
|
687
|
+
'登记此电脑以连接中转服务器。': 'Registrieren Sie diesen Computer, um eine Verbindung zum Relay herzustellen.',
|
|
688
|
+
'此电脑已登记。': 'Dieser Computer ist registriert.',
|
|
689
|
+
'中转地址': 'Relay-Adresse',
|
|
690
|
+
'中转服务器': 'Relay',
|
|
691
|
+
'已预填,可修改。': 'Vorausgefüllt; bearbeitbar.',
|
|
692
|
+
'由配置指定,不可修改。': 'Durch die Konfiguration festgelegt; nicht bearbeitbar.',
|
|
693
|
+
'请输入中转地址。': 'Geben Sie die Relay-Adresse ein.',
|
|
694
|
+
'中转地址无效。': 'Die Relay-Adresse ist nicht gültig.',
|
|
695
|
+
'请输入有效的中转地址。': 'Geben Sie eine gültige Relay-Adresse ein.',
|
|
696
|
+
'尝试次数过多,请稍后重试。': 'Zu viele Versuche; versuchen Sie es später erneut.',
|
|
697
|
+
'中转服务器未响应,请稍后重试。': 'Das Relay hat nicht geantwortet; versuchen Sie es später erneut.',
|
|
698
|
+
'中转服务器登记失败,请稍后重试。': 'Das Relay hat die Registrierung abgelehnt; versuchen Sie es später erneut.',
|
|
699
|
+
'中转服务器返回了无法识别的响应。': 'Das Relay hat eine unbekannte Antwort zurückgegeben.',
|
|
700
|
+
'连接器正在停止,请稍后重试。': 'Der Connector wird beendet; versuchen Sie es später erneut.',
|
|
701
|
+
'连接器内部错误,请稍后重试。': 'Interner Connector-Fehler; versuchen Sie es später erneut.',
|
|
702
|
+
'登记信息未能保存到此电脑,请检查磁盘权限。': 'Die Registrierung konnte nicht gespeichert werden; prüfen Sie die Datenträgerberechtigungen.',
|
|
703
|
+
'登记后将自动连接。': 'Die Verbindung wird nach der Registrierung automatisch hergestellt.',
|
|
704
|
+
'登记失败,请重试。': 'Registrierung fehlgeschlagen; versuchen Sie es erneut.',
|
|
705
|
+
'正在登记…': 'Wird registriert …',
|
|
706
|
+
'正在获取…': 'Wird abgerufen …',
|
|
707
|
+
'正在读取状态…': 'Status wird gelesen …',
|
|
708
|
+
'正在连接…': 'Verbindung wird hergestellt …',
|
|
709
|
+
'登记': 'Registrieren',
|
|
710
|
+
'重试': 'Erneut versuchen',
|
|
711
|
+
'无法读取连接状态。': 'Der Verbindungsstatus konnte nicht gelesen werden.',
|
|
712
|
+
'无法连接中转服务器,请检查地址与网络。': 'Das Relay war nicht erreichbar; überprüfen Sie die Adresse und das Netzwerk.',
|
|
713
|
+
'状态未知': 'Unbekannter Status',
|
|
714
|
+
'已连接': 'Verbunden',
|
|
715
|
+
'在线': 'online',
|
|
716
|
+
'离线': 'offline',
|
|
717
|
+
'未连接,正在重新连接': 'Nicht verbunden, Verbindung wird wiederhergestellt',
|
|
718
|
+
'连接器已停用': 'Connector deaktiviert',
|
|
719
|
+
'连接器已停用。': 'Connector deaktiviert.',
|
|
720
|
+
'连接器已停用。请在插件配置中启用后重试。': 'Connector deaktiviert. Aktivieren Sie ihn in der Plugin-Konfiguration und versuchen Sie es erneut.',
|
|
721
|
+
'配对二维码': 'Kopplungs-QR-Code',
|
|
722
|
+
'获取配对码': 'Kopplungscode abrufen',
|
|
723
|
+
'暂无配对码。': 'Noch kein Kopplungscode.',
|
|
724
|
+
'复制配对码': 'Kopplungscode kopieren',
|
|
725
|
+
'已复制': 'Kopiert',
|
|
726
|
+
'重新生成': 'Neu generieren',
|
|
727
|
+
'长期有效直到重新生成': 'Gültig bis zur Neugenerierung',
|
|
728
|
+
'已配对设备': 'Gekoppelte Geräte',
|
|
729
|
+
'暂无已配对设备': 'Keine gekoppelten Geräte',
|
|
730
|
+
'暂无已配对设备。': 'Keine gekoppelten Geräte.',
|
|
731
|
+
'无法读取全部已配对设备。': 'Die vollständige Geräteliste konnte nicht gelesen werden.',
|
|
732
|
+
'撤销中…': 'Wird widerrufen …',
|
|
733
|
+
'撤销失败,请稍后重试。': 'Widerruf fehlgeschlagen; versuchen Sie es später erneut.',
|
|
734
|
+
'撤销设备': 'Gerät widerrufen',
|
|
735
|
+
'撤销这台设备': 'Dieses Gerät widerrufen',
|
|
736
|
+
'撤销这台设备?': 'Dieses Gerät widerrufen?',
|
|
737
|
+
'这台设备不属于此电脑。': 'Dieses Gerät gehört nicht zu diesem Computer.',
|
|
738
|
+
'设备不存在或已被撤销。': 'Das Gerät existiert nicht oder wurde bereits widerrufen.',
|
|
739
|
+
'撤销': 'Widerrufen',
|
|
740
|
+
'这台设备': 'dieses Gerät',
|
|
741
|
+
'设备': 'Gerät',
|
|
742
|
+
'未命名设备': 'Unbenanntes Gerät',
|
|
743
|
+
'关闭': 'Schließen',
|
|
744
|
+
'取消': 'Abbrechen',
|
|
745
|
+
'完成': 'Fertig',
|
|
746
|
+
'尚未安装 iOS App?': 'Noch keine iOS-App?',
|
|
747
|
+
'安装 DSH Mobile': 'DSH Mobile installieren',
|
|
748
|
+
'查看安装方式': 'Installationsanleitung ansehen',
|
|
749
|
+
'打开 DSH Mobile,扫描电脑上的配对二维码。': 'Öffnen Sie DSH Mobile und scannen Sie den QR-Code auf diesem Computer.',
|
|
750
|
+
'在 iPhone 或 iPad 上通过 TestFlight 安装。': 'Installieren Sie es mit TestFlight auf iPhone oder iPad.',
|
|
751
|
+
'从 App Store 安装 TestFlight。': 'Installieren Sie TestFlight aus dem App Store.',
|
|
752
|
+
'打开 TestFlight 邀请链接,安装 DSH Mobile。': 'Öffnen Sie den TestFlight-Einladungslink und installieren Sie DSH Mobile.',
|
|
753
|
+
'需要 iOS 17、iPadOS 17 或更高版本。': 'Erfordert iOS 17, iPadOS 17 oder neuer.',
|
|
754
|
+
'命令行登记(适用于无界面环境)': 'Befehlszeilen-Registrierung (headless)',
|
|
755
|
+
'上次错误': 'Letzter Fehler',
|
|
756
|
+
'dsh-mobile-link enroll --relay <中转地址>': 'dsh-mobile-link enroll --relay <relay-address>',
|
|
757
|
+
'wss://<中转地址>/dsh-link': 'wss://<relay>/dsh-link',
|
|
758
|
+
'GitHub 仓库': 'GitHub-Repository',
|
|
759
|
+
'欢迎在 GitHub 上为项目加星标。': 'Wenn es Ihnen hilft, geben Sie dem Projekt auf GitHub einen Stern.',
|
|
760
|
+
'官方原子组件不可用,移动端连接入口未注册': 'Offizielle Basisfunktionen nicht verfügbar; der Eintrag wurde nicht registriert.',
|
|
761
|
+
'连不上本机连接器:${error}': 'Der lokale Connector ist nicht erreichbar: ${error}',
|
|
762
|
+
'面板状态层没起来:${error}': 'Die Statusebene konnte nicht gestartet werden: ${error}',
|
|
763
|
+
'小窗没注册上:${error}': 'Das Panel wurde nicht registriert: ${error}',
|
|
764
|
+
'侧栏入口没注册上:${error}': 'Der Seitenleisten-Eintrag wurde nicht registriert: ${error}',
|
|
765
|
+
'连接器应答 HTTP ${status}': 'Der Connector antwortete mit HTTP ${status}',
|
|
766
|
+
'连接器应答无法解析(HTTP ${status})': 'Nicht lesbare Connector-Antwort (HTTP ${status})',
|
|
767
|
+
'“${name}”将无法继续连接。需要重新扫码才能再次配对。': '„${name}“ wird sich nicht mehr verbinden. Scannen Sie den Code erneut, um es zu koppeln.',
|
|
768
|
+
'${online} 台在线': '${online} online',
|
|
769
|
+
'${total} 台在线': '${total} online',
|
|
770
|
+
'${online} 台在线,共 ${total} 台': '${online} von ${total} online',
|
|
771
|
+
'${online} 台在线,共 ${total} 台已配对': '${online} von ${total} gekoppelten Geräten online',
|
|
772
|
+
'有效期至 ${hasta}(剩余 ${resto})': 'Gültig bis ${hasta} (noch ${resto})',
|
|
773
|
+
},
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* 界面语言:宿主能表达就用宿主;宿主表达不了用户要的语言(例如宿主只有 zh / en 而浏览器报
|
|
778
|
+
* es)时,由面板补上自己有词表的那一档。宿主将来支持西语,面板会自动回到宿主,不需要再改。
|
|
779
|
+
* 宿主没有 locale 服务(旧宿主、纯浏览器调试)时完全按浏览器语言,行为与改动前一致。
|
|
780
|
+
*/
|
|
781
|
+
const PANEL_LOCALES = ['zh', 'en', 'es', 'zh-Hant', 'ja', 'ko', 'pt-BR', 'fr', 'de']
|
|
782
|
+
|
|
783
|
+
function baseLanguage(tag) {
|
|
784
|
+
const value = String(tag ?? '').trim().toLowerCase()
|
|
785
|
+
return value ? value.split('-')[0] : ''
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/** 面板有词表的语言档;没有词表返回 null。 */
|
|
789
|
+
function panelLocaleOf(tag) {
|
|
790
|
+
const raw = String(tag ?? '').trim().toLowerCase()
|
|
791
|
+
if (raw.startsWith('zh-hant') || raw.startsWith('zh-tw') || raw.startsWith('zh-hk')) return 'zh-Hant'
|
|
792
|
+
if (raw.startsWith('pt')) return 'pt-BR'
|
|
793
|
+
const base = baseLanguage(raw)
|
|
794
|
+
return PANEL_LOCALES.includes(base) ? base : null
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** 浏览器要的语言,按优先级排开;桌面版的这一串由宿主启动参数决定,这里只读。 */
|
|
798
|
+
function browserLanguages() {
|
|
799
|
+
if (typeof navigator === 'undefined') return []
|
|
800
|
+
const list = Array.isArray(navigator.languages) && navigator.languages.length > 0
|
|
801
|
+
? navigator.languages
|
|
802
|
+
: [navigator.language]
|
|
803
|
+
return list.filter((tag) => typeof tag === 'string' && tag)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** 宿主 locale 快照;没有 locale 服务的宿主返回 null。 */
|
|
807
|
+
/** 宿主自己维护的 `<html lang>`:locale 服务切换语言时会同步写它,服务取不到时用它兜底。 */
|
|
808
|
+
function documentLanguage() {
|
|
809
|
+
if (typeof document === 'undefined') return null
|
|
810
|
+
const lang = document?.documentElement?.lang
|
|
811
|
+
return typeof lang === 'string' && lang ? lang : null
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/** 宿主切换语言时会同步改写 `<html lang>`;订阅它,比"等服务挂到 ctx 上"可靠。 */
|
|
815
|
+
function watchDocumentLanguage(onChange) {
|
|
816
|
+
if (typeof MutationObserver === 'undefined' || typeof document === 'undefined'
|
|
817
|
+
|| !document.documentElement) return null
|
|
818
|
+
const observer = new MutationObserver(() => onChange())
|
|
819
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
|
|
820
|
+
return () => observer.disconnect()
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function hostSnapshot(ctx) {
|
|
824
|
+
try {
|
|
825
|
+
// 直属性与 `get` 两种取法都试:不同宿主版本里 locale 服务挂上的时机不同。
|
|
826
|
+
const service = ctx?.locale ?? ctx?.get?.('locale')
|
|
827
|
+
const snapshot = service?.getSnapshot?.()
|
|
828
|
+
return snapshot && typeof snapshot === 'object' ? snapshot : null
|
|
829
|
+
} catch {
|
|
830
|
+
return null
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function resolveLocale(ctx) {
|
|
835
|
+
const snapshot = hostSnapshot(ctx)
|
|
836
|
+
const hostActive = (typeof snapshot?.active === 'string' && snapshot.active)
|
|
837
|
+
|| documentLanguage()
|
|
838
|
+
// 语言表在不同版本里既可能是 `[{ id, label }]` 也可能是纯 id 字符串,两种都认。
|
|
839
|
+
const hostIds = new Set()
|
|
840
|
+
for (const item of Array.isArray(snapshot?.locales) ? snapshot.locales : []) {
|
|
841
|
+
const id = typeof item === 'string' ? item : item?.id
|
|
842
|
+
if (typeof id === 'string' && id) hostIds.add(id)
|
|
843
|
+
}
|
|
844
|
+
// 快照没带语言表(老宿主)时按 DSH 内置的 zh / en 兜底:把"表达不了"误判成"能表达",
|
|
845
|
+
// 西语就会一直落到 en,面板再也拿不到西语。
|
|
846
|
+
if (hostIds.size === 0) {
|
|
847
|
+
hostIds.add('zh')
|
|
848
|
+
hostIds.add('en')
|
|
849
|
+
}
|
|
850
|
+
const wanted = browserLanguages().find((tag) => baseLanguage(tag)) ?? ''
|
|
851
|
+
const wantedBase = baseLanguage(wanted)
|
|
852
|
+
const hostPanel = hostActive ? panelLocaleOf(hostActive) : null
|
|
853
|
+
const browserPanel = wanted ? panelLocaleOf(wanted) : null
|
|
854
|
+
const hostLeftDefault = baseLanguage(hostActive) === 'en'
|
|
855
|
+
// 宿主还停在默认 en 时,面板可以补浏览器明确要的语言(含 zh-Hant / pt-BR 这类带地区的档)。
|
|
856
|
+
if (hostPanel && (!wantedBase || (hostIds.has(wantedBase) && !hostLeftDefault))) return hostPanel
|
|
857
|
+
if (hostPanel && !(hostLeftDefault && browserPanel)) return hostPanel
|
|
858
|
+
return browserPanel ?? hostPanel ?? 'zh'
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** 当前界面语言:`apply` 时按宿主取值,之后由 `locale/change` 改写。 */
|
|
862
|
+
let LOCALE = resolveLocale(null)
|
|
863
|
+
|
|
864
|
+
/** Current-language sentence; `vars` fills the `${name}` placeholders. */
|
|
865
|
+
function T(text, vars) {
|
|
866
|
+
const raw = (MESSAGES[LOCALE] && MESSAGES[LOCALE][text]) || text
|
|
867
|
+
if (!vars) return raw
|
|
868
|
+
return raw.replace(/\$\{(\w+)\}/g, (m, key) => (key in vars ? String(vars[key]) : m))
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** 入口与标题在渲染时取值,跟随语言变化;不要在模块加载时定死。 */
|
|
872
|
+
function panelLabel() {
|
|
873
|
+
return T('移动端连接')
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function panelTitle() {
|
|
877
|
+
return T('移动端连接:二维码、已配对的手机与连接状态')
|
|
878
|
+
}
|
|
879
|
+
|
|
22
880
|
const PRIMITIVES = '@deepseek-ai/dsh-client-ui-primitives'
|
|
881
|
+
/** GitHub 仓库与 iOS 公开测试链接;都只做展示,不参与准入。 */
|
|
882
|
+
const GITHUB_URL = 'https://github.com/jayantTang/DSH_Mobile'
|
|
883
|
+
const TESTFLIGHT_URL = 'https://testflight.apple.com/join/tHKQsbCk'
|
|
23
884
|
/** 小窗打开时的状态轮询间隔;关闭时只为让侧栏状态点别停在旧值上。 */
|
|
24
885
|
const OPEN_POLL_MS = 2500
|
|
886
|
+
/** 已配对设备全量列表的刷新间隔;在线状态仍由 /status 的 2.5 秒轮询负责。 */
|
|
887
|
+
const PAIRED_POLL_MS = 10000
|
|
25
888
|
const IDLE_POLL_MS = 20000
|
|
26
889
|
/** 剩余寿命低于这个值就换一张:只剩几秒的码,扫它失败的原因用户看不见。 */
|
|
27
890
|
const CODE_FLOOR_MS = 60 * 1000
|
|
@@ -33,8 +896,12 @@ const COPIED_MS = 2000
|
|
|
33
896
|
const CSS = [
|
|
34
897
|
'.dsh-mobile-link-entry{display:flex;align-items:center;justify-content:flex-start;gap:8px;width:100%;font-size:13px}',
|
|
35
898
|
'.dsh-mobile-link-entry-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
36
|
-
'.dsh-mobile-link-dialog{width:min(420px,94vw)}',
|
|
37
|
-
'.dsh-mobile-link-
|
|
899
|
+
'.dsh-mobile-link-dialog{width:min(420px,94vw);max-height:calc(100dvh - 2 * max(24px,var(--dsh-frame-overlay-top,24px)))}',
|
|
900
|
+
'.dsh-mobile-link-content{flex:1 1 auto;min-height:0}',
|
|
901
|
+
'.dsh-mobile-link-shell{position:relative;display:flex;flex-direction:column;flex:1 1 auto;min-height:0}',
|
|
902
|
+
// 官方 Modal 不限高;正文自己收进可视区。减去的 102px 是官方头部与内边距
|
|
903
|
+
// (22+24+12 的头部、20 的正文上边距、24 的底部内边距)。
|
|
904
|
+
'.dsh-mobile-link-body{position:relative;display:flex;flex-direction:column;gap:12px;max-height:calc(100dvh - 2 * max(24px,var(--dsh-frame-overlay-top,24px)) - 102px);overflow-y:auto;overscroll-behavior:contain}',
|
|
38
905
|
'.dsh-mobile-link-status{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:500}',
|
|
39
906
|
'.dsh-mobile-link-section-title{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
40
907
|
'.dsh-mobile-link-kv{display:grid;grid-template-columns:auto minmax(0,1fr);gap:8px 16px;font-size:13px}',
|
|
@@ -42,20 +909,58 @@ const CSS = [
|
|
|
42
909
|
'.dsh-mobile-link-v{min-width:0;text-align:right;color:var(--dsw-alias-label-primary,inherit);overflow-wrap:anywhere}',
|
|
43
910
|
'.dsh-mobile-link-mono,.dsh-mobile-link-note,.dsh-mobile-link-device-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
44
911
|
'.dsh-mobile-link-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
45
|
-
'.dsh-mobile-link-card{display:flex;
|
|
46
|
-
'.dsh-mobile-link-qr{display:block;width:
|
|
912
|
+
'.dsh-mobile-link-card{display:flex;align-items:center;gap:12px;padding:12px;border-radius:12px;background:var(--dsw-alias-bg-module-platform,rgba(15,17,21,.04))}',
|
|
913
|
+
'.dsh-mobile-link-qr{display:block;flex:0 0 auto;width:160px;max-width:100%;height:auto;box-sizing:border-box;padding:10px;border-radius:10px;background:#fff}',
|
|
914
|
+
'.dsh-mobile-link-card-main{display:flex;flex-direction:column;align-items:flex-start;gap:10px;flex:1 1 auto;min-width:0}',
|
|
47
915
|
'.dsh-mobile-link-code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:20px;font-weight:600;letter-spacing:.16em;line-height:1;color:var(--dsw-alias-label-primary,inherit)}',
|
|
48
|
-
'.dsh-mobile-link-count{display:flex;flex-direction:column;align-items:
|
|
49
|
-
'.dsh-mobile-link-
|
|
916
|
+
'.dsh-mobile-link-count{display:flex;flex-direction:column;align-items:flex-start;gap:6px;width:100%;font-size:12px;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
917
|
+
'.dsh-mobile-link-count-tag{white-space:nowrap}',
|
|
918
|
+
'.dsh-mobile-link-bar{width:100%;height:3px;border-radius:2px;overflow:hidden;background:var(--dsw-alias-border-secondary,rgba(15,17,21,.12))}',
|
|
50
919
|
'.dsh-mobile-link-bar i{display:block;height:100%;border-radius:2px;background:var(--dsw-alias-state-business-primary,#4176e6);transition:width .9s linear}',
|
|
51
|
-
'.dsh-mobile-link-actions{display:flex;align-items:
|
|
920
|
+
'.dsh-mobile-link-actions{display:flex;flex-direction:column;align-items:stretch;gap:8px;width:100%}',
|
|
52
921
|
'.dsh-mobile-link-section{display:flex;flex-direction:column;gap:8px}',
|
|
53
922
|
'.dsh-mobile-link-devices{display:flex;flex-direction:column;gap:6px;max-height:184px;overflow:auto}',
|
|
54
923
|
'.dsh-mobile-link-device{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;background:var(--dsw-alias-bg-module-platform,rgba(15,17,21,.04))}',
|
|
55
924
|
'.dsh-mobile-link-device-name{font-size:13px;font-weight:500}',
|
|
56
|
-
'.dsh-mobile-link-device
|
|
925
|
+
'.dsh-mobile-link-device-line{display:flex;align-items:center;gap:6px;min-width:0}',
|
|
926
|
+
'.dsh-mobile-link-revoke{flex:0 0 auto;margin-left:auto;border:0;background:transparent;padding:4px 0 4px 8px;font:inherit;font-size:12px;color:var(--dsw-alias-label-tertiary,#8a919d);cursor:pointer}',
|
|
927
|
+
'.dsh-mobile-link-revoke:hover{color:var(--dsw-alias-label-secondary,#5f6672)}',
|
|
57
928
|
'.dsh-mobile-link-error{color:var(--dsw-alias-state-error-primary,#d1242f)}',
|
|
58
929
|
'.dsh-mobile-link-note{font-size:12px;line-height:1.65;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
930
|
+
'.dsh-mobile-link-form{display:flex;flex-direction:column;gap:12px}',
|
|
931
|
+
'.dsh-mobile-link-field{display:flex;flex-direction:column;gap:6px}',
|
|
932
|
+
'.dsh-mobile-link-field>span{width:100%}',
|
|
933
|
+
'.dsh-mobile-link-label{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
|
|
934
|
+
'.dsh-mobile-link-input{width:100%;height:36px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-secondary,rgba(15,17,21,.16));border-radius:8px;padding:0 10px;font-size:13px;font-family:inherit;color:var(--dsw-alias-label-primary,inherit);background:var(--dsw-alias-bg-module-platform,#fff)}',
|
|
935
|
+
'.dsh-mobile-link-input:focus{outline:none;border-color:var(--dsw-alias-state-business-primary,#4176e6);box-shadow:0 0 0 3px rgba(65,118,230,.14)}',
|
|
936
|
+
'.dsh-mobile-link-input[readonly]{opacity:.78}',
|
|
937
|
+
'.dsh-mobile-link-input.dsh-mobile-link-code-input{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em}',
|
|
938
|
+
'.dsh-mobile-link-error-inline{padding:8px 10px;border-radius:8px;font-size:12px;line-height:1.6;color:var(--dsw-alias-state-error-primary,#d1242f);background:var(--dsw-alias-bg-error,rgba(209,36,47,.08))}',
|
|
939
|
+
'.dsh-mobile-link-link{color:var(--dsw-alias-state-business-primary,#4176e6);text-decoration:underline;text-underline-offset:2px}',
|
|
940
|
+
'.dsh-mobile-link-cmd{font-size:11px;line-height:1.65;color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
941
|
+
'.dsh-mobile-link-cmd summary{cursor:pointer;user-select:none}',
|
|
942
|
+
'.dsh-mobile-link-cmd .dsh-mobile-link-mono{margin-top:6px;white-space:nowrap;overflow-x:auto}',
|
|
943
|
+
'.dsh-mobile-link-github{display:flex;flex-direction:column;gap:3px}',
|
|
944
|
+
'.dsh-mobile-link-github-line{display:flex;justify-content:space-between;gap:12px;font-size:13px;align-items:baseline}',
|
|
945
|
+
'.dsh-mobile-link-github-line .dsh-mobile-link-k{color:var(--dsw-alias-label-tertiary,inherit)}',
|
|
946
|
+
'.dsh-mobile-link-github-line a{flex:1 1 auto;min-width:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-state-business-primary,#4176e6);text-decoration:none;text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere;word-break:break-all;text-align:right}',
|
|
947
|
+
'.dsh-mobile-link-welcome{font-size:11px;color:var(--dsw-alias-label-tertiary,inherit);text-align:right}',
|
|
948
|
+
'.dsh-mobile-link-ioslink{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;border-radius:9px;background:var(--dsw-alias-bg-module-platform,rgba(15,17,21,.04));font-size:12px;color:var(--dsw-alias-label-secondary,inherit)}',
|
|
949
|
+
'.dsh-mobile-link-ioslink a{color:var(--dsw-alias-state-business-primary,#4176e6);text-decoration:none;font-weight:600;white-space:nowrap}',
|
|
950
|
+
'.dsh-mobile-link-overlay{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;padding:18px;background:rgba(23,33,52,.30);border-radius:16px}',
|
|
951
|
+
'.dsh-mobile-link-alert{width:min(360px,100%);display:flex;flex-direction:column;gap:12px;padding:16px;border-radius:12px;background:var(--dsw-alias-bg-module-platform,#fff);border:1px solid var(--dsw-alias-border-secondary,#e2e6ec);box-shadow:0 18px 40px rgba(23,33,52,.20)}',
|
|
952
|
+
'.dsh-mobile-link-alert-title{font-size:14px;font-weight:700}',
|
|
953
|
+
'.dsh-mobile-link-alert-actions{display:flex;gap:10px}',
|
|
954
|
+
'.dsh-mobile-link-alert-actions .dsh-mobile-link-btn{flex:1}',
|
|
955
|
+
'.dsh-mobile-link-guide{width:min(420px,100%);max-height:calc(100% - 36px);overflow:auto;display:flex;flex-direction:column;gap:12px;padding:16px;border-radius:12px;background:var(--dsw-alias-bg-module-platform,#fff);border:1px solid var(--dsw-alias-border-secondary,#e2e6ec);box-shadow:0 18px 40px rgba(23,33,52,.20)}',
|
|
956
|
+
'.dsh-mobile-link-guide-title{font-size:15px;font-weight:700}',
|
|
957
|
+
'.dsh-mobile-link-step{display:flex;gap:9px;align-items:flex-start}',
|
|
958
|
+
'.dsh-mobile-link-step-n{flex:0 0 18px;height:18px;border-radius:50%;background:rgba(65,118,230,.12);color:var(--dsw-alias-state-business-primary,#4176e6);display:grid;place-items:center;font-size:10px;font-weight:700}',
|
|
959
|
+
'.dsh-mobile-link-step-t{font-size:12px;line-height:1.6}',
|
|
960
|
+
'.dsh-mobile-link-testflight{padding:9px;border-radius:8px;background:var(--dsw-alias-bg-module-platform,rgba(15,17,21,.04));font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--dsw-alias-state-business-primary,#4176e6);overflow-wrap:anywhere}',
|
|
961
|
+
'.dsh-mobile-link-alert-actions>*{flex:1}',
|
|
962
|
+
'.dsh-mobile-link-danger{height:36px;border:0;border-radius:9px;background:var(--dsw-alias-state-error-primary,#d1242f);color:#fff;font:inherit;font-size:12px;font-weight:600;cursor:pointer}',
|
|
963
|
+
'.dsh-mobile-link-danger:disabled{opacity:.65;cursor:default}',
|
|
59
964
|
].join('')
|
|
60
965
|
|
|
61
966
|
function report(ctx, message) {
|
|
@@ -129,16 +1034,21 @@ async function requestJson(path, init) {
|
|
|
129
1034
|
try {
|
|
130
1035
|
response = await fetch(path, init)
|
|
131
1036
|
} catch (error) {
|
|
132
|
-
throw new Error(
|
|
1037
|
+
throw new Error(T('连不上本机连接器:${error}', { error: describe(error) }))
|
|
133
1038
|
}
|
|
134
1039
|
let body
|
|
135
1040
|
try {
|
|
136
1041
|
body = await response.json()
|
|
137
1042
|
} catch {
|
|
138
|
-
throw new Error(
|
|
1043
|
+
throw new Error(T('连接器应答无法解析(HTTP ${status})', { status: response.status }))
|
|
139
1044
|
}
|
|
140
1045
|
if (!response.ok || body?.ok === false) {
|
|
141
|
-
|
|
1046
|
+
const error = new Error(body?.message ?? T('连接器应答 HTTP ${status}', { status: response.status }))
|
|
1047
|
+
// Keep the machine code: the panel maps it to Chinese copy and must not
|
|
1048
|
+
// parse English messages to tell "used" from "expired".
|
|
1049
|
+
error.code = body?.code
|
|
1050
|
+
error.status = response.status
|
|
1051
|
+
throw error
|
|
142
1052
|
}
|
|
143
1053
|
return body
|
|
144
1054
|
}
|
|
@@ -149,12 +1059,12 @@ function needsEnroll(status) {
|
|
|
149
1059
|
}
|
|
150
1060
|
/** 状态点与状态词的唯一来源:侧栏入口与标题行共用,不会出现"点上是绿的、文字说断开"。 */
|
|
151
1061
|
function connectionState(state) {
|
|
152
|
-
if (state.statusError) return { dot: 'error', text: '状态未知' }
|
|
153
|
-
if (!state.status) return { dot: 'ongoing', text: '
|
|
154
|
-
if (needsEnroll(state.status)) return { dot: 'error', text: '
|
|
155
|
-
if (state.status.enabled === false) return { dot: 'idle', text: '连接器已停用' }
|
|
156
|
-
if (state.status.connected) return { dot: 'done', text: '
|
|
157
|
-
return { dot: 'warning', text: '
|
|
1062
|
+
if (state.statusError) return { dot: 'error', text: T('状态未知') }
|
|
1063
|
+
if (!state.status) return { dot: 'ongoing', text: T('正在读取状态…') }
|
|
1064
|
+
if (needsEnroll(state.status)) return { dot: 'error', text: T('此电脑尚未登记') }
|
|
1065
|
+
if (state.status.enabled === false) return { dot: 'idle', text: T('连接器已停用') }
|
|
1066
|
+
if (state.status.connected) return { dot: 'done', text: T('已连接') }
|
|
1067
|
+
return { dot: 'warning', text: T('未连接,正在重新连接') }
|
|
158
1068
|
}
|
|
159
1069
|
/** 手机报上来的版本:`hello` 存的是 `{name,version,build,label}`,直接插值会渲染成 [object Object]。 */
|
|
160
1070
|
function clientLabel(client) {
|
|
@@ -165,9 +1075,46 @@ function clientLabel(client) {
|
|
|
165
1075
|
/** 在线/总数分开说:`deviceCount` 把"连着但已离线"的记录也算进去了。 */
|
|
166
1076
|
function deviceSummary(devices) {
|
|
167
1077
|
const list = Array.isArray(devices) ? devices : []
|
|
168
|
-
if (list.length === 0) return '
|
|
1078
|
+
if (list.length === 0) return T('暂无已配对设备')
|
|
169
1079
|
const online = list.filter((device) => device.offlineSince === null || device.offlineSince === undefined).length
|
|
170
|
-
return online === list.length
|
|
1080
|
+
return online === list.length
|
|
1081
|
+
? T('${online} 台在线', { online })
|
|
1082
|
+
: T('${online} 台在线,共 ${total} 台', { online, total: list.length })
|
|
1083
|
+
}
|
|
1084
|
+
/** 全量已配对设备 + /status 的在线标记;离线设备也要有行。 */
|
|
1085
|
+
function pairedRows(state) {
|
|
1086
|
+
// 全量列表是行集合的所有者;/status 只补在线标记与运行时元数据。
|
|
1087
|
+
// 反过来把 status 独有项也拼进来,会把已撤销但本地 router 还留着的记录
|
|
1088
|
+
// 重新显示出来(计数与行数再次不一致)。
|
|
1089
|
+
const roster = Array.isArray(state.pairedDevices) ? state.pairedDevices : null
|
|
1090
|
+
const base = roster ?? (Array.isArray(state.status?.devices) ? state.status.devices : [])
|
|
1091
|
+
const rows = new Map()
|
|
1092
|
+
for (const device of base) {
|
|
1093
|
+
if (device?.deviceId) rows.set(device.deviceId, { ...device })
|
|
1094
|
+
}
|
|
1095
|
+
const statusDevices = Array.isArray(state.status?.devices) ? state.status.devices : []
|
|
1096
|
+
for (const device of statusDevices) {
|
|
1097
|
+
const existing = rows.get(device.deviceId)
|
|
1098
|
+
if (!existing) continue
|
|
1099
|
+
rows.set(device.deviceId, { ...existing, ...device })
|
|
1100
|
+
}
|
|
1101
|
+
const online = new Set(statusDevices
|
|
1102
|
+
.filter((device) => device.offlineSince === null || device.offlineSince === undefined)
|
|
1103
|
+
.map((device) => device.deviceId))
|
|
1104
|
+
return [...rows.values()].map((device) => ({ ...device, online: online.has(device.deviceId) }))
|
|
1105
|
+
}
|
|
1106
|
+
function pairedTotal(state) {
|
|
1107
|
+
if (typeof state.pairedCount === 'number') return state.pairedCount
|
|
1108
|
+
if (Array.isArray(state.pairedDevices)) return state.pairedDevices.length
|
|
1109
|
+
return Array.isArray(state.status?.devices) ? state.status.devices.length : 0
|
|
1110
|
+
}
|
|
1111
|
+
function deviceSummaryOf(state) {
|
|
1112
|
+
const total = pairedTotal(state)
|
|
1113
|
+
if (total === 0) return T('暂无已配对设备')
|
|
1114
|
+
const online = pairedRows(state).filter((device) => device.online).length
|
|
1115
|
+
return online === total
|
|
1116
|
+
? T('${total} 台在线', { total })
|
|
1117
|
+
: T('${online} 台在线,共 ${total} 台已配对', { online, total })
|
|
171
1118
|
}
|
|
172
1119
|
function clockText(expiresAt) {
|
|
173
1120
|
const date = new Date(Number(expiresAt))
|
|
@@ -191,6 +1138,21 @@ function createPanel() {
|
|
|
191
1138
|
copied: false,
|
|
192
1139
|
showError: false,
|
|
193
1140
|
now: Date.now(),
|
|
1141
|
+
// 登记表单只属于本次界面:草稿不落 localStorage,提交成功后清空。
|
|
1142
|
+
enrolling: false,
|
|
1143
|
+
enrollDraft: { relay: '', relayTouched: false },
|
|
1144
|
+
enrollError: null,
|
|
1145
|
+
pairedDevices: null,
|
|
1146
|
+
pairedMax: null,
|
|
1147
|
+
pairedCount: null,
|
|
1148
|
+
pairedError: null,
|
|
1149
|
+
pairedBusy: false,
|
|
1150
|
+
revokeTarget: null,
|
|
1151
|
+
revoking: false,
|
|
1152
|
+
revokeError: null,
|
|
1153
|
+
guideOpen: false,
|
|
1154
|
+
// 界面语言:由宿主 locale 决定,宿主切换时写入这份快照以推动重渲染。
|
|
1155
|
+
locale: LOCALE,
|
|
194
1156
|
}),
|
|
195
1157
|
disposed: false,
|
|
196
1158
|
style: null,
|
|
@@ -203,13 +1165,47 @@ function createPanel() {
|
|
|
203
1165
|
async function loadStatus(panel) {
|
|
204
1166
|
try {
|
|
205
1167
|
const status = await requestJson(STATUS_ROUTE, { method: 'GET' })
|
|
206
|
-
if (panel.disposed) return
|
|
1168
|
+
if (panel.disposed) return undefined
|
|
207
1169
|
panel.store.set({ status, statusError: null })
|
|
1170
|
+
return status
|
|
208
1171
|
} catch (error) {
|
|
209
|
-
if (panel.disposed) return
|
|
1172
|
+
if (panel.disposed) return undefined
|
|
210
1173
|
panel.store.set({ statusError: describe(error) })
|
|
1174
|
+
return undefined
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* 读中转上的全部已配对设备(含离线)。/status 里的 devices 只含当前接上的,
|
|
1179
|
+
* 单看它会把离线设备藏起来,导致“已配对 2/8”却只列出一台。二者在这里合并:
|
|
1180
|
+
* 全量列表决定行与配额,/status 决定在线标记。
|
|
1181
|
+
*/
|
|
1182
|
+
async function loadPairedDevices(panel) {
|
|
1183
|
+
if (panel.disposed) return
|
|
1184
|
+
const status = panel.store.get().status
|
|
1185
|
+
if (!status || needsEnroll(status)) {
|
|
1186
|
+
panel.store.set({ pairedDevices: null, pairedCount: null, pairedMax: null, pairedError: null })
|
|
1187
|
+
return
|
|
1188
|
+
}
|
|
1189
|
+
panel.store.set({ pairedBusy: true })
|
|
1190
|
+
try {
|
|
1191
|
+
const body = await requestJson(DEVICES_ROUTE, { method: 'GET' })
|
|
1192
|
+
if (panel.disposed) return
|
|
1193
|
+
panel.store.set({
|
|
1194
|
+
pairedDevices: Array.isArray(body.devices) ? body.devices : [],
|
|
1195
|
+
pairedCount: body.pairedDevices ?? null,
|
|
1196
|
+
pairedMax: body.maxPairedDevices ?? null,
|
|
1197
|
+
pairedError: null,
|
|
1198
|
+
pairedBusy: false,
|
|
1199
|
+
})
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
if (panel.disposed) return
|
|
1202
|
+
panel.store.set({
|
|
1203
|
+
pairedError: T('无法读取全部已配对设备。'),
|
|
1204
|
+
pairedBusy: false,
|
|
1205
|
+
})
|
|
211
1206
|
}
|
|
212
1207
|
}
|
|
1208
|
+
|
|
213
1209
|
/**
|
|
214
1210
|
* 取一张码。
|
|
215
1211
|
*
|
|
@@ -226,6 +1222,12 @@ async function refreshCode(panel, { regenerate = false } = {}) {
|
|
|
226
1222
|
panel.store.set({ code: null, codeError: null })
|
|
227
1223
|
return
|
|
228
1224
|
}
|
|
1225
|
+
// 登记成功 ≠ 已连接:链路还没起来时去取码只会失败,界面应该显示
|
|
1226
|
+
// "正在连接中转",而不是把一次连接等待误报成取码失败。
|
|
1227
|
+
if (panel.store.get().status?.connected !== true) {
|
|
1228
|
+
panel.store.set({ code: null, codeError: null })
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
229
1231
|
// 只有**持久码**才复用:限时码到期就得重新取一张(老行为),否则屏幕上会留一张死码。
|
|
230
1232
|
if (!regenerate && panel.store.get().code?.persistent) return
|
|
231
1233
|
panel.store.set({ busy: true, codeError: null })
|
|
@@ -272,12 +1274,26 @@ function tick(panel) {
|
|
|
272
1274
|
panel.lastStatusAt = now
|
|
273
1275
|
void loadStatus(panel)
|
|
274
1276
|
}
|
|
1277
|
+
if (now - (panel.lastPairedAt ?? 0) >= PAIRED_POLL_MS
|
|
1278
|
+
&& !panel.store.get().pairedBusy
|
|
1279
|
+
&& panel.store.get().status
|
|
1280
|
+
&& !needsEnroll(panel.store.get().status)) {
|
|
1281
|
+
panel.lastPairedAt = now
|
|
1282
|
+
void loadPairedDevices(panel)
|
|
1283
|
+
}
|
|
275
1284
|
// 持久码不因时间换码(032):只有用户点「重新生成」才换。限时码(老中转/老调用方)
|
|
276
1285
|
// 仍然按原来的规则在快过期时换一张,免得把一张马上就要失效的码摆在屏幕上。
|
|
277
|
-
const
|
|
1286
|
+
const state = panel.store.get()
|
|
1287
|
+
const code = state.code
|
|
278
1288
|
if (code && !code.persistent && code.expiresAt != null && code.expiresAt - now <= CODE_FLOOR_MS) {
|
|
279
1289
|
void refreshCode(panel)
|
|
280
1290
|
}
|
|
1291
|
+
// 连接恢复/登记完成后自动补一张码;取码失败后不自动重试(会把中转打爆,
|
|
1292
|
+
// 也让"重试"按钮失去意义),由用户点按钮重试。
|
|
1293
|
+
if (state.status?.connected === true && !needsEnroll(state.status)
|
|
1294
|
+
&& !state.code && !state.codeError && !state.busy) {
|
|
1295
|
+
void refreshCode(panel)
|
|
1296
|
+
}
|
|
281
1297
|
}
|
|
282
1298
|
function openPanel(panel) {
|
|
283
1299
|
if (panel.disposed) return
|
|
@@ -285,8 +1301,12 @@ function openPanel(panel) {
|
|
|
285
1301
|
if (!panel.style?.node?.isConnected) panel.style = ensureStyle()
|
|
286
1302
|
panel.store.set({ open: true, now: Date.now(), copied: false })
|
|
287
1303
|
panel.lastStatusAt = Date.now()
|
|
288
|
-
void loadStatus(panel)
|
|
289
|
-
|
|
1304
|
+
void loadStatus(panel).then((status) => {
|
|
1305
|
+
if (!panel.disposed && status?.connected === true && !needsEnroll(status)) {
|
|
1306
|
+
void refreshCode(panel)
|
|
1307
|
+
void loadPairedDevices(panel)
|
|
1308
|
+
}
|
|
1309
|
+
})
|
|
290
1310
|
}
|
|
291
1311
|
function closePanel(panel) {
|
|
292
1312
|
if (panel.disposed) return
|
|
@@ -334,6 +1354,155 @@ function copyCode(panel) {
|
|
|
334
1354
|
done()
|
|
335
1355
|
}
|
|
336
1356
|
}
|
|
1357
|
+
/** 服务端的机器码 → 面板文案;原始 message 留在 details 里给排障。 */
|
|
1358
|
+
function enrollTexts() {
|
|
1359
|
+
return {
|
|
1360
|
+
'enroll/bad-request': T('请输入中转地址。'),
|
|
1361
|
+
'enroll/invalid-relay': T('中转地址无效。'),
|
|
1362
|
+
'enroll/placeholder-relay': T('请输入有效的中转地址。'),
|
|
1363
|
+
'enroll/rate-limited': T('尝试次数过多,请稍后重试。'),
|
|
1364
|
+
'enroll/unreachable': T('无法连接中转服务器,请检查地址与网络。'),
|
|
1365
|
+
'enroll/timeout': T('中转服务器未响应,请稍后重试。'),
|
|
1366
|
+
'enroll/bad-response': T('中转服务器返回了无法识别的响应。'),
|
|
1367
|
+
'enroll/already-registered': T('此电脑已登记。'),
|
|
1368
|
+
'enroll/disabled': T('连接器已停用。'),
|
|
1369
|
+
'enroll/unavailable': T('连接器正在停止,请稍后重试。'),
|
|
1370
|
+
'enroll/state-write': T('登记信息未能保存到此电脑,请检查磁盘权限。'),
|
|
1371
|
+
'enroll/relay-error': T('中转服务器登记失败,请稍后重试。'),
|
|
1372
|
+
'enroll/internal': T('连接器内部错误,请稍后重试。'),
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function enrollMessage(error) {
|
|
1376
|
+
// 按调用时的语言现算:模块加载时定死会让宿主切换语言后错误提示停在旧语言。
|
|
1377
|
+
return enrollTexts()[error?.code] ?? describe(error) ?? T('登记失败,请重试。')
|
|
1378
|
+
}
|
|
1379
|
+
function revokeMessage(error) {
|
|
1380
|
+
const code = error?.code
|
|
1381
|
+
if (code === 'request/unknown-device') return T('设备不存在或已被撤销。')
|
|
1382
|
+
if (code === 'auth/agent-mismatch') return T('这台设备不属于此电脑。')
|
|
1383
|
+
return T('撤销失败,请稍后重试。')
|
|
1384
|
+
}
|
|
1385
|
+
/** 表单字段只改草稿;不在这里写“是否已登记”之类的业务状态。 */
|
|
1386
|
+
function setEnrollField(panel, field, value) {
|
|
1387
|
+
if (panel.disposed) return
|
|
1388
|
+
if (field !== 'relay') return
|
|
1389
|
+
const draft = { ...panel.store.get().enrollDraft, [field]: String(value ?? '') }
|
|
1390
|
+
draft.relayTouched = true
|
|
1391
|
+
panel.store.set({ enrollDraft: draft, enrollError: null })
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* 提交登记。
|
|
1395
|
+
*
|
|
1396
|
+
* 开放登记:只提交中转地址;电脑名称由连接器自动获取。成功时先乐观地切到
|
|
1397
|
+
* “已登记 / 正在连接”,随后用 `/status` 覆盖。
|
|
1398
|
+
*/
|
|
1399
|
+
async function submitEnroll(panel) {
|
|
1400
|
+
if (panel.disposed || panel.store.get().enrolling) return
|
|
1401
|
+
const state = panel.store.get()
|
|
1402
|
+
const status = state.status
|
|
1403
|
+
const draft = state.enrollDraft
|
|
1404
|
+
const locked = Boolean(status?.enroll?.relayLocked)
|
|
1405
|
+
const relayUrl = (locked
|
|
1406
|
+
? status?.enroll?.relayPrefill
|
|
1407
|
+
: (draft.relayTouched ? draft.relay : status?.enroll?.relayPrefill) || '').trim()
|
|
1408
|
+
if (!relayUrl) {
|
|
1409
|
+
panel.store.set({
|
|
1410
|
+
enrollError: { code: 'enroll/bad-request', message: T('请输入中转地址。') },
|
|
1411
|
+
})
|
|
1412
|
+
return
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
panel.store.set({ enrolling: true, enrollError: null })
|
|
1416
|
+
try {
|
|
1417
|
+
const body = await requestJson(ENROLL_ROUTE, {
|
|
1418
|
+
method: 'POST',
|
|
1419
|
+
headers: { 'content-type': 'application/json' },
|
|
1420
|
+
body: JSON.stringify({ relayUrl }),
|
|
1421
|
+
})
|
|
1422
|
+
if (panel.disposed) return
|
|
1423
|
+
const previous = panel.store.get().status ?? {}
|
|
1424
|
+
const enroll = previous.enroll ?? {}
|
|
1425
|
+
panel.store.set({
|
|
1426
|
+
enrolling: false,
|
|
1427
|
+
enrollDraft: { relay: '', relayTouched: false },
|
|
1428
|
+
enrollError: null,
|
|
1429
|
+
status: {
|
|
1430
|
+
...previous,
|
|
1431
|
+
state: previous.connected ? previous.state : 'connecting',
|
|
1432
|
+
connected: previous.connected === true,
|
|
1433
|
+
relayUrl: body.relayUrl ?? previous.relayUrl,
|
|
1434
|
+
agentId: body.agentId ?? previous.agentId,
|
|
1435
|
+
agentName: body.agentName ?? previous.agentName,
|
|
1436
|
+
enroll: {
|
|
1437
|
+
...enroll,
|
|
1438
|
+
registered: true,
|
|
1439
|
+
needsEnroll: false,
|
|
1440
|
+
relayUrl: body.relayUrl ?? enroll.relayUrl ?? null,
|
|
1441
|
+
hint: null,
|
|
1442
|
+
command: null,
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
statusError: null,
|
|
1446
|
+
})
|
|
1447
|
+
const next = await loadStatus(panel)
|
|
1448
|
+
if (!panel.disposed && next?.connected === true && !needsEnroll(next)) {
|
|
1449
|
+
void refreshCode(panel)
|
|
1450
|
+
void loadPairedDevices(panel)
|
|
1451
|
+
}
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (panel.disposed) return
|
|
1454
|
+
panel.store.set({
|
|
1455
|
+
enrolling: false,
|
|
1456
|
+
enrollError: {
|
|
1457
|
+
code: error?.code ?? 'enroll/internal',
|
|
1458
|
+
message: enrollMessage(error),
|
|
1459
|
+
detail: describe(error),
|
|
1460
|
+
},
|
|
1461
|
+
})
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
/** 打开撤销确认:只记录目标,不发送请求。 */
|
|
1466
|
+
function requestRevoke(panel, device) {
|
|
1467
|
+
if (panel.disposed || !device?.deviceId) return
|
|
1468
|
+
panel.store.set({ revokeTarget: device, revokeError: null, guideOpen: false })
|
|
1469
|
+
}
|
|
1470
|
+
function cancelRevoke(panel) {
|
|
1471
|
+
if (panel.disposed || panel.store.get().revoking) return
|
|
1472
|
+
panel.store.set({ revokeTarget: null, revokeError: null })
|
|
1473
|
+
}
|
|
1474
|
+
/** 确认后才调用宿主路由;成功后刷新状态,失败留在确认窗口里。 */
|
|
1475
|
+
async function confirmRevoke(panel) {
|
|
1476
|
+
const target = panel.store.get().revokeTarget
|
|
1477
|
+
if (panel.disposed || !target || panel.store.get().revoking) return
|
|
1478
|
+
panel.store.set({ revoking: true, revokeError: null })
|
|
1479
|
+
try {
|
|
1480
|
+
await requestJson(REVOKE_ROUTE, {
|
|
1481
|
+
method: 'POST',
|
|
1482
|
+
headers: { 'content-type': 'application/json' },
|
|
1483
|
+
body: JSON.stringify({ deviceId: target.deviceId }),
|
|
1484
|
+
})
|
|
1485
|
+
if (panel.disposed) return
|
|
1486
|
+
panel.store.set({ revoking: false, revokeTarget: null, revokeError: null })
|
|
1487
|
+
await loadStatus(panel)
|
|
1488
|
+
await loadPairedDevices(panel)
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (panel.disposed) return
|
|
1491
|
+
panel.store.set({
|
|
1492
|
+
revoking: false,
|
|
1493
|
+
revokeError: { message: revokeMessage(error), detail: describe(error) },
|
|
1494
|
+
})
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function openGuide(panel) {
|
|
1498
|
+
if (panel.disposed) return
|
|
1499
|
+
panel.store.set({ guideOpen: true, revokeTarget: null, revokeError: null })
|
|
1500
|
+
}
|
|
1501
|
+
function closeGuide(panel) {
|
|
1502
|
+
if (panel.disposed) return
|
|
1503
|
+
panel.store.set({ guideOpen: false })
|
|
1504
|
+
}
|
|
1505
|
+
|
|
337
1506
|
/**
|
|
338
1507
|
* 控制器:视图只读它的 store、只调它的动作。
|
|
339
1508
|
*
|
|
@@ -350,6 +1519,14 @@ function createController() {
|
|
|
350
1519
|
refreshCode: (options) => refreshCode(panel, options),
|
|
351
1520
|
copy: () => copyCode(panel),
|
|
352
1521
|
toggleError: () => panel.store.set({ showError: !panel.store.get().showError }),
|
|
1522
|
+
enroll: () => submitEnroll(panel),
|
|
1523
|
+
setEnrollField: (field, value) => setEnrollField(panel, field, value),
|
|
1524
|
+
reloadStatus: () => { void loadStatus(panel).then(() => loadPairedDevices(panel)) },
|
|
1525
|
+
requestRevoke: (device) => requestRevoke(panel, device),
|
|
1526
|
+
cancelRevoke: () => cancelRevoke(panel),
|
|
1527
|
+
confirmRevoke: () => { void confirmRevoke(panel) },
|
|
1528
|
+
openGuide: () => openGuide(panel),
|
|
1529
|
+
closeGuide: () => closeGuide(panel),
|
|
353
1530
|
start: () => startPanel(panel),
|
|
354
1531
|
}
|
|
355
1532
|
}
|
|
@@ -362,10 +1539,10 @@ function makeEntry(React, controller, ui) {
|
|
|
362
1539
|
const conn = connectionState(state)
|
|
363
1540
|
return React.createElement(ui.Button, {
|
|
364
1541
|
variant: 'ghost', type: 'button', id: ENTRY_ID, className: 'dsh-mobile-link-entry',
|
|
365
|
-
title: `${
|
|
1542
|
+
title: `${panelTitle()}(${conn.text})`, 'aria-label': `${panelLabel()}:${conn.text}`,
|
|
366
1543
|
onClick: () => controller.open(),
|
|
367
1544
|
}, React.createElement(ui.StateDot, { state: conn.dot }),
|
|
368
|
-
React.createElement('span', { className: 'dsh-mobile-link-entry-label' },
|
|
1545
|
+
React.createElement('span', { className: 'dsh-mobile-link-entry-label' }, panelLabel()))
|
|
369
1546
|
}
|
|
370
1547
|
}
|
|
371
1548
|
function row(React, key, value, mono = false) {
|
|
@@ -383,119 +1560,294 @@ function statusSection(React, ui, state) {
|
|
|
383
1560
|
React.createElement(ui.StateDot, { state: conn.dot }),
|
|
384
1561
|
React.createElement('span', null, conn.text)),
|
|
385
1562
|
state.statusError ? React.createElement('div', { className: 'dsh-mobile-link-note dsh-mobile-link-error' },
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
1563
|
+
T('无法读取连接状态。')) : null,
|
|
1564
|
+
state.status && !needsEnroll(state.status)
|
|
1565
|
+
? row(React, T('中转服务器'), state.status.relayUrl ?? '—', true)
|
|
1566
|
+
: null,
|
|
1567
|
+
state.status && !needsEnroll(state.status)
|
|
1568
|
+
? row(React, T('设备'), deviceSummaryOf(state))
|
|
1569
|
+
: null)
|
|
390
1570
|
}
|
|
391
|
-
/**
|
|
392
|
-
function
|
|
1571
|
+
/** 官方 Input 在旧宿主可能不存在;没有就用原生 input,表单不会因此消失。 */
|
|
1572
|
+
function enrollInput(React, ui, props) {
|
|
1573
|
+
const official = ui?.Input != null
|
|
1574
|
+
const Input = official ? ui.Input : 'input'
|
|
1575
|
+
const { className, ...rest } = props
|
|
1576
|
+
return React.createElement(Input, official
|
|
1577
|
+
? rest
|
|
1578
|
+
: { className: ['dsh-mobile-link-input', className].filter(Boolean).join(' '), ...rest })
|
|
1579
|
+
}
|
|
1580
|
+
function enrollField(React, id, labelText, children) {
|
|
1581
|
+
return React.createElement('div', { className: 'dsh-mobile-link-field', key: id },
|
|
1582
|
+
React.createElement('label', { className: 'dsh-mobile-link-label', htmlFor: id }, labelText),
|
|
1583
|
+
children)
|
|
1584
|
+
}
|
|
1585
|
+
/** 旧宿主没有面板登记能力时,退回命令行提示。 */
|
|
1586
|
+
function enrollCommandSection(React, status) {
|
|
393
1587
|
return React.createElement('div', { className: 'dsh-mobile-link-section' },
|
|
394
1588
|
React.createElement('div', { className: 'dsh-mobile-link-note' },
|
|
395
|
-
'
|
|
1589
|
+
T('此电脑尚未登记。请使用命令行完成登记:')),
|
|
396
1590
|
React.createElement('div', { className: 'dsh-mobile-link-mono dsh-mobile-link-v', style: { textAlign: 'left' } },
|
|
397
|
-
status?.enroll?.command ?? 'dsh-mobile-link enroll --
|
|
1591
|
+
status?.enroll?.command ?? T('dsh-mobile-link enroll --relay <中转地址>')))
|
|
398
1592
|
}
|
|
399
1593
|
/**
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
1594
|
+
* 未登记:开放登记。只提交中转地址;电脑名称由连接器自动获取。
|
|
1595
|
+
* 旧宿主没有 `enroll.panel` 时继续显示命令,作为半升级回退。
|
|
1596
|
+
*/
|
|
1597
|
+
function enrollSection(React, controller, ui, state) {
|
|
1598
|
+
const status = state.status
|
|
1599
|
+
if (status?.enroll?.panel !== true) return enrollCommandSection(React, status)
|
|
1600
|
+
if (status.enabled === false) {
|
|
1601
|
+
return React.createElement('div', { className: 'dsh-mobile-link-section' },
|
|
1602
|
+
React.createElement('div', { className: 'dsh-mobile-link-note' },
|
|
1603
|
+
T('连接器已停用。请在插件配置中启用后重试。')))
|
|
1604
|
+
}
|
|
1605
|
+
const enroll = status.enroll
|
|
1606
|
+
const locked = Boolean(enroll.relayLocked)
|
|
1607
|
+
const prefill = typeof enroll.relayPrefill === 'string' ? enroll.relayPrefill : ''
|
|
1608
|
+
const draft = state.enrollDraft ?? { relay: '', relayTouched: false }
|
|
1609
|
+
const relayValue = locked ? prefill : (draft.relayTouched ? draft.relay : prefill)
|
|
1610
|
+
const submitting = Boolean(state.enrolling)
|
|
1611
|
+
const error = state.enrollError
|
|
1612
|
+
const setField = (field) => (event) => controller.setEnrollField(field, event?.target?.value ?? '')
|
|
1613
|
+
const submitOnEnter = (event) => {
|
|
1614
|
+
if (event?.key !== 'Enter') return
|
|
1615
|
+
event.preventDefault?.()
|
|
1616
|
+
if (!submitting) controller.enroll()
|
|
1617
|
+
}
|
|
1618
|
+
const command = enroll.command ?? T('dsh-mobile-link enroll --relay <中转地址>')
|
|
1619
|
+
|
|
1620
|
+
return React.createElement('div', { className: 'dsh-mobile-link-form' },
|
|
1621
|
+
React.createElement('div', { className: 'dsh-mobile-link-note', key: 'intro' },
|
|
1622
|
+
T('登记此电脑以连接中转服务器。')),
|
|
1623
|
+
enrollField(React, 'mobile-link-relay', T('中转地址'),
|
|
1624
|
+
enrollInput(React, ui, {
|
|
1625
|
+
id: 'mobile-link-relay', type: 'text', inputMode: 'url', value: relayValue,
|
|
1626
|
+
placeholder: T('wss://<中转地址>/dsh-link'), readOnly: locked, disabled: submitting,
|
|
1627
|
+
autoComplete: 'off', spellCheck: false,
|
|
1628
|
+
onChange: setField('relay'), onKeyDown: submitOnEnter,
|
|
1629
|
+
})),
|
|
1630
|
+
React.createElement('div', { className: 'dsh-mobile-link-note', key: 'relay-hint' },
|
|
1631
|
+
locked ? T('由配置指定,不可修改。') : T('已预填,可修改。')),
|
|
1632
|
+
error ? React.createElement('div', {
|
|
1633
|
+
className: 'dsh-mobile-link-error-inline', role: 'alert', key: 'error',
|
|
1634
|
+
title: error.detail ?? undefined,
|
|
1635
|
+
}, error.message) : null,
|
|
1636
|
+
React.createElement(ui.Button, {
|
|
1637
|
+
key: 'submit', variant: 'primary', type: 'button', disabled: submitting,
|
|
1638
|
+
onClick: () => controller.enroll(),
|
|
1639
|
+
}, submitting ? T('正在登记…') : (error ? T('重试') : T('登记'))),
|
|
1640
|
+
React.createElement('div', { className: 'dsh-mobile-link-note', key: 'footnote' },
|
|
1641
|
+
T('登记后将自动连接。')),
|
|
1642
|
+
React.createElement('details', { className: 'dsh-mobile-link-cmd', key: 'cli' },
|
|
1643
|
+
React.createElement('summary', null, T('命令行登记(适用于无界面环境)')),
|
|
1644
|
+
React.createElement('div', { className: 'dsh-mobile-link-mono' }, command)))
|
|
1645
|
+
}
|
|
1646
|
+
function githubSection(React) {
|
|
1647
|
+
return React.createElement('div', { className: 'dsh-mobile-link-github', key: 'github' },
|
|
1648
|
+
React.createElement('div', { className: 'dsh-mobile-link-github-line' },
|
|
1649
|
+
React.createElement('span', { className: 'dsh-mobile-link-k' }, T('GitHub 仓库')),
|
|
1650
|
+
React.createElement('a', { href: GITHUB_URL, target: '_blank', rel: 'noreferrer' },
|
|
1651
|
+
'github.com/jayantTang/DSH_Mobile')),
|
|
1652
|
+
React.createElement('div', { className: 'dsh-mobile-link-welcome' },
|
|
1653
|
+
T('欢迎在 GitHub 上为项目加星标。')))
|
|
1654
|
+
}
|
|
1655
|
+
function iosLinkSection(React, controller) {
|
|
1656
|
+
return React.createElement('div', { className: 'dsh-mobile-link-ioslink', key: 'ios-link' },
|
|
1657
|
+
React.createElement('span', null, T('尚未安装 iOS App?')),
|
|
1658
|
+
React.createElement('a', {
|
|
1659
|
+
href: '#', onClick: (event) => { event?.preventDefault?.(); controller.openGuide() },
|
|
1660
|
+
}, T('查看安装方式')))
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* 二维码卡。默认是持久码,所以写“长期有效直到重新生成”,不再有倒计时;
|
|
1664
|
+
* 限时码(老中转/显式请求)仍然显示剩余时间。
|
|
405
1665
|
*/
|
|
406
1666
|
function codeSection(React, controller, ui, state) {
|
|
407
1667
|
const code = state.code
|
|
408
|
-
const
|
|
1668
|
+
const connecting = !code && !state.codeError && state.status?.connected !== true
|
|
1669
|
+
const main = []
|
|
409
1670
|
if (code) {
|
|
410
|
-
|
|
411
|
-
// 二维码:宿主给的 SVG 直接进 data URI,界面不碰图案本身。
|
|
412
|
-
children.push(React.createElement('img', {
|
|
413
|
-
key: 'qr', className: 'dsh-mobile-link-qr', alt: '配对二维码',
|
|
414
|
-
src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(String(code.svg))}`,
|
|
415
|
-
}))
|
|
1671
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-code', key: 'code' }, code.code))
|
|
416
1672
|
if (code.persistent || code.expiresAt == null) {
|
|
417
|
-
|
|
418
|
-
React.createElement('span', null,
|
|
419
|
-
|
|
420
|
-
|
|
1673
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-count', key: 'count' },
|
|
1674
|
+
React.createElement('span', null, T('长期有效直到重新生成')),
|
|
1675
|
+
code.maxPairedDevices
|
|
1676
|
+
? React.createElement('span', { className: 'dsh-mobile-link-count-tag' },
|
|
1677
|
+
T('(已配对 ${paired}/${max})', {
|
|
1678
|
+
paired: state.pairedCount ?? (Array.isArray(state.pairedDevices)
|
|
1679
|
+
? state.pairedDevices.length : (code.pairedDevices ?? 0)),
|
|
1680
|
+
max: code.maxPairedDevices,
|
|
1681
|
+
}))
|
|
1682
|
+
: null))
|
|
421
1683
|
} else {
|
|
422
1684
|
const remaining = Math.max(0, code.expiresAt - state.now)
|
|
423
1685
|
const percent = Math.max(0, Math.min(100, (remaining / (code.ttlMs || CODE_TTL_MS)) * 100))
|
|
424
|
-
|
|
1686
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-count', key: 'count' },
|
|
425
1687
|
React.createElement('div', { className: 'dsh-mobile-link-bar' }, React.createElement('i', { style: { width: `${percent.toFixed(1)}%` } })),
|
|
426
|
-
React.createElement('span', null,
|
|
1688
|
+
React.createElement('span', null, T('有效期至 ${hasta}(剩余 ${resto})', { hasta: clockText(code.expiresAt), resto: remainingText(remaining) }))))
|
|
427
1689
|
}
|
|
428
1690
|
} else if (state.codeError) {
|
|
429
|
-
|
|
1691
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-note dsh-mobile-link-error', key: 'err' }, state.codeError))
|
|
1692
|
+
} else if (connecting) {
|
|
1693
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-note', key: 'connecting' },
|
|
1694
|
+
state.enrolling ? T('正在登记…') : T('正在连接…')))
|
|
430
1695
|
} else {
|
|
431
|
-
|
|
432
|
-
state.busy ? '
|
|
1696
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-note', key: 'loading' },
|
|
1697
|
+
state.busy ? T('正在获取…') : T('暂无配对码。')))
|
|
433
1698
|
}
|
|
434
|
-
const buttons = [React.createElement(ui.Button, {
|
|
1699
|
+
const buttons = connecting ? [] : [React.createElement(ui.Button, {
|
|
435
1700
|
key: 'swap', variant: 'primary', type: 'button', disabled: Boolean(state.busy),
|
|
436
1701
|
onClick: () => void controller.refreshCode({ regenerate: Boolean(state.code) }),
|
|
437
|
-
}, state.busy ? '
|
|
438
|
-
: (state.codeError ? '重试' : (state.code ? '重新生成' : '
|
|
1702
|
+
}, state.busy ? T('正在获取…')
|
|
1703
|
+
: (state.codeError ? T('重试') : (state.code ? T('重新生成') : T('获取配对码'))))]
|
|
439
1704
|
if (code) {
|
|
440
1705
|
buttons.push(React.createElement(ui.Button, {
|
|
441
1706
|
key: 'copy', variant: 'outline', type: 'button', onClick: () => controller.copy(),
|
|
442
|
-
}, state.copied ? '已复制' : '复制配对码'))
|
|
1707
|
+
}, state.copied ? T('已复制') : T('复制配对码')))
|
|
1708
|
+
}
|
|
1709
|
+
if (buttons.length > 0) {
|
|
1710
|
+
main.push(React.createElement('div', { className: 'dsh-mobile-link-actions', key: 'actions' }, buttons))
|
|
1711
|
+
}
|
|
1712
|
+
const children = []
|
|
1713
|
+
if (code) {
|
|
1714
|
+
children.push(React.createElement('img', {
|
|
1715
|
+
key: 'qr', className: 'dsh-mobile-link-qr', alt: T('配对二维码'),
|
|
1716
|
+
src: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(String(code.svg))}`,
|
|
1717
|
+
}))
|
|
443
1718
|
}
|
|
444
|
-
children.push(React.createElement('div', { className: 'dsh-mobile-link-
|
|
1719
|
+
children.push(React.createElement('div', { className: 'dsh-mobile-link-card-main', key: 'main' }, main))
|
|
445
1720
|
return React.createElement('div', { className: 'dsh-mobile-link-card' }, children)
|
|
446
1721
|
}
|
|
447
|
-
function deviceRow(React, ui, device) {
|
|
448
|
-
const online = device.offlineSince === null || device.offlineSince === undefined
|
|
1722
|
+
function deviceRow(React, ui, device, online, controller) {
|
|
449
1723
|
const meta = [device.model, clientLabel(device.client)].filter(Boolean).join(' · ')
|
|
450
1724
|
return React.createElement('div', { className: 'dsh-mobile-link-device', key: device.deviceId ?? device.name },
|
|
451
1725
|
React.createElement(ui.StateDot, { state: online ? 'done' : 'idle' }),
|
|
452
1726
|
React.createElement('div', { style: { minWidth: 0 } },
|
|
453
|
-
React.createElement('div', { className: 'dsh-mobile-link-device-
|
|
1727
|
+
React.createElement('div', { className: 'dsh-mobile-link-device-line' },
|
|
1728
|
+
React.createElement('div', { className: 'dsh-mobile-link-device-name' }, device.name ?? T('未命名设备')),
|
|
1729
|
+
React.createElement(ui.Tag, { tone: online ? 'success' : 'quiet', className: 'dsh-mobile-link-tag' },
|
|
1730
|
+
online ? T('在线') : T('离线'))),
|
|
454
1731
|
meta ? React.createElement('div', { className: 'dsh-mobile-link-device-meta' }, meta) : null),
|
|
455
|
-
React.createElement(
|
|
456
|
-
|
|
1732
|
+
React.createElement('button', {
|
|
1733
|
+
type: 'button', className: 'dsh-mobile-link-revoke',
|
|
1734
|
+
onClick: () => controller.requestRevoke(device),
|
|
1735
|
+
}, T('撤销')))
|
|
457
1736
|
}
|
|
458
|
-
function devicesSection(React, ui,
|
|
459
|
-
const devices =
|
|
1737
|
+
function devicesSection(React, ui, state, controller) {
|
|
1738
|
+
const devices = pairedRows(state)
|
|
460
1739
|
const rows = devices.length === 0
|
|
461
|
-
? [React.createElement('div', { className: 'dsh-mobile-link-note', key: 'none' }, '
|
|
462
|
-
: devices.map((device) => deviceRow(React, ui, device))
|
|
1740
|
+
? [React.createElement('div', { className: 'dsh-mobile-link-note', key: 'none' }, T('暂无已配对设备。'))]
|
|
1741
|
+
: devices.map((device) => deviceRow(React, ui, device, device.online, controller))
|
|
463
1742
|
return React.createElement('div', { className: 'dsh-mobile-link-section' },
|
|
464
|
-
React.createElement('div', { className: 'dsh-mobile-link-section-title' }, '
|
|
465
|
-
React.createElement('div', { className: 'dsh-mobile-link-devices' }, rows)
|
|
1743
|
+
React.createElement('div', { className: 'dsh-mobile-link-section-title' }, T('已配对设备')),
|
|
1744
|
+
React.createElement('div', { className: 'dsh-mobile-link-devices' }, rows),
|
|
1745
|
+
state.pairedError
|
|
1746
|
+
? React.createElement('div', { className: 'dsh-mobile-link-note dsh-mobile-link-error' },
|
|
1747
|
+
state.pairedError)
|
|
1748
|
+
: null)
|
|
466
1749
|
}
|
|
467
|
-
/**
|
|
1750
|
+
/** 最近一次失败:平时收起来。 */
|
|
468
1751
|
function errorSection(React, controller, ui, status, open) {
|
|
469
1752
|
if (!status?.lastError) return null
|
|
470
1753
|
return React.createElement(ui.DisclosureRow, {
|
|
471
|
-
title: '
|
|
1754
|
+
title: T('上次错误'), open: Boolean(open), expandable: true,
|
|
472
1755
|
onToggle: () => controller.toggleError(),
|
|
473
1756
|
}, React.createElement('div', { className: 'dsh-mobile-link-mono dsh-mobile-link-error' }, status.lastError))
|
|
474
1757
|
}
|
|
1758
|
+
/** 撤销确认:浮层叠加在扫码页上,不重复设备列表。 */
|
|
1759
|
+
function revokeOverlay(React, ui, state, controller) {
|
|
1760
|
+
const target = state.revokeTarget
|
|
1761
|
+
if (!target) return null
|
|
1762
|
+
const name = target.name ?? T('这台设备')
|
|
1763
|
+
return React.createElement('div', { className: 'dsh-mobile-link-overlay', key: 'revoke-overlay' },
|
|
1764
|
+
React.createElement('div', {
|
|
1765
|
+
className: 'dsh-mobile-link-alert', role: 'dialog', 'aria-modal': 'true',
|
|
1766
|
+
'aria-label': T('撤销这台设备'),
|
|
1767
|
+
},
|
|
1768
|
+
React.createElement('div', { className: 'dsh-mobile-link-alert-title' }, T('撤销这台设备?')),
|
|
1769
|
+
React.createElement('div', { className: 'dsh-mobile-link-note' },
|
|
1770
|
+
T('“${name}”将无法继续连接。需要重新扫码才能再次配对。', { name })),
|
|
1771
|
+
state.revokeError ? React.createElement('div', { className: 'dsh-mobile-link-error-inline', role: 'alert' },
|
|
1772
|
+
state.revokeError.message) : null,
|
|
1773
|
+
React.createElement('div', { className: 'dsh-mobile-link-alert-actions' },
|
|
1774
|
+
React.createElement(ui.Button, {
|
|
1775
|
+
variant: 'outline', type: 'button', disabled: Boolean(state.revoking),
|
|
1776
|
+
onClick: () => controller.cancelRevoke(),
|
|
1777
|
+
}, T('取消')),
|
|
1778
|
+
React.createElement('button', {
|
|
1779
|
+
type: 'button', className: 'dsh-mobile-link-danger', disabled: Boolean(state.revoking),
|
|
1780
|
+
onClick: () => controller.confirmRevoke(),
|
|
1781
|
+
}, state.revoking ? T('撤销中…') : T('撤销设备')))))
|
|
1782
|
+
}
|
|
1783
|
+
/** iOS 安装指引:只给 TestFlight 公开链接。 */
|
|
1784
|
+
function guideOverlay(React, ui, controller) {
|
|
1785
|
+
return React.createElement('div', { className: 'dsh-mobile-link-overlay', key: 'guide-overlay' },
|
|
1786
|
+
React.createElement('div', {
|
|
1787
|
+
className: 'dsh-mobile-link-guide', role: 'dialog', 'aria-modal': 'true',
|
|
1788
|
+
'aria-label': T('安装 DSH Mobile'),
|
|
1789
|
+
},
|
|
1790
|
+
React.createElement('div', { className: 'dsh-mobile-link-guide-title' }, T('安装 DSH Mobile')),
|
|
1791
|
+
React.createElement('div', { className: 'dsh-mobile-link-note' }, T('在 iPhone 或 iPad 上通过 TestFlight 安装。')),
|
|
1792
|
+
React.createElement('div', { className: 'dsh-mobile-link-step' },
|
|
1793
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-n' }, '1'),
|
|
1794
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-t' }, T('从 App Store 安装 TestFlight。'))),
|
|
1795
|
+
React.createElement('div', { className: 'dsh-mobile-link-step' },
|
|
1796
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-n' }, '2'),
|
|
1797
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-t' }, T('打开 TestFlight 邀请链接,安装 DSH Mobile。'))),
|
|
1798
|
+
React.createElement('div', { className: 'dsh-mobile-link-step' },
|
|
1799
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-n' }, '3'),
|
|
1800
|
+
React.createElement('span', { className: 'dsh-mobile-link-step-t' }, T('打开 DSH Mobile,扫描电脑上的配对二维码。'))),
|
|
1801
|
+
React.createElement('a', {
|
|
1802
|
+
className: 'dsh-mobile-link-testflight', href: TESTFLIGHT_URL, target: '_blank', rel: 'noreferrer',
|
|
1803
|
+
}, TESTFLIGHT_URL),
|
|
1804
|
+
React.createElement('div', { className: 'dsh-mobile-link-note' }, T('需要 iOS 17、iPadOS 17 或更高版本。')),
|
|
1805
|
+
React.createElement(ui.Button, {
|
|
1806
|
+
variant: 'primary', type: 'button', onClick: () => controller.closeGuide(),
|
|
1807
|
+
}, T('完成'))))
|
|
1808
|
+
}
|
|
475
1809
|
function panelBody(React, controller, ui, state) {
|
|
476
1810
|
const children = [statusSection(React, ui, state)]
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
|
|
1811
|
+
if (!state.status) {
|
|
1812
|
+
children.push(React.createElement('div', { className: 'dsh-mobile-link-note', key: 'loading' },
|
|
1813
|
+
state.statusError ? T('无法读取连接状态。') : T('正在读取状态…')))
|
|
1814
|
+
if (state.statusError) {
|
|
1815
|
+
children.push(React.createElement(ui.Button, {
|
|
1816
|
+
key: 'retry', variant: 'outline', type: 'button', onClick: () => controller.reloadStatus(),
|
|
1817
|
+
}, T('重试')))
|
|
1818
|
+
}
|
|
1819
|
+
return React.createElement('div', { className: 'dsh-mobile-link-body' }, children)
|
|
1820
|
+
}
|
|
1821
|
+
if (needsEnroll(state.status)) {
|
|
1822
|
+
children.push(enrollSection(React, controller, ui, state))
|
|
1823
|
+
return React.createElement('div', { className: 'dsh-mobile-link-body' }, children)
|
|
1824
|
+
}
|
|
1825
|
+
children.push(codeSection(React, controller, ui, state))
|
|
1826
|
+
children.push(githubSection(React))
|
|
1827
|
+
children.push(iosLinkSection(React, controller))
|
|
1828
|
+
children.push(devicesSection(React, ui, state, controller))
|
|
480
1829
|
const error = errorSection(React, controller, ui, state.status, state.showError)
|
|
481
1830
|
if (error) children.push(error)
|
|
482
|
-
children.push(React.createElement('div', { className: 'dsh-mobile-link-note', key: 'hint' },
|
|
483
|
-
'用 DSH Mobile App 扫码配对:这串码长期有效,可以配多台设备(手机/浏览器)。'
|
|
484
|
-
+ '换掉它点「重新生成」——已经在用的设备不会掉线,只有拿旧码来配的人需要重新扫。'
|
|
485
|
-
+ '相机不方便时,也可以把上面那串配对码手动输进 App 连接页底部的手输入口。'))
|
|
486
1831
|
return React.createElement('div', { className: 'dsh-mobile-link-body' }, children)
|
|
487
1832
|
}
|
|
488
|
-
/**
|
|
1833
|
+
/**
|
|
1834
|
+
* 小窗:官方 Modal 负责遮罩、顶层 Esc 与焦点归位;内容只在打开时渲染。
|
|
1835
|
+
* 正文超出窗口高度时在正文区滚动;确认与安装指引放在滚动区之外,不跟着内容走。
|
|
1836
|
+
*/
|
|
489
1837
|
function makePanel(React, controller, ui) {
|
|
490
1838
|
return function MobileLinkPanel() {
|
|
491
1839
|
const state = useStore(React, controller.store)
|
|
492
1840
|
return React.createElement(ui.Modal, {
|
|
493
1841
|
open: state.open,
|
|
494
1842
|
onClose: () => controller.close(),
|
|
495
|
-
title:
|
|
496
|
-
closeLabel: '关闭',
|
|
1843
|
+
title: panelLabel(),
|
|
1844
|
+
closeLabel: T('关闭'),
|
|
497
1845
|
className: 'dsh-mobile-link-dialog',
|
|
498
|
-
|
|
1846
|
+
contentClassName: 'dsh-mobile-link-content',
|
|
1847
|
+
}, React.createElement('div', { className: 'dsh-mobile-link-shell' },
|
|
1848
|
+
panelBody(React, controller, ui, state),
|
|
1849
|
+
state.revokeTarget ? revokeOverlay(React, ui, state, controller) : null,
|
|
1850
|
+
state.guideOpen ? guideOverlay(React, ui, controller) : null))
|
|
499
1851
|
}
|
|
500
1852
|
}
|
|
501
1853
|
|
|
@@ -517,22 +1869,32 @@ window.__ModuleLoader__.load({
|
|
|
517
1869
|
if (!ui?.Modal || !ui?.Button || !ui?.StateDot) {
|
|
518
1870
|
// 官方原子组件是平台 seed 字(宿主启动时就冻结在模块表里),正常不会缺;缺了说明
|
|
519
1871
|
// 宿主形态不对,此时宁可不注册,也不摆一个与官方视觉无关的入口。
|
|
520
|
-
report(ctx, '官方原子组件不可用,移动端连接入口未注册')
|
|
1872
|
+
report(ctx, T('官方原子组件不可用,移动端连接入口未注册'))
|
|
521
1873
|
return
|
|
522
1874
|
}
|
|
1875
|
+
LOCALE = resolveLocale(ctx)
|
|
523
1876
|
const controller = createController()
|
|
1877
|
+
// 宿主切换语言时不重载面板:改写全局语言并推动一次重渲染,入口与小窗一起换。
|
|
1878
|
+
// 宿主切换语言时按同一套优先级重算,再推一次重渲染。
|
|
1879
|
+
const applyLocale = () => {
|
|
1880
|
+
LOCALE = resolveLocale(ctx)
|
|
1881
|
+
controller.store.set({ locale: LOCALE })
|
|
1882
|
+
}
|
|
1883
|
+
ctx.on?.('locale/change', applyLocale)
|
|
1884
|
+
const stopWatching = watchDocumentLanguage(applyLocale)
|
|
1885
|
+
if (stopWatching) ctx.effect(() => stopWatching, 'mobile-link: locale watcher')
|
|
524
1886
|
try {
|
|
525
1887
|
ctx.effect(() => controller.start(), 'mobile-link: panel state')
|
|
526
1888
|
} catch (error) {
|
|
527
|
-
report(ctx,
|
|
1889
|
+
report(ctx, T('面板状态层没起来:${error}', { error: describe(error) }))
|
|
528
1890
|
}
|
|
529
1891
|
try {
|
|
530
1892
|
ctx.slots.inject(SLOT_ENTRY, () => ctx.slots.register(
|
|
531
|
-
{ name: SLOT_ENTRY, id: ENTRY_ID, order: 20, label: () =>
|
|
1893
|
+
{ name: SLOT_ENTRY, id: ENTRY_ID, order: 20, label: () => panelLabel() },
|
|
532
1894
|
makeEntry(React, controller, ui),
|
|
533
1895
|
))
|
|
534
1896
|
} catch (error) {
|
|
535
|
-
report(ctx,
|
|
1897
|
+
report(ctx, T('侧栏入口没注册上:${error}', { error: describe(error) }))
|
|
536
1898
|
}
|
|
537
1899
|
try {
|
|
538
1900
|
ctx.slots.inject(SLOT_OVERLAY, () => ctx.slots.register(
|
|
@@ -540,7 +1902,7 @@ window.__ModuleLoader__.load({
|
|
|
540
1902
|
makePanel(React, controller, ui),
|
|
541
1903
|
))
|
|
542
1904
|
} catch (error) {
|
|
543
|
-
report(ctx,
|
|
1905
|
+
report(ctx, T('小窗没注册上:${error}', { error: describe(error) }))
|
|
544
1906
|
}
|
|
545
1907
|
}
|
|
546
1908
|
|