float range for js objects, scan progress, ui improvements
Mspeedhack.js
| @@ -536,15 +536,27 @@ | |||
|---|---|---|---|
| 536 | 536 | if (scanHandledQ.length > 400) scanHandled.delete(scanHandledQ.shift()); | |
| 537 | 537 | const replyWin = e.source; | |
| 538 | 538 | const scmd = m.cmd || m; // command payload is nested under `cmd` (avoids type-field collision) | |
| 539 | - | runScanCommand(scmd).then(function (res) { | |
| 539 | + | let lastFrac = -2; // throttle determinate progress to ~2% steps (indeterminate always relayed) | |
| 540 | + | runScanCommand(scmd, function (frac) { | |
| 541 | + | if (frac >= 0 && frac !== 1 && frac - lastFrac < 0.02) return; | |
| 542 | + | lastFrac = frac; | |
| 543 | + | try { if (replyWin) postTo(replyWin, { type: 'scan-progress', reqId: m.reqId, targetFrame: m.from, frac: frac }); } catch (e2) {} | |
| 544 | + | }).then(function (res) { | |
| 540 | 545 | res.type = 'scan-result'; res.reqId = m.reqId; res.targetFrame = m.from; | |
| 541 | 546 | try { if (replyWin) postTo(replyWin, res); } catch (e2) {} // reply to the sender directly... | |
| 542 | 547 | broadcastScanMsg(res); // ...and via window.top (reliable upward) | |
| 543 | 548 | }); | |
| 544 | 549 | break; | |
| 545 | 550 | } | |
| 551 | + | case 'scan-progress': { // controller: forward incremental progress to the UI | |
| 552 | + | if (m.targetFrame && m.targetFrame !== SELF_ID) break; | |
| 553 | + | const cb = scanProgress.get(m.reqId); | |
| 554 | + | if (cb) cb(m.frac); | |
| 555 | + | break; | |
| 556 | + | } | |
| 546 | 557 | case 'scan-result': { // controller: resolve the matching pending request | |
| 547 | 558 | if (m.targetFrame && m.targetFrame !== SELF_ID) break; | |
| 559 | + | scanProgress.delete(m.reqId); | |
| 548 | 560 | const resolve = scanPending.get(m.reqId); | |
| 549 | 561 | if (resolve) { scanPending.delete(m.reqId); resolve(m); } | |
| 550 | 562 | break; | |
| @@ -781,6 +793,16 @@ | |||
|---|---|---|---|
| 781 | 793 | if (base < 0) { const lo = base - p, hi = base; return { value: v, match: function (x) { return x > lo && x <= hi; } }; } | |
| 782 | 794 | const lo = base, hi = base + p; return { value: v, match: function (x) { return x >= lo && x < hi; } }; | |
| 783 | 795 | } | |
| 796 | + | // Union matcher for a type group ('all'/'allint'/'allfloat'): matches if ANY expanded | |
| 797 | + | // type's exact matcher does. Object-graph values are all f64, so this widens a whole | |
| 798 | + | // number like "15" to the float window [15,16) (matching 15.5) the way the WASM backend | |
| 799 | + | // already does per-type — otherwise a group would collapse to typeList[0]'s int matcher. | |
| 800 | + | function makeGroupMatcher(typeList, raw) { | |
| 801 | + | if (raw == null) return null; | |
| 802 | + | const ms = typeList.map(function (t) { return makeExactMatcher(t, raw); }).filter(Boolean); | |
| 803 | + | if (!ms.length) return null; | |
| 804 | + | return { match: function (x) { for (let i = 0; i < ms.length; i++) if (ms[i].match(x)) return true; return false; } }; | |
| 805 | + | } | |
| 784 | 806 | // refine criteria: 'exact' uses the typed-precision matcher; the rest compare a | |
| 785 | 807 | // fresh read `cur` against the stored previous value `prev`. | |
| 786 | 808 | function passesCriteria(type, criteria, cur, prev, matcher) { | |
| @@ -848,6 +870,7 @@ | |||
|---|---|---|---|
| 848 | 870 | if (accept(t, cur, prev)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ off: off, val: cur, ty: t }); else capped = true; } | |
| 849 | 871 | } | |
| 850 | 872 | if (off + sz > cap) { ti++; off = 0; } | |
| 873 | + | if (job.onProgress) job.onProgress(Math.min(1, (ti + (cap > 0 ? Math.min(1, off / cap) : 1)) / types.length)); | |
| 851 | 874 | return ti < types.length; | |
| 852 | 875 | }, function (cancelled) { | |
| 853 | 876 | if (cancelled) { done({ cancelled: true }); return; } | |
| @@ -893,6 +916,7 @@ | |||
|---|---|---|---|
| 893 | 916 | let cur; try { cur = SCAN_TYPES[c.ty].get(d, c.off); } catch (e) { continue; } | |
| 894 | 917 | if (passesCriteria(c.ty, criteria, cur, c.val, mfor(c.ty))) kept.push({ off: c.off, val: cur, ty: c.ty }); | |
| 895 | 918 | } | |
| 919 | + | if (job.onProgress) job.onProgress(src.length ? i / src.length : 1); | |
| 896 | 920 | return i < src.length; | |
| 897 | 921 | }, function (cancelled) { | |
| 898 | 922 | if (cancelled) { done({ cancelled: true }); return; } | |
| @@ -938,7 +962,8 @@ | |||
|---|---|---|---|
| 938 | 962 | * ------------------------------------------------------------------- */ | |
| 939 | 963 | const objectScan = (function () { | |
| 940 | 964 | const NODE_CAP = 200000, DEPTH_CAP = 12, BUDGET = 15000; // nodes per event-loop slice | |
| 941 | - | let type = 'f64'; | |
| 965 | + | let type = 'f64'; // display / default-write type (typeList[0]); values are all f64 | |
| 966 | + | let types = ['f64']; // full expanded type list of the current scan (for group matchers) | |
| 942 | 967 | let candidates = null; // [{ path:[...], val }] | |
| 943 | 968 | let snapshot = null; // Map(pathKey -> { path, val }) for "unknown initial value" | |
| 944 | 969 | ||
| @@ -973,6 +998,7 @@ | |||
|---|---|---|---|
| 973 | 998 | } | |
| 974 | 999 | } | |
| 975 | 1000 | } | |
| 1001 | + | if (job.onProgress) job.onProgress(-1); // total is unknown up front → indeterminate | |
| 976 | 1002 | return stack.length > 0 && nodes < NODE_CAP; | |
| 977 | 1003 | }, done); | |
| 978 | 1004 | } | |
| @@ -983,8 +1009,8 @@ | |||
|---|---|---|---|
| 983 | 1009 | // All JS numbers are f64, so the width only tunes equality (exact int vs. float | |
| 984 | 1010 | // cell); a multi-type selection just uses the first type's matcher here. | |
| 985 | 1011 | function firstExact(typeList, raw, job, done) { | |
| 986 | - | type = typeList[0] || 'f64'; snapshot = null; candidates = null; | |
| 987 | - | const matcher = makeExactMatcher(type, raw); | |
| 1012 | + | types = typeList.slice(); type = typeList[0] || 'f64'; snapshot = null; candidates = null; | |
| 1013 | + | const matcher = makeGroupMatcher(types, raw); | |
| 988 | 1014 | if (!matcher) { done({ error: 'bad value' }); return; } | |
| 989 | 1015 | const out = []; let count = 0, capped = false; | |
| 990 | 1016 | walkAsync(function (path, v) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: path, val: v }); else capped = true; }, | |
| @@ -994,7 +1020,7 @@ | |||
|---|---|---|---|
| 994 | 1020 | }); | |
| 995 | 1021 | } | |
| 996 | 1022 | function firstUnknown(typeList, job, done) { | |
| 997 | - | type = typeList[0] || 'f64'; candidates = null; | |
| 1023 | + | types = typeList.slice(); type = typeList[0] || 'f64'; candidates = null; | |
| 998 | 1024 | const snap = new Map(); | |
| 999 | 1025 | walkAsync(function (path, v) { snap.set(pathKey(path), { path: path, val: v }); }, null, job, function (cancelled) { | |
| 1000 | 1026 | if (cancelled) { done({ cancelled: true }); return; } | |
| @@ -1003,7 +1029,7 @@ | |||
|---|---|---|---|
| 1003 | 1029 | } | |
| 1004 | 1030 | function materialize(criteria, raw, job, done) { | |
| 1005 | 1031 | if (!snapshot) { done({ error: 'no snapshot' }); return; } | |
| 1006 | - | const matcher = (raw != null) ? makeExactMatcher(type, raw) : null; | |
| 1032 | + | const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null; | |
| 1007 | 1033 | const entries = Array.from(snapshot.values()); | |
| 1008 | 1034 | const out = []; let count = 0, capped = false, i = 0; | |
| 1009 | 1035 | chunkLoop(job, function () { | |
| @@ -1014,6 +1040,7 @@ | |||
|---|---|---|---|
| 1014 | 1040 | if (passesCriteria(type, criteria, cur, entry.val, matcher)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: entry.path, val: cur }); else capped = true; } | |
| 1015 | 1041 | entry.val = cur; // re-baseline | |
| 1016 | 1042 | } | |
| 1043 | + | if (job.onProgress) job.onProgress(entries.length ? i / entries.length : 1); | |
| 1017 | 1044 | return i < entries.length; | |
| 1018 | 1045 | }, function (cancelled) { | |
| 1019 | 1046 | if (cancelled) { done({ cancelled: true }); return; } | |
| @@ -1023,7 +1050,7 @@ | |||
|---|---|---|---|
| 1023 | 1050 | function refine(criteria, raw, job, done) { | |
| 1024 | 1051 | if (candidates === null && snapshot !== null) { materialize(criteria, raw, job, done); return; } | |
| 1025 | 1052 | if (candidates === null) { done({ error: 'no scan in progress' }); return; } | |
| 1026 | - | const matcher = (raw != null) ? makeExactMatcher(type, raw) : null; | |
| 1053 | + | const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null; | |
| 1027 | 1054 | const kept = []; | |
| 1028 | 1055 | for (let i = 0; i < candidates.length; i++) { | |
| 1029 | 1056 | const c = candidates[i]; const cur = resolve(c.path); | |
| @@ -1072,7 +1099,7 @@ | |||
|---|---|---|---|
| 1072 | 1099 | // cancelled — there are no timeouts anywhere; callers wait until done or cancel. | |
| 1073 | 1100 | const SCAN_ROW_LIMIT = 200; // most rows we ship/render at once | |
| 1074 | 1101 | let scanJob = null; | |
| 1075 | - | function runScanCommand(cmd) { | |
| 1102 | + | function runScanCommand(cmd, onProgress) { | |
| 1076 | 1103 | return new Promise(function (resolve) { | |
| 1077 | 1104 | try { | |
| 1078 | 1105 | const eng = scanEngine(cmd.engine); | |
| @@ -1093,7 +1120,7 @@ | |||
|---|---|---|---|
| 1093 | 1120 | } | |
| 1094 | 1121 | case 'first-exact': case 'first-unknown': case 'refine': { | |
| 1095 | 1122 | if (scanJob) scanJob.cancelled = true; // supersede any prior scan | |
| 1096 | - | const job = { cancelled: false }; scanJob = job; | |
| 1123 | + | const job = { cancelled: false, onProgress: onProgress || null }; scanJob = job; | |
| 1097 | 1124 | const done = function (r) { | |
| 1098 | 1125 | if (scanJob === job) scanJob = null; | |
| 1099 | 1126 | if (!r || r.error) { resolve({ ok: false, error: (r && r.error) || 'scan failed' }); return; } | |
| @@ -1123,13 +1150,15 @@ | |||
|---|---|---|---|
| 1123 | 1150 | * ------------------------------------------------------------------ */ | |
| 1124 | 1151 | let scanSeq = 1; | |
| 1125 | 1152 | const scanPending = new Map(); // reqId -> resolve (controller side) | |
| 1153 | + | const scanProgress = new Map(); // reqId -> onProgress(frac) (controller side, remote scans) | |
| 1126 | 1154 | const scanHandled = new Set(); // "from:reqId" of cmds already run (target side, dedup) | |
| 1127 | 1155 | const scanHandledQ = []; // FIFO to bound scanHandled | |
| 1128 | - | function sendScan(targetId, targetWin, cmd) { | |
| 1129 | - | if (!targetId) return runScanCommand(cmd); // null → this frame (local engine, no messaging) | |
| 1156 | + | function sendScan(targetId, targetWin, cmd, onProgress) { | |
| 1157 | + | if (!targetId) return runScanCommand(cmd, onProgress); // null → this frame (local engine, no messaging) | |
| 1130 | 1158 | return new Promise(function (resolve) { | |
| 1131 | 1159 | const reqId = scanSeq++; | |
| 1132 | 1160 | scanPending.set(reqId, resolve); | |
| 1161 | + | if (onProgress) scanProgress.set(reqId, onProgress); | |
| 1133 | 1162 | // Nest the command under `cmd` rather than flattening it into the envelope: | |
| 1134 | 1163 | // the scan value-type field is also called `type` and would otherwise overwrite | |
| 1135 | 1164 | // the envelope's `type: 'scan-cmd'`, so the target saw an unknown message type | |
| @@ -1225,6 +1254,15 @@ | |||
|---|---|---|---|
| 1225 | 1254 | .sc-cancel:hover { background: #4a2e1d; color: #ffcf9a; } | |
| 1226 | 1255 | .sc-cancel[hidden], #scMemDetect[hidden] { display: none; } | |
| 1227 | 1256 | #scRefine[hidden] { display: none; } | |
| 1257 | + | .sc-mode { display: flex; gap: 14px; flex-wrap: wrap; } | |
| 1258 | + | .sc-mode .tg { flex: 0 0 auto; } | |
| 1259 | + | .sc-mode input:disabled + span { opacity: .45; } | |
| 1260 | + | #scValue:disabled { opacity: .45; } | |
| 1261 | + | .sc-progress { height: 4px; background: #14161a; border-radius: 3px; overflow: hidden; } | |
| 1262 | + | .sc-progress[hidden] { display: none; } | |
| 1263 | + | .sc-bar { height: 100%; width: 0; background: #2f6df6; transition: width .12s linear; } | |
| 1264 | + | .sc-bar.indet { width: 40%; animation: shx-indet 1s linear infinite; } | |
| 1265 | + | @keyframes shx-indet { 0% { margin-left: -40%; } 100% { margin-left: 100%; } } | |
| 1228 | 1266 | .sc-count { color: #aeb2bb; font-size: 10.5px; } | |
| 1229 | 1267 | .sc-results { display: flex; flex-direction: column; gap: 4px; max-height: 180px; overflow: auto; } | |
| 1230 | 1268 | .sc-results:empty { display: none; } | |
| @@ -1328,6 +1366,10 @@ | |||
|---|---|---|---|
| 1328 | 1366 | </div> | |
| 1329 | 1367 | <select id="scMem" class="sc-sel" hidden></select> | |
| 1330 | 1368 | <button class="clk-btn sc-pause" id="scMemDetect" hidden>↻ Detect WASM memory</button> | |
| 1369 | + | <div class="sc-mode" id="scMode"> | |
| 1370 | + | <label class="tg"><input type="radio" name="shxScMode" value="exact" checked><span>Known value</span></label> | |
| 1371 | + | <label class="tg" title="Snapshot all values now, then narrow by how they change (Up/Down/Changed) — use when you don't know the value."><input type="radio" name="shxScMode" value="unknown"><span>Unknown initial value</span></label> | |
| 1372 | + | </div> | |
| 1331 | 1373 | <div class="sc-rowx"> | |
| 1332 | 1374 | <select id="scType" class="sc-sel sc-type"> | |
| 1333 | 1375 | <option value="i32" title="32-bit signed integer. The most common type — scores, counts, HP, currency in many games.">int32</option> | |
| @@ -1347,7 +1389,6 @@ | |||
|---|---|---|---|
| 1347 | 1389 | </div> | |
| 1348 | 1390 | <div class="sc-btns"> | |
| 1349 | 1391 | <button class="clk-btn" id="scFirst">First scan</button> | |
| 1350 | - | <button class="clk-btn" id="scUnknown" title="Snapshot all values now, then narrow by how they change (Up/Down/Changed) — use when you don't know the value.">Unknown initial</button> | |
| 1351 | 1392 | <button class="clk-btn sc-cancel" id="scCancel" hidden>Cancel</button> | |
| 1352 | 1393 | </div> | |
| 1353 | 1394 | <div class="sc-btns" id="scRefine" hidden> | |
| @@ -1357,6 +1398,7 @@ | |||
|---|---|---|---|
| 1357 | 1398 | <button class="clk-btn" data-crit="increased">▲ Up</button> | |
| 1358 | 1399 | <button class="clk-btn" data-crit="decreased">▼ Down</button> | |
| 1359 | 1400 | </div> | |
| 1401 | + | <div id="scProgress" class="sc-progress" hidden><div id="scBar" class="sc-bar"></div></div> | |
| 1360 | 1402 | <div id="scCount" class="sc-count">No scan yet.</div> | |
| 1361 | 1403 | <div id="scResults" class="sc-results"></div> | |
| 1362 | 1404 | <div class="sc-saved"> | |
| @@ -1575,8 +1617,14 @@ | |||
|---|---|---|---|
| 1575 | 1617 | /* ----- scan controls ----- */ | |
| 1576 | 1618 | const scTarget = $('#scTarget'), scPause = $('#scPause'), scStatus = $('#scStatus'); | |
| 1577 | 1619 | const scMem = $('#scMem'), scMemDetect = $('#scMemDetect'), scType = $('#scType'), scValue = $('#scValue'); | |
| 1578 | - | const scFirst = $('#scFirst'), scUnknown = $('#scUnknown'), scCancel = $('#scCancel'); | |
| 1620 | + | const scFirst = $('#scFirst'), scCancel = $('#scCancel'); | |
| 1621 | + | const scProgress = $('#scProgress'), scBar = $('#scBar'); | |
| 1622 | + | const scModeInputs = root.querySelectorAll('#scMode input'); | |
| 1579 | 1623 | const scRefine = $('#scRefine'), scCount = $('#scCount'), scResults = $('#scResults'), scSaved = $('#scSaved'); | |
| 1624 | + | function getScMode() { | |
| 1625 | + | for (let i = 0; i < scModeInputs.length; i++) if (scModeInputs[i].checked) return scModeInputs[i].value; | |
| 1626 | + | return 'exact'; | |
| 1627 | + | } | |
| 1580 | 1628 | const scEngineBtns = root.querySelectorAll('#scEngine button'); | |
| 1581 | 1629 | const scRefineBtns = root.querySelectorAll('#scRefine button'); | |
| 1582 | 1630 | ||
| @@ -1589,10 +1637,10 @@ | |||
|---|---|---|---|
| 1589 | 1637 | let scanRunning = false; // a scan op is in flight | |
| 1590 | 1638 | let pausedLocalView = false; | |
| 1591 | 1639 | ||
| 1592 | - | function scCmd(extra) { | |
| 1640 | + | function scCmd(extra, onProgress) { | |
| 1593 | 1641 | const cmd = { engine: scEngineName, type: scType.value }; | |
| 1594 | 1642 | for (const k in extra) cmd[k] = extra[k]; | |
| 1595 | - | return sendScan(scTargetId, scTargetWin, cmd); | |
| 1643 | + | return sendScan(scTargetId, scTargetWin, cmd, onProgress); | |
| 1596 | 1644 | } | |
| 1597 | 1645 | ||
| 1598 | 1646 | // Two independent status lines so they never clobber each other: | |
| @@ -1629,12 +1677,16 @@ | |||
|---|---|---|---|
| 1629 | 1677 | function updateButtons() { | |
| 1630 | 1678 | scFirst.textContent = scanActive ? 'New scan' : 'First scan'; | |
| 1631 | 1679 | scFirst.disabled = scanRunning; | |
| 1632 | - | scUnknown.hidden = scanActive; | |
| 1633 | - | scUnknown.disabled = scanRunning; | |
| 1680 | + | // Scan mode (Known value / Unknown initial) is locked once a scan exists — it only | |
| 1681 | + | // applies to starting a NEW scan. Start a new scan to change it. | |
| 1682 | + | scModeInputs.forEach(function (r) { r.disabled = scanRunning || scanActive; }); | |
| 1634 | 1683 | scCancel.hidden = !scanRunning; | |
| 1635 | 1684 | scRefine.hidden = !scanActive; | |
| 1636 | 1685 | scRefineBtns.forEach(function (b) { b.disabled = scanRunning || !scanActive; }); | |
| 1637 | - | scType.disabled = scanRunning; scValue.disabled = scanRunning; | |
| 1686 | + | scType.disabled = scanRunning; | |
| 1687 | + | // The value box stays editable during refine (so "Exact" refine can take a new value); | |
| 1688 | + | // it's disabled only while a scan runs, or for a fresh "Unknown initial" scan (no value). | |
| 1689 | + | scValue.disabled = scanRunning || (!scanActive && getScMode() === 'unknown'); | |
| 1638 | 1690 | scMemDetect.disabled = scanRunning; | |
| 1639 | 1691 | } | |
| 1640 | 1692 | ||
| @@ -1694,6 +1746,7 @@ | |||
|---|---|---|---|
| 1694 | 1746 | scanActive = false; | |
| 1695 | 1747 | scResults.textContent = ''; | |
| 1696 | 1748 | setScan('No scan yet.'); | |
| 1749 | + | showProgress(false); | |
| 1697 | 1750 | updateButtons(); | |
| 1698 | 1751 | scCmd({ op: 'reset' }); | |
| 1699 | 1752 | } | |
| @@ -1720,7 +1773,20 @@ | |||
|---|---|---|---|
| 1720 | 1773 | if (scanRunning) return; | |
| 1721 | 1774 | setScanRunning(true); | |
| 1722 | 1775 | setScan(progress); | |
| 1723 | - | scCmd(cmd).then(onScanDone); | |
| 1776 | + | showProgress(true); | |
| 1777 | + | scCmd(cmd, onScanProgress).then(function (res) { showProgress(false); onScanDone(res); }); | |
| 1778 | + | } | |
| 1779 | + | // Progress bar: starts indeterminate (animated stripe); flips to a determinate fill the | |
| 1780 | + | // first time the engine reports a real fraction (frac >= 0). frac < 0 stays indeterminate | |
| 1781 | + | // (the object-graph walk, whose total isn't known up front). | |
| 1782 | + | function showProgress(on) { | |
| 1783 | + | scProgress.hidden = !on; | |
| 1784 | + | if (on) { scBar.classList.add('indet'); scBar.style.width = '0%'; } | |
| 1785 | + | } | |
| 1786 | + | function onScanProgress(frac) { | |
| 1787 | + | if (typeof frac !== 'number' || frac < 0) return; | |
| 1788 | + | scBar.classList.remove('indet'); | |
| 1789 | + | scBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + '%'; | |
| 1724 | 1790 | } | |
| 1725 | 1791 | ||
| 1726 | 1792 | function renderRows(rows) { | |
| @@ -1798,13 +1864,14 @@ | |||
|---|---|---|---|
| 1798 | 1864 | scMem.addEventListener('change', function () { scCmd({ op: 'set-mem', mem: +scMem.value }).then(newScan); }); | |
| 1799 | 1865 | scMemDetect.addEventListener('click', function () { refreshMemList(); }); | |
| 1800 | 1866 | scType.addEventListener('change', function () { const o = scType.options[scType.selectedIndex]; scType.title = o ? o.title : ''; }); | |
| 1867 | + | scModeInputs.forEach(function (r) { r.addEventListener('change', updateButtons); }); | |
| 1801 | 1868 | scFirst.addEventListener('click', function () { | |
| 1802 | 1869 | if (scanActive) { newScan(); return; } // acts as "New scan" once a scan exists | |
| 1870 | + | if (getScMode() === 'unknown') { startScan({ op: 'first-unknown' }, 'Snapshotting…'); return; } | |
| 1803 | 1871 | const raw = scValue.value.trim(); | |
| 1804 | - | if (raw === '') { setScan('Enter a value, or use “Unknown initial”.'); return; } | |
| 1872 | + | if (raw === '') { setScan('Enter a value, or choose “Unknown initial value”.'); return; } | |
| 1805 | 1873 | startScan({ op: 'first-exact', value: raw }, 'Scanning…'); | |
| 1806 | 1874 | }); | |
| 1807 | - | scUnknown.addEventListener('click', function () { startScan({ op: 'first-unknown' }, 'Snapshotting…'); }); | |
| 1808 | 1875 | scCancel.addEventListener('click', function () { setScan('Cancelling…'); scCmd({ op: 'cancel' }); }); | |
| 1809 | 1876 | scRefineBtns.forEach(function (b) { | |
| 1810 | 1877 | b.addEventListener('click', function () { | |