dsh-remote 0.8.9 → 0.8.11
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/LICENSE +21 -21
- package/README.md +234 -234
- package/README.zh.md +190 -190
- package/lib/client.js +661 -171
- package/lib/credential.js +107 -107
- package/lib/errors.js +78 -78
- package/lib/forwards.js +219 -219
- package/lib/hostkey.js +48 -48
- package/lib/ignore.js +123 -123
- package/lib/index.js +2962 -2962
- package/lib/paths.js +217 -217
- package/lib/registry.js +89 -89
- package/lib/search.js +127 -127
- package/lib/sshconfig.js +57 -57
- package/lib/sync.js +270 -270
- package/lib/tasks.js +95 -95
- package/lib/update.js +196 -196
- package/package.json +66 -66
package/lib/client.js
CHANGED
|
@@ -28,12 +28,499 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
const React = require('react')
|
|
29
29
|
const name = 'dsh-remote'
|
|
30
30
|
|
|
31
|
+
// ── i18n (issue #14) ────────────────────────────────────────────────────
|
|
32
|
+
// Dictionaries are keyed by stable ids, zh + en share the same key set.
|
|
33
|
+
// Lookup order: active locale → en fallback → the key itself (so a missing
|
|
34
|
+
// translation stays visible instead of blanking the UI).
|
|
35
|
+
const NS = 'dsh-remote'
|
|
36
|
+
const L = {
|
|
37
|
+
zh: {
|
|
38
|
+
'settings.title': '远程工作区(dsh-remote)',
|
|
39
|
+
'settings.intro': '维护多台 SSH 机器(密码/私钥/agent/跳板机/钥匙串)。路径在新建/选择工作区时选:「本机」走系统文件夹对话框;「远程」选一台机器在远程目录中选择。',
|
|
40
|
+
'settings.machines': '已配置的机器',
|
|
41
|
+
'settings.noMachines': '还没有机器。在下方添加。',
|
|
42
|
+
'settings.current': '当前',
|
|
43
|
+
'settings.edit': '编辑',
|
|
44
|
+
'settings.delete': '删除',
|
|
45
|
+
'settings.setCurrent': '设为当前',
|
|
46
|
+
'settings.currentMachine': '当前远程机:{name}',
|
|
47
|
+
'settings.clearCurrent': '取消设为当前(active remote = none)',
|
|
48
|
+
'settings.noActive': '当前没有活动远程机:保存的机器只是备用连接,不会自动进入会话上下文(本地会话不受影响)。',
|
|
49
|
+
'settings.addMachine': '添加机器',
|
|
50
|
+
'settings.editMachine': '编辑机器',
|
|
51
|
+
'settings.sshImport': '从 ~/.ssh/config 导入',
|
|
52
|
+
'settings.sshCollapse': '收起',
|
|
53
|
+
'settings.sshNoEntries': '~/.ssh/config 里没有可导入的 Host 条目',
|
|
54
|
+
'settings.name': '名称',
|
|
55
|
+
'settings.host': '主机',
|
|
56
|
+
'settings.port': '端口',
|
|
57
|
+
'settings.user': '用户',
|
|
58
|
+
'settings.password': '密码',
|
|
59
|
+
'settings.advCollapse': '▲ 收起高级配置',
|
|
60
|
+
'settings.advExpand': '▼ 高级配置(私钥 / 跳板机 / agent 等)',
|
|
61
|
+
'settings.privateKey': '私钥路径',
|
|
62
|
+
'settings.passphrase': 'Passphrase',
|
|
63
|
+
'settings.workspace': '默认工作区',
|
|
64
|
+
'settings.hostKeyMode': 'HostKey 模式',
|
|
65
|
+
'settings.hkDefault': '(默认 accept-new)',
|
|
66
|
+
'settings.hkAccept': 'accept-new(信任首次,之后校验)',
|
|
67
|
+
'settings.hkVerify': 'verify(严格:拒绝陌生主机)',
|
|
68
|
+
'settings.hkOff': 'off(不校验,不推荐)',
|
|
69
|
+
'settings.agent': 'SSH agent(SSH_AUTH_SOCK)',
|
|
70
|
+
'settings.keyboardInteractive': 'keyboard-interactive(OTP/动态码)',
|
|
71
|
+
'settings.encryptPassword': '加密保存密码(系统钥匙串)',
|
|
72
|
+
'settings.proxyHost': '跳板机',
|
|
73
|
+
'settings.proxyPortUser': '端口/用户',
|
|
74
|
+
'settings.proxyKey': '跳板私钥',
|
|
75
|
+
'settings.cancelEdit': '取消编辑',
|
|
76
|
+
'settings.clear': '清空',
|
|
77
|
+
'settings.testing': '连接中…',
|
|
78
|
+
'settings.testOk': '✓ 连接成功',
|
|
79
|
+
'settings.testConn': '测试连接',
|
|
80
|
+
'settings.saving': '保存中…',
|
|
81
|
+
'settings.save': '保存',
|
|
82
|
+
'settings.recentWorkspaces': '当前机器最近工作区(一键切换)',
|
|
83
|
+
'settings.switch': '切换',
|
|
84
|
+
'settings.forwards': '端口转发(SSH 隧道)',
|
|
85
|
+
'settings.fwdStop': '停止',
|
|
86
|
+
'settings.fwdStart': '启动',
|
|
87
|
+
'settings.fwdRemove': '删除',
|
|
88
|
+
'settings.fwdDirLocal': '本地→远端',
|
|
89
|
+
'settings.fwdDirReverse': '远端→本地',
|
|
90
|
+
'settings.fwdListen': '监听端口',
|
|
91
|
+
'settings.fwdTargetHost': '目标主机',
|
|
92
|
+
'settings.fwdTargetPort': '目标端口',
|
|
93
|
+
'settings.fwdAuto': '自动重连',
|
|
94
|
+
'settings.fwdAdd': '添加转发',
|
|
95
|
+
'settings.audit': '最近执行的远程命令(审计日志)',
|
|
96
|
+
'settings.auditEmpty': '暂无记录(在远端执行的命令会记录到 $DSH_HOME/remote-workspaces/audit.log)',
|
|
97
|
+
'settings.update': '更新',
|
|
98
|
+
'settings.updateChecking': '版本信息加载中…',
|
|
99
|
+
'settings.checking': '检查中…',
|
|
100
|
+
'settings.checkUpdate': '检查更新',
|
|
101
|
+
'settings.updateNow': '立即更新',
|
|
102
|
+
'settings.updateMode': '更新模式:',
|
|
103
|
+
'settings.updateManual': '手动',
|
|
104
|
+
'settings.updateAuto': '自动',
|
|
105
|
+
'settings.updateOff': '关闭',
|
|
106
|
+
'settings.like': '觉得好用?',
|
|
107
|
+
'settings.star': '⭐ 去 GitHub 点个 Star',
|
|
108
|
+
'settings.feedback': '💬 反馈建议 / 提 issue',
|
|
109
|
+
'settings.deleted': '已删除',
|
|
110
|
+
'settings.saved': '已保存更新',
|
|
111
|
+
'settings.added': '已添加 — 可设为当前',
|
|
112
|
+
'settings.switched': '已切换为当前远程机',
|
|
113
|
+
'settings.cleared': '已取消当前远程机 — 本地会话不再自动使用远程上下文',
|
|
114
|
+
'settings.filledSsh': '已从 ~/.ssh/config 填入(仅引用私钥路径,不读取内容)',
|
|
115
|
+
'settings.workspaceSwitched': '已切换到工作区 {path}',
|
|
116
|
+
'settings.connOk': '连接成功',
|
|
117
|
+
'settings.connOkWith': '连接成功({user}@{host},{latency}ms){shell}',
|
|
118
|
+
'settings.connFail': '连接失败',
|
|
119
|
+
'settings.shellWin': '(Windows · Git Bash)',
|
|
120
|
+
'settings.shellWinNoGit': '(Windows · 未找到 Git Bash)',
|
|
121
|
+
'settings.shellPosix': '(POSIX)',
|
|
122
|
+
'settings.updateFail': '检查更新失败',
|
|
123
|
+
'settings.updateAvailable': '发现新版本 v{version}',
|
|
124
|
+
'settings.updateLatest': '已是最新版本(v{version})',
|
|
125
|
+
'settings.updateConfirm': '将更新 dsh-remote 到 v{version},替换本机插件文件。更新后需重启 Harness 生效。继续?',
|
|
126
|
+
'settings.updateApplied': '✅ 已更新到 v{to}({from} → {to})。重启 Harness 生效。',
|
|
127
|
+
'settings.updateFailed': '更新失败',
|
|
128
|
+
'settings.updateFailedMsg': '更新失败: {msg}',
|
|
129
|
+
'settings.registryUnreachable': '无法连接 npm registry',
|
|
130
|
+
'settings.updateModeSet': '更新模式已设为「{mode}」',
|
|
131
|
+
'settings.updateModeSaveFail': '模式保存失败: {msg}',
|
|
132
|
+
'settings.deleteConfirm': '确定删除这台机器?',
|
|
133
|
+
'settings.sshReadFail': '读取 ~/.ssh/config 失败: {msg}',
|
|
134
|
+
'settings.delFail': '删除失败: {msg}',
|
|
135
|
+
'settings.addFail': '保存失败: {msg}',
|
|
136
|
+
'settings.modeAuto': '自动',
|
|
137
|
+
'settings.modeOff': '关闭',
|
|
138
|
+
'settings.modeManual': '手动',
|
|
139
|
+
'picker.title': '选择工作目录',
|
|
140
|
+
'picker.close': '关闭 ✕',
|
|
141
|
+
'picker.tabLocal': '本机',
|
|
142
|
+
'picker.tabRemote': '远程',
|
|
143
|
+
'picker.localIntro': '弹出系统文件夹对话框选择;不可用时用内置目录浏览,也可直接输入本机目录。',
|
|
144
|
+
'picker.localPlaceholder': '本机目录,如 C:\\Users\\you\\project',
|
|
145
|
+
'picker.useLocalPath': '选用此本地路径',
|
|
146
|
+
'picker.openSysPicker': '打开系统文件夹选择器',
|
|
147
|
+
'picker.opening': '打开中…',
|
|
148
|
+
'picker.remoteMachine': '远程机器:',
|
|
149
|
+
'picker.chooseMachine': '— 选择机器 —',
|
|
150
|
+
'picker.noMachines': '还没有机器,请先在设置里添加',
|
|
151
|
+
'picker.recent': '最近:',
|
|
152
|
+
'picker.pathPlaceholder': '输入远程路径(自动补全)',
|
|
153
|
+
'picker.pathPlaceholderWin': '输入远程路径,如 C:\\Users\\…(自动补全)',
|
|
154
|
+
'picker.chooseMachineFirst': '先选远程机器',
|
|
155
|
+
'picker.browse': '浏览…',
|
|
156
|
+
'picker.home': '~ 主目录',
|
|
157
|
+
'picker.setRemoteWs': '设为远程工作区',
|
|
158
|
+
'picker.mirroring': '镜像中…',
|
|
159
|
+
'picker.cancel': '取消',
|
|
160
|
+
'picker.selectDir': '选择工作目录',
|
|
161
|
+
'picker.selected': '所选: {path}',
|
|
162
|
+
'picker.driveRoot': '(驱动器根)',
|
|
163
|
+
'picker.emptyDir': '(空目录)',
|
|
164
|
+
'picker.loading': '加载中…',
|
|
165
|
+
'picker.readingRoot': '正在读取根目录…',
|
|
166
|
+
'picker.back': '回上一级 ▴',
|
|
167
|
+
'picker.closeBtn': '关闭 ✕',
|
|
168
|
+
'picker.newDir': '新建目录',
|
|
169
|
+
'picker.newFolder': '新建文件夹',
|
|
170
|
+
'picker.create': '新建',
|
|
171
|
+
'picker.chooseThis': '选用此路径',
|
|
172
|
+
'picker.enterDir': '进入 {name}',
|
|
173
|
+
'picker.enterDrive': '进入驱动器 {name}',
|
|
174
|
+
'picker.fileAt': '文件: {name}',
|
|
175
|
+
'picker.cancelled': '已取消选择',
|
|
176
|
+
'picker.noSysPicker': '无法打开系统文件夹选择器,可直接在输入框填本地路径',
|
|
177
|
+
'picker.timeout': '目录选择已超时,请重试或直接在输入框填本地路径',
|
|
178
|
+
'picker.pickerBinNotFound': '未找到目录选择器程序',
|
|
179
|
+
'picker.noSysPickerHost': '当前系统不支持自动打开目录选择器,请在输入框直接填本地路径',
|
|
180
|
+
'picker.noHomeWin': 'Windows 远程暂不支持 ~ 解析,请直接输入绝对路径',
|
|
181
|
+
'picker.typePath': ' — 可直接在输入框填本地路径',
|
|
182
|
+
'picker.noHome': '无法解析主目录',
|
|
183
|
+
'picker.newDirName': '新建目录名(在 {path} 下)',
|
|
184
|
+
'picker.noSubdir': '(没有子目录 — 可直接选用当前目录)',
|
|
185
|
+
'picker.truncated': '(条目过多已截断)',
|
|
186
|
+
'picker.thisPc': '此电脑',
|
|
187
|
+
'picker.chooseDrive': ' 选择驱动器',
|
|
188
|
+
'picker.homeBtn': '🏠',
|
|
189
|
+
'picker.goHome': '回家目录',
|
|
190
|
+
'explorer.refresh': '刷新',
|
|
191
|
+
'explorer.newDir': '新建目录',
|
|
192
|
+
'explorer.more': '…',
|
|
193
|
+
'explorer.remoteWs': '远程工作区: {path}{host}',
|
|
194
|
+
'explorer.hostTag': ' ({user}@{host})',
|
|
195
|
+
'explorer.noRemoteWs': '未设置远程工作区',
|
|
196
|
+
'explorer.sessionLocal': '当前会话未使用远程工作区',
|
|
197
|
+
'explorer.localSession': '当前会话是本地工作区,未使用远程文件。\n\n如需访问远程,可在设置页将某台机器「设为当前」,再用上方「…」选择远程工作区。',
|
|
198
|
+
'explorer.loading': '加载中…',
|
|
199
|
+
'explorer.dirMissing': '目录不存在或已被删除',
|
|
200
|
+
'explorer.notConnected': '未连接远程主机(重启后需在设置中将机器「设为当前」,或刷新右侧列)',
|
|
201
|
+
'explorer.softError': '⚠ {msg}(显示缓存,可点 ↻ 刷新)',
|
|
202
|
+
'explorer.busy': '处理中… ',
|
|
203
|
+
'explorer.menuExpand': '📂 展开/收起',
|
|
204
|
+
'explorer.menuOpen': '📄 打开文件',
|
|
205
|
+
'explorer.menuDownload': '⬇ 下载到本地镜像',
|
|
206
|
+
'explorer.menuCopyRel': '📋 复制相对地址',
|
|
207
|
+
'explorer.menuCopyAbs': '📋 复制绝对地址',
|
|
208
|
+
'explorer.menuRename': '✏ 重命名',
|
|
209
|
+
'explorer.menuDelete': '🗑 删除',
|
|
210
|
+
'explorer.menuDeleteDir': '🗑 删除(含子目录)',
|
|
211
|
+
'explorer.renamePrompt': '重命名为(新名字)',
|
|
212
|
+
'explorer.deleteConfirm': '确认删除 {path}?',
|
|
213
|
+
'explorer.copied': '已复制',
|
|
214
|
+
'explorer.tabTitle': '远程文件',
|
|
215
|
+
'file.readonly': '只读',
|
|
216
|
+
'file.editing': '编辑中',
|
|
217
|
+
'file.saveToRemote': '保存到远程',
|
|
218
|
+
'file.saving': '保存中…',
|
|
219
|
+
'file.cancel': '取消',
|
|
220
|
+
'file.edit': '编辑',
|
|
221
|
+
'file.download': '下载',
|
|
222
|
+
'file.loading': '加载中…',
|
|
223
|
+
'file.binary': '二进制文件 ({size}) — 请用「下载」或 rw_download / 本地镜像查看。',
|
|
224
|
+
'file.unknownSize': '未知大小',
|
|
225
|
+
'file.empty': '(空文件)',
|
|
226
|
+
'file.truncated': '… 内容已截断(只读远程查看,完整文件请用 rw_read_file / 下载)',
|
|
227
|
+
'file.conflict': '远端文件已变化,请重新读取',
|
|
228
|
+
'file.downloaded': '已下载到本地镜像{local}',
|
|
229
|
+
'file.downloadedAt': ': {path}',
|
|
230
|
+
'file.readFail': '远程读取失败: {msg}',
|
|
231
|
+
'file.listFail': '远程目录读取失败: {msg}',
|
|
232
|
+
'common.refresh': '刷新',
|
|
233
|
+
'common.close': '关闭',
|
|
234
|
+
'common.confirm': '确认',
|
|
235
|
+
},
|
|
236
|
+
en: {
|
|
237
|
+
'settings.title': 'Remote workspace (dsh-remote)',
|
|
238
|
+
'settings.intro': 'Manage multiple SSH machines (password / private key / agent / proxy jump / keychain). Pick a path when creating/selecting a workspace: "Local" opens the system folder dialog; "Remote" picks a machine and a directory on it.',
|
|
239
|
+
'settings.machines': 'Configured machines',
|
|
240
|
+
'settings.noMachines': 'No machines yet. Add one below.',
|
|
241
|
+
'settings.current': 'current',
|
|
242
|
+
'settings.edit': 'Edit',
|
|
243
|
+
'settings.delete': 'Delete',
|
|
244
|
+
'settings.setCurrent': 'Set as current',
|
|
245
|
+
'settings.currentMachine': 'Current remote: {name}',
|
|
246
|
+
'settings.clearCurrent': 'Clear current (active remote = none)',
|
|
247
|
+
'settings.noActive': 'No active remote machine: saved machines are standby connections and never enter a session context automatically (local sessions are unaffected).',
|
|
248
|
+
'settings.addMachine': 'Add machine',
|
|
249
|
+
'settings.editMachine': 'Edit machine',
|
|
250
|
+
'settings.sshImport': 'Import from ~/.ssh/config',
|
|
251
|
+
'settings.sshCollapse': 'Collapse',
|
|
252
|
+
'settings.sshNoEntries': 'No importable Host entries in ~/.ssh/config',
|
|
253
|
+
'settings.name': 'Name',
|
|
254
|
+
'settings.host': 'Host',
|
|
255
|
+
'settings.port': 'Port',
|
|
256
|
+
'settings.user': 'User',
|
|
257
|
+
'settings.password': 'Password',
|
|
258
|
+
'settings.advCollapse': '▲ Collapse advanced config',
|
|
259
|
+
'settings.advExpand': '▼ Advanced (private key / proxy jump / agent, etc.)',
|
|
260
|
+
'settings.privateKey': 'Private key path',
|
|
261
|
+
'settings.passphrase': 'Passphrase',
|
|
262
|
+
'settings.workspace': 'Default workspace',
|
|
263
|
+
'settings.hostKeyMode': 'HostKey mode',
|
|
264
|
+
'settings.hkDefault': '(default accept-new)',
|
|
265
|
+
'settings.hkAccept': 'accept-new (trust first, verify later)',
|
|
266
|
+
'settings.hkVerify': 'verify (strict: reject unknown hosts)',
|
|
267
|
+
'settings.hkOff': 'off (no verification, not recommended)',
|
|
268
|
+
'settings.agent': 'SSH agent (SSH_AUTH_SOCK)',
|
|
269
|
+
'settings.keyboardInteractive': 'keyboard-interactive (OTP / dynamic code)',
|
|
270
|
+
'settings.encryptPassword': 'Encrypt saved password (system keychain)',
|
|
271
|
+
'settings.proxyHost': 'Proxy jump host',
|
|
272
|
+
'settings.proxyPortUser': 'Port / user',
|
|
273
|
+
'settings.proxyKey': 'Proxy jump private key',
|
|
274
|
+
'settings.cancelEdit': 'Cancel edit',
|
|
275
|
+
'settings.clear': 'Clear',
|
|
276
|
+
'settings.testing': 'Connecting…',
|
|
277
|
+
'settings.testOk': '✓ Connected',
|
|
278
|
+
'settings.testConn': 'Test connection',
|
|
279
|
+
'settings.saving': 'Saving…',
|
|
280
|
+
'settings.save': 'Save',
|
|
281
|
+
'settings.recentWorkspaces': 'Recent workspaces of the current machine (one-click switch)',
|
|
282
|
+
'settings.switch': 'Switch',
|
|
283
|
+
'settings.forwards': 'Port forwarding (SSH tunnels)',
|
|
284
|
+
'settings.fwdStop': 'Stop',
|
|
285
|
+
'settings.fwdStart': 'Start',
|
|
286
|
+
'settings.fwdRemove': 'Remove',
|
|
287
|
+
'settings.fwdDirLocal': 'Local → remote',
|
|
288
|
+
'settings.fwdDirReverse': 'Remote → local',
|
|
289
|
+
'settings.fwdListen': 'Listen port',
|
|
290
|
+
'settings.fwdTargetHost': 'Target host',
|
|
291
|
+
'settings.fwdTargetPort': 'Target port',
|
|
292
|
+
'settings.fwdAuto': 'Auto reconnect',
|
|
293
|
+
'settings.fwdAdd': 'Add forward',
|
|
294
|
+
'settings.audit': 'Recent remote commands (audit log)',
|
|
295
|
+
'settings.auditEmpty': 'No records yet (commands run remotely are logged to $DSH_HOME/remote-workspaces/audit.log)',
|
|
296
|
+
'settings.update': 'Update',
|
|
297
|
+
'settings.updateChecking': 'Loading version info…',
|
|
298
|
+
'settings.checking': 'Checking…',
|
|
299
|
+
'settings.checkUpdate': 'Check for updates',
|
|
300
|
+
'settings.updateNow': 'Update now',
|
|
301
|
+
'settings.updateMode': 'Update mode:',
|
|
302
|
+
'settings.updateManual': 'Manual',
|
|
303
|
+
'settings.updateAuto': 'Auto',
|
|
304
|
+
'settings.updateOff': 'Off',
|
|
305
|
+
'settings.like': 'Like it?',
|
|
306
|
+
'settings.star': '⭐ Star on GitHub',
|
|
307
|
+
'settings.feedback': '💬 Feedback / report issue',
|
|
308
|
+
'settings.deleted': 'Deleted',
|
|
309
|
+
'settings.saved': 'Saved',
|
|
310
|
+
'settings.added': 'Added — you can set it as current',
|
|
311
|
+
'settings.switched': 'Switched to current remote machine',
|
|
312
|
+
'settings.cleared': 'Cleared current remote — local sessions no longer use remote context automatically',
|
|
313
|
+
'settings.filledSsh': 'Filled from ~/.ssh/config (only references the private key path, never reads its content)',
|
|
314
|
+
'settings.workspaceSwitched': 'Switched to workspace {path}',
|
|
315
|
+
'settings.connOk': 'Connection successful',
|
|
316
|
+
'settings.connOkWith': 'Connection successful ({user}@{host}, {latency}ms){shell}',
|
|
317
|
+
'settings.connFail': 'Connection failed',
|
|
318
|
+
'settings.shellWin': ' (Windows · Git Bash)',
|
|
319
|
+
'settings.shellWinNoGit': ' (Windows · no Git Bash found)',
|
|
320
|
+
'settings.shellPosix': ' (POSIX)',
|
|
321
|
+
'settings.updateFail': 'Failed to check for updates',
|
|
322
|
+
'settings.updateAvailable': 'New version found: v{version}',
|
|
323
|
+
'settings.updateLatest': 'Already up to date (v{version})',
|
|
324
|
+
'settings.updateConfirm': 'Update dsh-remote to v{version}? This replaces the local plugin files and requires a Harness restart to take effect. Continue?',
|
|
325
|
+
'settings.updateApplied': '✅ Updated to v{to} ({from} → {to}). Restart Harness to apply.',
|
|
326
|
+
'settings.updateFailed': 'Update failed',
|
|
327
|
+
'settings.updateFailedMsg': 'Update failed: {msg}',
|
|
328
|
+
'settings.registryUnreachable': 'Cannot reach the npm registry',
|
|
329
|
+
'settings.updateModeSet': 'Update mode set to "{mode}"',
|
|
330
|
+
'settings.updateModeSaveFail': 'Failed to save mode: {msg}',
|
|
331
|
+
'settings.deleteConfirm': 'Delete this machine?',
|
|
332
|
+
'settings.sshReadFail': 'Failed to read ~/.ssh/config: {msg}',
|
|
333
|
+
'settings.delFail': 'Delete failed: {msg}',
|
|
334
|
+
'settings.addFail': 'Save failed: {msg}',
|
|
335
|
+
'settings.modeAuto': 'Auto',
|
|
336
|
+
'settings.modeOff': 'Off',
|
|
337
|
+
'settings.modeManual': 'Manual',
|
|
338
|
+
'picker.title': 'Select working directory',
|
|
339
|
+
'picker.close': 'Close ✕',
|
|
340
|
+
'picker.tabLocal': 'Local',
|
|
341
|
+
'picker.tabRemote': 'Remote',
|
|
342
|
+
'picker.localIntro': 'Opens the system folder dialog; when unavailable, uses the built-in directory browser. You can also type a local path directly.',
|
|
343
|
+
'picker.localPlaceholder': 'Local directory, e.g. C:\\Users\\you\\project',
|
|
344
|
+
'picker.useLocalPath': 'Use this local path',
|
|
345
|
+
'picker.openSysPicker': 'Open system folder picker',
|
|
346
|
+
'picker.opening': 'Opening…',
|
|
347
|
+
'picker.remoteMachine': 'Remote machine:',
|
|
348
|
+
'picker.chooseMachine': '— choose a machine —',
|
|
349
|
+
'picker.noMachines': 'No machines yet — add one in Settings first',
|
|
350
|
+
'picker.recent': 'Recent:',
|
|
351
|
+
'picker.pathPlaceholder': 'Enter remote path (auto-complete)',
|
|
352
|
+
'picker.pathPlaceholderWin': 'Enter remote path, e.g. C:\\Users\\… (auto-complete)',
|
|
353
|
+
'picker.chooseMachineFirst': 'Choose a remote machine first',
|
|
354
|
+
'picker.browse': 'Browse…',
|
|
355
|
+
'picker.home': '~ Home',
|
|
356
|
+
'picker.setRemoteWs': 'Set as remote workspace',
|
|
357
|
+
'picker.mirroring': 'Mirroring…',
|
|
358
|
+
'picker.cancel': 'Cancel',
|
|
359
|
+
'picker.selectDir': 'Select working directory',
|
|
360
|
+
'picker.selected': 'Selected: {path}',
|
|
361
|
+
'picker.driveRoot': ' (drive root)',
|
|
362
|
+
'picker.emptyDir': ' (empty directory)',
|
|
363
|
+
'picker.loading': 'Loading…',
|
|
364
|
+
'picker.readingRoot': 'Reading root directory…',
|
|
365
|
+
'picker.back': 'Back ▴',
|
|
366
|
+
'picker.closeBtn': 'Close ✕',
|
|
367
|
+
'picker.newDir': 'New directory',
|
|
368
|
+
'picker.newFolder': 'New folder',
|
|
369
|
+
'picker.create': 'Create',
|
|
370
|
+
'picker.chooseThis': 'Use this path',
|
|
371
|
+
'picker.enterDir': 'Enter {name}',
|
|
372
|
+
'picker.enterDrive': 'Enter drive {name}',
|
|
373
|
+
'picker.fileAt': 'File: {name}',
|
|
374
|
+
'picker.cancelled': 'Selection cancelled',
|
|
375
|
+
'picker.noSysPicker': 'Cannot open the system folder picker — you can type a local path in the input box',
|
|
376
|
+
'picker.timeout': 'Directory selection timed out — retry or type a local path directly',
|
|
377
|
+
'picker.pickerBinNotFound': 'Directory picker program not found',
|
|
378
|
+
'picker.noSysPickerHost': 'This system cannot auto-open a directory picker — type a local path in the input box',
|
|
379
|
+
'picker.noHomeWin': 'Windows remotes do not support ~ resolution — enter an absolute path',
|
|
380
|
+
'picker.typePath': ' — type a local path in the input box instead',
|
|
381
|
+
'picker.noHome': 'Cannot resolve home directory',
|
|
382
|
+
'picker.newDirName': 'New directory name (under {path})',
|
|
383
|
+
'picker.noSubdir': ' (no subdirectories — you can use the current directory)',
|
|
384
|
+
'picker.truncated': ' (too many entries, truncated)',
|
|
385
|
+
'picker.thisPc': 'This PC',
|
|
386
|
+
'picker.chooseDrive': ' choose a drive',
|
|
387
|
+
'picker.homeBtn': '🏠',
|
|
388
|
+
'picker.goHome': 'Go to home',
|
|
389
|
+
'explorer.refresh': 'Refresh',
|
|
390
|
+
'explorer.newDir': 'New directory',
|
|
391
|
+
'explorer.more': '…',
|
|
392
|
+
'explorer.remoteWs': 'Remote workspace: {path}{host}',
|
|
393
|
+
'explorer.hostTag': ' ({user}@{host})',
|
|
394
|
+
'explorer.noRemoteWs': 'No remote workspace set',
|
|
395
|
+
'explorer.sessionLocal': 'This session does not use a remote workspace',
|
|
396
|
+
'explorer.localSession': 'This session is a local workspace and does not use remote files.\n\nTo access remote, set a machine as "current" in Settings, then pick a remote workspace with the "…" button above.',
|
|
397
|
+
'explorer.loading': 'Loading…',
|
|
398
|
+
'explorer.dirMissing': 'Directory does not exist or was deleted',
|
|
399
|
+
'explorer.notConnected': 'Not connected to the remote host (after a restart, set a machine as "current" in Settings, or refresh the right column)',
|
|
400
|
+
'explorer.softError': '⚠ {msg} (showing cache, click ↻ to refresh)',
|
|
401
|
+
'explorer.busy': 'Processing… ',
|
|
402
|
+
'explorer.menuExpand': '📂 Expand/collapse',
|
|
403
|
+
'explorer.menuOpen': '📄 Open file',
|
|
404
|
+
'explorer.menuDownload': '⬇ Download to local mirror',
|
|
405
|
+
'explorer.menuCopyRel': '📋 Copy relative path',
|
|
406
|
+
'explorer.menuCopyAbs': '📋 Copy absolute path',
|
|
407
|
+
'explorer.menuRename': '✏ Rename',
|
|
408
|
+
'explorer.menuDelete': '🗑 Delete',
|
|
409
|
+
'explorer.menuDeleteDir': '🗑 Delete (with contents)',
|
|
410
|
+
'explorer.renamePrompt': 'Rename to (new name)',
|
|
411
|
+
'explorer.deleteConfirm': 'Delete {path}?',
|
|
412
|
+
'explorer.copied': 'Copied',
|
|
413
|
+
'explorer.tabTitle': 'Remote Files',
|
|
414
|
+
'file.readonly': 'read-only',
|
|
415
|
+
'file.editing': 'editing',
|
|
416
|
+
'file.saveToRemote': 'Save to remote',
|
|
417
|
+
'file.saving': 'Saving…',
|
|
418
|
+
'file.cancel': 'Cancel',
|
|
419
|
+
'file.edit': 'Edit',
|
|
420
|
+
'file.download': 'Download',
|
|
421
|
+
'file.loading': 'Loading…',
|
|
422
|
+
'file.binary': 'Binary file ({size}) — use "Download", rw_download or the local mirror to view.',
|
|
423
|
+
'file.unknownSize': 'unknown size',
|
|
424
|
+
'file.empty': ' (empty file)',
|
|
425
|
+
'file.truncated': '… content truncated (read-only remote view; use rw_read_file / Download for the full file)',
|
|
426
|
+
'file.conflict': 'The remote file changed, please re-read it',
|
|
427
|
+
'file.downloaded': 'Downloaded to local mirror{local}',
|
|
428
|
+
'file.downloadedAt': ': {path}',
|
|
429
|
+
'file.readFail': 'Failed to read remote: {msg}',
|
|
430
|
+
'file.listFail': 'Failed to list remote directory: {msg}',
|
|
431
|
+
'common.refresh': 'Refresh',
|
|
432
|
+
'common.close': 'Close',
|
|
433
|
+
'common.confirm': 'Confirm',
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Active dictionary: resolved from the DSH locale service when available
|
|
438
|
+
// (its active locale + en fallback), else from the browser language, else en.
|
|
439
|
+
let ACTIVE_LANG = 'en'
|
|
440
|
+
try {
|
|
441
|
+
const nav = (typeof navigator !== 'undefined' && navigator.languages) ? navigator.languages : (typeof navigator !== 'undefined' ? [navigator.language] : [])
|
|
442
|
+
for (const tag of nav) {
|
|
443
|
+
const primary = String(tag || '').toLowerCase().split('-')[0]
|
|
444
|
+
if (primary === 'zh') { ACTIVE_LANG = 'zh'; break }
|
|
445
|
+
if (primary === 'en') { ACTIVE_LANG = 'en'; break }
|
|
446
|
+
}
|
|
447
|
+
} catch { /* keep en */ }
|
|
448
|
+
|
|
449
|
+
// The DSH locale service binds a translate thunk; we call it first and fall
|
|
450
|
+
// back to our own dictionary so missing keys never blank the UI.
|
|
451
|
+
function makeTr() {
|
|
452
|
+
const dict = L[ACTIVE_LANG] || L.en
|
|
453
|
+
return (key, params) => {
|
|
454
|
+
let text = dict[key]
|
|
455
|
+
if (text === undefined && ACTIVE_LANG !== 'en') text = L.en[key]
|
|
456
|
+
if (text === undefined) text = key
|
|
457
|
+
if (params) text = String(text).replace(/\{(\w+)\}/g, (m, p) => (p in params ? String(params[p]) : m))
|
|
458
|
+
return text
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
let tr = makeTr()
|
|
462
|
+
// Called from apply() once the real locale service is reachable; registers
|
|
463
|
+
// both dictionaries and rebinds tr so live locale switches take effect.
|
|
464
|
+
function wireLocale(ctx) {
|
|
465
|
+
try {
|
|
466
|
+
const locale = ctx && ctx.get && ctx.get('locale')
|
|
467
|
+
if (!locale || typeof locale.register !== 'function') return
|
|
468
|
+
const dispose = locale.register(NS, { zh: L.zh, en: L.en })
|
|
469
|
+
const bound = locale.bind(NS)
|
|
470
|
+
tr = (key, params) => {
|
|
471
|
+
let text = bound(key, params)
|
|
472
|
+
if (text === key) {
|
|
473
|
+
const d = L[ACTIVE_LANG] || L.en
|
|
474
|
+
text = d[key] !== undefined ? d[key] : L.en[key]
|
|
475
|
+
if (text === undefined) text = key
|
|
476
|
+
if (params) text = String(text).replace(/\{(\w+)\}/g, (m, p) => (p in params ? String(params[p]) : m))
|
|
477
|
+
}
|
|
478
|
+
return text
|
|
479
|
+
}
|
|
480
|
+
if (ctx.effect) ctx.effect(() => dispose, 'dsh-remote.locale')
|
|
481
|
+
} catch (e) { /* keep local dict fallback */ }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Known host-side error messages (lib/index.js) are Chinese. Map them to
|
|
485
|
+
// dictionary keys so non-zh locales see localized errors instead of raw
|
|
486
|
+
// zh text. Unknown / server-generated messages pass through untouched.
|
|
487
|
+
const HOST_ERROR_KEYS = {
|
|
488
|
+
'目录选择已超时,请重试或直接在输入框填本地路径': 'picker.timeout',
|
|
489
|
+
'无法打开系统文件夹选择器': 'picker.noSysPicker',
|
|
490
|
+
'当前系统不支持自动打开目录选择器,请在输入框直接填本地路径': 'picker.noSysPickerHost',
|
|
491
|
+
'Windows 远程暂不支持 ~ 解析,请直接输入绝对路径': 'picker.noHomeWin',
|
|
492
|
+
'无法连接 npm registry': 'settings.registryUnreachable',
|
|
493
|
+
}
|
|
494
|
+
const HOST_ERROR_PREFIX = [
|
|
495
|
+
['未找到目录选择器程序', 'picker.pickerBinNotFound'],
|
|
496
|
+
]
|
|
497
|
+
const HOST_ERROR_SUFFIX = [
|
|
498
|
+
[' — 可直接在输入框填本地路径', 'picker.typePath'],
|
|
499
|
+
]
|
|
500
|
+
function trHostError(msg) {
|
|
501
|
+
if (!msg || ACTIVE_LANG === 'zh') return msg
|
|
502
|
+
if (HOST_ERROR_KEYS[msg]) return tr(HOST_ERROR_KEYS[msg])
|
|
503
|
+
for (const [prefix, key] of HOST_ERROR_PREFIX) {
|
|
504
|
+
if (msg.startsWith(prefix)) {
|
|
505
|
+
const en = L.en[key]
|
|
506
|
+
if (en !== undefined) return en + msg.slice(prefix.length)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
for (const [suffix, key] of HOST_ERROR_SUFFIX) {
|
|
510
|
+
if (msg.endsWith(suffix)) {
|
|
511
|
+
const en = L.en[key]
|
|
512
|
+
if (en !== undefined) return msg.slice(0, -suffix.length) + en
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return msg
|
|
516
|
+
}
|
|
517
|
+
|
|
31
518
|
async function api(method, path, body) {
|
|
32
519
|
const opts = { method, headers: {} }
|
|
33
520
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
34
521
|
const res = await fetch(path, opts)
|
|
35
522
|
const data = await res.json().catch(() => ({}))
|
|
36
|
-
if (!res.ok) throw new Error((data && (data.error || data.message)) || 'HTTP ' + res.status)
|
|
523
|
+
if (!res.ok) throw new Error(trHostError((data && (data.error || data.message))) || 'HTTP ' + res.status)
|
|
37
524
|
return data
|
|
38
525
|
}
|
|
39
526
|
|
|
@@ -43,6 +530,8 @@ window.__ModuleLoader__.load({
|
|
|
43
530
|
if (body) { opts.headers['Content-Type'] = 'application/json'; opts.body = JSON.stringify(body) }
|
|
44
531
|
const res = await fetch(path, opts)
|
|
45
532
|
const data = await res.json().catch(() => ({}))
|
|
533
|
+
if (data && typeof data.error === 'string') data.error = trHostError(data.error)
|
|
534
|
+
if (data && typeof data.message === 'string') data.message = trHostError(data.message)
|
|
46
535
|
return { status: res.status, data }
|
|
47
536
|
}
|
|
48
537
|
|
|
@@ -142,10 +631,10 @@ window.__ModuleLoader__.load({
|
|
|
142
631
|
})
|
|
143
632
|
.then((r) => {
|
|
144
633
|
if (r && r.ok) {
|
|
145
|
-
const shellTag = r.platform === 'windows' ? (r.gitBash ? '
|
|
146
|
-
setTst('ok'); setMsg(r.latencyMs != null ?
|
|
634
|
+
const shellTag = r.platform === 'windows' ? (r.gitBash ? tr('settings.shellWin') : tr('settings.shellWinNoGit')) : (r.platform === 'posix' ? tr('settings.shellPosix') : '')
|
|
635
|
+
setTst('ok'); setMsg(r.latencyMs != null ? tr('settings.connOkWith', { user: r.user, host: r.host, latency: r.latencyMs, shell: shellTag }) : tr('settings.connOk')); refresh()
|
|
147
636
|
}
|
|
148
|
-
else { setTst('failing'); setErr((r && r.error) || '
|
|
637
|
+
else { setTst('failing'); setErr((r && r.error) || tr('settings.connFail')) }
|
|
149
638
|
})
|
|
150
639
|
.catch((e) => { setTst('failing'); setErr(String((e && e.message) || e)) })
|
|
151
640
|
}
|
|
@@ -167,30 +656,30 @@ window.__ModuleLoader__.load({
|
|
|
167
656
|
setUpdBusy(true); setUpdMsg('')
|
|
168
657
|
api('GET', '/dsh-remote/update-check')
|
|
169
658
|
.then((r) => {
|
|
170
|
-
if (!r || !r.ok) { setUpdMsg((r && r.error) || '
|
|
659
|
+
if (!r || !r.ok) { setUpdMsg((r && r.error) || tr('settings.updateFail')); return }
|
|
171
660
|
setUpd(r)
|
|
172
661
|
if (r.updateMode) setUpdMode(r.updateMode)
|
|
173
|
-
if (!quiet) setUpdMsg(r.updateAvailable ? '
|
|
662
|
+
if (!quiet) setUpdMsg(r.updateAvailable ? tr('settings.updateAvailable', { version: r.latest }) : tr('settings.updateLatest', { version: r.current }))
|
|
174
663
|
})
|
|
175
664
|
.catch((e) => setUpdMsg(String((e && e.message) || e)))
|
|
176
665
|
.finally(() => setUpdBusy(false))
|
|
177
666
|
}
|
|
178
667
|
const applyUpdateNow = () => {
|
|
179
668
|
if (!upd || !upd.latest) return
|
|
180
|
-
if (!window.confirm('
|
|
669
|
+
if (!window.confirm(tr('settings.updateConfirm', { version: upd.latest }))) return
|
|
181
670
|
setUpdBusy(true); setUpdMsg('')
|
|
182
671
|
api('POST', '/dsh-remote/update-apply', { version: upd.latest })
|
|
183
672
|
.then((r) => {
|
|
184
|
-
if (r && r.ok) setUpdMsg('
|
|
185
|
-
else setUpdMsg((r && r.error) || '
|
|
673
|
+
if (r && r.ok) setUpdMsg(tr('settings.updateApplied', { to: r.to, from: (r.from || '') }))
|
|
674
|
+
else setUpdMsg((r && r.error) || tr('settings.updateFailed'))
|
|
186
675
|
checkUpdate(true)
|
|
187
676
|
})
|
|
188
|
-
.catch((e) => setUpdMsg('
|
|
677
|
+
.catch((e) => setUpdMsg(tr('settings.updateFailedMsg', { msg: String((e && e.message) || e) })))
|
|
189
678
|
.finally(() => setUpdBusy(false))
|
|
190
679
|
}
|
|
191
680
|
const saveUpdateMode = (mode) => {
|
|
192
|
-
setUpdMode(mode); setUpdMsg('
|
|
193
|
-
api('POST', '/dsh-remote/update-mode', { mode }).catch((e) => setUpdMsg('
|
|
681
|
+
setUpdMode(mode); setUpdMsg(tr('settings.updateModeSet', { mode: mode === 'auto' ? tr('settings.modeAuto') : mode === 'off' ? tr('settings.modeOff') : tr('settings.modeManual') }))
|
|
682
|
+
api('POST', '/dsh-remote/update-mode', { mode }).catch((e) => setUpdMsg(tr('settings.updateModeSaveFail', { msg: String((e && e.message) || e) })))
|
|
194
683
|
}
|
|
195
684
|
React.useEffect(() => { checkUpdate(true) }, [])
|
|
196
685
|
|
|
@@ -218,7 +707,7 @@ window.__ModuleLoader__.load({
|
|
|
218
707
|
const save = (action) => {
|
|
219
708
|
setBusy(true); setErr(''); setMsg('')
|
|
220
709
|
if (action === 'delete') {
|
|
221
|
-
api('POST', '/dsh-remote/machines', { action: 'delete', id: form.id }).then(refresh).then(() => { clearForm(); setMsg('
|
|
710
|
+
api('POST', '/dsh-remote/machines', { action: 'delete', id: form.id }).then(refresh).then(() => { clearForm(); setMsg(tr('settings.deleted')) }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setBusy(false))
|
|
222
711
|
return
|
|
223
712
|
}
|
|
224
713
|
api('POST', '/dsh-remote/machines', {
|
|
@@ -227,17 +716,17 @@ window.__ModuleLoader__.load({
|
|
|
227
716
|
workspace: form.workspace, hostKeyMode: form.hostKeyMode, useAgent: form.useAgent, keyboardInteractive: form.keyboardInteractive,
|
|
228
717
|
encryptPassword: form.encryptPassword,
|
|
229
718
|
proxy: form.proxyHost ? { host: form.proxyHost, port: Number(form.proxyPort) || 22, username: form.proxyUser || '', password: form.proxyPassword || '', privateKeyPath: form.proxyKey || '' } : undefined,
|
|
230
|
-
}).then((r) => { refresh(); clearForm(); setMsg(form.id ? '
|
|
719
|
+
}).then((r) => { refresh(); clearForm(); setMsg(form.id ? tr('settings.saved') : tr('settings.added')) }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setBusy(false))
|
|
231
720
|
}
|
|
232
|
-
const useNow = (id) => { setBusy(true); api('POST', '/dsh-remote/current', { id }).then((r) => { setCurrentId(r.currentId); setMsg('
|
|
721
|
+
const useNow = (id) => { setBusy(true); api('POST', '/dsh-remote/current', { id }).then((r) => { setCurrentId(r.currentId); setMsg(tr('settings.switched')) }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setBusy(false)) }
|
|
233
722
|
// Issue #13: "active remote = none" — saved machines stay, but no machine
|
|
234
723
|
// is current, so local sessions are never dragged into a remote context.
|
|
235
|
-
const clearCurrent = () => { setBusy(true); api('POST', '/dsh-remote/current', { id: '' }).then((r) => { setCurrentId(''); setMsg('
|
|
236
|
-
const del = (id) => { if (window.confirm('
|
|
724
|
+
const clearCurrent = () => { setBusy(true); api('POST', '/dsh-remote/current', { id: '' }).then((r) => { setCurrentId(''); setMsg(tr('settings.cleared')) }).catch((e) => setErr(String((e && e.message) || e))).finally(() => setBusy(false)) }
|
|
725
|
+
const del = (id) => { if (window.confirm(tr('settings.deleteConfirm'))) { api('POST', '/dsh-remote/machines', { action: 'delete', id }).then(refresh).then(() => setMsg(tr('settings.deleted'))).catch((e) => setErr(String((e && e.message) || e))) } }
|
|
237
726
|
|
|
238
727
|
const importSsh = () => {
|
|
239
728
|
api('GET', '/dsh-remote/ssh-config').then((r) => setSshEntries(r && r.present ? (r.entries || []) : []))
|
|
240
|
-
.catch((e) => setErr('
|
|
729
|
+
.catch((e) => setErr(tr('settings.sshReadFail', { msg: String((e && e.message) || e) })))
|
|
241
730
|
}
|
|
242
731
|
const applySshEntry = (e) => {
|
|
243
732
|
setForm({ ...form,
|
|
@@ -245,12 +734,12 @@ window.__ModuleLoader__.load({
|
|
|
245
734
|
username: e.user || 'root', privateKeyPath: e.identityFile || '',
|
|
246
735
|
})
|
|
247
736
|
setSshEntries(null)
|
|
248
|
-
setMsg('
|
|
737
|
+
setMsg(tr('settings.filledSsh'))
|
|
249
738
|
}
|
|
250
739
|
|
|
251
740
|
const setWorkspaceNow = (p) => {
|
|
252
741
|
api('POST', '/dsh-remote/workspace', { path: p })
|
|
253
|
-
.then(() => setMsg('
|
|
742
|
+
.then(() => setMsg(tr('settings.workspaceSwitched', { path: p })))
|
|
254
743
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
255
744
|
}
|
|
256
745
|
|
|
@@ -271,11 +760,11 @@ window.__ModuleLoader__.load({
|
|
|
271
760
|
React.createElement('label', { style: { width: 84, fontSize: 12, opacity: 0.8, flexShrink: 0 } }, label), ctrl)
|
|
272
761
|
|
|
273
762
|
return React.createElement('div', { style: { padding: 16, display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 860 } },
|
|
274
|
-
React.createElement('div', { style: { fontSize: 15, fontWeight: 600 } }, '
|
|
763
|
+
React.createElement('div', { style: { fontSize: 15, fontWeight: 600 } }, tr('settings.title')),
|
|
275
764
|
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } },
|
|
276
|
-
'
|
|
765
|
+
tr('settings.intro')),
|
|
277
766
|
React.createElement('div', { style: boxS },
|
|
278
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '
|
|
767
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.machines')),
|
|
279
768
|
machines.length
|
|
280
769
|
? machines.map((m) => React.createElement('div', { key: m.id, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '6px 0', borderBottom: '1px solid ' + T.border } },
|
|
281
770
|
React.createElement('div', { style: { flex: 1, fontSize: 13 } },
|
|
@@ -285,134 +774,134 @@ window.__ModuleLoader__.load({
|
|
|
285
774
|
(m.credentialBackend && m.credentialBackend !== 'plain') ? ' 🗝' : '',
|
|
286
775
|
m.proxy && m.proxy.host ? ' ⛳' : '',
|
|
287
776
|
m.latencyMs != null ? React.createElement('span', { style: { fontSize: 11, opacity: 0.6 } }, ` · ${m.latencyMs}ms`) : null,
|
|
288
|
-
m.id === currentId ? React.createElement('span', { style: { color: T.ok, fontSize: 12 } }, ' ·
|
|
289
|
-
React.createElement('button', { style: buttonS, onClick: () => startEdit(m) }, '
|
|
290
|
-
React.createElement('button', { style: buttonS, onClick: () => del(m.id) }, '
|
|
291
|
-
React.createElement('button', { style: { ...buttonS, padding: '6px 10px', whiteSpace: 'nowrap' }, onClick: () => useNow(m.id), disabled: m.id === currentId }, '
|
|
292
|
-
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, '
|
|
777
|
+
m.id === currentId ? React.createElement('span', { style: { color: T.ok, fontSize: 12 } }, ' · ' + tr('settings.current')) : null),
|
|
778
|
+
React.createElement('button', { style: buttonS, onClick: () => startEdit(m) }, tr('settings.edit')),
|
|
779
|
+
React.createElement('button', { style: buttonS, onClick: () => del(m.id) }, tr('settings.delete')),
|
|
780
|
+
React.createElement('button', { style: { ...buttonS, padding: '6px 10px', whiteSpace: 'nowrap' }, onClick: () => useNow(m.id), disabled: m.id === currentId }, tr('settings.setCurrent'))))
|
|
781
|
+
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.noMachines')),
|
|
293
782
|
currentId
|
|
294
783
|
? React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', paddingTop: 6, fontSize: 12, color: T.ok } },
|
|
295
|
-
'
|
|
296
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, '
|
|
297
|
-
: React.createElement('div', { style: { paddingTop: 6, fontSize: 12, opacity: 0.6 } }, '
|
|
784
|
+
tr('settings.currentMachine', { name: ((machines.find((m) => m.id === currentId) || {}).name || currentId) }),
|
|
785
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px', fontSize: 12, marginLeft: 'auto' }, onClick: clearCurrent }, tr('settings.clearCurrent')))
|
|
786
|
+
: React.createElement('div', { style: { paddingTop: 6, fontSize: 12, opacity: 0.6 } }, tr('settings.noActive')),
|
|
298
787
|
),
|
|
299
788
|
React.createElement('div', { style: boxS },
|
|
300
789
|
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 } },
|
|
301
|
-
React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, form.id ? '
|
|
302
|
-
React.createElement('button', { style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? '
|
|
790
|
+
React.createElement('div', { style: { fontSize: 13, fontWeight: 600 } }, form.id ? tr('settings.editMachine') : tr('settings.addMachine')),
|
|
791
|
+
React.createElement('button', { style: { ...buttonS, fontSize: 12, padding: '3px 8px' }, onClick: () => (sshEntries ? setSshEntries(null) : importSsh()) }, sshEntries ? tr('settings.sshCollapse') : tr('settings.sshImport'))),
|
|
303
792
|
sshEntries ? React.createElement('div', { style: { marginBottom: 8, maxHeight: 160, overflowY: 'auto', border: '1px solid ' + T.border, borderRadius: T.radius } },
|
|
304
793
|
sshEntries.length ? sshEntries.map((e) => React.createElement('div', { key: e.host, onClick: () => applySshEntry(e), style: { padding: '6px 10px', cursor: 'pointer', fontSize: 12, borderBottom: '1px solid ' + T.border } },
|
|
305
794
|
e.host + ' → ' + (e.hostName || '?') + (e.user ? ' (' + e.user + ')' : '') + (e.identityFile ? ' [key]' : '') + (e.proxyJump ? ' ⛳' : '')))
|
|
306
|
-
: React.createElement('div', { style: { padding: 8, fontSize: 12, opacity: 0.6 } }, '
|
|
795
|
+
: React.createElement('div', { style: { padding: 8, fontSize: 12, opacity: 0.6 } }, tr('settings.sshNoEntries')))
|
|
307
796
|
: null,
|
|
308
|
-
row('
|
|
309
|
-
row('
|
|
310
|
-
row('
|
|
311
|
-
row('
|
|
312
|
-
row('
|
|
797
|
+
row(tr('settings.name'), React.createElement('input', { value: form.name, onChange: setF('name'), placeholder: tr('settings.name') + ' (e.g. build-server)', style: inputS }), 'n'),
|
|
798
|
+
row(tr('settings.host'), React.createElement('input', { value: form.host, onChange: setF('host'), placeholder: 'IP or hostname', style: inputS }), 'h'),
|
|
799
|
+
row(tr('settings.port'), React.createElement('input', { value: form.port, onChange: setF('port'), placeholder: '22', style: { ...inputS, width: 70 } }), 'p'),
|
|
800
|
+
row(tr('settings.user'), React.createElement('input', { value: form.username, onChange: setF('username'), placeholder: 'root', style: inputS }), 'u'),
|
|
801
|
+
row(tr('settings.password'), React.createElement('input', { type: 'password', value: form.password, onChange: setF('password'), placeholder: 'SSH password (hidden)', style: inputS }), 'w'),
|
|
313
802
|
// 高级配置折叠开关
|
|
314
803
|
React.createElement('button', {
|
|
315
804
|
type: 'button',
|
|
316
805
|
onClick: () => setAdvOpen(!advOpen),
|
|
317
806
|
style: { ...buttonS, width: '100%', marginBottom: advOpen ? 10 : 0, color: advOpen ? T.primary : 'inherit', fontWeight: advOpen ? 600 : 400, borderStyle: 'dashed' },
|
|
318
|
-
}, advOpen ? '
|
|
807
|
+
}, advOpen ? tr('settings.advCollapse') : tr('settings.advExpand')),
|
|
319
808
|
advOpen ? React.createElement('div', { key: 'adv', style: { paddingTop: 2 } },
|
|
320
|
-
row('
|
|
321
|
-
row('
|
|
322
|
-
row('
|
|
323
|
-
row('
|
|
324
|
-
React.createElement('option', { value: '' }, '
|
|
325
|
-
React.createElement('option', { value: 'accept-new' }, '
|
|
326
|
-
React.createElement('option', { value: 'verify' }, '
|
|
327
|
-
React.createElement('option', { value: 'off' }, '
|
|
809
|
+
row(tr('settings.privateKey'), React.createElement('input', { value: form.privateKeyPath, onChange: setF('privateKeyPath'), placeholder: 'Empty = password or agent', style: inputS }), 'k'),
|
|
810
|
+
row(tr('settings.passphrase'), React.createElement('input', { type: 'password', value: form.passphrase, onChange: setF('passphrase'), placeholder: 'Encrypted private key passphrase', style: inputS }), 'pp'),
|
|
811
|
+
row(tr('settings.workspace'), React.createElement('input', { value: form.workspace, onChange: setF('workspace'), placeholder: 'Optional: default remote dir for this machine', style: inputS }), 'ws'),
|
|
812
|
+
row(tr('settings.hostKeyMode'), React.createElement('select', { value: form.hostKeyMode, onChange: setF('hostKeyMode'), style: { ...inputS, maxWidth: 220 } },
|
|
813
|
+
React.createElement('option', { value: '' }, tr('settings.hkDefault')),
|
|
814
|
+
React.createElement('option', { value: 'accept-new' }, tr('settings.hkAccept')),
|
|
815
|
+
React.createElement('option', { value: 'verify' }, tr('settings.hkVerify')),
|
|
816
|
+
React.createElement('option', { value: 'off' }, tr('settings.hkOff'))), 'hkm'),
|
|
328
817
|
React.createElement('div', { style: { display: 'flex', gap: 16, paddingLeft: 90, flexWrap: 'wrap', marginBottom: 8 } },
|
|
329
818
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
330
|
-
React.createElement('input', { type: 'checkbox', checked: form.useAgent, onChange: setFb('useAgent') }), '
|
|
819
|
+
React.createElement('input', { type: 'checkbox', checked: form.useAgent, onChange: setFb('useAgent') }), tr('settings.agent')),
|
|
331
820
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
332
|
-
React.createElement('input', { type: 'checkbox', checked: form.keyboardInteractive, onChange: setFb('keyboardInteractive') }), '
|
|
821
|
+
React.createElement('input', { type: 'checkbox', checked: form.keyboardInteractive, onChange: setFb('keyboardInteractive') }), tr('settings.keyboardInteractive')),
|
|
333
822
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
334
|
-
React.createElement('input', { type: 'checkbox', checked: form.encryptPassword, onChange: setFb('encryptPassword') }), '
|
|
335
|
-
row('
|
|
336
|
-
row('
|
|
337
|
-
React.createElement('input', { value: form.proxyPort, onChange: setF('proxyPort'), placeholder: '
|
|
338
|
-
React.createElement('input', { value: form.proxyUser, onChange: setF('proxyUser'), placeholder: '
|
|
339
|
-
React.createElement('input', { type: 'password', value: form.proxyPassword, onChange: setF('proxyPassword'), placeholder: '
|
|
340
|
-
row('
|
|
823
|
+
React.createElement('input', { type: 'checkbox', checked: form.encryptPassword, onChange: setFb('encryptPassword') }), tr('settings.encryptPassword'))),
|
|
824
|
+
row(tr('settings.proxyHost'), React.createElement('input', { value: form.proxyHost, onChange: setF('proxyHost'), placeholder: 'Route through this host (optional)', style: inputS }), 'ph'),
|
|
825
|
+
row(tr('settings.proxyPortUser'), React.createElement('div', { style: { display: 'flex', gap: 6, flex: 1, flexWrap: 'wrap' } },
|
|
826
|
+
React.createElement('input', { value: form.proxyPort, onChange: setF('proxyPort'), placeholder: 'Port (22)', style: { ...inputS, minWidth: 90, maxWidth: 120 } }),
|
|
827
|
+
React.createElement('input', { value: form.proxyUser, onChange: setF('proxyUser'), placeholder: 'Jump user', style: { ...inputS, minWidth: 140 } }),
|
|
828
|
+
React.createElement('input', { type: 'password', value: form.proxyPassword, onChange: setF('proxyPassword'), placeholder: 'Jump password', style: { ...inputS, minWidth: 140 } })), 'pu'),
|
|
829
|
+
row(tr('settings.proxyKey'), React.createElement('input', { value: form.proxyKey, onChange: setF('proxyKey'), placeholder: 'Jump private key path (optional)', style: inputS }), 'pk'),
|
|
341
830
|
) : null,
|
|
342
831
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', flexWrap: 'wrap', marginTop: 4 } },
|
|
343
832
|
msg ? React.createElement('span', { style: { color: T.ok, fontSize: 12, marginRight: 'auto' } }, msg) : null,
|
|
344
|
-
form.id ? React.createElement('button', { style: buttonS, onClick: () => save('delete') }, '
|
|
345
|
-
React.createElement('button', { style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? '
|
|
833
|
+
form.id ? React.createElement('button', { style: buttonS, onClick: () => save('delete') }, tr('settings.delete')) : null,
|
|
834
|
+
React.createElement('button', { style: buttonS, onClick: () => { clearForm(); setErr(''); setTst('idle'); setAdvOpen(false) } }, form.id ? tr('settings.cancelEdit') : tr('settings.clear')),
|
|
346
835
|
React.createElement('button', { style: { ...buttonS, fontFamily: 'monospace', whiteSpace: 'nowrap' }, onClick: testConn, disabled: busy || !form.host.trim() || tst === 'testing' },
|
|
347
|
-
tst === 'testing' ? '
|
|
348
|
-
React.createElement('button', { style: { ...buttonS, fontWeight: 700 }, onClick: () => save(form.id ? 'update' : 'add'), disabled: busy || !form.host.trim() }, busy ? '
|
|
836
|
+
tst === 'testing' ? tr('settings.testing') : tst === 'ok' ? tr('settings.testOk') : tr('settings.testConn')),
|
|
837
|
+
React.createElement('button', { style: { ...buttonS, fontWeight: 700 }, onClick: () => save(form.id ? 'update' : 'add'), disabled: busy || !form.host.trim() }, busy ? tr('settings.saving') : tr('settings.save')),
|
|
349
838
|
),
|
|
350
839
|
),
|
|
351
840
|
// 快捷切换工作区(该机器的最近工作区)
|
|
352
841
|
workspaces.length ? React.createElement('div', { style: boxS },
|
|
353
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '
|
|
842
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.recentWorkspaces')),
|
|
354
843
|
workspaces.map((w, i) => React.createElement('div', { key: w + i, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0' } },
|
|
355
844
|
React.createElement('code', { style: { flex: 1, fontSize: 12, fontFamily: 'monospace' } }, w),
|
|
356
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, '
|
|
845
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => setWorkspaceNow(w) }, tr('settings.switch'))))) : null,
|
|
357
846
|
// 端口转发面板
|
|
358
847
|
React.createElement('div', { style: boxS },
|
|
359
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '
|
|
848
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.forwards')),
|
|
360
849
|
forwards.length ? forwards.map((f) => React.createElement('div', { key: f.id, style: { display: 'flex', gap: 8, alignItems: 'center', padding: '4px 0', fontSize: 12, fontFamily: 'monospace' } },
|
|
361
850
|
React.createElement('span', { style: { color: f.active ? T.ok : T.muted } }, f.active ? '●' : '○'),
|
|
362
|
-
React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [
|
|
851
|
+
React.createElement('span', { style: { flex: 1 } }, `${f.direction} 127.0.0.1:${f.listenPort} → ${f.targetHost}:${f.targetPort}${f.autoStart ? ' [auto]' : ''}`),
|
|
363
852
|
f.active
|
|
364
|
-
? React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, '
|
|
365
|
-
: React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, '
|
|
366
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, '
|
|
853
|
+
? React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('stop', f.id) }, tr('settings.fwdStop'))
|
|
854
|
+
: React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('start', f.id) }, tr('settings.fwdStart')),
|
|
855
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => fwdAction('remove', f.id) }, tr('settings.fwdRemove')))) : null,
|
|
367
856
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', marginTop: 6, flexWrap: 'wrap' } },
|
|
368
857
|
React.createElement('select', { value: fwdForm.direction, onChange: (e) => setFwdForm({ ...fwdForm, direction: e.target.value }), style: { ...inputS, maxWidth: 110 } },
|
|
369
|
-
React.createElement('option', { value: 'local' }, '
|
|
370
|
-
React.createElement('option', { value: 'reverse' }, '
|
|
371
|
-
React.createElement('input', { value: fwdForm.listenPort, onChange: (e) => setFwdForm({ ...fwdForm, listenPort: e.target.value }), placeholder: '
|
|
372
|
-
React.createElement('input', { value: fwdForm.targetHost, onChange: (e) => setFwdForm({ ...fwdForm, targetHost: e.target.value }), placeholder: '
|
|
373
|
-
React.createElement('input', { value: fwdForm.targetPort, onChange: (e) => setFwdForm({ ...fwdForm, targetPort: e.target.value }), placeholder: '
|
|
858
|
+
React.createElement('option', { value: 'local' }, tr('settings.fwdDirLocal')),
|
|
859
|
+
React.createElement('option', { value: 'reverse' }, tr('settings.fwdDirReverse'))),
|
|
860
|
+
React.createElement('input', { value: fwdForm.listenPort, onChange: (e) => setFwdForm({ ...fwdForm, listenPort: e.target.value }), placeholder: tr('settings.fwdListen'), style: { ...inputS, maxWidth: 90 } }),
|
|
861
|
+
React.createElement('input', { value: fwdForm.targetHost, onChange: (e) => setFwdForm({ ...fwdForm, targetHost: e.target.value }), placeholder: tr('settings.fwdTargetHost'), style: { ...inputS, maxWidth: 120 } }),
|
|
862
|
+
React.createElement('input', { value: fwdForm.targetPort, onChange: (e) => setFwdForm({ ...fwdForm, targetPort: e.target.value }), placeholder: tr('settings.fwdTargetPort'), style: { ...inputS, maxWidth: 90 } }),
|
|
374
863
|
React.createElement('label', { style: { fontSize: 12, display: 'flex', gap: 4, alignItems: 'center' } },
|
|
375
|
-
React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), '
|
|
376
|
-
React.createElement('button', { style: { ...buttonS, fontWeight: 700 }, onClick: fwdDefine, disabled: !fwdForm.listenPort }, '
|
|
864
|
+
React.createElement('input', { type: 'checkbox', checked: fwdForm.autoStart, onChange: (e) => setFwdForm({ ...fwdForm, autoStart: e.target.checked }) }), tr('settings.fwdAuto')),
|
|
865
|
+
React.createElement('button', { style: { ...buttonS, fontWeight: 700 }, onClick: fwdDefine, disabled: !fwdForm.listenPort }, tr('settings.fwdAdd'))),
|
|
377
866
|
),
|
|
378
867
|
// 审计日志
|
|
379
868
|
React.createElement('div', { style: boxS },
|
|
380
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '
|
|
869
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.audit')),
|
|
381
870
|
auditLines.length
|
|
382
871
|
? React.createElement('div', { style: { maxHeight: 180, overflowY: 'auto', fontSize: 11, fontFamily: 'monospace', lineHeight: 1.6 } },
|
|
383
872
|
auditLines.map((l, i) => React.createElement('div', { key: i, style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' } }, l)))
|
|
384
|
-
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, '
|
|
873
|
+
: React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, tr('settings.auditEmpty')),
|
|
385
874
|
),
|
|
386
875
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 13 } }, err) : null,
|
|
387
876
|
React.createElement('div', { style: boxS },
|
|
388
|
-
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, '
|
|
877
|
+
React.createElement('div', { style: { marginBottom: 6, fontSize: 13, fontWeight: 600 } }, tr('settings.update')),
|
|
389
878
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', fontSize: 12 } },
|
|
390
879
|
upd
|
|
391
880
|
? React.createElement('span', { style: { opacity: 0.85 } },
|
|
392
|
-
'
|
|
393
|
-
upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→
|
|
394
|
-
: React.createElement('span', { style: { opacity: 0.6 } }, '
|
|
395
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? '
|
|
881
|
+
'v' + upd.current,
|
|
882
|
+
upd.updateAvailable ? React.createElement('b', { style: { color: T.ok, marginLeft: 6 } }, '→ v' + upd.latest) : null)
|
|
883
|
+
: React.createElement('span', { style: { opacity: 0.6 } }, tr('settings.updateChecking')),
|
|
884
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px' }, onClick: () => checkUpdate(false), disabled: updBusy }, updBusy ? tr('settings.checking') : tr('settings.checkUpdate')),
|
|
396
885
|
upd && upd.updateAvailable
|
|
397
|
-
? React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontWeight: 700, color: T.ok }, onClick: applyUpdateNow, disabled: updBusy }, '
|
|
886
|
+
? React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontWeight: 700, color: T.ok }, onClick: applyUpdateNow, disabled: updBusy }, tr('settings.updateNow'))
|
|
398
887
|
: null,
|
|
399
888
|
),
|
|
400
889
|
React.createElement('div', { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6, fontSize: 12 } },
|
|
401
|
-
React.createElement('span', { style: { opacity: 0.8 } }, '
|
|
890
|
+
React.createElement('span', { style: { opacity: 0.8 } }, tr('settings.updateMode')),
|
|
402
891
|
['manual', 'auto', 'off'].map((m) =>
|
|
403
892
|
React.createElement('button', {
|
|
404
893
|
key: m, style: { ...buttonS, padding: '2px 10px', fontWeight: updMode === m ? 700 : 400, color: updMode === m ? T.ok : 'inherit' },
|
|
405
894
|
onClick: () => saveUpdateMode(m), disabled: updBusy,
|
|
406
|
-
}, m === 'manual' ? '
|
|
895
|
+
}, m === 'manual' ? tr('settings.modeManual') : m === 'auto' ? tr('settings.modeAuto') : tr('settings.modeOff')),
|
|
407
896
|
),
|
|
408
897
|
),
|
|
409
|
-
updMsg ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: updMsg.startsWith('✅')
|
|
898
|
+
updMsg ? React.createElement('div', { style: { marginTop: 6, fontSize: 12, color: updMsg.startsWith('✅') ? T.ok : 'inherit', opacity: 0.9 } }, updMsg) : null,
|
|
410
899
|
),
|
|
411
900
|
React.createElement('div', { style: { display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', fontSize: 11, opacity: 0.65, borderTop: '1px solid ' + T.border, paddingTop: 10, marginTop: 4 } },
|
|
412
|
-
React.createElement('span', {}, '
|
|
413
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
901
|
+
React.createElement('span', {}, tr('settings.like')),
|
|
902
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('settings.star')),
|
|
414
903
|
React.createElement('span', {}, '·'),
|
|
415
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
904
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, tr('settings.feedback')),
|
|
416
905
|
React.createElement('span', { style: { marginLeft: 'auto', opacity: 0.5 } }, 'dsh-remote v' + (upd && upd.current ? upd.current : '?.?.?')),
|
|
417
906
|
),
|
|
418
907
|
)
|
|
@@ -440,7 +929,7 @@ window.__ModuleLoader__.load({
|
|
|
440
929
|
function breadcrumb(active, cur, win, jumpTo) {
|
|
441
930
|
const raw = String(cur || '')
|
|
442
931
|
const isWinPath = win && (raw === '' || /^[a-zA-Z]:/.test(raw))
|
|
443
|
-
const rootLabel = isWinPath ? '
|
|
932
|
+
const rootLabel = isWinPath ? tr('picker.thisPc') : '/'
|
|
444
933
|
const crumbs = []
|
|
445
934
|
const rootStyle = { cursor: active ? 'pointer' : 'default', color: active ? T.ok : T.muted, whiteSpace: 'nowrap' }
|
|
446
935
|
crumbs.push(React.createElement('span', { key: 'root', style: rootStyle, onClick: active ? () => jumpTo('') : undefined }, rootLabel))
|
|
@@ -467,7 +956,7 @@ window.__ModuleLoader__.load({
|
|
|
467
956
|
crumbs.push(React.createElement('span', { key: acc, style: { cursor: active ? 'pointer' : 'default', color: active ? T.label : T.muted, fontWeight: acc === raw ? 700 : 400, whiteSpace: 'nowrap' }, onClick: active ? () => jumpTo(acc) : undefined }, s))
|
|
468
957
|
}
|
|
469
958
|
}
|
|
470
|
-
if (crumbs.length === 1) crumbs.push(React.createElement('span', { key: 'empty', style: { color: T.muted } }, isWinPath ? '
|
|
959
|
+
if (crumbs.length === 1) crumbs.push(React.createElement('span', { key: 'empty', style: { color: T.muted } }, isWinPath ? tr('picker.chooseDrive') : ''))
|
|
471
960
|
return React.createElement('span', { key: 'crumb', style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'inline-block', maxWidth: '100%' } }, crumbs)
|
|
472
961
|
}
|
|
473
962
|
|
|
@@ -581,10 +1070,10 @@ window.__ModuleLoader__.load({
|
|
|
581
1070
|
// browse 后端(DSH Desktop / 无显示的远程宿主):没有 OS 对话框可开
|
|
582
1071
|
// → 打开内置本机目录浏览器。
|
|
583
1072
|
else if (r && r.kind === 'browse') loadLocal('')
|
|
584
|
-
else if (r && r.cancelled) { setLoading(false); setErr('
|
|
585
|
-
else { setLoading(false); setErr((r && r.error) || '
|
|
1073
|
+
else if (r && r.cancelled) { setLoading(false); setErr(tr('picker.cancelled')) }
|
|
1074
|
+
else { setLoading(false); setErr((r && r.error) || tr('picker.noSysPicker')) }
|
|
586
1075
|
})
|
|
587
|
-
.catch((e) => { setLoading(false); setErr(String((e && e.message) || e) + '
|
|
1076
|
+
.catch((e) => { setLoading(false); setErr(String((e && e.message) || e) + tr('picker.typePath')) })
|
|
588
1077
|
}
|
|
589
1078
|
|
|
590
1079
|
// 在浮层当前目录下新建文件夹(browse 后端 createDirectory),成功后刷新列表。
|
|
@@ -724,14 +1213,14 @@ window.__ModuleLoader__.load({
|
|
|
724
1213
|
const pickHome = () => {
|
|
725
1214
|
api('POST', '/dsh-remote/home').then((r) => {
|
|
726
1215
|
if (r && r.home) { setPath(r.home); setErr(''); loadSuggestions(r.home) }
|
|
727
|
-
else setErr((r && (r.hint || r.error)) || '
|
|
1216
|
+
else setErr((r && (r.hint || r.error)) || tr('picker.noHome'))
|
|
728
1217
|
}).catch((e) => setErr(String((e && e.message) || e)))
|
|
729
1218
|
}
|
|
730
1219
|
|
|
731
1220
|
const mkdirHere = () => {
|
|
732
1221
|
const last = levels && levels.length ? levels[levels.length - 1] : null
|
|
733
1222
|
const base = (last && last.path) || '/'
|
|
734
|
-
const nm = window.prompt('
|
|
1223
|
+
const nm = window.prompt(tr('picker.newDirName', { path: base }))
|
|
735
1224
|
if (!nm || !nm.trim()) return
|
|
736
1225
|
const sep = base.includes('\\') ? '\\' : '/'
|
|
737
1226
|
const full = base === '/' ? '/' + nm.trim() : base + sep + nm.trim()
|
|
@@ -770,10 +1259,10 @@ window.__ModuleLoader__.load({
|
|
|
770
1259
|
},
|
|
771
1260
|
React.createElement('div', { style: { background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 10, boxShadow: '0 10px 40px rgba(0,0,0,0.5)', width: 'min(560px, 94vw)', minWidth: 320, display: 'flex', flexDirection: 'column', maxHeight: 'min(460px, 84vh)', overflow: 'hidden' }, onClick: (e) => e.stopPropagation() },
|
|
772
1261
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '8px 12px', borderBottom: '1px solid ' + T.border } },
|
|
773
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 10px' }, onClick: () => parent && loadLocal(parent), disabled: !parent || loading, title: parent || '' }, '
|
|
774
|
-
cur.home ? React.createElement('button', { style: { ...buttonS, padding: '3px 8px' }, onClick: () => loadLocal(cur.home), disabled: loading, title:
|
|
1262
|
+
React.createElement('button', { style: { ...buttonS, padding: '3px 10px' }, onClick: () => parent && loadLocal(parent), disabled: !parent || loading, title: parent || '' }, tr('picker.back')),
|
|
1263
|
+
cur.home ? React.createElement('button', { style: { ...buttonS, padding: '3px 8px' }, onClick: () => loadLocal(cur.home), disabled: loading, title: tr('picker.goHome') }, tr('picker.homeBtn')) : null,
|
|
775
1264
|
React.createElement('div', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, localCrumbs(cur)),
|
|
776
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 10px' }, onClick: () => setLocalPop(null) }, '
|
|
1265
|
+
React.createElement('button', { style: { ...buttonS, padding: '3px 10px' }, onClick: () => setLocalPop(null) }, tr('picker.closeBtn')),
|
|
777
1266
|
),
|
|
778
1267
|
// 盘符切换行(Windows 无统一根目录,browse 面包屑到不了其他盘)。
|
|
779
1268
|
(cur.drives && cur.drives.length > 1) ? React.createElement('div', { style: { display: 'flex', gap: 4, flexWrap: 'wrap', padding: '6px 12px', borderBottom: '1px solid ' + T.border } },
|
|
@@ -789,23 +1278,23 @@ window.__ModuleLoader__.load({
|
|
|
789
1278
|
}),
|
|
790
1279
|
) : null,
|
|
791
1280
|
React.createElement('div', { style: { overflowY: 'auto', overflowX: 'hidden' } },
|
|
792
|
-
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12 } }, '
|
|
1281
|
+
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12 } }, tr('picker.loading'))
|
|
793
1282
|
: (cur.entries.length ? cur.entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
794
|
-
key: it.path || i, title: '
|
|
1283
|
+
key: it.path || i, title: tr('picker.enterDir', { name: it.name }),
|
|
795
1284
|
onClick: () => loadLocal(it.path),
|
|
796
1285
|
style: { padding: '7px 12px', cursor: 'pointer', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
797
1286
|
},
|
|
798
1287
|
React.createElement('span', null, '📁'),
|
|
799
1288
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis' } }, it.name),
|
|
800
|
-
)) : React.createElement('div', { style: { opacity: 0.6, padding: 12 } }, '
|
|
801
|
-
cur.truncated ? React.createElement('div', { style: { opacity: 0.6, padding: '6px 12px', fontSize: 11 } }, '
|
|
1289
|
+
)) : React.createElement('div', { style: { opacity: 0.6, padding: 12 } }, tr('picker.noSubdir'))),
|
|
1290
|
+
cur.truncated ? React.createElement('div', { style: { opacity: 0.6, padding: '6px 12px', fontSize: 11 } }, tr('picker.truncated')) : null,
|
|
802
1291
|
),
|
|
803
1292
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12, padding: '4px 12px' } }, err) : null,
|
|
804
1293
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '8px 12px', borderTop: '1px solid ' + T.border } },
|
|
805
|
-
React.createElement('input', { value: newDirName, onChange: (e) => setNewDirName(e.target.value), placeholder: '
|
|
806
|
-
React.createElement('button', { style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, '
|
|
807
|
-
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, '
|
|
808
|
-
React.createElement('button', { style: { ...buttonS, fontWeight: 600, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, '
|
|
1294
|
+
React.createElement('input', { value: newDirName, onChange: (e) => setNewDirName(e.target.value), placeholder: tr('picker.newFolder'), style: { ...inputS, width: 110, flex: '0 0 auto', padding: '4px 8px', fontSize: 12 } }),
|
|
1295
|
+
React.createElement('button', { style: { ...buttonS, padding: '4px 10px', whiteSpace: 'nowrap' }, onClick: mkdirLocal, disabled: loading || !newDirName.trim() || !cur.path }, tr('picker.create')),
|
|
1296
|
+
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: cur.path })),
|
|
1297
|
+
React.createElement('button', { style: { ...buttonS, fontWeight: 600, whiteSpace: 'nowrap' }, onClick: () => acceptLocalPick(cur.path) }, tr('picker.chooseThis')),
|
|
809
1298
|
),
|
|
810
1299
|
),
|
|
811
1300
|
)
|
|
@@ -813,21 +1302,21 @@ window.__ModuleLoader__.load({
|
|
|
813
1302
|
|
|
814
1303
|
function renderDirPopup() {
|
|
815
1304
|
if (!levels || !levels.length) {
|
|
816
|
-
return React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, (loading ? '
|
|
1305
|
+
return React.createElement('div', { style: { opacity: 0.6, fontSize: 12 } }, (loading ? tr('picker.loading') : tr('picker.readingRoot')))
|
|
817
1306
|
}
|
|
818
1307
|
const last = levels[levels.length - 1]
|
|
819
1308
|
const entries = last.all || []
|
|
820
1309
|
return React.createElement('div', { style: { border: '1px solid ' + T.borderStrong, borderRadius: 10, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), boxShadow: '0 8px 32px rgba(0,0,0,0.4)', display: 'flex', flexDirection: 'column', maxHeight: 'min(300px, 46vh)', overflow: 'hidden' } },
|
|
821
1310
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 10px', borderBottom: '1px solid ' + T.border } },
|
|
822
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: goUp, disabled: loading || (levels.length <= 1 && !parentOf(last.path)) }, '
|
|
1311
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: goUp, disabled: loading || (levels.length <= 1 && !parentOf(last.path)) }, tr('picker.back')),
|
|
823
1312
|
React.createElement('div', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, breadcrumb(!!machineId, last.path, win, (p) => setLevels((prev) => { const cut = (prev || []).findIndex((lv) => lv.path === p); return cut >= 0 ? prev.slice(0, cut + 1) : prev }))),
|
|
824
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: mkdirHere }, '
|
|
825
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => setPopOpen(false) }, '
|
|
1313
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: mkdirHere }, tr('picker.newDir')),
|
|
1314
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => setPopOpen(false) }, tr('picker.closeBtn')),
|
|
826
1315
|
),
|
|
827
1316
|
React.createElement('div', { style: { overflowY: 'auto', overflowX: 'hidden' } },
|
|
828
|
-
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12 } }, '
|
|
1317
|
+
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12 } }, tr('picker.loading'))
|
|
829
1318
|
: (entries.length ? entries.slice(0, 400).map((it, i) => React.createElement('div', {
|
|
830
|
-
key: i, title: it.drive ? '
|
|
1319
|
+
key: i, title: it.drive ? tr('picker.enterDrive', { name: it.name }) : (it.dir ? tr('picker.enterDir', { name: it.name }) : tr('picker.fileAt', { name: it.name })),
|
|
831
1320
|
onClick: it.dir ? () => enterDir(it) : undefined,
|
|
832
1321
|
style: { padding: '6px 10px', cursor: it.dir ? 'pointer' : 'default', color: T.label, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', display: 'flex', gap: 8, alignItems: 'center', fontSize: 13, borderBottom: '1px solid ' + T.border },
|
|
833
1322
|
},
|
|
@@ -836,11 +1325,11 @@ window.__ModuleLoader__.load({
|
|
|
836
1325
|
: React.createElement('svg', { width: 14, height: 14, viewBox: '0 0 16 16', fill: 'currentColor', opacity: 0.85 }, React.createElement('path', { d: 'M3.5 1.5a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V5.2a1 1 0 0 0-.29-.7L10.5 1.79a1 1 0 0 0-.7-.29H3.5Zm1.5 3h2.25a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5ZM5 9.5h6a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Zm0 2.25h4a.75.75 0 0 1 0 1.5H5a.75.75 0 0 1 0-1.5Z' })))),
|
|
837
1326
|
React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 } }, it.name),
|
|
838
1327
|
React.createElement('span', { style: { fontSize: 11, opacity: 0.55, flexShrink: 0, fontFamily: 'monospace' } }, it.dir ? '' : (humanSize(it.size) + (it.mtime ? ' ' + fmtTime(it.mtime) : ''))),
|
|
839
|
-
)) : React.createElement('div', { style: { opacity: 0.6, padding: 12 } }, '
|
|
1328
|
+
)) : React.createElement('div', { style: { opacity: 0.6, padding: 12 } }, tr('picker.emptyDir'))),
|
|
840
1329
|
),
|
|
841
1330
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center', padding: '6px 10px', borderTop: '1px solid ' + T.border } },
|
|
842
|
-
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, '
|
|
843
|
-
React.createElement('button', { style: { ...buttonS, fontWeight: 600 }, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, '
|
|
1331
|
+
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, tr('picker.selected', { path: (last.path || (win ? tr('picker.driveRoot') : '/')) })),
|
|
1332
|
+
React.createElement('button', { style: { ...buttonS, fontWeight: 600 }, onClick: () => { acceptBrowserPick(last.path); setPopOpen(false) } }, tr('picker.chooseThis')),
|
|
844
1333
|
),
|
|
845
1334
|
)
|
|
846
1335
|
}
|
|
@@ -850,49 +1339,49 @@ window.__ModuleLoader__.load({
|
|
|
850
1339
|
return React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16 }, onClick: () => { if (!busy) onCancel() } },
|
|
851
1340
|
React.createElement('div', { style: { background: v('--dsw-alias-bg-layer-1', '#18181b'), border: '1px solid ' + T.borderStrong, borderRadius: 12, boxShadow: '0 12px 48px rgba(0,0,0,0.5)', width: 'min(600px, 94vw)', maxHeight: 'min(680px, 92vh)', display: 'flex', flexDirection: 'column', padding: 16, boxSizing: 'border-box' }, onClick: (e) => e.stopPropagation() },
|
|
852
1341
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 } },
|
|
853
|
-
React.createElement('div', null, '
|
|
854
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, '
|
|
1342
|
+
React.createElement('div', null, tr('picker.title')),
|
|
1343
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 8px' }, onClick: () => { if (!busy) onCancel() }, disabled: busy }, tr('picker.close')),
|
|
855
1344
|
),
|
|
856
1345
|
React.createElement('div', { style: { display: 'flex', gap: 6, marginBottom: 8 } },
|
|
857
|
-
tabBtn('local', '
|
|
858
|
-
tabBtn('remote', '
|
|
1346
|
+
tabBtn('local', tr('picker.tabLocal')),
|
|
1347
|
+
tabBtn('remote', tr('picker.tabRemote')),
|
|
859
1348
|
),
|
|
860
1349
|
tab === 'local'
|
|
861
1350
|
? React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8 } },
|
|
862
|
-
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } }, '
|
|
1351
|
+
React.createElement('div', { style: { fontSize: 12, opacity: 0.8 } }, tr('picker.localIntro')),
|
|
863
1352
|
React.createElement('div', { style: { display: 'flex', gap: 6 } },
|
|
864
|
-
React.createElement('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: '
|
|
865
|
-
React.createElement('button', { style: buttonS, onClick: () => (path.trim() ? onPicked(path) : undefined), disabled: !path.trim() }, '
|
|
1353
|
+
React.createElement('input', { value: path, onChange: (e) => setPath(e.target.value), placeholder: tr('picker.localPlaceholder'), style: inputS }),
|
|
1354
|
+
React.createElement('button', { style: buttonS, onClick: () => (path.trim() ? onPicked(path) : undefined), disabled: !path.trim() }, tr('picker.useLocalPath')),
|
|
866
1355
|
),
|
|
867
|
-
React.createElement('button', { style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? '
|
|
1356
|
+
React.createElement('button', { style: { ...buttonS, alignSelf: 'flex-start' }, onClick: chooseLocal, disabled: loading }, loading ? tr('picker.opening') : tr('picker.openSysPicker')),
|
|
868
1357
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
869
1358
|
localPop ? renderLocalPopup() : null,
|
|
870
1359
|
)
|
|
871
1360
|
: React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 8, minHeight: 0, overflowY: 'auto' } },
|
|
872
1361
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center' } },
|
|
873
|
-
React.createElement('label', { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'nowrap' } }, '
|
|
1362
|
+
React.createElement('label', { style: { fontSize: 12, opacity: 0.8, whiteSpace: 'nowrap' } }, tr('picker.remoteMachine')),
|
|
874
1363
|
React.createElement('div', { ref: ddRef, style: { position: 'relative', display: 'inline-block' } },
|
|
875
1364
|
React.createElement('button', { style: { ...inputS, textAlign: 'left', cursor: 'pointer', minWidth: 200, maxWidth: 320, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, onClick: () => setMOpen((x) => !x) },
|
|
876
|
-
(machines.find((m) => m.id === machineId) || {}).name || (machineId ? '…' : '
|
|
1365
|
+
(machines.find((m) => m.id === machineId) || {}).name || (machineId ? '…' : tr('picker.chooseMachine')),
|
|
877
1366
|
),
|
|
878
1367
|
mOpen
|
|
879
1368
|
? React.createElement('div', { style: { position: 'absolute', top: '100%', left: 0, zIndex: 1000, marginTop: 2, minWidth: 300, maxHeight: 260, overflow: 'auto', background: v('--dsw-alias-bg-overlay', '#1f1f23'), border: '1px solid ' + T.border, borderRadius: T.radius, padding: 4 } },
|
|
880
1369
|
machines.length
|
|
881
1370
|
? machines.map((m) => React.createElement('div', { key: m.id, onClick: () => { setMachineId(m.id); setRecent(m.recentWorkspaces || []); setLevels(null); setPath(''); setMOpen(false); if (m.id) loadLevels(m.id, '', 0) }, style: { padding: '6px 8px', borderRadius: 4, cursor: 'pointer', color: m.id === machineId ? T.ok : 'inherit' } }, m.name + ' (' + m.username + '@' + m.host + ':' + m.port + ')'))
|
|
882
|
-
: React.createElement('div', { style: { padding: 6, opacity: 0.6 } }, '
|
|
1371
|
+
: React.createElement('div', { style: { padding: 6, opacity: 0.6 } }, tr('picker.noMachines')),
|
|
883
1372
|
)
|
|
884
1373
|
: null,
|
|
885
1374
|
),
|
|
886
1375
|
),
|
|
887
1376
|
// 最近工作区快捷入口
|
|
888
1377
|
recent.length ? React.createElement('div', { style: { display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' } },
|
|
889
|
-
React.createElement('span', { style: { fontSize: 11, opacity: 0.6, whiteSpace: 'nowrap' } }, '
|
|
1378
|
+
React.createElement('span', { style: { fontSize: 11, opacity: 0.6, whiteSpace: 'nowrap' } }, tr('picker.recent')),
|
|
890
1379
|
recent.map((w, i) => React.createElement('button', { key: w + i, onClick: () => { setPath(w); setErr(''); continueSuggest(w) }, style: { ...buttonS, padding: '2px 8px', fontSize: 11, fontFamily: 'monospace' } }, w))) : null,
|
|
891
1380
|
React.createElement('div', { ref: suggestRef, style: { position: 'relative' } },
|
|
892
1381
|
React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } },
|
|
893
|
-
React.createElement('input', { value: path, onChange: (e) => onPathChange(e.target.value), onFocus: () => loadSuggestions(path), placeholder: (machineId ? (win ? '
|
|
894
|
-
React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: () => { if (!machineId) return; const p = path.trim(); loadLevels(machineId, p || '', 0); setPopOpen(true) }, disabled: !machineId }, '
|
|
895
|
-
React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, '
|
|
1382
|
+
React.createElement('input', { value: path, onChange: (e) => onPathChange(e.target.value), onFocus: () => loadSuggestions(path), placeholder: (machineId ? (win ? tr('picker.pathPlaceholderWin') : tr('picker.pathPlaceholder')) : tr('picker.chooseMachineFirst')), disabled: !machineId, style: { ...inputS, flex: 1, minWidth: 120 } }),
|
|
1383
|
+
React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: () => { if (!machineId) return; const p = path.trim(); loadLevels(machineId, p || '', 0); setPopOpen(true) }, disabled: !machineId }, tr('picker.browse')),
|
|
1384
|
+
React.createElement('button', { style: { ...buttonS, whiteSpace: 'nowrap' }, onClick: pickHome, disabled: !machineId }, tr('picker.home')),
|
|
896
1385
|
),
|
|
897
1386
|
(suggestOpen && suggest.length)
|
|
898
1387
|
? React.createElement('div', { style: { marginTop: 6, background: v('--dsw-alias-bg-overlay', '#1e1e1e'), border: '1px solid ' + T.borderStrong, borderRadius: 8, maxHeight: 200, overflowY: 'auto', boxShadow: '0 6px 24px rgba(0,0,0,0.25)' } },
|
|
@@ -903,15 +1392,15 @@ window.__ModuleLoader__.load({
|
|
|
903
1392
|
popOpen ? renderDirPopup() : null,
|
|
904
1393
|
err ? React.createElement('div', { style: { color: T.danger, fontSize: 12 } }, err) : null,
|
|
905
1394
|
React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end', alignItems: 'center' } },
|
|
906
|
-
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, path ? '
|
|
907
|
-
React.createElement('button', { style: { ...buttonS, fontWeight: 600 }, onClick: () => commitPath(path), disabled: busy || !machineId || !path.trim() }, busy ? '
|
|
1395
|
+
React.createElement('span', { style: { fontSize: 11, opacity: 0.75, fontFamily: 'monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 } }, path ? tr('picker.selected', { path }) : ''),
|
|
1396
|
+
React.createElement('button', { style: { ...buttonS, fontWeight: 600 }, onClick: () => commitPath(path), disabled: busy || !machineId || !path.trim() }, busy ? tr('picker.mirroring') : tr('picker.setRemoteWs')),
|
|
908
1397
|
),
|
|
909
1398
|
),
|
|
910
|
-
(tab === 'local') ? React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, React.createElement('button', { style: { background: 'transparent' }, onClick: onCancel }, '
|
|
1399
|
+
(tab === 'local') ? React.createElement('div', { style: { display: 'flex', gap: 8, justifyContent: 'flex-end' } }, React.createElement('button', { style: { background: 'transparent' }, onClick: onCancel }, tr('picker.cancel'))) : null,
|
|
911
1400
|
React.createElement('div', { style: { display: 'flex', gap: 10, justifyContent: 'center', alignItems: 'center', paddingTop: 6, fontSize: 10, opacity: 0.5 } },
|
|
912
1401
|
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '⭐ Star dsh-remote'),
|
|
913
1402
|
React.createElement('span', {}, '·'),
|
|
914
|
-
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, '
|
|
1403
|
+
React.createElement('a', { href: 'https://github.com/flymysql/dsh-remote/issues/new', target: '_blank', rel: 'noopener noreferrer', style: { color: 'inherit' } }, 'Feedback / report issue'),
|
|
915
1404
|
),
|
|
916
1405
|
),
|
|
917
1406
|
)
|
|
@@ -956,13 +1445,13 @@ window.__ModuleLoader__.load({
|
|
|
956
1445
|
|
|
957
1446
|
function readRemoteFile(path, maxBytes) {
|
|
958
1447
|
return api('POST', '/dsh-remote/read', { path, maxBytes }).catch((e) => {
|
|
959
|
-
throw new Error('
|
|
1448
|
+
throw new Error(tr('file.readFail', { msg: ((e && e.message) || e) }))
|
|
960
1449
|
})
|
|
961
1450
|
}
|
|
962
1451
|
|
|
963
1452
|
function listRemoteDir(path) {
|
|
964
1453
|
return api('GET', '/dsh-remote/ls?path=' + encodeURIComponent(path || '')).catch((e) => {
|
|
965
|
-
throw new Error('
|
|
1454
|
+
throw new Error(tr('file.listFail', { msg: ((e && e.message) || e) }))
|
|
966
1455
|
})
|
|
967
1456
|
}
|
|
968
1457
|
|
|
@@ -1070,7 +1559,7 @@ window.__ModuleLoader__.load({
|
|
|
1070
1559
|
// show a clear disconnected state instead of firing ls requests
|
|
1071
1560
|
// that all fail with AggregateError / 500 in a loop.
|
|
1072
1561
|
if (!connected) {
|
|
1073
|
-
storeLevel(r, { error: '
|
|
1562
|
+
storeLevel(r, { error: tr('explorer.notConnected') })
|
|
1074
1563
|
return
|
|
1075
1564
|
}
|
|
1076
1565
|
if (!sameRoot) {
|
|
@@ -1140,12 +1629,12 @@ window.__ModuleLoader__.load({
|
|
|
1140
1629
|
d.addEventListener('click', (e) => { e.stopPropagation(); setMenu(null); fn() })
|
|
1141
1630
|
return d
|
|
1142
1631
|
}
|
|
1143
|
-
box.appendChild(mkItem(menu.isDir ? '
|
|
1144
|
-
box.appendChild(mkItem('
|
|
1145
|
-
box.appendChild(mkItem('
|
|
1146
|
-
box.appendChild(mkItem('
|
|
1147
|
-
box.appendChild(mkItem('
|
|
1148
|
-
box.appendChild(mkItem(
|
|
1632
|
+
box.appendChild(mkItem(menu.isDir ? tr('explorer.menuExpand') : tr('explorer.menuOpen'), false, () => { if (menu.isDir) { if (props.onToggleDir) props.onToggleDir(menu.path) } else openFile(menu.path) }))
|
|
1633
|
+
box.appendChild(mkItem(tr('explorer.menuDownload'), false, () => doFs('download', { path: menu.path })))
|
|
1634
|
+
box.appendChild(mkItem(tr('explorer.menuCopyRel'), false, () => copyPath(relPath(menu.path), menu.path)))
|
|
1635
|
+
box.appendChild(mkItem(tr('explorer.menuCopyAbs'), false, () => copyPath(menu.path, menu.path)))
|
|
1636
|
+
box.appendChild(mkItem(tr('explorer.menuRename'), false, () => { const nm = window.prompt(tr('explorer.renamePrompt'), menu.name); if (nm && nm.trim() && nm.trim() !== menu.name) doFs('rename', { path: menu.path, dest: joinRemote(String(menu.path).replace(/[\\/][^\\/]*$/, ''), nm.trim()) }, true) }))
|
|
1637
|
+
box.appendChild(mkItem(menu.isDir ? tr('explorer.menuDeleteDir') : tr('explorer.menuDelete'), true, () => { if (window.confirm(tr('explorer.deleteConfirm', { path: menu.path }))) doFs('remove', { path: menu.path }, true) }))
|
|
1149
1638
|
host.appendChild(box)
|
|
1150
1639
|
document.body.appendChild(host)
|
|
1151
1640
|
const close = (e) => { if (!box.contains(e.target)) { setMenu(null) } }
|
|
@@ -1207,7 +1696,7 @@ window.__ModuleLoader__.load({
|
|
|
1207
1696
|
}
|
|
1208
1697
|
/** Trailing「已复制」feedback for a row (after a successful copy). */
|
|
1209
1698
|
const copiedMark = (path) => (copiedPath === path
|
|
1210
|
-
? React.createElement('span', { style: { fontSize: 11, opacity: 0.75, flexShrink: 0, color: T.ok } }, '
|
|
1699
|
+
? React.createElement('span', { style: { fontSize: 11, opacity: 0.75, flexShrink: 0, color: T.ok } }, tr('explorer.copied'))
|
|
1211
1700
|
: null)
|
|
1212
1701
|
|
|
1213
1702
|
// One tree row (dir toggles via the framework's onToggleDir; file opens).
|
|
@@ -1265,17 +1754,17 @@ window.__ModuleLoader__.load({
|
|
|
1265
1754
|
const renderLevel = (dir, depth) => {
|
|
1266
1755
|
const level = data[dir]
|
|
1267
1756
|
if (level === undefined || (level.loading && !level.entries)) {
|
|
1268
|
-
return React.createElement('div', { key: dir + ':loading', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, '
|
|
1757
|
+
return React.createElement('div', { key: dir + ':loading', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, tr('explorer.loading'))
|
|
1269
1758
|
}
|
|
1270
1759
|
if (level.missing) {
|
|
1271
|
-
return React.createElement('div', { key: dir + ':missing', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, '
|
|
1760
|
+
return React.createElement('div', { key: dir + ':missing', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, opacity: 0.6 } }, tr('explorer.dirMissing'))
|
|
1272
1761
|
}
|
|
1273
1762
|
if (level.error !== undefined) {
|
|
1274
1763
|
return React.createElement('div', { key: dir + ':err', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 13, color: '#e06c75' } }, level.error)
|
|
1275
1764
|
}
|
|
1276
1765
|
const items = level.entries || []
|
|
1277
1766
|
const soft = level.softError
|
|
1278
|
-
? React.createElement('div', { key: dir + ':softerr', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 11, opacity: 0.6, color: '#e6c07b' } }, '
|
|
1767
|
+
? React.createElement('div', { key: dir + ':softerr', style: { display: 'flex', alignItems: 'center', gap: 6, padding: '5px 8px', paddingLeft: depth * 22 + 6, fontSize: 11, opacity: 0.6, color: '#e6c07b' } }, tr('explorer.softError', { msg: level.softError }))
|
|
1279
1768
|
: null
|
|
1280
1769
|
if (!items.length) return soft || null
|
|
1281
1770
|
return React.createElement('div', { key: dir + ':list' },
|
|
@@ -1289,22 +1778,22 @@ window.__ModuleLoader__.load({
|
|
|
1289
1778
|
|
|
1290
1779
|
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 } },
|
|
1291
1780
|
React.createElement('div', { style: { display: 'flex', gap: 6, alignItems: 'center', padding: '6px 8px', borderBottom: '1px solid rgba(128,128,128,0.25)' } },
|
|
1292
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: '
|
|
1781
|
+
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: refreshStatus, title: tr('explorer.refresh') }, '↻'),
|
|
1293
1782
|
React.createElement('button', {
|
|
1294
1783
|
style: { ...buttonS, padding: '3px 8px', fontSize: 12 },
|
|
1295
|
-
onClick: () => { const nm = window.prompt('
|
|
1296
|
-
}, '
|
|
1297
|
-
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, '
|
|
1784
|
+
onClick: () => { const nm = window.prompt(tr('picker.newDirName', { path: (root || '/') })); if (nm && nm.trim()) doFs('mkdir', { path: joinRemote(root, nm.trim()) }, true) },
|
|
1785
|
+
}, tr('explorer.newDir')),
|
|
1786
|
+
React.createElement('button', { style: { ...buttonS, padding: '3px 8px', fontSize: 12 }, onClick: () => setPicker(true) }, tr('explorer.more')),
|
|
1298
1787
|
),
|
|
1299
1788
|
status && status.workspace && root
|
|
1300
|
-
? React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px', wordBreak: 'break-all' } }, '
|
|
1301
|
-
: React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px' } }, root ? '
|
|
1789
|
+
? React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px', wordBreak: 'break-all' } }, tr('explorer.remoteWs', { path: root, host: (status.host ? tr('explorer.hostTag', { user: status.username, host: status.host }) : '') }))
|
|
1790
|
+
: React.createElement('div', { style: { fontSize: 11, opacity: 0.7, padding: '4px 8px' } }, root ? tr('explorer.noRemoteWs') : tr('explorer.sessionLocal')),
|
|
1302
1791
|
picker ? React.createElement('div', { style: { padding: 8 } }, React.createElement(DirPicker, {
|
|
1303
1792
|
open: true, busy: false, onPicked: () => { setPicker(false); refreshStatus() }, onCancel: () => setPicker(false),
|
|
1304
1793
|
})) : null,
|
|
1305
1794
|
React.createElement('div', { style: { flex: 1, overflowY: 'auto', minHeight: 0, padding: '2px 0' } },
|
|
1306
1795
|
!root
|
|
1307
|
-
? React.createElement('div', { style: { opacity: 0.6, padding: 12, fontSize: 13 } }, '
|
|
1796
|
+
? React.createElement('div', { style: { opacity: 0.6, padding: 12, fontSize: 13 } }, tr('explorer.localSession'))
|
|
1308
1797
|
: React.createElement('div', { key: root },
|
|
1309
1798
|
React.createElement('div', {
|
|
1310
1799
|
role: 'button', tabIndex: 0,
|
|
@@ -1322,7 +1811,7 @@ window.__ModuleLoader__.load({
|
|
|
1322
1811
|
),
|
|
1323
1812
|
// context menu — now rendered via DOM portal in useEffect above
|
|
1324
1813
|
null,
|
|
1325
|
-
err ? React.createElement('div', { style: { color: '#e06c75', fontSize: 12, padding: '4px 8px' } }, (busy ? '
|
|
1814
|
+
err ? React.createElement('div', { style: { color: '#e06c75', fontSize: 12, padding: '4px 8px' } }, (busy ? tr('explorer.busy') : '') + err) : null,
|
|
1326
1815
|
)
|
|
1327
1816
|
}
|
|
1328
1817
|
|
|
@@ -1358,7 +1847,7 @@ window.__ModuleLoader__.load({
|
|
|
1358
1847
|
apiRaw('POST', '/dsh-remote/write', { path, content: draft })
|
|
1359
1848
|
.then((r) => {
|
|
1360
1849
|
if (r.status === 409) {
|
|
1361
|
-
setErr(String((r.data && r.data.error) || '
|
|
1850
|
+
setErr(String((r.data && r.data.error) || tr('file.conflict')))
|
|
1362
1851
|
return
|
|
1363
1852
|
}
|
|
1364
1853
|
if (r.status >= 400) throw new Error((r.data && (r.data.error || r.data.message)) || 'HTTP ' + r.status)
|
|
@@ -1371,7 +1860,7 @@ window.__ModuleLoader__.load({
|
|
|
1371
1860
|
const downloadMirror = () => {
|
|
1372
1861
|
setSaving(true); setErr('')
|
|
1373
1862
|
api('POST', '/dsh-remote/fs', { op: 'download', path })
|
|
1374
|
-
.then((r) => setErr('
|
|
1863
|
+
.then((r) => setErr(tr('file.downloaded', { local: (r && r.local ? tr('file.downloadedAt', { path: r.local }) : '') })))
|
|
1375
1864
|
.catch((e) => setErr(String((e && e.message) || e)))
|
|
1376
1865
|
.finally(() => setSaving(false))
|
|
1377
1866
|
}
|
|
@@ -1381,27 +1870,27 @@ window.__ModuleLoader__.load({
|
|
|
1381
1870
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderBottom: '1px solid rgba(128,128,128,0.25)', fontSize: 12 } },
|
|
1382
1871
|
React.createElement('span', { style: { fontWeight: 600, whiteSpace: 'nowrap' } }, '📄 ' + baseName),
|
|
1383
1872
|
React.createElement('code', { style: { flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', opacity: 0.75 } }, path),
|
|
1384
|
-
React.createElement('span', { style: { opacity: 0.7, fontSize: 11, flexShrink: 0 } }, edit ? '
|
|
1873
|
+
React.createElement('span', { style: { opacity: 0.7, fontSize: 11, flexShrink: 0 } }, edit ? tr('file.editing') : tr('file.readonly')),
|
|
1385
1874
|
edit
|
|
1386
1875
|
? React.createElement(React.Fragment, null,
|
|
1387
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: saveRemote, disabled: saving }, saving ? '
|
|
1388
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, '
|
|
1876
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: saveRemote, disabled: saving }, saving ? tr('file.saving') : tr('file.saveToRemote')),
|
|
1877
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: () => { setEdit(false); setErr('') } }, tr('file.cancel')))
|
|
1389
1878
|
: React.createElement(React.Fragment, null,
|
|
1390
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, '
|
|
1391
|
-
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, '
|
|
1879
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: startEdit, disabled: !data || data.binary }, tr('file.edit')),
|
|
1880
|
+
React.createElement('button', { style: { ...buttonS, padding: '2px 10px', fontSize: 12 }, onClick: downloadMirror, disabled: saving }, tr('file.download'))),
|
|
1392
1881
|
),
|
|
1393
1882
|
React.createElement('div', { style: { flex: 1, overflow: 'auto', minHeight: 0 } },
|
|
1394
|
-
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12, fontSize: 13 } }, '
|
|
1883
|
+
loading ? React.createElement('div', { style: { opacity: 0.7, padding: 12, fontSize: 13 } }, tr('file.loading'))
|
|
1395
1884
|
: err ? React.createElement('div', { style: { color: '#e06c75', padding: 12, fontSize: 13 } }, err)
|
|
1396
1885
|
: (data && data.binary
|
|
1397
1886
|
? React.createElement('div', { style: { padding: 12, fontSize: 13, color: '#e6c07b' } },
|
|
1398
|
-
|
|
1887
|
+
tr('file.binary', { size: (data.size != null ? data.size + ' bytes' : tr('file.unknownSize')) }))
|
|
1399
1888
|
: (edit
|
|
1400
1889
|
? React.createElement('textarea', { value: draft, onChange: (e) => setDraft(e.target.value), spellCheck: false, style: { width: '100%', height: '100%', boxSizing: 'border-box', border: 'none', outline: 'none', resize: 'none', background: 'transparent', color: '#e4e4e7', fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } })
|
|
1401
1890
|
: React.createElement('div', { style: { fontSize: 13, lineHeight: 1.55, fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace', padding: '10px 12px', whiteSpace: 'pre-wrap', wordBreak: 'break-word' } },
|
|
1402
|
-
(data && data.content != null) ? data.content : '
|
|
1891
|
+
(data && data.content != null) ? data.content : tr('file.empty'),
|
|
1403
1892
|
(data && data.truncated)
|
|
1404
|
-
? React.createElement('div', { style: { color: '#e6c07b', fontSize: 12, marginTop: 8 } }, '
|
|
1893
|
+
? React.createElement('div', { style: { color: '#e6c07b', fontSize: 12, marginTop: 8 } }, tr('file.truncated'))
|
|
1405
1894
|
: null,
|
|
1406
1895
|
))),
|
|
1407
1896
|
),
|
|
@@ -1416,7 +1905,7 @@ window.__ModuleLoader__.load({
|
|
|
1416
1905
|
try {
|
|
1417
1906
|
disposers.push(bs.registerTab({
|
|
1418
1907
|
id: SIDEBAR_EXPLORER_ID,
|
|
1419
|
-
title: () => '
|
|
1908
|
+
title: () => tr('explorer.tabTitle'),
|
|
1420
1909
|
icon: (size) => React.createElement('span', { style: { fontSize: (size || 14), lineHeight: 1 } }, '🌐'),
|
|
1421
1910
|
order: 55,
|
|
1422
1911
|
single: true,
|
|
@@ -1424,7 +1913,7 @@ window.__ModuleLoader__.load({
|
|
|
1424
1913
|
}))
|
|
1425
1914
|
disposers.push(bs.registerTab({
|
|
1426
1915
|
id: SIDEBAR_FILE_ID,
|
|
1427
|
-
title: () => '
|
|
1916
|
+
title: () => tr('explorer.tabTitle'),
|
|
1428
1917
|
hidden: true, // not in the + menu; opened from the explorer tree
|
|
1429
1918
|
dedupeKey: (tab) => tab.path,
|
|
1430
1919
|
component: (props) => React.createElement(RemoteFileTab, props),
|
|
@@ -1450,7 +1939,7 @@ window.__ModuleLoader__.load({
|
|
|
1450
1939
|
if (!remotePath || autoOpenedFor.has(sessionId)) return
|
|
1451
1940
|
autoOpenedFor.add(sessionId)
|
|
1452
1941
|
try {
|
|
1453
|
-
bs.openTab({ type: SIDEBAR_EXPLORER_ID, title: '
|
|
1942
|
+
bs.openTab({ type: SIDEBAR_EXPLORER_ID, title: tr('explorer.tabTitle'), path: remotePath })
|
|
1454
1943
|
} catch (e2) {
|
|
1455
1944
|
console.warn('[dsh-remote] better-sidebar auto-open skipped:', e2)
|
|
1456
1945
|
}
|
|
@@ -1467,10 +1956,11 @@ window.__ModuleLoader__.load({
|
|
|
1467
1956
|
function apply(ctx) {
|
|
1468
1957
|
const slots = ctx.get('slots')
|
|
1469
1958
|
if (slots === undefined) return
|
|
1959
|
+
wireLocale(ctx)
|
|
1470
1960
|
WORKSPACES = (ctx && ((ctx.get && ctx.get('workspaces')) || ctx.workspaces)) || null
|
|
1471
1961
|
SESSIONS = (ctx && ((ctx.get && ctx.get('sessions')) || null)) || null
|
|
1472
1962
|
slots.inject('settings.section', () =>
|
|
1473
|
-
slots.register({ name: 'settings.section', id: 'dsh-remote', priority: 40, label: () => '
|
|
1963
|
+
slots.register({ name: 'settings.section', id: 'dsh-remote', priority: 40, label: () => tr('settings.title') }, () => React.createElement(RemoteWorkspacePage, null)),
|
|
1474
1964
|
)
|
|
1475
1965
|
slots.inject(
|
|
1476
1966
|
'conversation.hero.workspace.directoryFlow',
|