@ponydev/dsh-phone 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -1
- package/CHANGELOG.md +26 -0
- package/README.md +33 -8
- package/cordis.patch.yml +4 -0
- package/docs/DESIGN.md +20 -0
- package/lib/client.js +361 -3
- package/lib/hos.js +1159 -0
- package/lib/index.js +171 -13
- package/lib/ios.js +327 -0
- package/package.json +10 -6
package/AGENTS.md
CHANGED
|
@@ -6,13 +6,15 @@
|
|
|
6
6
|
|
|
7
7
|
DSH Web 插件:**右侧栏预览并点按控制已连接的 Android 设备**(adb),H.264 实时流 + 无 WebCodecs 时回退截图轮询。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**能力边界**:Android 走 adb;iOS 只做**模拟器**(simctl 生命周期 + idb 实时/输入,物理 iPhone 不支持);HarmonyOS 模拟器走 DevEco `Emulator` CLI、真机走 hdc。UI 容器固定用 DSH 原生 `sidebar-right`,不自造悬浮层。
|
|
10
10
|
|
|
11
11
|
## 1. 目录与职责
|
|
12
12
|
|
|
13
13
|
```
|
|
14
14
|
lib/index.js Host 入口(/phone/* RPC、connection.admit 围栏、targets、stream 路由、错误映射)
|
|
15
15
|
lib/adb.js adb 定位 / devices 解析 / getprop 平台 / wm size / 截图 / 输入 / 串行化
|
|
16
|
+
lib/ios.js iOS 模拟器:simctl 列表/创建/boot/删除/截图 + idb H.264 流/输入
|
|
17
|
+
lib/hos.js HarmonyOS:Emulator CLI(生命周期/创建/截图/点击)+ hdc(真机/截图/uitest)
|
|
16
18
|
lib/stream.js screenrecord H.264 子进程(续接、背压、断开 kill)、清晰度预设、按比例算 size
|
|
17
19
|
lib/targets.js 常用连接目标($DSH_HOME/phone/targets.json,0600,只存地址/时间)
|
|
18
20
|
lib/transport.js PhoneTransport 接口 + AdbTransport(hdc / iOS 的挂载点)
|
|
@@ -22,6 +24,7 @@ scripts/smoke.mjs 离线冒烟(解析/白名单/targets/尺寸计
|
|
|
22
24
|
scripts/check-i18n.mjs zh/en 字典键一致 + 代码用键存在
|
|
23
25
|
scripts/release-notes.mjs 从 CHANGELOG 抽取当前版本条目
|
|
24
26
|
scripts/render-test.mjs 离线渲染 harness(列表/预览/浮窗/诊断/流五种状态)
|
|
27
|
+
scripts/hos-smoke.mjs 鸿蒙 transport 只读冒烟(Emulator CLI + hdc)
|
|
25
28
|
docs/DESIGN.md 架构与布局;docs/mockups 原型;docs/screenshots 截图
|
|
26
29
|
```
|
|
27
30
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-10-26
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
- 鸿蒙模拟器名称规则与 CLI 对齐:只允许字母、数字、空格、下划线 `_`、加号 `+`(之前允许连字符/中文,`-create` 会直接失败);创建表单占位符改为 `dsh_phone_hos`
|
|
7
|
+
- 鸿蒙模拟器 `start` 改为 detached 启动 + 截图 warm-up:`Emulator -start` 是常驻进程,之前前台等待导致 90s 超时;现在启动后等显示服务就绪再返回
|
|
8
|
+
|
|
9
|
+
### 验证(0.4.1 发布前实测)
|
|
10
|
+
- iOS 模拟器:create → boot → 截图 → H.264 流(idb,100 帧/3s)→ tap / Home 通过;像素→point(density=3)用真实弹窗按钮验证
|
|
11
|
+
- HarmonyOS 模拟器:create → start(45s 含 warm-up)→ 截图 → tap / swipe → stop → delete 全链路通过
|
|
12
|
+
- HarmonyOS 真机(Mate 60 Pro):截图 / keyEvent / tap / text 通过
|
|
13
|
+
|
|
14
|
+
## 0.4.0 — 2026-10-26
|
|
15
|
+
|
|
16
|
+
### 新增
|
|
17
|
+
- **iOS 模拟器**:`xcrun simctl` 列表 / 创建 / boot / 关机 / 删除 / 截图;面板可自动进入并轮询预览
|
|
18
|
+
- **HarmonyOS 模拟器**:DevEco `Emulator` CLI 列表 / 创建 / 启动 / 停止 / 删除 / 截图 / 点击 / 滑动 / 旋转 / 电源
|
|
19
|
+
- **HarmonyOS 真机**:`hdc` 设备列表 / 截图 / 点击 / 滑动 / 按键 / 文字输入
|
|
20
|
+
- 「添加设备」菜单:Android 连接 / 创建 iOS 模拟器 / 创建鸿蒙模拟器
|
|
21
|
+
- 设备操作菜单:iOS 模拟器关机 / 删除,鸿蒙模拟器停止 / 删除
|
|
22
|
+
- 平台图标与标签:iOS 模拟器、HarmonyOS 模拟器 / 真机
|
|
23
|
+
|
|
24
|
+
### 变更
|
|
25
|
+
- Host `/phone/*` 按设备 transport 分发(adb / simctl / emulator / hdc);`health` 返回 iOS 与 HarmonyOS 能力
|
|
26
|
+
- 鸿蒙暂不支持实时流,自动回退截图轮询(iOS 实时流与输入需要 idb,未装时只读预览)
|
|
27
|
+
- 新增 `lib/ios.js`、`lib/hos.js` 与 `scripts/hos-smoke.mjs`
|
|
28
|
+
|
|
3
29
|
## 0.3.2 — 2026-10-26
|
|
4
30
|
|
|
5
31
|
### 变更
|
package/README.md
CHANGED
|
@@ -110,22 +110,45 @@ DSH 右侧栏第一次打开是**视口宽度的 45%**(1440 屏 ≈ 648px)
|
|
|
110
110
|
- 顶部:设备下拉、旋转、更多(电源、重连、诊断、浮窗、复制 serial、设备列表)。
|
|
111
111
|
- 「断开连接」放在设备下拉菜单里,需要点两次确认(防止误触)。
|
|
112
112
|
|
|
113
|
+
## 平台支持
|
|
114
|
+
|
|
115
|
+
| 平台 | 后端 | 画面 | 操作 | 创建 |
|
|
116
|
+
|---|---|---|---|---|
|
|
117
|
+
| Android 真机 / 云手机 | adb(USB / TCP / Tailscale) | H.264 20–30fps | tap / swipe / 按键 | — |
|
|
118
|
+
| iOS 模拟器 | `xcrun simctl` + idb(可选) | 截图轮询 1–2fps;装 idb 后 H.264 | 装 idb 后 tap / swipe / 按键 | ✅ 选择机型 + 运行时 |
|
|
119
|
+
| HarmonyOS 模拟器 | DevEco `Emulator` CLI | 截图轮询 1–2fps | tap / swipe / 旋转 / 电源 / 按键 / 文字 | ✅ 选择系统镜像 |
|
|
120
|
+
| HarmonyOS 真机 | hdc | 截图轮询 1–2fps | tap / swipe / 按键 / 文字 | — |
|
|
121
|
+
|
|
122
|
+
- iOS 依赖 macOS + Xcode;实时画面/输入需要 `idb`(`brew install facebook/fb/idb-companion` + `pip install fb-idb`),未装时只读预览。
|
|
123
|
+
- 如果 Homebrew 报 "Xcode alone is not sufficient on Tahoe"(需要 Command Line Tools),可改用官方 release 手工安装:
|
|
124
|
+
`gh release download v1.7.7 --repo facebook/idb --pattern 'idb-companion.macos-arm64.tar.gz'`,解压到 `~/.local/lib/idb-companion`,
|
|
125
|
+
再把 `idb_companion` / `idb-repl` 软链到 `~/.local/bin`;客户端 `python3 -m venv ~/.local/venvs/fb-idb && ~/.local/venvs/fb-idb/bin/pip install fb-idb`。
|
|
126
|
+
- HarmonyOS 依赖 DevEco Studio(`Emulator` CLI)与 `hdc`;hdc 没有 screenrecord,只能截图轮询。
|
|
127
|
+
- 物理 iPhone / iPad 不支持(Apple 未公开投屏/注入 API,Claude Code Desktop 也只支持模拟器)。
|
|
128
|
+
|
|
129
|
+
## 验证状态
|
|
130
|
+
|
|
131
|
+
- **iOS 模拟器**(iPhone Air / iOS 26.5):create / boot / 截图 / idb H.264 流 / tap / Home 实测通过;像素→point 坐标换算用真实弹窗验证。
|
|
132
|
+
- **HarmonyOS 模拟器**(HarmonyOS 7.0.0):create / start / 截图 / tap / swipe / rotate / stop / delete 全链路实测通过。
|
|
133
|
+
- **HarmonyOS 真机**(Mate 60 Pro,hdc):截图 / keyEvent / tap / text 实测通过。
|
|
134
|
+
- **Android**:真机与云手机的 tap / swipe / back / home / 截图 / H.264 流实测通过。
|
|
135
|
+
|
|
113
136
|
## 已知限制
|
|
114
137
|
|
|
115
|
-
- 画面默认走 H.264 流(约 20–30fps);没有 WebCodecs 的浏览器会回退到 1–2fps 轮询截图。
|
|
116
|
-
- 流是 `screenrecord`,没有声音;单次 180s 上限由 Host 自动续接(断开 <1s)。
|
|
138
|
+
- Android 画面默认走 H.264 流(约 20–30fps);没有 WebCodecs 的浏览器会回退到 1–2fps 轮询截图。
|
|
139
|
+
- Android 流是 `screenrecord`,没有声音;单次 180s 上限由 Host 自动续接(断开 <1s)。
|
|
117
140
|
- 单个右侧栏 tab,设备在 tab 内切换;不支持同一 pane 多设备并排(后续可开 `multiple`)。
|
|
118
|
-
-
|
|
141
|
+
- Android 文字输入(`adb shell input text` 只支持 ASCII,中文要 ADBKeyboard);鸿蒙用 `uitest uiInput text`。
|
|
119
142
|
- 「已保存」目标是地址级记录,没有备注名;显示名直接是 `ip:port` / 主机名。
|
|
120
|
-
- 只支持 Android/adb;hdc(鸿蒙)和 iOS 走 `PhoneTransport` 接口后加。
|
|
121
143
|
- 右侧栏是 per-session 的,没有会话的页面不显示手机图标。
|
|
122
|
-
- 需要设备上已授权 USB 调试;`unauthorized` 会提示去手机上点允许。
|
|
144
|
+
- Android 需要设备上已授权 USB 调试;`unauthorized` 会提示去手机上点允许。
|
|
123
145
|
|
|
124
146
|
## 开发
|
|
125
147
|
|
|
126
148
|
```sh
|
|
127
149
|
npm run check # 语法 + i18n + 隐私/截图门禁
|
|
128
150
|
npm run smoke # 解析/白名单/真机 adb 冒烟
|
|
151
|
+
npm run smoke:hos # 鸿蒙 Emulator CLI / hdc 只读冒烟(需要 DevEco)
|
|
129
152
|
node scripts/render-test.mjs [screenshot.png]
|
|
130
153
|
# 离线渲染列表 + 预览,输出 /tmp/dsh-phone-render.html/.png
|
|
131
154
|
```
|
|
@@ -136,9 +159,11 @@ node scripts/render-test.mjs [screenshot.png]
|
|
|
136
159
|
## 结构
|
|
137
160
|
|
|
138
161
|
```
|
|
139
|
-
lib/index.js Host:/phone
|
|
140
|
-
lib/adb.js adb 定位 / devices 解析 / 截图 / 输入 / 串行化
|
|
141
|
-
lib/
|
|
162
|
+
lib/index.js Host:/phone 路由、认证围栏、按 transport 分发
|
|
163
|
+
lib/adb.js Android:adb 定位 / devices 解析 / 截图 / 输入 / 串行化
|
|
164
|
+
lib/ios.js iOS 模拟器:simctl 生命周期/创建/截图 + idb 实时流/输入
|
|
165
|
+
lib/hos.js HarmonyOS:DevEco Emulator CLI + hdc(真机)
|
|
166
|
+
lib/transport.js PhoneTransport 接口 + AdbTransport
|
|
142
167
|
lib/client.js ModuleLoader bundle:tab 类型、列表、预览、i18n、样式
|
|
143
168
|
cordis.patch.yml bundle 补丁
|
|
144
169
|
scripts/ smoke + 离线渲染
|
package/cordis.patch.yml
CHANGED
package/docs/DESIGN.md
CHANGED
|
@@ -244,3 +244,23 @@ Host 侧定义 `PhoneTransport { id, list, connect, shot, input }`:
|
|
|
244
244
|
2. 设备多时:默认「一个 tab 内切换」(本文)还是「每台设备一个 tab」?
|
|
245
245
|
|
|
246
246
|
(图标位置 = 会话头部工具区、名字 = `dsh-phone`、无元数据列、无输入框,已按你的意思定下。)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
## 多平台 transport(0.4.0)
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
/phone/* ── resolveDevice(serial) ──┬── adb Android 真机 / 云手机(H.264 screenrecord)
|
|
253
|
+
├── simctl iOS 模拟器(截图;idb 提供 H.264 + HID)
|
|
254
|
+
├── emulator HarmonyOS 模拟器(DevEco Emulator CLI)
|
|
255
|
+
└── hdc HarmonyOS 真机(snapshot_display + uitest)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- **iOS**:只支持模拟器。生命周期/创建走公开 `xcrun simctl`;实时画面与点击/输入走 `idb`
|
|
259
|
+
(Meta FBSimulatorControl,Claude Code Desktop 内部也是 CoreSimulator/SimulatorKit 私有栈)。
|
|
260
|
+
idb 的 `video-stream --format h264` 输出 Annex-B,直接复用客户端的 WebCodecs 管线;
|
|
261
|
+
`idb describe` 的 `screen_dimensions.density` 用于像素 → point 坐标换算。
|
|
262
|
+
- **HarmonyOS 模拟器**:`Emulator -list/-create/-start/-stop/-delete/-screenshot/-click/-slide/-rotation/-power`;
|
|
263
|
+
按键与文字借道 hdc `uitest uiInput`(模拟器没有对应 CLI)。
|
|
264
|
+
- **HarmonyOS 真机**:`hdc list targets` + `snapshot_display` + `uitest uiInput`;没有 screenrecord,只能截图轮询。
|
|
265
|
+
- **物理 iPhone / iPad 不支持**:Apple 没有公开投屏/HID 注入 API(Claude 同样只做模拟器;
|
|
266
|
+
Codex 用 Xcode 27 CoreDevice 私有 HEVC + UniversalHID,不在本项目范围)。
|
package/lib/client.js
CHANGED
|
@@ -52,6 +52,24 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
diagnostics: 'adb 诊断',
|
|
53
53
|
quality: '显示清晰度',
|
|
54
54
|
immersiveOn: '沉浸模式',
|
|
55
|
+
addDevice: '添加设备',
|
|
56
|
+
connectAndroid: '连接 Android 设备',
|
|
57
|
+
createIos: '创建 iOS 模拟器',
|
|
58
|
+
createHos: '创建鸿蒙模拟器',
|
|
59
|
+
hosImage: '系统镜像',
|
|
60
|
+
hosStarting: '正在启动鸿蒙模拟器…',
|
|
61
|
+
stopHos: '关闭鸿蒙模拟器',
|
|
62
|
+
deleteHos: '删除鸿蒙模拟器',
|
|
63
|
+
iosName: '名称',
|
|
64
|
+
iosDeviceType: '机型',
|
|
65
|
+
iosRuntime: '系统版本',
|
|
66
|
+
create: '创建',
|
|
67
|
+
booting: '正在启动模拟器…',
|
|
68
|
+
simulator: '模拟器',
|
|
69
|
+
shutdownSimulator: '关闭模拟器',
|
|
70
|
+
deleteSimulator: '删除模拟器',
|
|
71
|
+
confirmDeleteSimulator: '确认删除?',
|
|
72
|
+
idbHint: 'iOS 输入/实时画面需要 idb',
|
|
55
73
|
immersiveOff: '显示操作栏',
|
|
56
74
|
qualityLow: '流畅',
|
|
57
75
|
qualityMedium: '标准',
|
|
@@ -102,6 +120,24 @@ window.__ModuleLoader__.load({
|
|
|
102
120
|
diagnostics: 'adb diagnostics',
|
|
103
121
|
quality: 'Stream quality',
|
|
104
122
|
immersiveOn: 'Immersive mode',
|
|
123
|
+
addDevice: 'Add device',
|
|
124
|
+
connectAndroid: 'Connect Android device',
|
|
125
|
+
createIos: 'Create iOS simulator',
|
|
126
|
+
createHos: 'Create HarmonyOS emulator',
|
|
127
|
+
hosImage: 'System image',
|
|
128
|
+
hosStarting: 'Starting HarmonyOS emulator…',
|
|
129
|
+
stopHos: 'Shut down HarmonyOS emulator',
|
|
130
|
+
deleteHos: 'Delete HarmonyOS emulator',
|
|
131
|
+
iosName: 'Name',
|
|
132
|
+
iosDeviceType: 'Device type',
|
|
133
|
+
iosRuntime: 'Runtime',
|
|
134
|
+
create: 'Create',
|
|
135
|
+
booting: 'Booting simulator…',
|
|
136
|
+
simulator: 'Simulator',
|
|
137
|
+
shutdownSimulator: 'Shut down simulator',
|
|
138
|
+
deleteSimulator: 'Delete simulator',
|
|
139
|
+
confirmDeleteSimulator: 'Confirm delete?',
|
|
140
|
+
idbHint: 'iOS input/streaming requires idb',
|
|
105
141
|
immersiveOff: 'Show controls',
|
|
106
142
|
qualityLow: 'Low',
|
|
107
143
|
qualityMedium: 'Medium',
|
|
@@ -187,6 +223,8 @@ window.__ModuleLoader__.load({
|
|
|
187
223
|
.dph-field{display:flex;gap:8px}
|
|
188
224
|
.dph-input{flex:1;height:32px;border:1px solid var(--dsw-alias-border-l2,#dededf);border-radius:8px;padding:0 10px;font:11.5px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--dsw-alias-label-primary,#17171a);background:var(--dsw-alias-bg-layer-1,#fff);outline:none}
|
|
189
225
|
.dph-input:focus{border-color:var(--dsw-alias-brand-primary,#3b6cff)}
|
|
226
|
+
.dph-select-input{appearance:none;-webkit-appearance:none;padding-right:24px}
|
|
227
|
+
.dph-label{display:block;font-size:11px;color:var(--dsw-alias-label-tertiary,#9a9aa1);margin:10px 0 4px}
|
|
190
228
|
.dph-primary{height:32px;padding:0 14px;border:0;border-radius:8px;background:var(--dsw-alias-label-primary,#17171a);color:var(--dsw-alias-bg-layer-1,#fff);font-size:12px;cursor:pointer}
|
|
191
229
|
.dph-primary:disabled{opacity:.5;cursor:default}
|
|
192
230
|
.dph-err{color:var(--dsw-alias-state-error-primary,#dc2626);font-size:11.5px;margin-top:8px;word-break:break-word}
|
|
@@ -395,6 +433,17 @@ window.__ModuleLoader__.load({
|
|
|
395
433
|
screenInfo: (serial, options) => unwrap('screen-info', { serial, fresh: Boolean(options && options.fresh) }),
|
|
396
434
|
ping: (serial) => unwrap('ping', { serial }),
|
|
397
435
|
targets: () => unwrap('targets'),
|
|
436
|
+
iosTypes: () => unwrap('ios/types'),
|
|
437
|
+
iosRuntimes: () => unwrap('ios/runtimes'),
|
|
438
|
+
iosCreate: (payload) => unwrap('ios/create', payload),
|
|
439
|
+
iosBoot: (serial) => unwrap('ios/boot', { serial }),
|
|
440
|
+
iosShutdown: (serial) => unwrap('ios/shutdown', { serial }),
|
|
441
|
+
iosDelete: (serial) => unwrap('ios/delete', { serial }),
|
|
442
|
+
hosImages: (deviceType) => unwrap('hos/images', { deviceType }),
|
|
443
|
+
hosCreate: (payload) => unwrap('hos/create', payload),
|
|
444
|
+
hosStart: (serial) => unwrap('hos/start', { serial }),
|
|
445
|
+
hosStop: (serial) => unwrap('hos/stop', { serial }),
|
|
446
|
+
hosDelete: (serial) => unwrap('hos/delete', { serial }),
|
|
398
447
|
removeTarget: (address) => unwrap('targets/remove', { address }),
|
|
399
448
|
shot: async (serial) => {
|
|
400
449
|
const res = await unwrap('shot', { serial })
|
|
@@ -462,6 +511,7 @@ window.__ModuleLoader__.load({
|
|
|
462
511
|
if (kind === 'tailscale') return 'Tailscale'
|
|
463
512
|
if (kind === 'wifi') return 'Wi-Fi'
|
|
464
513
|
if (kind === 'emulator') return 'emulator'
|
|
514
|
+
if (kind === 'simulator') return 'Simulator'
|
|
465
515
|
return 'USB'
|
|
466
516
|
}
|
|
467
517
|
|
|
@@ -1104,8 +1154,56 @@ window.__ModuleLoader__.load({
|
|
|
1104
1154
|
}
|
|
1105
1155
|
})
|
|
1106
1156
|
const [disconnectArmed, setDisconnectArmed] = useState(false)
|
|
1157
|
+
const [addMenuOpen, setAddMenuOpen] = useState(false)
|
|
1158
|
+
const [iosCreateOpen, setIosCreateOpen] = useState(false)
|
|
1159
|
+
const [iosTypes, setIosTypes] = useState([])
|
|
1160
|
+
const [iosRuntimes, setIosRuntimes] = useState([])
|
|
1161
|
+
const [iosForm, setIosForm] = useState({ name: '', deviceType: '', runtime: '' })
|
|
1162
|
+
const [iosBusy, setIosBusy] = useState(false)
|
|
1163
|
+
const [simDeleteArmed, setSimDeleteArmed] = useState(false)
|
|
1164
|
+
const [hosCreateOpen, setHosCreateOpen] = useState(false)
|
|
1165
|
+
const [hosImages, setHosImages] = useState([])
|
|
1166
|
+
const [hosForm, setHosForm] = useState({ name: '', osVersion: '' })
|
|
1167
|
+
const [hosBusy, setHosBusy] = useState(false)
|
|
1107
1168
|
const autoSelected = useRef(false)
|
|
1108
1169
|
const { devices, error, reload } = useDeviceList(visible, nonce)
|
|
1170
|
+
useEffect(() => {
|
|
1171
|
+
if (!hosCreateOpen) return undefined
|
|
1172
|
+
let alive = true
|
|
1173
|
+
api
|
|
1174
|
+
.hosImages('phone')
|
|
1175
|
+
.catch(() => ({ images: [] }))
|
|
1176
|
+
.then((value) => {
|
|
1177
|
+
if (!alive) return
|
|
1178
|
+
const images = (Array.isArray(value.images) ? value.images : []).filter((item) => item.downloaded)
|
|
1179
|
+
setHosImages(images)
|
|
1180
|
+
setHosForm((prev) => ({ name: prev.name, osVersion: prev.osVersion || (images[0] && images[0].osVersion) || '' }))
|
|
1181
|
+
})
|
|
1182
|
+
return () => {
|
|
1183
|
+
alive = false
|
|
1184
|
+
}
|
|
1185
|
+
}, [hosCreateOpen])
|
|
1186
|
+
|
|
1187
|
+
useEffect(() => {
|
|
1188
|
+
if (!iosCreateOpen) return undefined
|
|
1189
|
+
let alive = true
|
|
1190
|
+
Promise.all([api.iosTypes().catch(() => ({ types: [] })), api.iosRuntimes().catch(() => ({ runtimes: [] }))]).then(([t, r]) => {
|
|
1191
|
+
if (!alive) return
|
|
1192
|
+
const types = Array.isArray(t.types) ? t.types : []
|
|
1193
|
+
const runtimes = Array.isArray(r.runtimes) ? r.runtimes : []
|
|
1194
|
+
setIosTypes(types)
|
|
1195
|
+
setIosRuntimes(runtimes)
|
|
1196
|
+
setIosForm((prev) => ({
|
|
1197
|
+
name: prev.name,
|
|
1198
|
+
deviceType: prev.deviceType || (types[0] && types[0].identifier) || '',
|
|
1199
|
+
runtime: prev.runtime || (runtimes[0] && runtimes[0].identifier) || '',
|
|
1200
|
+
}))
|
|
1201
|
+
})
|
|
1202
|
+
return () => {
|
|
1203
|
+
alive = false
|
|
1204
|
+
}
|
|
1205
|
+
}, [iosCreateOpen])
|
|
1206
|
+
|
|
1109
1207
|
const quality = selected && QUALITY_ORDER.includes(qualityMap[selected]) ? qualityMap[selected] : 'medium'
|
|
1110
1208
|
const setQualityForSelected = (next) => {
|
|
1111
1209
|
if (!selected) return
|
|
@@ -1161,6 +1259,8 @@ window.__ModuleLoader__.load({
|
|
|
1161
1259
|
const device = rows ? rows.find((item) => item.serial === selected) || null : null
|
|
1162
1260
|
const tabId = tab && tab.id ? tab.id : null
|
|
1163
1261
|
const adbMissing = health && health.adb === null
|
|
1262
|
+
const iosReady = Boolean(health && health.ios && health.ios.available)
|
|
1263
|
+
const hosReady = Boolean(health && health.harmony && health.harmony.emulator && health.harmony.emulator.available)
|
|
1164
1264
|
|
|
1165
1265
|
const connectTo = async (address) => {
|
|
1166
1266
|
setConnecting(true)
|
|
@@ -1218,6 +1318,30 @@ window.__ModuleLoader__.load({
|
|
|
1218
1318
|
})
|
|
1219
1319
|
return
|
|
1220
1320
|
}
|
|
1321
|
+
if (picked && picked.transport === 'emulator' && picked.state !== 'device') {
|
|
1322
|
+
preview.setToast(t('hosStarting'))
|
|
1323
|
+
api
|
|
1324
|
+
.hosStart(picked.serial)
|
|
1325
|
+
.then(() => {
|
|
1326
|
+
rememberDevice(picked.serial)
|
|
1327
|
+
setNonce((value) => value + 1)
|
|
1328
|
+
setSelected(picked.serial)
|
|
1329
|
+
})
|
|
1330
|
+
.catch((err) => preview.setToast(err && err.message ? err.message : 'start failed'))
|
|
1331
|
+
return
|
|
1332
|
+
}
|
|
1333
|
+
if (picked && picked.kind === 'simulator' && picked.state !== 'device') {
|
|
1334
|
+
preview.setToast(t('booting'))
|
|
1335
|
+
api
|
|
1336
|
+
.iosBoot(picked.serial)
|
|
1337
|
+
.then(() => {
|
|
1338
|
+
rememberDevice(picked.serial)
|
|
1339
|
+
setNonce((value) => value + 1)
|
|
1340
|
+
setSelected(picked.serial)
|
|
1341
|
+
})
|
|
1342
|
+
.catch((err) => preview.setToast(err && err.message ? err.message : 'boot failed'))
|
|
1343
|
+
return
|
|
1344
|
+
}
|
|
1221
1345
|
rememberDevice(picked.serial)
|
|
1222
1346
|
setSelected(picked.serial)
|
|
1223
1347
|
}
|
|
@@ -1303,6 +1427,44 @@ window.__ModuleLoader__.load({
|
|
|
1303
1427
|
},
|
|
1304
1428
|
},
|
|
1305
1429
|
{ id: 'copy', icon: 'copy', label: t('copySerial'), run: copySerial },
|
|
1430
|
+
...(device && device.kind === 'simulator'
|
|
1431
|
+
? [
|
|
1432
|
+
{
|
|
1433
|
+
id: 'shutdown-sim',
|
|
1434
|
+
icon: 'power',
|
|
1435
|
+
label: t('shutdownSimulator'),
|
|
1436
|
+
run: () => {
|
|
1437
|
+
api
|
|
1438
|
+
.iosShutdown(device.serial)
|
|
1439
|
+
.catch(() => {})
|
|
1440
|
+
.then(() => {
|
|
1441
|
+
setSelected(null)
|
|
1442
|
+
setNonce((value) => value + 1)
|
|
1443
|
+
})
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
id: 'delete-sim',
|
|
1448
|
+
icon: 'unlink',
|
|
1449
|
+
label: simDeleteArmed ? t('confirmDeleteSimulator') : t('deleteSimulator'),
|
|
1450
|
+
run: () => {
|
|
1451
|
+
if (!simDeleteArmed) {
|
|
1452
|
+
setSimDeleteArmed(true)
|
|
1453
|
+
setTimeout(() => setSimDeleteArmed(false), 3000)
|
|
1454
|
+
return
|
|
1455
|
+
}
|
|
1456
|
+
setSimDeleteArmed(false)
|
|
1457
|
+
api
|
|
1458
|
+
.iosDelete(device.serial)
|
|
1459
|
+
.catch((err) => preview.setToast(err && err.message ? err.message : 'delete failed'))
|
|
1460
|
+
.then(() => {
|
|
1461
|
+
setSelected(null)
|
|
1462
|
+
setNonce((value) => value + 1)
|
|
1463
|
+
})
|
|
1464
|
+
},
|
|
1465
|
+
},
|
|
1466
|
+
]
|
|
1467
|
+
: []),
|
|
1306
1468
|
{
|
|
1307
1469
|
id: 'list',
|
|
1308
1470
|
icon: 'back',
|
|
@@ -1347,7 +1509,7 @@ window.__ModuleLoader__.load({
|
|
|
1347
1509
|
className: 'dph-link',
|
|
1348
1510
|
onClick: () => {
|
|
1349
1511
|
setMenuOpen(false)
|
|
1350
|
-
|
|
1512
|
+
setAddMenuOpen(true)
|
|
1351
1513
|
},
|
|
1352
1514
|
},
|
|
1353
1515
|
'+ ' + t('add'),
|
|
@@ -1588,7 +1750,7 @@ window.__ModuleLoader__.load({
|
|
|
1588
1750
|
{ className: 'dph-empty' },
|
|
1589
1751
|
h('div', { className: 'dph-empty-icon' }, h(Icon, { name: 'phone', size: 30 })),
|
|
1590
1752
|
h('div', { className: 'dph-empty-title' }, t('empty')),
|
|
1591
|
-
h('button', { type: 'button', className: 'dph-primary', onClick: () =>
|
|
1753
|
+
h('button', { type: 'button', className: 'dph-primary', onClick: () => setAddMenuOpen(true) }, '+ ' + t('addDevice')),
|
|
1592
1754
|
)
|
|
1593
1755
|
: h(DeviceRows, { t, devices: rows, selected, onSelect })
|
|
1594
1756
|
|
|
@@ -1613,7 +1775,7 @@ window.__ModuleLoader__.load({
|
|
|
1613
1775
|
),
|
|
1614
1776
|
h(
|
|
1615
1777
|
'button',
|
|
1616
|
-
{ key: 'add', type: 'button', className: 'dph-ibtn', title: t('
|
|
1778
|
+
{ key: 'add', type: 'button', className: 'dph-ibtn', title: t('addDevice'), onClick: () => setAddMenuOpen(true) },
|
|
1617
1779
|
h(Icon, { name: 'plus' }),
|
|
1618
1780
|
),
|
|
1619
1781
|
]
|
|
@@ -1646,6 +1808,199 @@ window.__ModuleLoader__.load({
|
|
|
1646
1808
|
)
|
|
1647
1809
|
: null
|
|
1648
1810
|
|
|
1811
|
+
const hosCreateSheet = hosCreateOpen
|
|
1812
|
+
? h(
|
|
1813
|
+
'div',
|
|
1814
|
+
null,
|
|
1815
|
+
h('div', {
|
|
1816
|
+
className: 'dph-backdrop',
|
|
1817
|
+
onClick: () => {
|
|
1818
|
+
if (!hosBusy) setHosCreateOpen(false)
|
|
1819
|
+
},
|
|
1820
|
+
}),
|
|
1821
|
+
h(
|
|
1822
|
+
'form',
|
|
1823
|
+
{
|
|
1824
|
+
className: 'dph-connect',
|
|
1825
|
+
style: { maxWidth: 380 },
|
|
1826
|
+
onSubmit: async (event) => {
|
|
1827
|
+
event.preventDefault()
|
|
1828
|
+
if (hosBusy) return
|
|
1829
|
+
setHosBusy(true)
|
|
1830
|
+
try {
|
|
1831
|
+
const created = await api.hosCreate({ name: hosForm.name, osVersion: hosForm.osVersion, deviceType: 'phone' })
|
|
1832
|
+
setHosCreateOpen(false)
|
|
1833
|
+
setNonce((value) => value + 1)
|
|
1834
|
+
if (created && created.serial) {
|
|
1835
|
+
rememberDevice(created.serial)
|
|
1836
|
+
setSelected(created.serial)
|
|
1837
|
+
}
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
preview.setToast(error && error.message ? error.message : 'create failed')
|
|
1840
|
+
} finally {
|
|
1841
|
+
setHosBusy(false)
|
|
1842
|
+
}
|
|
1843
|
+
},
|
|
1844
|
+
},
|
|
1845
|
+
h('div', { className: 'ttl' }, t('createHos')),
|
|
1846
|
+
h('div', { className: 'sub' }, 'DevEco Emulator CLI'),
|
|
1847
|
+
h('label', { className: 'dph-label' }, t('iosName')),
|
|
1848
|
+
h('input', {
|
|
1849
|
+
className: 'dph-input',
|
|
1850
|
+
value: hosForm.name,
|
|
1851
|
+
placeholder: 'dsh_phone_hos',
|
|
1852
|
+
onChange: (event) => setHosForm((prev) => ({ ...prev, name: event.target.value })),
|
|
1853
|
+
spellCheck: false,
|
|
1854
|
+
}),
|
|
1855
|
+
h('label', { className: 'dph-label' }, t('hosImage')),
|
|
1856
|
+
h(
|
|
1857
|
+
'select',
|
|
1858
|
+
{
|
|
1859
|
+
className: 'dph-input dph-select-input',
|
|
1860
|
+
value: hosForm.osVersion,
|
|
1861
|
+
onChange: (event) => setHosForm((prev) => ({ ...prev, osVersion: event.target.value })),
|
|
1862
|
+
},
|
|
1863
|
+
hosImages.map((item) => h('option', { key: item.osVersion, value: item.osVersion }, item.osVersion)),
|
|
1864
|
+
),
|
|
1865
|
+
h(
|
|
1866
|
+
'div',
|
|
1867
|
+
{ className: 'dph-field', style: { marginTop: 12 } },
|
|
1868
|
+
h('button', { type: 'submit', className: 'dph-primary', disabled: hosBusy || !hosForm.name.trim() || !hosForm.osVersion }, hosBusy ? t('loading') : t('create')),
|
|
1869
|
+
h('button', { type: 'button', className: 'dph-ghost', onClick: () => setHosCreateOpen(false) }, t('cancel')),
|
|
1870
|
+
),
|
|
1871
|
+
),
|
|
1872
|
+
)
|
|
1873
|
+
: null
|
|
1874
|
+
|
|
1875
|
+
const addMenu = addMenuOpen
|
|
1876
|
+
? h(
|
|
1877
|
+
'div',
|
|
1878
|
+
null,
|
|
1879
|
+
h('div', { className: 'dph-backdrop', onClick: () => setAddMenuOpen(false) }),
|
|
1880
|
+
h(
|
|
1881
|
+
'div',
|
|
1882
|
+
{ className: 'dph-menu', style: { top: 48, left: 8, width: 270 } },
|
|
1883
|
+
h(
|
|
1884
|
+
'button',
|
|
1885
|
+
{
|
|
1886
|
+
type: 'button',
|
|
1887
|
+
className: 'dph-row',
|
|
1888
|
+
onClick: () => {
|
|
1889
|
+
setAddMenuOpen(false)
|
|
1890
|
+
setConnectOpen(true)
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1893
|
+
h('span', { className: 'dph-av', style: { width: 26, height: 26, borderRadius: 7 } }, h(Icon, { name: 'android', size: 16 })),
|
|
1894
|
+
h('span', { className: 'dph-meta' }, h('span', { className: 'dph-model', style: { fontWeight: 400 } }, t('connectAndroid'))),
|
|
1895
|
+
),
|
|
1896
|
+
iosReady
|
|
1897
|
+
? h(
|
|
1898
|
+
'button',
|
|
1899
|
+
{
|
|
1900
|
+
type: 'button',
|
|
1901
|
+
className: 'dph-row',
|
|
1902
|
+
onClick: () => {
|
|
1903
|
+
setAddMenuOpen(false)
|
|
1904
|
+
setIosCreateOpen(true)
|
|
1905
|
+
},
|
|
1906
|
+
},
|
|
1907
|
+
h('span', { className: 'dph-av', style: { width: 26, height: 26, borderRadius: 7 } }, h(Icon, { name: 'apple', size: 16 })),
|
|
1908
|
+
h('span', { className: 'dph-meta' }, h('span', { className: 'dph-model', style: { fontWeight: 400 } }, t('createIos'))),
|
|
1909
|
+
)
|
|
1910
|
+
: null,
|
|
1911
|
+
hosReady
|
|
1912
|
+
? h(
|
|
1913
|
+
'button',
|
|
1914
|
+
{
|
|
1915
|
+
type: 'button',
|
|
1916
|
+
className: 'dph-row',
|
|
1917
|
+
onClick: () => {
|
|
1918
|
+
setAddMenuOpen(false)
|
|
1919
|
+
setHosCreateOpen(true)
|
|
1920
|
+
},
|
|
1921
|
+
},
|
|
1922
|
+
h('span', { className: 'dph-av', style: { width: 26, height: 26, borderRadius: 7 } }, h(Icon, { name: 'harmony', size: 16 })),
|
|
1923
|
+
h('span', { className: 'dph-meta' }, h('span', { className: 'dph-model', style: { fontWeight: 400 } }, t('createHos'))),
|
|
1924
|
+
)
|
|
1925
|
+
: null,
|
|
1926
|
+
),
|
|
1927
|
+
)
|
|
1928
|
+
: null
|
|
1929
|
+
|
|
1930
|
+
const iosCreateSheet = iosCreateOpen
|
|
1931
|
+
? h(
|
|
1932
|
+
'div',
|
|
1933
|
+
null,
|
|
1934
|
+
h('div', {
|
|
1935
|
+
className: 'dph-backdrop',
|
|
1936
|
+
onClick: () => {
|
|
1937
|
+
if (!iosBusy) setIosCreateOpen(false)
|
|
1938
|
+
},
|
|
1939
|
+
}),
|
|
1940
|
+
h(
|
|
1941
|
+
'form',
|
|
1942
|
+
{
|
|
1943
|
+
className: 'dph-connect',
|
|
1944
|
+
style: { maxWidth: 380 },
|
|
1945
|
+
onSubmit: async (event) => {
|
|
1946
|
+
event.preventDefault()
|
|
1947
|
+
if (iosBusy) return
|
|
1948
|
+
setIosBusy(true)
|
|
1949
|
+
try {
|
|
1950
|
+
const created = await api.iosCreate(iosForm)
|
|
1951
|
+
setIosCreateOpen(false)
|
|
1952
|
+
setNonce((value) => value + 1)
|
|
1953
|
+
if (created && created.serial) {
|
|
1954
|
+
rememberDevice(created.serial)
|
|
1955
|
+
setSelected(created.serial)
|
|
1956
|
+
}
|
|
1957
|
+
} catch (error) {
|
|
1958
|
+
preview.setToast(error && error.message ? error.message : 'create failed')
|
|
1959
|
+
} finally {
|
|
1960
|
+
setIosBusy(false)
|
|
1961
|
+
}
|
|
1962
|
+
},
|
|
1963
|
+
},
|
|
1964
|
+
h('div', { className: 'ttl' }, t('createIos')),
|
|
1965
|
+
h('div', { className: 'sub' }, 'xcrun simctl create'),
|
|
1966
|
+
h('label', { className: 'dph-label' }, t('iosName')),
|
|
1967
|
+
h('input', {
|
|
1968
|
+
className: 'dph-input',
|
|
1969
|
+
value: iosForm.name,
|
|
1970
|
+
placeholder: 'dsh-phone',
|
|
1971
|
+
onChange: (event) => setIosForm((prev) => ({ ...prev, name: event.target.value })),
|
|
1972
|
+
spellCheck: false,
|
|
1973
|
+
}),
|
|
1974
|
+
h('label', { className: 'dph-label' }, t('iosDeviceType')),
|
|
1975
|
+
h(
|
|
1976
|
+
'select',
|
|
1977
|
+
{
|
|
1978
|
+
className: 'dph-input dph-select-input',
|
|
1979
|
+
value: iosForm.deviceType,
|
|
1980
|
+
onChange: (event) => setIosForm((prev) => ({ ...prev, deviceType: event.target.value })),
|
|
1981
|
+
},
|
|
1982
|
+
iosTypes.map((item) => h('option', { key: item.identifier, value: item.identifier }, item.name)),
|
|
1983
|
+
),
|
|
1984
|
+
h('label', { className: 'dph-label' }, t('iosRuntime')),
|
|
1985
|
+
h(
|
|
1986
|
+
'select',
|
|
1987
|
+
{
|
|
1988
|
+
className: 'dph-input dph-select-input',
|
|
1989
|
+
value: iosForm.runtime,
|
|
1990
|
+
onChange: (event) => setIosForm((prev) => ({ ...prev, runtime: event.target.value })),
|
|
1991
|
+
},
|
|
1992
|
+
iosRuntimes.map((item) => h('option', { key: item.identifier, value: item.identifier }, item.name)),
|
|
1993
|
+
),
|
|
1994
|
+
h(
|
|
1995
|
+
'div',
|
|
1996
|
+
{ className: 'dph-field', style: { marginTop: 12 } },
|
|
1997
|
+
h('button', { type: 'submit', className: 'dph-primary', disabled: iosBusy || !iosForm.name.trim() || !iosForm.deviceType || !iosForm.runtime }, iosBusy ? t('loading') : t('create')),
|
|
1998
|
+
h('button', { type: 'button', className: 'dph-ghost', onClick: () => setIosCreateOpen(false) }, t('cancel')),
|
|
1999
|
+
),
|
|
2000
|
+
),
|
|
2001
|
+
)
|
|
2002
|
+
: null
|
|
2003
|
+
|
|
1649
2004
|
return h(
|
|
1650
2005
|
'div',
|
|
1651
2006
|
{ className: 'dph-root' },
|
|
@@ -1694,6 +2049,9 @@ window.__ModuleLoader__.load({
|
|
|
1694
2049
|
moreMenu,
|
|
1695
2050
|
connectSheet,
|
|
1696
2051
|
diagSheet,
|
|
2052
|
+
addMenu,
|
|
2053
|
+
iosCreateSheet,
|
|
2054
|
+
hosCreateSheet,
|
|
1697
2055
|
)
|
|
1698
2056
|
}
|
|
1699
2057
|
|