dsh-apk-vm 0.3.13 → 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/client.js CHANGED
@@ -118,7 +118,7 @@ window.__ModuleLoader__.load({
118
118
  'upload.drop': 'Drop APK to install',
119
119
  'upload.detail': 'Installs into the local emulator and launches it.',
120
120
  'upload.uploading': 'Uploading {name}…',
121
- 'upload.invalid': 'Please choose an .apk file',
121
+ 'upload.invalid': 'Please choose an .apk, .apks or .xapk package',
122
122
  'upload.failed': 'Upload/install failed',
123
123
  'upload.done': 'Installed {name}',
124
124
  'apks.empty': 'No APK files found in the scan roots.',
@@ -169,6 +169,38 @@ window.__ModuleLoader__.load({
169
169
  'debug.rotate': 'Rotate',
170
170
  'debug.screenshot': 'Screenshot',
171
171
  'debug.recording': 'Recording 5s…',
172
+ 'inspect.toggle': 'Inspect UI',
173
+ 'inspect.title': 'UI inspector',
174
+ 'inspect.loading': 'Reading UI tree…',
175
+ 'inspect.empty': 'No visible UI nodes',
176
+ 'inspect.hint': 'Click a highlighted node to generate an apk_vm command.',
177
+ 'inspect.selected': 'Selected',
178
+ 'inspect.copyTap': 'Copy tap_text',
179
+ 'inspect.copyAssert': 'Copy assert_text',
180
+ 'inspect.copied': 'Command copied',
181
+ 'inspect.refresh': 'Refresh tree',
182
+ 'inspect.close': 'Close inspector',
183
+ 'network.title': 'Network',
184
+ 'network.proxy': 'HTTP proxy',
185
+ 'network.host': 'Proxy host',
186
+ 'network.port': 'Port',
187
+ 'network.apply': 'Apply proxy',
188
+ 'network.clear': 'Clear proxy',
189
+ 'network.caPath': 'CA certificate path (.pem/.crt)',
190
+ 'network.installCa': 'Install CA',
191
+ 'network.current': 'Current: {value}',
192
+ 'flow.title': 'Flow recorder',
193
+ 'flow.start': 'Record',
194
+ 'flow.stop': 'Stop',
195
+ 'flow.name': 'Flow name',
196
+ 'flow.play': 'Play',
197
+ 'flow.delete': 'Delete',
198
+ 'flow.empty': 'No saved flows',
199
+ 'flow.recording': 'Recording input…',
200
+ 'flow.saved': 'Saved {name}',
201
+ 'flow.done': 'Replayed {name}',
202
+ 'flow.count': '{n} step(s)',
203
+ 'apk.bundle': 'Bundle',
172
204
  'setup.title': 'Environment',
173
205
  'setup.ok': 'Ready',
174
206
  'setup.missing': 'Setup needed',
@@ -284,7 +316,7 @@ window.__ModuleLoader__.load({
284
316
  'upload.drop': '松开以安装 APK',
285
317
  'upload.detail': '上传后自动安装到本地模拟器并启动。',
286
318
  'upload.uploading': '正在上传 {name}…',
287
- 'upload.invalid': '请选择 .apk 文件',
319
+ 'upload.invalid': '请选择 .apk、.apks 或 .xapk 安装包',
288
320
  'upload.failed': '上传/安装失败',
289
321
  'upload.done': '已安装 {name}',
290
322
  'apks.empty': '扫描目录中没有找到 APK 文件。',
@@ -335,6 +367,38 @@ window.__ModuleLoader__.load({
335
367
  'debug.rotate': '旋转',
336
368
  'debug.screenshot': '截图',
337
369
  'debug.recording': '正在录屏 5s…',
370
+ 'inspect.toggle': '检查 UI',
371
+ 'inspect.title': 'UI 检查器',
372
+ 'inspect.loading': '正在读取 UI 树…',
373
+ 'inspect.empty': '没有可见 UI 节点',
374
+ 'inspect.hint': '点击高亮节点,生成可用的 apk_vm 命令。',
375
+ 'inspect.selected': '已选择',
376
+ 'inspect.copyTap': '复制 tap_text',
377
+ 'inspect.copyAssert': '复制 assert_text',
378
+ 'inspect.copied': '命令已复制',
379
+ 'inspect.refresh': '刷新 UI 树',
380
+ 'inspect.close': '关闭检查器',
381
+ 'network.title': '网络',
382
+ 'network.proxy': 'HTTP 代理',
383
+ 'network.host': '代理主机',
384
+ 'network.port': '端口',
385
+ 'network.apply': '应用代理',
386
+ 'network.clear': '清除代理',
387
+ 'network.caPath': 'CA 证书路径(.pem/.crt)',
388
+ 'network.installCa': '安装 CA',
389
+ 'network.current': '当前:{value}',
390
+ 'flow.title': '操作录制',
391
+ 'flow.start': '录制',
392
+ 'flow.stop': '停止',
393
+ 'flow.name': '流程名称',
394
+ 'flow.play': '回放',
395
+ 'flow.delete': '删除',
396
+ 'flow.empty': '还没有录制的流程',
397
+ 'flow.recording': '正在录制输入…',
398
+ 'flow.saved': '已保存 {name}',
399
+ 'flow.done': '已回放 {name}',
400
+ 'flow.count': '{n} 步',
401
+ 'apk.bundle': '安装包',
338
402
  'setup.title': '环境',
339
403
  'setup.ok': '就绪',
340
404
  'setup.missing': '需要配置',
@@ -661,6 +725,7 @@ window.__ModuleLoader__.load({
661
725
  name: typeof item.name === 'string' && item.name ? item.name : baseName(item.path),
662
726
  size: Number(item.size),
663
727
  mtime: Number(item.mtime),
728
+ kind: typeof item.kind === 'string' ? item.kind : String(item.name || '').split('.').pop(),
664
729
  });
665
730
  }
666
731
  return { roots: roots, apks: apks };
@@ -818,6 +883,14 @@ window.__ModuleLoader__.load({
818
883
  '.apkvm-phone{display:flex;height:100%;padding:8px;border-radius:26px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);max-width:100%;max-height:100%;min-height:0;flex:none}' +
819
884
  '.apkvm-screen{position:relative;flex:none;height:100%;max-width:100%;aspect-ratio:9/19.5;border-radius:19px;overflow:hidden;background:var(--dsw-alias-bg-layer-1);display:flex;align-items:center;justify-content:center;touch-action:none;user-select:none;-webkit-user-select:none}' +
820
885
  '.apkvm-screen.ready{cursor:crosshair}' +
886
+ '.apkvm-inspect-layer{position:absolute;inset:0;z-index:3;overflow:hidden;pointer-events:none}' +
887
+ '.apkvm-inspect-box{position:absolute;border:1.5px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent);border-radius:2px;pointer-events:auto;cursor:pointer;box-sizing:border-box}' +
888
+ '.apkvm-inspect-box:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 24%,transparent);border-color:var(--dsw-alias-label-primary)}' +
889
+ '.apkvm-inspect-box.selected{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 25%,transparent);border-color:var(--dsw-alias-state-warn-primary);box-shadow:0 0 0 1px var(--dsw-alias-state-warn-primary)}' +
890
+ '.apkvm-inspect-box.target{border-style:dashed;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent)}' +
891
+ '.apkvm-inspect-bar{position:absolute;left:8px;right:8px;bottom:8px;z-index:4;display:flex;flex-wrap:wrap;align-items:center;gap:6px;padding:7px 8px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-base) 90%,transparent);backdrop-filter:blur(4px);font-size:11px}' +
892
+ '.apkvm-inspect-label{font-weight:600;color:var(--dsw-alias-label-primary);max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
893
+ '.apkvm-inspect-meta{color:var(--dsw-alias-label-secondary);font-size:10.5px;white-space:nowrap}' +
821
894
  '.apkvm-frame{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}' +
822
895
  '.apkvm-overlay{position:absolute;inset:0;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:9px;padding:22px 18px;text-align:center;background:var(--dsw-alias-bg-layer-1)}' +
823
896
  '.apkvm-overlay-title{font-size:12.5px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);word-break:break-word}' +
@@ -1105,6 +1178,45 @@ window.__ModuleLoader__.load({
1105
1178
  var setLogLive = logLiveState[1];
1106
1179
  var logLiveTimerRef = React.useRef(null);
1107
1180
 
1181
+ var inspectModeState = React.useState(false);
1182
+ var inspectMode = inspectModeState[0];
1183
+ var setInspectMode = inspectModeState[1];
1184
+ var inspectNodesState = React.useState([]);
1185
+ var inspectNodes = inspectNodesState[0];
1186
+ var setInspectNodes = inspectNodesState[1];
1187
+ var inspectSelectedState = React.useState(null);
1188
+ var inspectSelected = inspectSelectedState[0];
1189
+ var setInspectSelected = inspectSelectedState[1];
1190
+ var inspectBusyState = React.useState(false);
1191
+ var inspectBusy = inspectBusyState[0];
1192
+ var setInspectBusy = inspectBusyState[1];
1193
+ var inspectErrorState = React.useState('');
1194
+ var inspectError = inspectErrorState[0];
1195
+ var setInspectError = inspectErrorState[1];
1196
+
1197
+ var flowsState = React.useState([]);
1198
+ var flows = flowsState[0];
1199
+ var setFlows = flowsState[1];
1200
+ var flowRecordingState = React.useState(false);
1201
+ var flowRecording = flowRecordingState[0];
1202
+ var setFlowRecording = flowRecordingState[1];
1203
+ var flowNameState = React.useState('');
1204
+ var flowName = flowNameState[0];
1205
+ var setFlowName = flowNameState[1];
1206
+ var flowBusyState = React.useState('');
1207
+ var flowBusy = flowBusyState[0];
1208
+ var setFlowBusy = flowBusyState[1];
1209
+
1210
+ var proxyState = React.useState({ host: '', port: '' });
1211
+ var proxy = proxyState[0];
1212
+ var setProxy = proxyState[1];
1213
+ var proxyBusyState = React.useState(false);
1214
+ var proxyBusy = proxyBusyState[0];
1215
+ var setProxyBusy = proxyBusyState[1];
1216
+ var caPathState = React.useState('');
1217
+ var caPath = caPathState[0];
1218
+ var setCaPath = caPathState[1];
1219
+
1108
1220
  var actualFpsState = React.useState(0);
1109
1221
  var actualFps = actualFpsState[0];
1110
1222
  var setActualFps = actualFpsState[1];
@@ -1279,6 +1391,7 @@ window.__ModuleLoader__.load({
1279
1391
  loadApks();
1280
1392
  loadInstances();
1281
1393
  loadDevices();
1394
+ loadFlows();
1282
1395
  var pollId = setInterval(function () {
1283
1396
  if (stateBusyRef.current) return;
1284
1397
  stateBusyRef.current = true;
@@ -1672,6 +1785,10 @@ window.__ModuleLoader__.load({
1672
1785
  setActualFps(0);
1673
1786
  setInstalledApps([]);
1674
1787
  setAppsError('');
1788
+ setInspectMode(false);
1789
+ setInspectNodes([]);
1790
+ setInspectSelected(null);
1791
+ loadFlows();
1675
1792
  stopLiveLogcat();
1676
1793
  frameStatsRef.current = [];
1677
1794
  loadState();
@@ -1791,6 +1908,271 @@ window.__ModuleLoader__.load({
1791
1908
  });
1792
1909
  }
1793
1910
 
1911
+ function loadInspector() {
1912
+ if (!ready || inspectBusy) return Promise.resolve(null);
1913
+ setInspectBusy(true);
1914
+ setInspectError('');
1915
+ return requestJson(instancePath('/debug/inspect'), null, T).then(function (result) {
1916
+ if (!mountedRef.current) return result;
1917
+ setInspectBusy(false);
1918
+ if (!result.ok) {
1919
+ setInspectError(result.error || T('err.action'));
1920
+ return result;
1921
+ }
1922
+ var data = isObject(result.data) ? result.data : {};
1923
+ var nodes = Array.isArray(data.nodes) ? data.nodes : [];
1924
+ setInspectNodes(nodes);
1925
+ setInspectSelected(null);
1926
+ return result;
1927
+ });
1928
+ }
1929
+
1930
+ function toggleInspectMode() {
1931
+ var next = !inspectMode;
1932
+ setInspectMode(next);
1933
+ if (next) {
1934
+ loadInspector();
1935
+ } else {
1936
+ setInspectSelected(null);
1937
+ }
1938
+ }
1939
+
1940
+ function copyInspectCommand(kind) {
1941
+ var node = inspectSelected;
1942
+ if (!node || !node.selector) return;
1943
+ var text = kind === 'assert' ? node.selector.assertCommand : node.selector.tapCommand;
1944
+ try {
1945
+ if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
1946
+ navigator.clipboard.writeText(text).then(
1947
+ function () {
1948
+ pushToast('success', T('inspect.copied') + ': ' + text);
1949
+ },
1950
+ function () {
1951
+ pushToast('error', T('inspect.copied') + ': ' + text);
1952
+ }
1953
+ );
1954
+ } else {
1955
+ pushToast('success', T('inspect.copied') + ': ' + text);
1956
+ }
1957
+ } catch (error) {
1958
+ pushToast('success', T('inspect.copied') + ': ' + text);
1959
+ }
1960
+ }
1961
+
1962
+ function nodeBoundingStyle(node) {
1963
+ var target = inputScreen || displayScreen;
1964
+ if (!target || !node || !node.bounds) return null;
1965
+ var b = node.bounds;
1966
+ var width = Number(target.width) || 0;
1967
+ var height = Number(target.height) || 0;
1968
+ if (!(width > 0) || !(height > 0) || !(b.width > 0) || !(b.height > 0)) return null;
1969
+ return {
1970
+ left: ((b.x1 / width) * 100).toFixed(4) + '%',
1971
+ top: ((b.y1 / height) * 100).toFixed(4) + '%',
1972
+ width: ((b.width / width) * 100).toFixed(4) + '%',
1973
+ height: ((b.height / height) * 100).toFixed(4) + '%',
1974
+ };
1975
+ }
1976
+
1977
+ function renderInspector() {
1978
+ if (!inspectMode || !ready) return null;
1979
+ var nodes = inspectNodes.filter(function (node) {
1980
+ return node && node.bounds && (node.interactive || node.clickable || node.text || node.contentDesc);
1981
+ });
1982
+ var boxes = nodes.slice(0, 160).map(function (node) {
1983
+ var style = nodeBoundingStyle(node);
1984
+ if (!style) return null;
1985
+ var selected = inspectSelected && inspectSelected.order === node.order;
1986
+ return h('div', {
1987
+ key: 'inspect-' + node.order,
1988
+ className: 'apkvm-inspect-box' + (selected ? ' selected' : ''),
1989
+ style: style,
1990
+ title: node.selector ? node.selector.selector : node.label,
1991
+ onClick: function (event) {
1992
+ if (event && typeof event.stopPropagation === 'function') event.stopPropagation();
1993
+ setInspectSelected(node);
1994
+ },
1995
+ });
1996
+ });
1997
+ var bar = h(
1998
+ 'div',
1999
+ { className: 'apkvm-inspect-bar' },
2000
+ h('span', { className: 'apkvm-inspect-label' }, T('inspect.title')),
2001
+ h(
2002
+ 'span',
2003
+ { className: 'apkvm-inspect-meta' },
2004
+ inspectBusy ? T('inspect.loading') : T('flow.count', { n: nodes.length })
2005
+ ),
2006
+ h('span', { style: { flex: '1 1 auto' } }),
2007
+ actionBtn(T('inspect.refresh'), {
2008
+ key: 'inspect-refresh',
2009
+ small: true,
2010
+ disabled: inspectBusy,
2011
+ onClick: loadInspector,
2012
+ }),
2013
+ actionBtn(T('inspect.close'), {
2014
+ key: 'inspect-close',
2015
+ small: true,
2016
+ onClick: toggleInspectMode,
2017
+ })
2018
+ );
2019
+ var selectedBar = inspectSelected
2020
+ ? h(
2021
+ 'div',
2022
+ { className: 'apkvm-inspect-bar', style: { bottom: '52px' } },
2023
+ h('span', { className: 'apkvm-inspect-label' }, inspectSelected.label || T('inspect.selected')),
2024
+ h(
2025
+ 'span',
2026
+ { className: 'apkvm-inspect-meta' },
2027
+ inspectSelected.selector ? inspectSelected.selector.selector : ''
2028
+ ),
2029
+ h('span', { style: { flex: '1 1 auto' } }),
2030
+ actionBtn(T('inspect.copyTap'), {
2031
+ key: 'inspect-copy-tap',
2032
+ small: true,
2033
+ onClick: function () {
2034
+ copyInspectCommand('tap');
2035
+ },
2036
+ }),
2037
+ actionBtn(T('inspect.copyAssert'), {
2038
+ key: 'inspect-copy-assert',
2039
+ small: true,
2040
+ onClick: function () {
2041
+ copyInspectCommand('assert');
2042
+ },
2043
+ })
2044
+ )
2045
+ : null;
2046
+ return h(
2047
+ 'div',
2048
+ { className: 'apkvm-inspect-layer' },
2049
+ boxes,
2050
+ inspectError ? h('div', { className: 'apkvm-inspect-bar', style: { bottom: '52px' } }, inspectError) : null,
2051
+ bar,
2052
+ selectedBar
2053
+ );
2054
+ }
2055
+
2056
+ function flowPath(suffix) {
2057
+ var id = activeInstanceRef.current || 'default';
2058
+ var base = id === 'default' ? '/apk-vm/flows' : '/apk-vm/instances/' + encodeURIComponent(id) + '/flows';
2059
+ return base + (suffix || '');
2060
+ }
2061
+
2062
+ function loadFlows() {
2063
+ return requestJson(flowPath(''), null, T).then(function (result) {
2064
+ if (!mountedRef.current) return result;
2065
+ if (result.ok && isObject(result.data) && Array.isArray(result.data.flows)) {
2066
+ setFlows(result.data.flows);
2067
+ setFlowRecording(result.data.recording === true);
2068
+ }
2069
+ return result;
2070
+ });
2071
+ }
2072
+
2073
+ function toggleFlowRecording() {
2074
+ var starting = !flowRecording;
2075
+ requestJson(
2076
+ flowPath(''),
2077
+ { method: 'POST', body: JSON.stringify({ action: starting ? 'start' : 'stop', name: flowName || '' }) },
2078
+ T
2079
+ ).then(function (result) {
2080
+ if (!mountedRef.current) return;
2081
+ if (!result.ok) {
2082
+ pushToast('error', T('err.action') + ': ' + result.error);
2083
+ return;
2084
+ }
2085
+ setFlowRecording(starting);
2086
+ if (!starting) {
2087
+ setFlowName('');
2088
+ loadFlows();
2089
+ pushToast('success', T('flow.saved', { name: (result.data && result.data.name) || 'flow' }));
2090
+ } else {
2091
+ pushToast('success', T('flow.recording'));
2092
+ }
2093
+ });
2094
+ }
2095
+
2096
+ function replayFlow(id) {
2097
+ if (!id || flowBusy) return;
2098
+ setFlowBusy(id);
2099
+ requestJson(flowPath(''), { method: 'POST', body: JSON.stringify({ action: 'replay', id: id }) }, T).then(function (result) {
2100
+ if (!mountedRef.current) return;
2101
+ setFlowBusy('');
2102
+ if (!result.ok) {
2103
+ pushToast('error', T('err.action') + ': ' + result.error);
2104
+ return;
2105
+ }
2106
+ pushToast('success', T('flow.done', { name: id }));
2107
+ loadState();
2108
+ });
2109
+ }
2110
+
2111
+ function deleteFlow(id) {
2112
+ if (!id || flowBusy) return;
2113
+ setFlowBusy(id);
2114
+ requestJson(flowPath(''), { method: 'POST', body: JSON.stringify({ action: 'delete', id: id }) }, T).then(function (result) {
2115
+ if (!mountedRef.current) return;
2116
+ setFlowBusy('');
2117
+ if (!result.ok) {
2118
+ pushToast('error', T('err.action') + ': ' + result.error);
2119
+ return;
2120
+ }
2121
+ loadFlows();
2122
+ });
2123
+ }
2124
+
2125
+ function applyProxy() {
2126
+ if (proxyBusy) return;
2127
+ setProxyBusy(true);
2128
+ requestJson(
2129
+ instancePath('/debug/proxy'),
2130
+ { method: 'POST', body: JSON.stringify({ host: proxy.host, port: proxy.port, enabled: true }) },
2131
+ T
2132
+ ).then(function (result) {
2133
+ if (!mountedRef.current) return;
2134
+ setProxyBusy(false);
2135
+ if (!result.ok) {
2136
+ pushToast('error', T('err.action') + ': ' + result.error);
2137
+ return;
2138
+ }
2139
+ pushToast('success', T('network.current', { value: (result.data && result.data.proxy) || 'off' }));
2140
+ });
2141
+ }
2142
+
2143
+ function clearProxy() {
2144
+ if (proxyBusy) return;
2145
+ setProxyBusy(true);
2146
+ requestJson(instancePath('/debug/proxy'), { method: 'DELETE' }, T).then(function (result) {
2147
+ if (!mountedRef.current) return;
2148
+ setProxyBusy(false);
2149
+ if (!result.ok) {
2150
+ pushToast('error', T('err.action') + ': ' + result.error);
2151
+ return;
2152
+ }
2153
+ pushToast('success', T('network.clear'));
2154
+ });
2155
+ }
2156
+
2157
+ function installCa() {
2158
+ if (proxyBusy) return;
2159
+ var certPath = String(caPath || '').trim();
2160
+ if (!certPath) {
2161
+ pushToast('error', T('network.caPath'));
2162
+ return;
2163
+ }
2164
+ setProxyBusy(true);
2165
+ requestJson(instancePath('/debug/ca'), { method: 'POST', body: JSON.stringify({ path: certPath }) }, T).then(function (result) {
2166
+ if (!mountedRef.current) return;
2167
+ setProxyBusy(false);
2168
+ if (!result.ok) {
2169
+ pushToast('error', T('err.action') + ': ' + result.error);
2170
+ return;
2171
+ }
2172
+ pushToast('success', T('network.installCa') + ': ' + (result.data && result.data.hash ? result.data.hash + '.0' : 'ok'));
2173
+ });
2174
+ }
2175
+
1794
2176
  function refreshAll() {
1795
2177
  loadState();
1796
2178
  loadApks();
@@ -2020,7 +2402,7 @@ window.__ModuleLoader__.load({
2020
2402
  function uploadApkFile(file) {
2021
2403
  if (!file) return;
2022
2404
  var name = String(file.name || 'upload.apk');
2023
- if (!/\.apk$/i.test(name)) {
2405
+ if (!/\.(?:apk|apks|xapk)$/i.test(name)) {
2024
2406
  pushToast('error', T('upload.invalid'));
2025
2407
  return;
2026
2408
  }
@@ -2363,6 +2745,7 @@ window.__ModuleLoader__.load({
2363
2745
  }
2364
2746
 
2365
2747
  function onPointerDown(event) {
2748
+ if (inspectMode) return;
2366
2749
  if (!ready || !inputScreen) return;
2367
2750
  if (event.pointerType === 'mouse' && event.button !== 0 && event.button !== undefined) return;
2368
2751
  var node = screenRef.current;
@@ -2433,6 +2816,7 @@ window.__ModuleLoader__.load({
2433
2816
  }
2434
2817
 
2435
2818
  function onPointerUp(event) {
2819
+ if (inspectMode) return;
2436
2820
  var multi = multiTouchRef.current;
2437
2821
  if (multi && multi.pointers.has(event.pointerId)) {
2438
2822
  postTouch('up', event.pointerId, event.clientX, event.clientY);
@@ -2578,6 +2962,9 @@ window.__ModuleLoader__.load({
2578
2962
  },
2579
2963
  }),
2580
2964
  h('span', { className: 'apkvm-apk-name', title: apk.path }, apk.name),
2965
+ apk.kind && apk.kind !== 'apk'
2966
+ ? h('span', { className: 'apkvm-tag' }, String(apk.kind).toUpperCase())
2967
+ : null,
2581
2968
  h('span', { className: 'apkvm-dim', style: { flex: 'none' } }, formatBytes(apk.size))
2582
2969
  ),
2583
2970
  h(
@@ -2892,7 +3279,7 @@ window.__ModuleLoader__.load({
2892
3279
  ref: fileInputRef,
2893
3280
  className: 'apkvm-fileinput',
2894
3281
  type: 'file',
2895
- accept: '.apk,application/vnd.android.package-archive',
3282
+ accept: '.apk,.apks,.xapk,application/vnd.android.package-archive,application/octet-stream',
2896
3283
  onChange: onFileInputChange,
2897
3284
  'aria-label': T('upload.button'),
2898
3285
  }),
@@ -3274,6 +3661,159 @@ window.__ModuleLoader__.load({
3274
3661
  )
3275
3662
  );
3276
3663
 
3664
+ var flowCard = h(
3665
+ 'details',
3666
+ { className: 'apkvm-card apkvm-debug' },
3667
+ h(
3668
+ 'summary',
3669
+ { className: 'apkvm-debug-summary' },
3670
+ h('span', { className: 'apkvm-card-title' }, T('flow.title')),
3671
+ h('span', { className: 'apkvm-tag' + (flowRecording ? ' ok' : '') }, flowRecording ? T('flow.recording') : T('flow.count', { n: flows.length })),
3672
+ h('span', { className: 'apkvm-chevron', 'aria-hidden': 'true' }, '▸')
3673
+ ),
3674
+ h(
3675
+ 'div',
3676
+ { className: 'apkvm-debug-body' },
3677
+ h(
3678
+ 'div',
3679
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
3680
+ h('input', {
3681
+ className: 'apkvm-input',
3682
+ type: 'text',
3683
+ value: flowName,
3684
+ placeholder: T('flow.name'),
3685
+ disabled: flowRecording,
3686
+ onChange: function (event) {
3687
+ setFlowName(event && event.target ? String(event.target.value) : '');
3688
+ },
3689
+ 'aria-label': T('flow.name'),
3690
+ }),
3691
+ actionBtn(flowRecording ? T('flow.stop') : T('flow.start'), {
3692
+ key: 'flow-record',
3693
+ small: true,
3694
+ variant: flowRecording ? 'danger' : 'primary',
3695
+ disabled: !ready,
3696
+ onClick: toggleFlowRecording,
3697
+ })
3698
+ ),
3699
+ flows.length
3700
+ ? h(
3701
+ 'div',
3702
+ { className: 'apkvm-list', style: { maxHeight: '140px' } },
3703
+ flows.slice(0, 12).map(function (flow) {
3704
+ return h(
3705
+ 'div',
3706
+ { className: 'apkvm-apk', key: flow.id },
3707
+ h('span', { className: 'apkvm-apk-name' }, flow.name || flow.id),
3708
+ h(
3709
+ 'span',
3710
+ { style: { display: 'flex', gap: '5px', alignItems: 'center' } },
3711
+ h('span', { className: 'apkvm-dim' }, T('flow.count', { n: flow.events || 0 })),
3712
+ actionBtn(T('flow.play'), {
3713
+ key: 'flow-play-' + flow.id,
3714
+ small: true,
3715
+ disabled: !ready || Boolean(flowBusy),
3716
+ onClick: function () {
3717
+ replayFlow(flow.id);
3718
+ },
3719
+ }),
3720
+ actionBtn(T('flow.delete'), {
3721
+ key: 'flow-delete-' + flow.id,
3722
+ small: true,
3723
+ variant: 'danger',
3724
+ disabled: Boolean(flowBusy),
3725
+ onClick: function () {
3726
+ deleteFlow(flow.id);
3727
+ },
3728
+ })
3729
+ )
3730
+ );
3731
+ })
3732
+ )
3733
+ : h('div', { className: 'apkvm-dim' }, T('flow.empty'))
3734
+ )
3735
+ );
3736
+
3737
+ var networkCard = h(
3738
+ 'details',
3739
+ { className: 'apkvm-card apkvm-debug' },
3740
+ h(
3741
+ 'summary',
3742
+ { className: 'apkvm-debug-summary' },
3743
+ h('span', { className: 'apkvm-card-title' }, T('network.title')),
3744
+ h('span', { className: 'apkvm-tag' }, T('network.proxy')),
3745
+ h('span', { className: 'apkvm-chevron', 'aria-hidden': 'true' }, '▸')
3746
+ ),
3747
+ h(
3748
+ 'div',
3749
+ { className: 'apkvm-debug-body' },
3750
+ h(
3751
+ 'div',
3752
+ { style: { display: 'flex', alignItems: 'center', gap: '6px' } },
3753
+ h('input', {
3754
+ className: 'apkvm-input',
3755
+ type: 'text',
3756
+ value: proxy.host,
3757
+ placeholder: T('network.host'),
3758
+ spellCheck: false,
3759
+ disabled: proxyBusy,
3760
+ onChange: function (event) {
3761
+ setProxy({ host: event && event.target ? String(event.target.value) : '', port: proxy.port });
3762
+ },
3763
+ 'aria-label': T('network.host'),
3764
+ }),
3765
+ h('input', {
3766
+ className: 'apkvm-input',
3767
+ type: 'text',
3768
+ value: proxy.port,
3769
+ placeholder: T('network.port'),
3770
+ spellCheck: false,
3771
+ disabled: proxyBusy,
3772
+ style: { maxWidth: '86px' },
3773
+ onChange: function (event) {
3774
+ setProxy({ host: proxy.host, port: event && event.target ? String(event.target.value) : '' });
3775
+ },
3776
+ 'aria-label': T('network.port'),
3777
+ })
3778
+ ),
3779
+ h('input', {
3780
+ className: 'apkvm-input',
3781
+ type: 'text',
3782
+ value: caPath,
3783
+ placeholder: T('network.caPath'),
3784
+ spellCheck: false,
3785
+ disabled: proxyBusy,
3786
+ onChange: function (event) {
3787
+ setCaPath(event && event.target ? String(event.target.value) : '');
3788
+ },
3789
+ 'aria-label': T('network.caPath'),
3790
+ }),
3791
+ h(
3792
+ 'div',
3793
+ { style: { display: 'flex', alignItems: 'center', gap: '5px', flexWrap: 'wrap' } },
3794
+ actionBtn(T('network.apply'), {
3795
+ key: 'proxy-apply',
3796
+ small: true,
3797
+ disabled: !ready || proxyBusy || !proxy.host.trim() || !proxy.port.trim(),
3798
+ onClick: applyProxy,
3799
+ }),
3800
+ actionBtn(T('network.clear'), {
3801
+ key: 'proxy-clear',
3802
+ small: true,
3803
+ disabled: !ready || proxyBusy,
3804
+ onClick: clearProxy,
3805
+ }),
3806
+ actionBtn(T('network.installCa'), {
3807
+ key: 'ca-install',
3808
+ small: true,
3809
+ disabled: !ready || proxyBusy,
3810
+ onClick: installCa,
3811
+ })
3812
+ ),
3813
+ h('div', { className: 'apkvm-dim' }, T('network.caPath'))
3814
+ )
3815
+ );
3816
+
3277
3817
  var primaryAction =
3278
3818
  ready || busyState
3279
3819
  ? actionBtn(vmBusy ? T('action.stopping') : T('action.stop'), {
@@ -3458,7 +3998,7 @@ window.__ModuleLoader__.load({
3458
3998
  : null;
3459
3999
 
3460
4000
  function onScreenKeyDown(event) {
3461
- if (!ready || !event) return;
4001
+ if (!ready || inspectMode || !event) return;
3462
4002
  var target = event.target;
3463
4003
  if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return;
3464
4004
  var keyMap = {
@@ -3520,6 +4060,7 @@ window.__ModuleLoader__.load({
3520
4060
  },
3521
4061
  })
3522
4062
  : null,
4063
+ renderInspector(),
3523
4064
  renderOverlay()
3524
4065
  );
3525
4066
 
@@ -3544,6 +4085,13 @@ window.__ModuleLoader__.load({
3544
4085
  disabled: !ready,
3545
4086
  onClick: downloadScreenshot,
3546
4087
  }),
4088
+ actionBtn(inspectMode ? T('inspect.close') : T('inspect.toggle'), {
4089
+ key: 'inspect-toggle',
4090
+ small: true,
4091
+ variant: inspectMode ? 'primary' : undefined,
4092
+ disabled: !ready || inspectBusy,
4093
+ onClick: toggleInspectMode,
4094
+ }),
3547
4095
  h(
3548
4096
  'div',
3549
4097
  { className: 'apkvm-textrow' },
@@ -3599,6 +4147,8 @@ window.__ModuleLoader__.load({
3599
4147
  apkCard,
3600
4148
  instancesCard,
3601
4149
  debugCard,
4150
+ flowCard,
4151
+ networkCard,
3602
4152
  appsCard,
3603
4153
  diagCard,
3604
4154
  h('div', { className: 'apkvm-footnote', title: T('hint.boot') }, T('hint.short')),