@ponydev/dsh-phone 0.3.1 → 0.4.0

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/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 })
@@ -437,7 +486,7 @@ window.__ModuleLoader__.load({
437
486
  }
438
487
 
439
488
  function kindForAddress(address) {
440
- if (/^unicom-openclaw-|\.ts\.net/.test(address)) return 'tailscale'
489
+ if (/\.ts\.net/.test(address)) return 'tailscale'
441
490
  if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(address)) return 'tailscale'
442
491
  if (address.includes(':')) return 'wifi'
443
492
  return 'usb'
@@ -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
- setConnectOpen(true)
1512
+ setAddMenuOpen(true)
1351
1513
  },
1352
1514
  },
1353
1515
  '+ ' + t('add'),
@@ -1406,7 +1568,7 @@ window.__ModuleLoader__.load({
1406
1568
  h('input', {
1407
1569
  className: 'dph-input',
1408
1570
  value: address,
1409
- placeholder: '100.90.248.53:5555',
1571
+ placeholder: '192.0.2.53:5555',
1410
1572
  onChange: (event) => setAddress(event.target.value),
1411
1573
  autoFocus: true,
1412
1574
  spellCheck: false,
@@ -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: () => setConnectOpen(true) }, '+ ' + t('add')),
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('add'), onClick: () => setConnectOpen(true) },
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