dsh-apk-vm 0.6.1 → 0.8.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/client.js CHANGED
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
30
30
  var SWIPE_THRESHOLD_PX = 24;
31
31
  var FRAME_INTERVAL_MS = 500;
32
32
  var STATE_INTERVAL_MS = 2000;
33
- var CLIENT_VERSION = '0.2.1';
33
+ var CLIENT_VERSION = '0.8.0';
34
34
 
35
35
  /* ------------------------------------------------------------------ *
36
36
  * Optional dsh-better-sidebar integration.
@@ -53,6 +53,26 @@ window.__ModuleLoader__.load({
53
53
  return betterSidebar;
54
54
  }
55
55
 
56
+ function readUiFlag(key, fallback) {
57
+ try {
58
+ if (typeof window === 'undefined' || !window.localStorage) return fallback;
59
+ var value = window.localStorage.getItem(key);
60
+ return value === null ? fallback : value === '1';
61
+ } catch (error) {
62
+ return fallback;
63
+ }
64
+ }
65
+
66
+ function writeUiFlag(key, value) {
67
+ try {
68
+ if (typeof window !== 'undefined' && window.localStorage) {
69
+ window.localStorage.setItem(key, value ? '1' : '0');
70
+ }
71
+ } catch (error) {
72
+ /* localStorage may be unavailable in hardened webviews */
73
+ }
74
+ }
75
+
56
76
  function publishBetterSidebar(next) {
57
77
  if (betterSidebar === next) return;
58
78
  betterSidebar = next;
@@ -103,6 +123,20 @@ window.__ModuleLoader__.load({
103
123
  'action.installLaunch': 'Install & launch',
104
124
  'action.dismiss': 'Dismiss',
105
125
  'action.openSidebar': 'Open in side bar',
126
+ 'action.expand': 'Expand',
127
+ 'action.restore': 'Exit expanded',
128
+ 'action.float': 'Float window',
129
+ 'action.dock': 'Dock panel',
130
+ 'action.tools': 'Tools',
131
+ 'action.hideTools': 'Hide tools',
132
+ 'stream.quality': 'Quality',
133
+ 'stream.quality.low': 'Low 540p · 30 fps',
134
+ 'stream.quality.balanced': 'Balanced 720p · 60 fps',
135
+ 'stream.quality.high': 'High 960p · 60 fps',
136
+ 'stream.quality.custom': 'Custom',
137
+ 'stage.firstBoot': 'First boot can take ~30 s; later starts use the quick-boot snapshot.',
138
+ 'version.mismatch': 'The panel UI is stale (client {client}, host {host}). Reload the panel; if it stays stale, restart DSH once.',
139
+ 'version.reload': 'Reload UI',
106
140
  'a11y.refresh': 'Refresh VM state',
107
141
  'a11y.retry': 'Retry the request',
108
142
  'a11y.openSidebar': 'Open the APK VM in dsh-better-sidebar',
@@ -157,7 +191,12 @@ window.__ModuleLoader__.load({
157
191
  'debug.liveStop': 'Stop live',
158
192
  'debug.crash': 'Crash log',
159
193
  'debug.clipboardSet': 'Copy field to device',
160
- 'debug.clipboardGet': 'Get device clipboard',
194
+ 'debug.clipboardGet': 'Copy device clipboard to host',
195
+ 'debug.clipboardPasteHost': 'Paste host clipboard to device',
196
+ 'debug.clipboardPasteHostDone': 'Host clipboard pasted to device',
197
+ 'debug.clipboardUnavailable': 'Browser clipboard is unavailable; use the field instead',
198
+ 'debug.visualTest': 'UI check',
199
+ 'debug.visualTestDone': 'UI check complete',
161
200
  'debug.clipboardEmpty': 'Type text in the input field first',
162
201
  'debug.clipboardSetDone': 'Device clipboard updated',
163
202
  'debug.clipboardGot': 'Device clipboard copied to host',
@@ -206,6 +245,10 @@ window.__ModuleLoader__.load({
206
245
  'network.testFail': 'Connectivity check failed',
207
246
  'network.pcap': 'Download pcap',
208
247
  'network.pcapPath': 'Host pcap path (.pcap, next start)',
248
+ 'network.harPath': 'HAR file path (.har)',
249
+ 'network.harLoad': 'Load HAR',
250
+ 'network.harLoaded': 'HAR loaded',
251
+ 'network.harEmpty': 'No matching requests.',
209
252
  'network.restartRequired': 'Restart the VM to apply custom DNS',
210
253
  'network.sharedHint': 'Shared forwards are reachable from other VMs through the emulator host alias.',
211
254
  'network.guestUrl': 'Guest URL: {url}',
@@ -220,6 +263,15 @@ window.__ModuleLoader__.load({
220
263
  'flow.saved': 'Saved {name}',
221
264
  'flow.done': 'Replayed {name}',
222
265
  'flow.count': '{n} step(s)',
266
+ 'snapshots.title': 'VM snapshots',
267
+ 'snapshots.placeholder': 'snapshot name',
268
+ 'snapshots.save': 'Save',
269
+ 'snapshots.restore': 'Restore',
270
+ 'snapshots.delete': 'Delete',
271
+ 'snapshots.empty': 'No named snapshots yet.',
272
+ 'snapshots.saved': 'Snapshot saved: {name}',
273
+ 'snapshots.restored': 'Restored snapshot: {name}',
274
+ 'snapshots.deleted': 'Deleted snapshot: {name}',
223
275
  'storage.title': 'Storage',
224
276
  'storage.refresh': 'Refresh',
225
277
  'storage.cleanTemp': 'Clean temp',
@@ -247,6 +299,9 @@ window.__ModuleLoader__.load({
247
299
  'setup.installImage': 'Download image',
248
300
  'setup.installTools': 'Install SDK tools',
249
301
  'setup.bootstrapSdk': 'Bootstrap SDK',
302
+ 'setup.oneClick': 'One-click setup',
303
+ 'setup.oneClickRunning': 'Setting up SDK, image and AVD…',
304
+ 'setup.oneClickDone': 'Setup finished; starting the VM…',
250
305
  'setup.refresh': 'Re-check',
251
306
  'setup.switch': 'Switch AVD',
252
307
  'setup.loading': 'Checking environment…',
@@ -258,12 +313,16 @@ window.__ModuleLoader__.load({
258
313
  'instances.created': 'Created instance {id}',
259
314
  'instances.destroyed': 'Destroyed instance {id}',
260
315
  'instances.manage': 'VM instances',
316
+ 'grid.title': 'Grid view',
317
+ 'grid.empty': 'No running instances to show.',
318
+ 'grid.close': 'Close grid',
261
319
  'instances.stop': 'Stop',
262
320
  'instances.stopped': 'Stopped instance {id}',
263
321
  'instances.idle': 'idle {seconds}s',
264
322
  'devices.title': 'Physical/remote adb device',
265
323
  'devices.select': 'Select device…',
266
324
  'devices.attach': 'Attach',
325
+ 'devices.remotePlaceholder': 'host:port',
267
326
  'devices.detach': 'Detach',
268
327
  'devices.attached': 'Attached {serial}',
269
328
  'devices.detached': 'Detached external device',
@@ -274,8 +333,16 @@ window.__ModuleLoader__.load({
274
333
  'apps.empty': 'No third-party apps installed.',
275
334
  'apps.thirdParty': 'third-party only',
276
335
  'apps.stop': 'Stop',
336
+ 'apps.launch': 'Launch',
277
337
  'apps.clear': 'Clear data',
278
338
  'apps.uninstall': 'Uninstall',
339
+ 'apps.deepLink': 'Deep link',
340
+ 'apps.deepLinkPlaceholder': 'myapp://path or https://…',
341
+ 'apps.openDeepLink': 'Open link',
342
+ 'apps.permission': 'Permission',
343
+ 'apps.permissionPlaceholder': 'android.permission.CAMERA',
344
+ 'apps.grant': 'Grant',
345
+ 'apps.revoke': 'Revoke',
279
346
  'apps.done': '{action}: {packageName}',
280
347
  'debug.empty': 'No debug output yet.',
281
348
  'debug.identity': '{release} · API {sdk} · {abi}',
@@ -336,6 +403,20 @@ window.__ModuleLoader__.load({
336
403
  'action.installLaunch': '安装并启动',
337
404
  'action.dismiss': '关闭',
338
405
  'action.openSidebar': '在侧栏打开',
406
+ 'action.expand': '放大',
407
+ 'action.restore': '退出放大',
408
+ 'action.float': '浮窗',
409
+ 'action.dock': '停靠面板',
410
+ 'action.tools': '工具',
411
+ 'action.hideTools': '收起工具',
412
+ 'stream.quality': '清晰度',
413
+ 'stream.quality.low': '低 540p · 30 fps',
414
+ 'stream.quality.balanced': '均衡 720p · 60 fps',
415
+ 'stream.quality.high': '高 960p · 60 fps',
416
+ 'stream.quality.custom': '自定义',
417
+ 'stage.firstBoot': '首次启动约需 30 秒;之后会使用快速启动快照。',
418
+ 'version.mismatch': '面板 UI 是旧缓存(客户端 {client},主机 {host})。刷新面板;仍不更新则重启一次 DSH。',
419
+ 'version.reload': '刷新 UI',
339
420
  'a11y.refresh': '刷新虚拟机状态',
340
421
  'a11y.retry': '重试请求',
341
422
  'a11y.openSidebar': '在 dsh-better-sidebar 中打开 APK 虚拟机',
@@ -390,7 +471,12 @@ window.__ModuleLoader__.load({
390
471
  'debug.liveStop': '停止实时',
391
472
  'debug.crash': '崩溃日志',
392
473
  'debug.clipboardSet': '文本写入设备剪贴板',
393
- 'debug.clipboardGet': '读取设备剪贴板',
474
+ 'debug.clipboardGet': '复制设备剪贴板到电脑',
475
+ 'debug.clipboardPasteHost': '把电脑剪贴板粘贴到设备',
476
+ 'debug.clipboardPasteHostDone': '已把电脑剪贴板写入设备',
477
+ 'debug.clipboardUnavailable': '浏览器剪贴板不可用,请改用输入框',
478
+ 'debug.visualTest': 'UI 检查',
479
+ 'debug.visualTestDone': 'UI 检查完成',
394
480
  'debug.clipboardEmpty': '请先在上方输入框输入文本',
395
481
  'debug.clipboardSetDone': '已写入设备剪贴板',
396
482
  'debug.clipboardGot': '已复制设备剪贴板到主机',
@@ -439,6 +525,10 @@ window.__ModuleLoader__.load({
439
525
  'network.testFail': '连通性检查失败',
440
526
  'network.pcap': '下载 pcap',
441
527
  'network.pcapPath': '宿主机 pcap 路径(.pcap,下次启动)',
528
+ 'network.harPath': 'HAR 文件路径(.har)',
529
+ 'network.harLoad': '加载 HAR',
530
+ 'network.harLoaded': 'HAR 已加载',
531
+ 'network.harEmpty': '没有匹配的请求。',
442
532
  'network.restartRequired': '重启虚拟机后生效自定义 DNS',
443
533
  'network.sharedHint': '共享转发可被其他 VM 通过模拟器宿主别名访问。',
444
534
  'network.guestUrl': 'Guest URL: {url}',
@@ -453,6 +543,15 @@ window.__ModuleLoader__.load({
453
543
  'flow.saved': '已保存 {name}',
454
544
  'flow.done': '已回放 {name}',
455
545
  'flow.count': '{n} 步',
546
+ 'snapshots.title': 'VM 快照',
547
+ 'snapshots.placeholder': '快照名称',
548
+ 'snapshots.save': '保存',
549
+ 'snapshots.restore': '恢复',
550
+ 'snapshots.delete': '删除',
551
+ 'snapshots.empty': '还没有命名快照。',
552
+ 'snapshots.saved': '已保存快照:{name}',
553
+ 'snapshots.restored': '已恢复快照:{name}',
554
+ 'snapshots.deleted': '已删除快照:{name}',
456
555
  'storage.title': '存储',
457
556
  'storage.refresh': '刷新',
458
557
  'storage.cleanTemp': '清理临时文件',
@@ -480,6 +579,9 @@ window.__ModuleLoader__.load({
480
579
  'setup.installImage': '下载镜像',
481
580
  'setup.installTools': '安装 SDK 工具',
482
581
  'setup.bootstrapSdk': '从零安装 SDK',
582
+ 'setup.oneClick': '一键初始化',
583
+ 'setup.oneClickRunning': '正在安装 SDK、镜像并创建 AVD…',
584
+ 'setup.oneClickDone': '初始化完成,正在启动虚拟机…',
483
585
  'setup.refresh': '重新检测',
484
586
  'setup.switch': '切换 AVD',
485
587
  'setup.loading': '正在检测环境…',
@@ -491,12 +593,16 @@ window.__ModuleLoader__.load({
491
593
  'instances.created': '已创建实例 {id}',
492
594
  'instances.destroyed': '已销毁实例 {id}',
493
595
  'instances.manage': '虚拟机实例',
596
+ 'grid.title': '网格视图',
597
+ 'grid.empty': '没有正在运行的实例。',
598
+ 'grid.close': '关闭网格',
494
599
  'instances.stop': '停止',
495
600
  'instances.stopped': '已停止实例 {id}',
496
601
  'instances.idle': '空闲 {seconds}s',
497
602
  'devices.title': '物理/远程 adb 设备',
498
603
  'devices.select': '选择设备…',
499
604
  'devices.attach': '连接',
605
+ 'devices.remotePlaceholder': '主机:端口',
500
606
  'devices.detach': '断开',
501
607
  'devices.attached': '已连接 {serial}',
502
608
  'devices.detached': '已断开外部设备',
@@ -507,8 +613,16 @@ window.__ModuleLoader__.load({
507
613
  'apps.empty': '没有已安装的第三方应用。',
508
614
  'apps.thirdParty': '仅第三方',
509
615
  'apps.stop': '停止',
616
+ 'apps.launch': '启动',
510
617
  'apps.clear': '清数据',
511
618
  'apps.uninstall': '卸载',
619
+ 'apps.deepLink': 'Deep Link',
620
+ 'apps.deepLinkPlaceholder': 'myapp://path 或 https://…',
621
+ 'apps.openDeepLink': '打开链接',
622
+ 'apps.permission': '权限',
623
+ 'apps.permissionPlaceholder': 'android.permission.CAMERA',
624
+ 'apps.grant': '授予',
625
+ 'apps.revoke': '撤销',
512
626
  'apps.done': '{action}:{packageName}',
513
627
  'debug.empty': '暂无调试输出。',
514
628
  'debug.identity': '{release} · API {sdk} · {abi}',
@@ -1064,7 +1178,38 @@ window.__ModuleLoader__.load({
1064
1178
  '.apkvm-embedded .apkvm-phonewrap{flex:0 0 auto;min-height:320px}' +
1065
1179
  '.apkvm-embedded .apkvm-controls{max-width:none}' +
1066
1180
  '@media (max-width:900px){.apkvm-header{flex-wrap:wrap}}' +
1067
- '.apkvm-embedded .apkvm-header{flex-wrap:wrap}';
1181
+ '.apkvm-embedded .apkvm-header{flex-wrap:wrap}' +
1182
+ '.apkvm-page.apkvm-maximized{position:fixed;inset:0;z-index:2147483000;width:100vw;height:100vh;background:var(--dsw-alias-bg-base);box-shadow:0 0 0 1px var(--dsw-alias-border-l2)}' +
1183
+ '.apkvm-page.apkvm-maximized .apkvm-header{flex:0 0 auto;background:var(--dsw-alias-bg-base);padding-right:142px}' +
1184
+ '.apkvm-page.apkvm-maximized .apkvm-body{flex:1 1 auto;min-height:0;padding:10px 16px 14px;gap:10px;overflow:hidden;flex-direction:row}' +
1185
+ '.apkvm-page.apkvm-maximized .apkvm-aside{display:none}' +
1186
+ '.apkvm-page.apkvm-maximized .apkvm-stage{flex:1 1 auto;min-height:0;max-height:none}' +
1187
+ '.apkvm-page.apkvm-maximized .apkvm-phonewrap{flex:1 1 auto;min-height:0;padding:0}' +
1188
+ '.apkvm-page.apkvm-maximized .apkvm-phone{height:100%;max-height:calc(100vh - 150px);padding:6px;border-radius:22px}' +
1189
+ '.apkvm-page.apkvm-maximized .apkvm-screen{height:100%;max-height:calc(100vh - 170px);border-radius:16px}' +
1190
+ '.apkvm-page.apkvm-maximized .apkvm-controls{max-width:760px}' +
1191
+ '.apkvm-page.apkvm-maximized .apkvm-stagehint{max-width:760px}' +
1192
+ '.apkvm-page.apkvm-maximized .apkvm-subtitle,.apkvm-page.apkvm-maximized .apkvm-pillmeta{display:none}' +
1193
+ '.apkvm-page.apkvm-tools-collapsed .apkvm-aside{display:none}' +
1194
+ '.apkvm-max-close{position:absolute;top:9px;right:12px;z-index:6;appearance:none;font:inherit;font-size:12px;font-weight:600;height:30px;padding:0 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer}' +
1195
+ '.apkvm-max-close:hover{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-brand-primary)}' +
1196
+ '.apkvm-quality{font:inherit;font-size:11px;height:24px;max-width:150px;padding:0 5px;border-radius:7px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);flex:none}' +
1197
+ '.apkvm-version-banner{flex:0 0 auto}' +
1198
+ '.apkvm-version-banner .apkvm-banner-text{white-space:normal}' +
1199
+ '.apkvm-grid-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-base);display:flex;flex-direction:column;gap:10px;padding:12px 14px}' +
1200
+ '.apkvm-grid-head{display:flex;align-items:center;gap:8px;flex:0 0 auto}' +
1201
+ '.apkvm-grid{flex:1 1 auto;min-height:0;overflow:auto;display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px;align-content:start}' +
1202
+ '.apkvm-grid-card{appearance:none;font:inherit;text-align:left;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:8px;display:flex;flex-direction:column;gap:6px;min-height:260px}' +
1203
+ '.apkvm-grid-card:hover{border-color:var(--dsw-alias-brand-primary)}' +
1204
+ '.apkvm-grid-card img{flex:1 1 auto;min-height:0;width:100%;height:240px;object-fit:contain;background:var(--dsw-alias-bg-base);border-radius:8px}' +
1205
+ '.apkvm-grid-meta{display:flex;align-items:center;gap:6px;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
1206
+ '.apkvm-page.apkvm-floating .apkvm-stage{position:fixed;right:14px;bottom:14px;z-index:2147483000;width:min(360px,92vw);height:min(700px,88vh);padding:6px;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
1207
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-phonewrap{min-height:0;flex:1 1 auto}' +
1208
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-phone{height:100%;padding:4px;border-radius:14px}' +
1209
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-screen{border-radius:11px}' +
1210
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-controls{gap:3px;max-width:100%}' +
1211
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-textrow{display:none}' +
1212
+ '.apkvm-page.apkvm-floating .apkvm-stage .apkvm-stagehint{display:none}';
1068
1213
 
1069
1214
  /* ------------------------------------------------------------------ *
1070
1215
  * Render-error boundary: an unexpected exception inside the panel keeps
@@ -1172,6 +1317,15 @@ window.__ModuleLoader__.load({
1172
1317
  var appActionBusyState = React.useState('');
1173
1318
  var appActionBusy = appActionBusyState[0];
1174
1319
  var setAppActionBusy = appActionBusyState[1];
1320
+ var deepLinkState = React.useState('');
1321
+ var deepLink = deepLinkState[0];
1322
+ var setDeepLink = deepLinkState[1];
1323
+ var permissionPackageState = React.useState('');
1324
+ var permissionPackage = permissionPackageState[0];
1325
+ var setPermissionPackage = permissionPackageState[1];
1326
+ var permissionNameState = React.useState('');
1327
+ var permissionName = permissionNameState[0];
1328
+ var setPermissionName = permissionNameState[1];
1175
1329
 
1176
1330
  var vmBusyState = React.useState(false);
1177
1331
  var vmBusy = vmBusyState[0];
@@ -1212,6 +1366,9 @@ window.__ModuleLoader__.load({
1212
1366
  var devicePickState = React.useState('');
1213
1367
  var devicePick = devicePickState[0];
1214
1368
  var setDevicePick = devicePickState[1];
1369
+ var remoteSerialState = React.useState('');
1370
+ var remoteSerial = remoteSerialState[0];
1371
+ var setRemoteSerial = remoteSerialState[1];
1215
1372
  var deviceBusyState = React.useState(false);
1216
1373
  var deviceBusy = deviceBusyState[0];
1217
1374
  var setDeviceBusy = deviceBusyState[1];
@@ -1226,6 +1383,12 @@ window.__ModuleLoader__.load({
1226
1383
  var instancesListState = React.useState([]);
1227
1384
  var instancesList = instancesListState[0];
1228
1385
  var setInstancesList = instancesListState[1];
1386
+ var gridOpenState = React.useState(false);
1387
+ var gridOpen = gridOpenState[0];
1388
+ var setGridOpen = gridOpenState[1];
1389
+ var gridTickState = React.useState(Date.now());
1390
+ var gridTick = gridTickState[0];
1391
+ var setGridTick = gridTickState[1];
1229
1392
  var instanceBusyState = React.useState(false);
1230
1393
  var instanceBusy = instanceBusyState[0];
1231
1394
  var setInstanceBusy = instanceBusyState[1];
@@ -1289,6 +1452,15 @@ window.__ModuleLoader__.load({
1289
1452
  var storageConfirmState = React.useState('');
1290
1453
  var storageConfirm = storageConfirmState[0];
1291
1454
  var setStorageConfirm = storageConfirmState[1];
1455
+ var snapshotNamesState = React.useState([]);
1456
+ var snapshotNames = snapshotNamesState[0];
1457
+ var setSnapshotNames = snapshotNamesState[1];
1458
+ var snapshotInputState = React.useState('');
1459
+ var snapshotInput = snapshotInputState[0];
1460
+ var setSnapshotInput = snapshotInputState[1];
1461
+ var snapshotBusyState = React.useState('');
1462
+ var snapshotBusy = snapshotBusyState[0];
1463
+ var setSnapshotBusy = snapshotBusyState[1];
1292
1464
 
1293
1465
  var proxyState = React.useState({ host: '', port: '' });
1294
1466
  var proxy = proxyState[0];
@@ -1323,6 +1495,15 @@ window.__ModuleLoader__.load({
1323
1495
  var pcapPathState = React.useState('');
1324
1496
  var pcapPath = pcapPathState[0];
1325
1497
  var setPcapPath = pcapPathState[1];
1498
+ var harPathState = React.useState('');
1499
+ var harPath = harPathState[0];
1500
+ var setHarPath = harPathState[1];
1501
+ var harResultState = React.useState(null);
1502
+ var harResult = harResultState[0];
1503
+ var setHarResult = harResultState[1];
1504
+ var harBusyState = React.useState(false);
1505
+ var harBusy = harBusyState[0];
1506
+ var setHarBusy = harBusyState[1];
1326
1507
 
1327
1508
  var actualFpsState = React.useState(0);
1328
1509
  var actualFps = actualFpsState[0];
@@ -1373,9 +1554,32 @@ window.__ModuleLoader__.load({
1373
1554
  var canvasRef = React.useRef(null);
1374
1555
  var decoderRef = React.useRef(null);
1375
1556
  var wsRef = React.useRef(null);
1557
+ var pageRef = React.useRef(null);
1558
+ var maximizedState = React.useState(function () {
1559
+ return readUiFlag('apk-vm.maximized', false);
1560
+ });
1561
+ var isMaximized = maximizedState[0];
1562
+ var setIsMaximized = maximizedState[1];
1563
+ var toolsState = React.useState(!embedded);
1564
+ var toolsOpen = toolsState[0];
1565
+ var setToolsOpen = toolsState[1];
1566
+ var floatingState = React.useState(false);
1567
+ var isFloating = floatingState[0];
1568
+ var setIsFloating = floatingState[1];
1569
+ var qualityBusyState = React.useState('');
1570
+ var qualityBusy = qualityBusyState[0];
1571
+ var setQualityBusy = qualityBusyState[1];
1376
1572
 
1377
1573
  /* ---- derived snapshot values --------------------------------- */
1378
1574
  var vmInfo = snapshot ? snapshot.vm : null;
1575
+ var hostVersion = snapshot && snapshot.pluginVersion ? String(snapshot.pluginVersion) : '';
1576
+ var versionMismatch = Boolean(hostVersion && hostVersion !== CLIENT_VERSION);
1577
+ var h264Status =
1578
+ snapshot && isObject(snapshot.stream) && isObject(snapshot.stream.h264)
1579
+ ? snapshot.stream.h264
1580
+ : null;
1581
+ var streamPreset = h264Status && h264Status.preset ? String(h264Status.preset) : 'custom';
1582
+ var streamQualityValue = qualityBusy || streamPreset;
1379
1583
  var clientReport = snapshot && isObject(snapshot.client) ? snapshot.client : null;
1380
1584
  var wsOrigin = snapshot && snapshot.origin ? snapshot.origin : '';
1381
1585
  var apkInfo = snapshot ? snapshot.apk : null;
@@ -1433,6 +1637,7 @@ window.__ModuleLoader__.load({
1433
1637
  pillMeta.push(T(bootKey, { s: bootSeconds }));
1434
1638
  }
1435
1639
  }
1640
+ pillMeta.push('v' + CLIENT_VERSION);
1436
1641
  var subtitleText = apkInfo
1437
1642
  ? T('apk.current', { name: apkInfo.label || apkInfo.packageName || apkInfo.path })
1438
1643
  : T('subtitle');
@@ -1525,9 +1730,40 @@ window.__ModuleLoader__.load({
1525
1730
  // Load network details once the VM becomes ready (the first render often
1526
1731
  // happens while the state is still loading).
1527
1732
  React.useEffect(function () {
1528
- if (ready) loadNetwork();
1733
+ if (ready) {
1734
+ loadNetwork();
1735
+ loadSnapshots();
1736
+ }
1529
1737
  }, [ready, activeInstance]);
1530
1738
 
1739
+ React.useEffect(function () {
1740
+ if (!gridOpen) return undefined;
1741
+ var timer = setInterval(function () {
1742
+ if (mountedRef.current) setGridTick(Date.now());
1743
+ }, 1500);
1744
+ return function () {
1745
+ clearInterval(timer);
1746
+ };
1747
+ }, [gridOpen]);
1748
+
1749
+ // Escape leaves the expanded VM view; the expand button is inside the
1750
+ // full-screen overlay too, so mouse-only users are never trapped.
1751
+ React.useEffect(function () {
1752
+ if (!isMaximized || typeof document === 'undefined' || typeof document.addEventListener !== 'function') {
1753
+ return undefined;
1754
+ }
1755
+ function onKeyDown(event) {
1756
+ if (event && event.key === 'Escape') {
1757
+ setIsMaximized(false);
1758
+ writeUiFlag('apk-vm.maximized', false);
1759
+ }
1760
+ }
1761
+ document.addEventListener('keydown', onKeyDown);
1762
+ return function () {
1763
+ document.removeEventListener('keydown', onKeyDown);
1764
+ };
1765
+ }, [isMaximized]);
1766
+
1531
1767
  // Track document visibility so the frame stream pauses when hidden.
1532
1768
  React.useEffect(function () {
1533
1769
  if (typeof document === 'undefined' || typeof document.addEventListener !== 'function') {
@@ -1887,6 +2123,11 @@ window.__ModuleLoader__.load({
1887
2123
  return id === 'default' ? '/apk-vm' + pathname : '/apk-vm/instances/' + encodeURIComponent(id) + pathname;
1888
2124
  }
1889
2125
 
2126
+ function instanceFrameSrc(id) {
2127
+ var prefix = id === 'default' ? '/apk-vm/frame' : '/apk-vm/instances/' + encodeURIComponent(id) + '/frame';
2128
+ return prefix + '?t=' + String(gridTick);
2129
+ }
2130
+
1890
2131
  function setActiveInstanceId(id) {
1891
2132
  var next = id || 'default';
1892
2133
  activeInstanceRef.current = next;
@@ -2239,6 +2480,64 @@ window.__ModuleLoader__.load({
2239
2480
  });
2240
2481
  }
2241
2482
 
2483
+ function toggleMaximized() {
2484
+ setIsMaximized(function (value) {
2485
+ var next = !value;
2486
+ writeUiFlag('apk-vm.maximized', next);
2487
+ return next;
2488
+ });
2489
+ }
2490
+
2491
+ function toggleTools() {
2492
+ setToolsOpen(function (value) {
2493
+ return !value;
2494
+ });
2495
+ }
2496
+
2497
+ function applyStreamQuality(preset) {
2498
+ if (!ready || qualityBusy || activeInstance !== 'default') return;
2499
+ setQualityBusy(preset);
2500
+ requestJson(
2501
+ '/apk-vm/stream/quality',
2502
+ { method: 'POST', body: JSON.stringify({ preset: preset }) },
2503
+ T
2504
+ ).then(function (result) {
2505
+ if (!mountedRef.current) return;
2506
+ setQualityBusy('');
2507
+ if (!result.ok) {
2508
+ pushToast('error', T('err.action') + ': ' + result.error);
2509
+ return;
2510
+ }
2511
+ pushToast('success', T('stream.quality') + ': ' + T('stream.quality.' + preset));
2512
+ loadState();
2513
+ });
2514
+ }
2515
+
2516
+ function reloadUi() {
2517
+ if (typeof window !== 'undefined' && window.location && typeof window.location.reload === 'function') {
2518
+ window.location.reload();
2519
+ }
2520
+ }
2521
+
2522
+ function loadHar() {
2523
+ var target = String(harPath || '').trim();
2524
+ if (harBusy || !target) return;
2525
+ setHarBusy(true);
2526
+ requestJson(instancePath('/network/har'), {
2527
+ method: 'POST',
2528
+ body: JSON.stringify({ path: target }),
2529
+ }, T).then(function (result) {
2530
+ if (!mountedRef.current) return;
2531
+ setHarBusy(false);
2532
+ if (!result.ok) {
2533
+ pushToast('error', (result.error || T('err.action')).slice(0, 500));
2534
+ return;
2535
+ }
2536
+ setHarResult(isObject(result.data) ? result.data : null);
2537
+ pushToast('success', T('network.harLoaded'));
2538
+ });
2539
+ }
2540
+
2242
2541
  function loadNetwork() {
2243
2542
  if (!ready) return Promise.resolve(null);
2244
2543
  setNetworkBusy('load');
@@ -2384,6 +2683,37 @@ window.__ModuleLoader__.load({
2384
2683
  });
2385
2684
  }
2386
2685
 
2686
+ function loadSnapshots() {
2687
+ return requestJson(instancePath('/snapshots'), null, T).then(function (result) {
2688
+ if (!mountedRef.current) return result;
2689
+ if (result.ok && isObject(result.data) && Array.isArray(result.data.names)) {
2690
+ setSnapshotNames(result.data.names);
2691
+ }
2692
+ return result;
2693
+ });
2694
+ }
2695
+
2696
+ function snapshotAction(action, name) {
2697
+ if (snapshotBusy) return;
2698
+ setSnapshotBusy(action + ':' + name);
2699
+ requestJson(instancePath('/snapshots'), {
2700
+ method: 'POST',
2701
+ body: JSON.stringify({ action: action, name: name }),
2702
+ }, T).then(function (result) {
2703
+ if (!mountedRef.current) return;
2704
+ setSnapshotBusy('');
2705
+ if (!result.ok) {
2706
+ pushToast('error', (result.error || T('err.action')).slice(0, 400));
2707
+ return;
2708
+ }
2709
+ var key = action === 'save' ? 'snapshots.saved' : action === 'load' ? 'snapshots.restored' : 'snapshots.deleted';
2710
+ pushToast('success', T(key, { name: name }));
2711
+ if (action === 'save') setSnapshotInput('');
2712
+ loadSnapshots();
2713
+ if (action === 'load') loadState();
2714
+ });
2715
+ }
2716
+
2387
2717
  function loadStorage() {
2388
2718
  setStorageBusy('load');
2389
2719
  setStorageError('');
@@ -2499,6 +2829,53 @@ window.__ModuleLoader__.load({
2499
2829
  });
2500
2830
  }
2501
2831
 
2832
+ function openDeepLinkAction() {
2833
+ if (!ready || appActionBusy) return;
2834
+ var target = String(deepLink || '').trim();
2835
+ if (!target) {
2836
+ pushToast('error', T('apps.deepLinkPlaceholder'));
2837
+ return;
2838
+ }
2839
+ setAppActionBusy('open-url');
2840
+ requestJson(instancePath('/debug/action'), {
2841
+ method: 'POST',
2842
+ body: JSON.stringify({ action: 'open-url', url: target }),
2843
+ }, T).then(function (result) {
2844
+ if (!mountedRef.current) return;
2845
+ setAppActionBusy('');
2846
+ if (!result.ok) {
2847
+ pushToast('error', (result.error || T('err.action')).slice(0, 400));
2848
+ return;
2849
+ }
2850
+ pushToast('success', T('apps.deepLink') + ': ' + target);
2851
+ loadState();
2852
+ });
2853
+ }
2854
+
2855
+ function changePermission(granted) {
2856
+ if (!ready || appActionBusy) return;
2857
+ var pkg = String(permissionPackage || '').trim();
2858
+ var permission = String(permissionName || '').trim();
2859
+ if (!pkg || !permission) {
2860
+ pushToast('error', T('apps.permissionPlaceholder'));
2861
+ return;
2862
+ }
2863
+ var action = granted ? 'grant' : 'revoke';
2864
+ setAppActionBusy(action + ':' + pkg);
2865
+ requestJson(instancePath('/debug/action'), {
2866
+ method: 'POST',
2867
+ body: JSON.stringify({ action: action, packageName: pkg, permission: permission }),
2868
+ }, T).then(function (result) {
2869
+ if (!mountedRef.current) return;
2870
+ setAppActionBusy('');
2871
+ if (!result.ok) {
2872
+ pushToast('error', (result.error || T('err.action')).slice(0, 400));
2873
+ return;
2874
+ }
2875
+ pushToast('success', T('apps.permission') + ': ' + permission);
2876
+ });
2877
+ }
2878
+
2502
2879
  function toggleApps() {
2503
2880
  var next = !appsOpen;
2504
2881
  setAppsOpen(next);
@@ -2579,6 +2956,27 @@ window.__ModuleLoader__.load({
2579
2956
  });
2580
2957
  }
2581
2958
 
2959
+ function attachRemoteDevice() {
2960
+ var serial = String(remoteSerial || '').trim();
2961
+ if (!serial || deviceBusy) return;
2962
+ setDeviceBusy(true);
2963
+ setDeviceError('');
2964
+ requestJson('/apk-vm/device', { method: 'POST', body: JSON.stringify({ serial: serial }) }, T).then(function (result) {
2965
+ if (!mountedRef.current) return;
2966
+ setDeviceBusy(false);
2967
+ if (!result.ok) {
2968
+ var data = isObject(result.data) ? result.data : {};
2969
+ pushToast('error', (data.error || result.error || T('err.action')).slice(0, 500));
2970
+ return;
2971
+ }
2972
+ pushToast('success', T('devices.attached', { serial: serial }));
2973
+ setRemoteSerial('');
2974
+ loadState();
2975
+ loadDevices();
2976
+ loadSetup(true);
2977
+ });
2978
+ }
2979
+
2582
2980
  function detachDevice() {
2583
2981
  if (deviceBusy) return;
2584
2982
  setDeviceBusy(true);
@@ -2600,7 +2998,11 @@ window.__ModuleLoader__.load({
2600
2998
  function runSetupAction(action) {
2601
2999
  if (setupBusy) return;
2602
3000
  setSetupBusy(true);
2603
- requestJson('/apk-vm/setup', { method: 'POST', body: JSON.stringify({ action: action }) }, T).then(function (result) {
3001
+ requestJson(
3002
+ '/apk-vm/setup',
3003
+ { method: 'POST', body: JSON.stringify({ action: action, confirmDownload: true }) },
3004
+ T
3005
+ ).then(function (result) {
2604
3006
  if (!mountedRef.current) return;
2605
3007
  setSetupBusy(false);
2606
3008
  var data = isObject(result.data) ? result.data : {};
@@ -2615,6 +3017,57 @@ window.__ModuleLoader__.load({
2615
3017
  });
2616
3018
  }
2617
3019
 
3020
+ function runOneClickSetup() {
3021
+ if (setupBusy) return;
3022
+ var order = ['bootstrap-sdk', 'install-tools', 'install-image', 'create-avd'];
3023
+ var steps = order.filter(function (id) {
3024
+ return (setupActions || []).some(function (entry) {
3025
+ return entry.id === id;
3026
+ });
3027
+ });
3028
+ if (!steps.length) {
3029
+ pushToast('error', T('setup.ok'));
3030
+ return;
3031
+ }
3032
+ setSetupBusy(true);
3033
+ pushToast('success', T('setup.oneClickRunning'));
3034
+ var failed = false;
3035
+ var chain = Promise.resolve();
3036
+ steps.forEach(function (action) {
3037
+ chain = chain.then(function () {
3038
+ if (failed) return null;
3039
+ return requestJson(
3040
+ '/apk-vm/setup',
3041
+ { method: 'POST', body: JSON.stringify({ action: action, confirmDownload: true }) },
3042
+ T
3043
+ ).then(function (result) {
3044
+ var data = isObject(result.data) ? result.data : {};
3045
+ if (!result.ok || data.ok === false) {
3046
+ failed = true;
3047
+ var detail = data.result && data.result.error ? data.result.error : (data.result && data.result.output ? data.result.output : result.error || '');
3048
+ pushToast('error', String(detail || T('err.state')).slice(0, 500));
3049
+ }
3050
+ });
3051
+ });
3052
+ });
3053
+ chain
3054
+ .then(function () {
3055
+ if (!mountedRef.current) return;
3056
+ setSetupBusy(false);
3057
+ loadSetup(true);
3058
+ loadState();
3059
+ if (!failed) {
3060
+ pushToast('success', T('setup.oneClickDone'));
3061
+ runVmAction('start');
3062
+ }
3063
+ })
3064
+ .catch(function (error) {
3065
+ if (!mountedRef.current) return;
3066
+ setSetupBusy(false);
3067
+ pushToast('error', String(error && error.message ? error.message : error).slice(0, 500));
3068
+ });
3069
+ }
3070
+
2618
3071
  /* ---- actions -------------------------------------------------- */
2619
3072
  function pushToast(kind, text) {
2620
3073
  if (!mountedRef.current) return;
@@ -2839,6 +3292,9 @@ window.__ModuleLoader__.load({
2839
3292
  });
2840
3293
  }
2841
3294
 
3295
+ // Clipboard is intentionally passive: host clipboard is read or written
3296
+ // only inside these explicit click handlers. Never poll, listen to
3297
+ // clipboard events, or sync in the background.
2842
3298
  function setDeviceClipboard() {
2843
3299
  if (!ready) return;
2844
3300
  var text = String(textInput == null ? '' : textInput);
@@ -2876,6 +3332,61 @@ window.__ModuleLoader__.load({
2876
3332
  });
2877
3333
  }
2878
3334
 
3335
+ function pasteHostClipboardToDevice() {
3336
+ if (!ready) return;
3337
+ if (
3338
+ typeof navigator === 'undefined' ||
3339
+ !navigator.clipboard ||
3340
+ typeof navigator.clipboard.readText !== 'function'
3341
+ ) {
3342
+ pushToast('error', T('debug.clipboardUnavailable'));
3343
+ return;
3344
+ }
3345
+ navigator.clipboard.readText().then(
3346
+ function (text) {
3347
+ if (!mountedRef.current) return;
3348
+ if (!text) {
3349
+ pushToast('error', T('debug.clipboardEmpty'));
3350
+ return;
3351
+ }
3352
+ requestJson(
3353
+ instancePath('/clipboard'),
3354
+ { method: 'POST', body: JSON.stringify({ text: text, paste: false }) },
3355
+ T
3356
+ ).then(function (result) {
3357
+ if (!mountedRef.current) return;
3358
+ if (!result.ok) {
3359
+ pushToast('error', (result.error || T('err.action')).slice(0, 300));
3360
+ return;
3361
+ }
3362
+ setDebugOutput('clipboard host -> device (' + text.length + ' chars)');
3363
+ pushToast('success', T('debug.clipboardPasteHostDone'));
3364
+ });
3365
+ },
3366
+ function () {
3367
+ pushToast('error', T('debug.clipboardUnavailable'));
3368
+ }
3369
+ );
3370
+ }
3371
+
3372
+ function runUiTest() {
3373
+ if (!ready || debugBusy) return;
3374
+ setDebugBusy(true);
3375
+ requestJson(instancePath('/ui-test'), {
3376
+ method: 'POST',
3377
+ body: JSON.stringify({ name: 'panel', assertions: [], updateBaseline: false }),
3378
+ }, T).then(function (result) {
3379
+ if (!mountedRef.current) return;
3380
+ setDebugBusy(false);
3381
+ if (!result.ok) {
3382
+ pushToast('error', (result.error || T('err.action')).slice(0, 500));
3383
+ return;
3384
+ }
3385
+ setDebugOutput(String((result.data && (result.data.markdown || result.data.report)) || ''));
3386
+ pushToast('success', T('debug.visualTestDone'));
3387
+ });
3388
+ }
3389
+
2879
3390
  function runDebugInfo() {
2880
3391
  runDebugRequest(instancePath('/debug/info'), null, function (data) {
2881
3392
  return JSON.stringify(data, null, 2);
@@ -3441,6 +3952,31 @@ window.__ModuleLoader__.load({
3441
3952
  h(
3442
3953
  'div',
3443
3954
  { className: 'apkvm-toolsline' },
3955
+ h('input', {
3956
+ className: 'apkvm-input',
3957
+ type: 'text',
3958
+ value: remoteSerial,
3959
+ placeholder: T('devices.remotePlaceholder'),
3960
+ spellCheck: false,
3961
+ disabled: deviceBusy,
3962
+ style: { flex: '0 1 96px', minWidth: '0' },
3963
+ onChange: function (event) {
3964
+ setRemoteSerial(event && event.target ? String(event.target.value) : '');
3965
+ },
3966
+ }),
3967
+ h(
3968
+ 'button',
3969
+ {
3970
+ type: 'button',
3971
+ className: 'apkvm-mini',
3972
+ disabled: deviceBusy || !remoteSerial.trim(),
3973
+ onClick: function (event) {
3974
+ event.preventDefault();
3975
+ attachRemoteDevice();
3976
+ },
3977
+ },
3978
+ T('devices.attach')
3979
+ ),
3444
3980
  h(
3445
3981
  'select',
3446
3982
  {
@@ -3490,6 +4026,15 @@ window.__ModuleLoader__.load({
3490
4026
  h(
3491
4027
  'div',
3492
4028
  { style: { display: 'flex', alignItems: 'center', gap: '5px', flexWrap: 'wrap' } },
4029
+ setupActions.length > 1
4030
+ ? actionBtn(T('setup.oneClick'), {
4031
+ key: 'setup-one-click',
4032
+ small: true,
4033
+ variant: 'primary',
4034
+ disabled: setupBusy,
4035
+ onClick: runOneClickSetup,
4036
+ })
4037
+ : null,
3493
4038
  setupActions.map(function (entry) {
3494
4039
  var label = entry.id === 'create-avd'
3495
4040
  ? T('setup.createAvd')
@@ -3551,7 +4096,7 @@ window.__ModuleLoader__.load({
3551
4096
  ref: fileInputRef,
3552
4097
  className: 'apkvm-fileinput',
3553
4098
  type: 'file',
3554
- accept: '.apk,.apks,.xapk,application/vnd.android.package-archive,application/octet-stream',
4099
+ accept: '.apk,.apks,.xapk,.aab,application/vnd.android.package-archive,application/octet-stream',
3555
4100
  onChange: onFileInputChange,
3556
4101
  'aria-label': T('upload.button'),
3557
4102
  }),
@@ -3733,6 +4278,89 @@ window.__ModuleLoader__.load({
3733
4278
  ),
3734
4279
  h('span', { className: 'apkvm-dim' }, T('apps.thirdParty'))
3735
4280
  ),
4281
+ h(
4282
+ 'div',
4283
+ { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
4284
+ h('input', {
4285
+ className: 'apkvm-input',
4286
+ type: 'text',
4287
+ value: deepLink,
4288
+ placeholder: T('apps.deepLinkPlaceholder'),
4289
+ spellCheck: false,
4290
+ disabled: !ready || Boolean(appActionBusy),
4291
+ style: { flex: '1 1 160px', minWidth: '0' },
4292
+ onChange: function (event) {
4293
+ setDeepLink(event && event.target ? String(event.target.value) : '');
4294
+ },
4295
+ }),
4296
+ h(
4297
+ 'button',
4298
+ {
4299
+ type: 'button',
4300
+ className: 'apkvm-mini',
4301
+ disabled: !ready || Boolean(appActionBusy) || !deepLink.trim(),
4302
+ onClick: function (event) {
4303
+ event.preventDefault();
4304
+ openDeepLinkAction();
4305
+ },
4306
+ },
4307
+ T('apps.openDeepLink')
4308
+ )
4309
+ ),
4310
+ h(
4311
+ 'div',
4312
+ { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
4313
+ h('input', {
4314
+ className: 'apkvm-input',
4315
+ type: 'text',
4316
+ value: permissionPackage,
4317
+ placeholder: 'package',
4318
+ spellCheck: false,
4319
+ disabled: !ready || Boolean(appActionBusy),
4320
+ style: { flex: '0 1 120px', minWidth: '0' },
4321
+ onChange: function (event) {
4322
+ setPermissionPackage(event && event.target ? String(event.target.value) : '');
4323
+ },
4324
+ }),
4325
+ h('input', {
4326
+ className: 'apkvm-input',
4327
+ type: 'text',
4328
+ value: permissionName,
4329
+ placeholder: T('apps.permissionPlaceholder'),
4330
+ spellCheck: false,
4331
+ disabled: !ready || Boolean(appActionBusy),
4332
+ style: { flex: '1 1 150px', minWidth: '0' },
4333
+ onChange: function (event) {
4334
+ setPermissionName(event && event.target ? String(event.target.value) : '');
4335
+ },
4336
+ }),
4337
+ h(
4338
+ 'button',
4339
+ {
4340
+ type: 'button',
4341
+ className: 'apkvm-mini',
4342
+ disabled: !ready || Boolean(appActionBusy) || !permissionPackage.trim() || !permissionName.trim(),
4343
+ onClick: function (event) {
4344
+ event.preventDefault();
4345
+ changePermission(true);
4346
+ },
4347
+ },
4348
+ T('apps.grant')
4349
+ ),
4350
+ h(
4351
+ 'button',
4352
+ {
4353
+ type: 'button',
4354
+ className: 'apkvm-mini',
4355
+ disabled: !ready || Boolean(appActionBusy) || !permissionPackage.trim() || !permissionName.trim(),
4356
+ onClick: function (event) {
4357
+ event.preventDefault();
4358
+ changePermission(false);
4359
+ },
4360
+ },
4361
+ T('apps.revoke')
4362
+ )
4363
+ ),
3736
4364
  appsError ? h('div', { className: 'apkvm-error' }, appsError) : null,
3737
4365
  installedApps.length
3738
4366
  ? h(
@@ -3748,6 +4376,19 @@ window.__ModuleLoader__.load({
3748
4376
  h(
3749
4377
  'span',
3750
4378
  { className: 'apkvm-appactions' },
4379
+ h(
4380
+ 'button',
4381
+ {
4382
+ type: 'button',
4383
+ className: 'apkvm-mini',
4384
+ disabled: !ready || Boolean(appActionBusy),
4385
+ onClick: function (event) {
4386
+ event.preventDefault();
4387
+ runAppAction('launch', pkg);
4388
+ },
4389
+ },
4390
+ T('apps.launch')
4391
+ ),
3751
4392
  h(
3752
4393
  'button',
3753
4394
  {
@@ -3857,6 +4498,7 @@ window.__ModuleLoader__.load({
3857
4498
  onClick: toggleLiveLogcat,
3858
4499
  }),
3859
4500
  actionBtn(T('debug.crash'), { key: 'debug-crash', small: true, disabled: !ready || debugBusy, onClick: runCrashLogcat }),
4501
+ actionBtn(T('debug.visualTest'), { key: 'debug-ui-test', small: true, disabled: !ready || debugBusy, onClick: runUiTest }),
3860
4502
  actionBtn(T('debug.clipboardSet'), {
3861
4503
  key: 'debug-clipboard-set',
3862
4504
  small: true,
@@ -3869,6 +4511,12 @@ window.__ModuleLoader__.load({
3869
4511
  disabled: !ready,
3870
4512
  onClick: getDeviceClipboard,
3871
4513
  }),
4514
+ actionBtn(T('debug.clipboardPasteHost'), {
4515
+ key: 'debug-clipboard-paste-host',
4516
+ small: true,
4517
+ disabled: !ready,
4518
+ onClick: pasteHostClipboardToDevice,
4519
+ }),
3872
4520
  actionBtn(T('debug.props'), { key: 'debug-props', small: true, disabled: !ready || debugBusy, onClick: runDebugInfo }),
3873
4521
  actionBtn(T('debug.rootAction'), { key: 'debug-root', small: true, disabled: !ready || debugBusy || rootActive, onClick: runDebugRoot }),
3874
4522
  actionBtn(T('debug.ui'), { key: 'debug-ui', small: true, disabled: !ready || debugBusy, onClick: runDebugUi }),
@@ -4233,6 +4881,57 @@ window.__ModuleLoader__.load({
4233
4881
  },
4234
4882
  'aria-label': T('network.pcapPath'),
4235
4883
  }),
4884
+ h(
4885
+ 'div',
4886
+ { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
4887
+ h('input', {
4888
+ className: 'apkvm-input',
4889
+ type: 'text',
4890
+ value: harPath,
4891
+ placeholder: T('network.harPath'),
4892
+ spellCheck: false,
4893
+ disabled: harBusy,
4894
+ style: { flex: '1 1 150px', minWidth: '0' },
4895
+ onChange: function (event) {
4896
+ setHarPath(event && event.target ? String(event.target.value) : '');
4897
+ },
4898
+ }),
4899
+ actionBtn(harBusy ? '…' : T('network.harLoad'), {
4900
+ key: 'har-load',
4901
+ small: true,
4902
+ disabled: harBusy || !harPath.trim(),
4903
+ onClick: loadHar,
4904
+ })
4905
+ ),
4906
+ harResult && harResult.summary
4907
+ ? h(
4908
+ 'div',
4909
+ { className: 'apkvm-dim' },
4910
+ 'HAR ' +
4911
+ harResult.summary.total +
4912
+ ' · 2xx ' +
4913
+ (harResult.summary.statusClasses && harResult.summary.statusClasses['2xx'] ? harResult.summary.statusClasses['2xx'] : 0) +
4914
+ ' · 4xx ' +
4915
+ (harResult.summary.statusClasses && harResult.summary.statusClasses['4xx'] ? harResult.summary.statusClasses['4xx'] : 0) +
4916
+ ' · 5xx ' +
4917
+ (harResult.summary.statusClasses && harResult.summary.statusClasses['5xx'] ? harResult.summary.statusClasses['5xx'] : 0) +
4918
+ ' · ' +
4919
+ formatBytes(harResult.summary.totalBytes || 0)
4920
+ )
4921
+ : null,
4922
+ harResult && Array.isArray(harResult.entries) && harResult.entries.length
4923
+ ? h(
4924
+ 'div',
4925
+ { style: { display: 'flex', flexDirection: 'column', gap: '2px', maxHeight: '130px', overflow: 'auto' } },
4926
+ harResult.entries.slice(0, 10).map(function (entry, index) {
4927
+ return h(
4928
+ 'div',
4929
+ { key: 'har-entry-' + index, className: 'apkvm-dim', title: entry.curl || entry.url, style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } },
4930
+ String(entry.method || '') + ' ' + String(entry.status || '') + ' · ' + String(entry.url || '')
4931
+ );
4932
+ })
4933
+ )
4934
+ : null,
4236
4935
  h(
4237
4936
  'div',
4238
4937
  { style: { display: 'flex', alignItems: 'center', gap: '5px', flexWrap: 'wrap' } },
@@ -4384,6 +5083,71 @@ window.__ModuleLoader__.load({
4384
5083
  )
4385
5084
  );
4386
5085
 
5086
+ var snapshotCard = h(
5087
+ 'details',
5088
+ { className: 'apkvm-card apkvm-debug' },
5089
+ h(
5090
+ 'div',
5091
+ { className: 'apkvm-debug-summary', style: { cursor: 'default' } },
5092
+ h('span', { className: 'apkvm-card-title' }, T('snapshots.title')),
5093
+ h('span', { className: 'apkvm-dim', style: { marginLeft: 'auto' } }, String(snapshotNames.length))
5094
+ ),
5095
+ h(
5096
+ 'div',
5097
+ { className: 'apkvm-debug-body' },
5098
+ h(
5099
+ 'div',
5100
+ { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'wrap' } },
5101
+ h('input', {
5102
+ className: 'apkvm-input',
5103
+ type: 'text',
5104
+ value: snapshotInput,
5105
+ placeholder: T('snapshots.placeholder'),
5106
+ spellCheck: false,
5107
+ disabled: Boolean(snapshotBusy),
5108
+ style: { flex: '1 1 120px', minWidth: '0' },
5109
+ onChange: function (event) {
5110
+ setSnapshotInput(event && event.target ? String(event.target.value) : '');
5111
+ },
5112
+ }),
5113
+ actionBtn(T('snapshots.save'), {
5114
+ key: 'snapshot-save',
5115
+ small: true,
5116
+ disabled: !ready || Boolean(snapshotBusy) || !snapshotInput.trim(),
5117
+ onClick: function () {
5118
+ snapshotAction('save', snapshotInput.trim());
5119
+ },
5120
+ })
5121
+ ),
5122
+ snapshotNames.length
5123
+ ? snapshotNames.map(function (name) {
5124
+ return h(
5125
+ 'div',
5126
+ { key: 'snapshot-' + name, style: { display: 'flex', alignItems: 'center', gap: '3px' } },
5127
+ h('span', { className: 'apkvm-dim', style: { flex: '1 1 auto', minWidth: '0', overflow: 'hidden', textOverflow: 'ellipsis' } }, name),
5128
+ actionBtn(T('snapshots.restore'), {
5129
+ key: 'snapshot-restore-' + name,
5130
+ small: true,
5131
+ disabled: Boolean(snapshotBusy) || !ready,
5132
+ onClick: function () {
5133
+ snapshotAction('load', name);
5134
+ },
5135
+ }),
5136
+ actionBtn(T('snapshots.delete'), {
5137
+ key: 'snapshot-delete-' + name,
5138
+ small: true,
5139
+ variant: 'danger',
5140
+ disabled: Boolean(snapshotBusy),
5141
+ onClick: function () {
5142
+ snapshotAction('delete', name);
5143
+ },
5144
+ })
5145
+ );
5146
+ })
5147
+ : h('div', { className: 'apkvm-dim' }, T('snapshots.empty'))
5148
+ )
5149
+ );
5150
+
4387
5151
  var primaryAction =
4388
5152
  ready || busyState
4389
5153
  ? actionBtn(vmBusy ? T('action.stopping') : T('action.stop'), {
@@ -4495,6 +5259,16 @@ window.__ModuleLoader__.load({
4495
5259
  onClick: destroyActiveInstance,
4496
5260
  })
4497
5261
  : null,
5262
+ instancesList.length > 1
5263
+ ? actionBtn(T('grid.title'), {
5264
+ key: 'grid-view',
5265
+ small: true,
5266
+ onClick: function () {
5267
+ setGridTick(Date.now());
5268
+ setGridOpen(true);
5269
+ },
5270
+ })
5271
+ : null,
4498
5272
  h(
4499
5273
  'span',
4500
5274
  {
@@ -4515,7 +5289,48 @@ window.__ModuleLoader__.load({
4515
5289
  ' · ' +
4516
5290
  (videoMode === 'h264' ? T('fps.mode.h264') : T('fps.mode.png'))
4517
5291
  ),
5292
+ h264Status
5293
+ ? h(
5294
+ 'select',
5295
+ {
5296
+ className: 'apkvm-quality',
5297
+ value: streamQualityValue,
5298
+ title: T('stream.quality'),
5299
+ disabled: !ready || Boolean(qualityBusy) || activeInstance !== 'default' || h264Status.enabled === false,
5300
+ onChange: function (event) {
5301
+ applyStreamQuality(event && event.target ? String(event.target.value) : '');
5302
+ },
5303
+ },
5304
+ ['low', 'balanced', 'high'].map(function (name) {
5305
+ return h('option', { key: name, value: name }, T('stream.quality.' + name));
5306
+ }),
5307
+ streamPreset === 'custom' ? h('option', { key: 'custom', value: 'custom', disabled: true }, T('stream.quality.custom')) : null
5308
+ )
5309
+ : null,
4518
5310
  primaryAction,
5311
+ embedded
5312
+ ? actionBtn(T(toolsOpen ? 'action.hideTools' : 'action.tools'), {
5313
+ key: 'tools',
5314
+ small: true,
5315
+ onClick: toggleTools,
5316
+ })
5317
+ : null,
5318
+ actionBtn(T(isFloating ? 'action.dock' : 'action.float'), {
5319
+ key: 'float-window',
5320
+ small: true,
5321
+ onClick: function () {
5322
+ setIsFloating(function (value) {
5323
+ return !value;
5324
+ });
5325
+ },
5326
+ }),
5327
+ actionBtn(T(isMaximized ? 'action.restore' : 'action.expand'), {
5328
+ key: 'maximize',
5329
+ small: true,
5330
+ icon: h('span', { 'aria-hidden': 'true', style: { fontSize: '14px', lineHeight: '14px' } }, '⛶'),
5331
+ ariaLabel: T(isMaximized ? 'action.restore' : 'action.expand'),
5332
+ onClick: toggleMaximized,
5333
+ }),
4519
5334
  !embedded && sidebarService
4520
5335
  ? actionBtn(T('action.openSidebar'), {
4521
5336
  key: 'open-sidebar',
@@ -4552,6 +5367,19 @@ window.__ModuleLoader__.load({
4552
5367
  )
4553
5368
  : null;
4554
5369
 
5370
+ var versionBanner = versionMismatch
5371
+ ? h(
5372
+ 'div',
5373
+ { className: 'apkvm-banner error apkvm-version-banner', role: 'alert' },
5374
+ h('span', { className: 'apkvm-banner-text' }, T('version.mismatch', { client: CLIENT_VERSION, host: hostVersion })),
5375
+ actionBtn(T('version.reload'), {
5376
+ key: 'version-reload',
5377
+ small: true,
5378
+ onClick: reloadUi,
5379
+ })
5380
+ )
5381
+ : null;
5382
+
4555
5383
  var stateErrorBanner =
4556
5384
  stateError && !toast
4557
5385
  ? h(
@@ -4610,6 +5438,13 @@ window.__ModuleLoader__.load({
4610
5438
  onKeyDown: onScreenKeyDown,
4611
5439
  onContextMenu: onContextMenu,
4612
5440
  onDragStart: onContextMenu,
5441
+ onDoubleClick: function (event) {
5442
+ if (event) {
5443
+ event.preventDefault();
5444
+ event.stopPropagation();
5445
+ }
5446
+ toggleMaximized();
5447
+ },
4613
5448
  },
4614
5449
  frameUrl && videoMode !== 'h264'
4615
5450
  ? h('img', { className: 'apkvm-frame', src: frameUrl, alt: T('screen.alt'), draggable: false })
@@ -4655,6 +5490,12 @@ window.__ModuleLoader__.load({
4655
5490
  disabled: !ready,
4656
5491
  onClick: downloadScreenshot,
4657
5492
  }),
5493
+ actionBtn(T('debug.rotate'), {
5494
+ key: 'quick-rotate',
5495
+ small: true,
5496
+ disabled: !ready || debugBusy,
5497
+ onClick: runDebugRotate,
5498
+ }),
4658
5499
  actionBtn(inspectMode ? T('inspect.close') : T('inspect.toggle'), {
4659
5500
  key: 'inspect-toggle',
4660
5501
  small: true,
@@ -4686,6 +5527,9 @@ window.__ModuleLoader__.load({
4686
5527
  )
4687
5528
  ),
4688
5529
  h('div', { className: 'apkvm-stagehint' }, T('screen.hint')),
5530
+ !ready && (displayState === 'starting' || displayState === 'booting')
5531
+ ? h('div', { className: 'apkvm-stagehint' }, T('stage.firstBoot'))
5532
+ : null,
4689
5533
  ready && videoMode !== 'h264' && clientReport && clientReport.h264Supported === false
4690
5534
  ? h('div', { className: 'apkvm-stagehint' }, T('fps.fallbackHint'))
4691
5535
  : null,
@@ -4697,7 +5541,13 @@ window.__ModuleLoader__.load({
4697
5541
  return h(
4698
5542
  'div',
4699
5543
  {
4700
- className: 'apkvm-page' + (embedded ? ' apkvm-embedded' : ''),
5544
+ className:
5545
+ 'apkvm-page' +
5546
+ (embedded ? ' apkvm-embedded' : '') +
5547
+ (isMaximized ? ' apkvm-maximized' : '') +
5548
+ (isFloating ? ' apkvm-floating' : '') +
5549
+ (embedded && !toolsOpen ? ' apkvm-tools-collapsed' : ''),
5550
+ ref: pageRef,
4701
5551
  onDragEnter: onPageDragEnter,
4702
5552
  onDragOver: onPageDragOver,
4703
5553
  onDragLeave: onPageDragLeave,
@@ -4705,6 +5555,20 @@ window.__ModuleLoader__.load({
4705
5555
  },
4706
5556
  h('style', { key: 'apkvm-style' }, CSS_TEXT),
4707
5557
  headerEl,
5558
+ isMaximized
5559
+ ? h(
5560
+ 'button',
5561
+ {
5562
+ type: 'button',
5563
+ className: 'apkvm-max-close',
5564
+ onClick: toggleMaximized,
5565
+ 'aria-label': T('action.restore'),
5566
+ title: T('action.restore'),
5567
+ },
5568
+ '✕ ' + T('action.restore')
5569
+ )
5570
+ : null,
5571
+ versionBanner,
4708
5572
  stateErrorBanner,
4709
5573
  toastBanner,
4710
5574
  h(
@@ -4720,6 +5584,7 @@ window.__ModuleLoader__.load({
4720
5584
  flowCard,
4721
5585
  networkCard,
4722
5586
  storageCard,
5587
+ snapshotCard,
4723
5588
  appsCard,
4724
5589
  diagCard,
4725
5590
  h('div', { className: 'apkvm-footnote', title: T('hint.boot') }, T('hint.short')),
@@ -4734,6 +5599,54 @@ window.__ModuleLoader__.load({
4734
5599
  h('span', null, T('upload.drop')),
4735
5600
  h('span', { className: 'apkvm-dim' }, T('upload.detail'))
4736
5601
  )
5602
+ : null,
5603
+ gridOpen
5604
+ ? h(
5605
+ 'div',
5606
+ { className: 'apkvm-grid-overlay' },
5607
+ h(
5608
+ 'div',
5609
+ { className: 'apkvm-grid-head' },
5610
+ h('span', { className: 'apkvm-card-title' }, T('grid.title')),
5611
+ h('span', { className: 'apkvm-dim' }, instancesList.length + ' VM'),
5612
+ h('span', { className: 'apkvm-spacer' }),
5613
+ actionBtn(T('grid.close'), {
5614
+ key: 'grid-close',
5615
+ small: true,
5616
+ onClick: function () {
5617
+ setGridOpen(false);
5618
+ },
5619
+ })
5620
+ ),
5621
+ h(
5622
+ 'div',
5623
+ { className: 'apkvm-grid' },
5624
+ instancesList.map(function (entry) {
5625
+ var isReady = entry.state === 'ready';
5626
+ return h(
5627
+ 'button',
5628
+ {
5629
+ type: 'button',
5630
+ className: 'apkvm-grid-card',
5631
+ key: 'grid-' + entry.id,
5632
+ onClick: function () {
5633
+ setActiveInstanceId(entry.id);
5634
+ setGridOpen(false);
5635
+ },
5636
+ },
5637
+ isReady
5638
+ ? h('img', { src: instanceFrameSrc(entry.id), alt: entry.avdName || entry.id })
5639
+ : h('div', { className: 'apkvm-dim' }, entry.state || 'stopped'),
5640
+ h(
5641
+ 'span',
5642
+ { className: 'apkvm-grid-meta' },
5643
+ h('span', { className: 'apkvm-appname' }, entry.avdName || entry.id),
5644
+ h('span', { className: 'apkvm-dim' }, entry.state || '')
5645
+ )
5646
+ );
5647
+ })
5648
+ )
5649
+ )
4737
5650
  : null
4738
5651
  );
4739
5652
  }