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/CHANGELOG.md +38 -0
- package/README.md +57 -12
- package/client.js +555 -5
- package/docs/LINUX-E2E.md +81 -0
- package/docs/README.md +15 -0
- package/docs/SUBMISSION.md +5 -2
- package/index.js +246 -10
- package/lib/android.js +219 -55
- package/lib/apk-bundle.js +165 -0
- package/lib/flows.js +198 -0
- package/lib/ui-inspector.js +174 -0
- package/lib/x509.js +107 -0
- package/package.json +8 -3
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
|
|
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')),
|