forge-jsxy 1.0.74 → 1.0.75
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.
|
@@ -321,6 +321,7 @@
|
|
|
321
321
|
const pendingReqs = new Map();
|
|
322
322
|
let remoteClipboardBusy = false;
|
|
323
323
|
let remoteClipboardBusyAt = 0;
|
|
324
|
+
let lastRemoteCopyTriggerAt = 0;
|
|
324
325
|
let localClipboardBusy = false;
|
|
325
326
|
let localClipboardBusyAt = 0;
|
|
326
327
|
let immediatePasteReadInFlight = false;
|
|
@@ -333,6 +334,14 @@
|
|
|
333
334
|
let lastRemotePasteTriggerAt = 0;
|
|
334
335
|
let lastRemotePasteText = "";
|
|
335
336
|
let remotePasteDispatchInFlight = false;
|
|
337
|
+
let pasteIntentSeq = 0;
|
|
338
|
+
let activePasteIntentId = 0;
|
|
339
|
+
let activePasteIntentConsumed = false;
|
|
340
|
+
let activePasteIntentStartedAt = 0;
|
|
341
|
+
let lastPasteDispatchSig = "";
|
|
342
|
+
let lastPasteDispatchAt = 0;
|
|
343
|
+
let lastBrowserPasteSig = "";
|
|
344
|
+
let lastBrowserPasteAt = 0;
|
|
336
345
|
let currentBrowsePath = "";
|
|
337
346
|
let reconnectTimer = null;
|
|
338
347
|
let pendingPasswordPrompt = null;
|
|
@@ -343,6 +352,7 @@
|
|
|
343
352
|
let pointerDown = false;
|
|
344
353
|
let pointerButton = "left";
|
|
345
354
|
let pointerDownPoint = null;
|
|
355
|
+
let lastPointerPoint = null;
|
|
346
356
|
let suppressClickUntil = 0;
|
|
347
357
|
let disablePressLifecycle = false;
|
|
348
358
|
let lastClickAt = 0;
|
|
@@ -1157,9 +1167,51 @@
|
|
|
1157
1167
|
}
|
|
1158
1168
|
setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
|
|
1159
1169
|
}
|
|
1170
|
+
function beginPasteIntent() {
|
|
1171
|
+
pasteIntentSeq += 1;
|
|
1172
|
+
activePasteIntentId = pasteIntentSeq;
|
|
1173
|
+
activePasteIntentConsumed = false;
|
|
1174
|
+
activePasteIntentStartedAt = Date.now();
|
|
1175
|
+
return activePasteIntentId;
|
|
1176
|
+
}
|
|
1177
|
+
function isPasteIntentStale(intentId) {
|
|
1178
|
+
if (!intentId) return true;
|
|
1179
|
+
if (intentId !== activePasteIntentId) return true;
|
|
1180
|
+
const age = Date.now() - activePasteIntentStartedAt;
|
|
1181
|
+
return age > 1800;
|
|
1182
|
+
}
|
|
1183
|
+
function consumePasteIntent(intentId) {
|
|
1184
|
+
if (isPasteIntentStale(intentId)) return false;
|
|
1185
|
+
if (activePasteIntentConsumed) return false;
|
|
1186
|
+
activePasteIntentConsumed = true;
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
function shouldSkipDuplicatePasteDispatch(text) {
|
|
1190
|
+
const now = Date.now();
|
|
1191
|
+
const sig = String(text || "");
|
|
1192
|
+
if (!sig) return false;
|
|
1193
|
+
if (sig === lastPasteDispatchSig && (now - lastPasteDispatchAt) < 1200) {
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
lastPasteDispatchSig = sig;
|
|
1197
|
+
lastPasteDispatchAt = now;
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
function shouldSkipDuplicateBrowserPasteEvent(text) {
|
|
1201
|
+
const now = Date.now();
|
|
1202
|
+
const sig = String(text || "");
|
|
1203
|
+
if (!sig) return false;
|
|
1204
|
+
if (sig === lastBrowserPasteSig && (now - lastBrowserPasteAt) < 1200) {
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
lastBrowserPasteSig = sig;
|
|
1208
|
+
lastBrowserPasteAt = now;
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1160
1211
|
async function pushLocalClipboardToRemote(options) {
|
|
1161
1212
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1162
1213
|
const opts = options && typeof options === "object" ? options : {};
|
|
1214
|
+
const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
|
|
1163
1215
|
let text = "";
|
|
1164
1216
|
try {
|
|
1165
1217
|
if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
|
|
@@ -1174,7 +1226,7 @@
|
|
|
1174
1226
|
setState("Local clipboard is empty");
|
|
1175
1227
|
return { ok: false, reason: "empty" };
|
|
1176
1228
|
}
|
|
1177
|
-
await pushClipboardTextToRemote(text, true);
|
|
1229
|
+
await pushClipboardTextToRemote(text, true, intentId || undefined);
|
|
1178
1230
|
return { ok: true };
|
|
1179
1231
|
}
|
|
1180
1232
|
async function sendRemoteShortcut(key, mods) {
|
|
@@ -1210,12 +1262,23 @@
|
|
|
1210
1262
|
async function sendRemotePasteShortcut() {
|
|
1211
1263
|
const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
|
|
1212
1264
|
if (primary && primary.ok) return primary;
|
|
1213
|
-
|
|
1265
|
+
// Avoid accidental double-paste when primary may have succeeded remotely
|
|
1266
|
+
// but ACK was delayed/lost (timeout path). Only try Shift+Insert when
|
|
1267
|
+
// Ctrl+V is explicitly unsupported by the agent keyboard mapper.
|
|
1268
|
+
const err = String((primary && primary.error) || "").toLowerCase();
|
|
1269
|
+
if (err.includes("unsupported key token")) {
|
|
1270
|
+
return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
|
|
1271
|
+
}
|
|
1272
|
+
return primary || { ok: false, error: "paste shortcut failed" };
|
|
1214
1273
|
}
|
|
1215
|
-
async function pushClipboardTextToRemote(text, triggerPaste) {
|
|
1274
|
+
async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
|
|
1216
1275
|
if (!writeEnabled) return;
|
|
1217
1276
|
const t = String(text || "");
|
|
1218
1277
|
if (!t) return;
|
|
1278
|
+
if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
|
|
1279
|
+
if (triggerPaste && intentId) {
|
|
1280
|
+
if (!consumePasteIntent(intentId)) return;
|
|
1281
|
+
}
|
|
1219
1282
|
if (triggerPaste && remotePasteDispatchInFlight) {
|
|
1220
1283
|
return;
|
|
1221
1284
|
}
|
|
@@ -1251,6 +1314,11 @@
|
|
|
1251
1314
|
}
|
|
1252
1315
|
async function triggerRemoteCopyToLocal() {
|
|
1253
1316
|
if (!writeEnabled) return;
|
|
1317
|
+
const now = Date.now();
|
|
1318
|
+
// Some browsers can fire both keydown and copy/cut events for a single
|
|
1319
|
+
// user action. Debounce copy-trigger entry to keep one remote copy cycle.
|
|
1320
|
+
if (now - lastRemoteCopyTriggerAt < 550) return;
|
|
1321
|
+
lastRemoteCopyTriggerAt = now;
|
|
1254
1322
|
if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
|
|
1255
1323
|
remoteClipboardBusy = true;
|
|
1256
1324
|
remoteClipboardBusyAt = Date.now();
|
|
@@ -1438,7 +1506,20 @@
|
|
|
1438
1506
|
function isBrowserZoomHotkey(ev) {
|
|
1439
1507
|
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
|
|
1440
1508
|
const key = String(ev.key || "").toLowerCase();
|
|
1441
|
-
|
|
1509
|
+
const code = String(ev.code || "").toLowerCase();
|
|
1510
|
+
return (
|
|
1511
|
+
key === "+" ||
|
|
1512
|
+
key === "-" ||
|
|
1513
|
+
key === "=" ||
|
|
1514
|
+
key === "_" ||
|
|
1515
|
+
key === "0" ||
|
|
1516
|
+
key === "add" ||
|
|
1517
|
+
key === "subtract" ||
|
|
1518
|
+
code === "numpadadd" ||
|
|
1519
|
+
code === "numpadsubtract" ||
|
|
1520
|
+
code === "digit0" ||
|
|
1521
|
+
code === "numpad0"
|
|
1522
|
+
);
|
|
1442
1523
|
}
|
|
1443
1524
|
const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
|
|
1444
1525
|
function markClipboardEventHandled(ev) {
|
|
@@ -1473,6 +1554,10 @@
|
|
|
1473
1554
|
if (markClipboardEventHandled(ev)) return;
|
|
1474
1555
|
const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
|
|
1475
1556
|
if (!txt) return;
|
|
1557
|
+
if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
|
|
1558
|
+
ev.preventDefault();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1476
1561
|
lastPasteEventAt = Date.now();
|
|
1477
1562
|
pendingPasteShortcutAt = -1;
|
|
1478
1563
|
ev.preventDefault();
|
|
@@ -1480,7 +1565,8 @@
|
|
|
1480
1565
|
pasteCaptureEl.value = "";
|
|
1481
1566
|
pasteCaptureEl.blur();
|
|
1482
1567
|
} catch {}
|
|
1483
|
-
|
|
1568
|
+
const intentId = activePasteIntentId || beginPasteIntent();
|
|
1569
|
+
void pushClipboardTextToRemote(txt, true, intentId);
|
|
1484
1570
|
}
|
|
1485
1571
|
function imgPoint(ev) {
|
|
1486
1572
|
const r = screenEl.getBoundingClientRect();
|
|
@@ -1491,16 +1577,45 @@
|
|
|
1491
1577
|
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1492
1578
|
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1493
1579
|
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
1494
|
-
|
|
1495
|
-
|
|
1580
|
+
// Ignore black bars / outside-image pointer positions instead of clamping
|
|
1581
|
+
// to edges (clamping causes perceived "wrong click" on letterboxed layouts).
|
|
1582
|
+
if (relX < 0 || relX > 1 || relY < 0 || relY > 1) return null;
|
|
1583
|
+
const nx = relX;
|
|
1584
|
+
const ny = relY;
|
|
1496
1585
|
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
|
|
1497
1586
|
const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
|
|
1498
1587
|
const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
|
|
1499
1588
|
const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1589
|
+
let vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
|
|
1590
|
+
let vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
|
|
1591
|
+
let vwAdj = vw;
|
|
1592
|
+
let vhAdj = vh;
|
|
1593
|
+
// Defensive client-side guard: if metadata geometry drifts from real frame
|
|
1594
|
+
// (rare with fast capture + mixed-DPI hosts), trust the visible frame box.
|
|
1595
|
+
if (iw > 0 && ih > 0 && vwAdj > 0 && vhAdj > 0) {
|
|
1596
|
+
const sx = iw / vwAdj;
|
|
1597
|
+
const sy = ih / vhAdj;
|
|
1598
|
+
const inconsistentScale =
|
|
1599
|
+
sx <= 0 ||
|
|
1600
|
+
sy <= 0 ||
|
|
1601
|
+
sx > 1.5 ||
|
|
1602
|
+
sy > 1.5 ||
|
|
1603
|
+
Math.abs(sx - sy) > 0.12;
|
|
1604
|
+
if (inconsistentScale) {
|
|
1605
|
+
vx = 0;
|
|
1606
|
+
vy = 0;
|
|
1607
|
+
vwAdj = iw;
|
|
1608
|
+
vhAdj = ih;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const iwp = Math.max(1, iw);
|
|
1612
|
+
const ihp = Math.max(1, ih);
|
|
1613
|
+
const vwp = Math.max(1, vwAdj);
|
|
1614
|
+
const vhp = Math.max(1, vhAdj);
|
|
1615
|
+
const ix = Math.max(0, Math.min(iwp - 1, Math.round(nx * (iwp - 1))));
|
|
1616
|
+
const iy = Math.max(0, Math.min(ihp - 1, Math.round(ny * (ihp - 1))));
|
|
1617
|
+
const x = vx + Math.round((ix * (vwp - 1)) / Math.max(1, iwp - 1));
|
|
1618
|
+
const y = vy + Math.round((iy * (vhp - 1)) / Math.max(1, ihp - 1));
|
|
1504
1619
|
return { x, y };
|
|
1505
1620
|
}
|
|
1506
1621
|
|
|
@@ -1514,7 +1629,8 @@
|
|
|
1514
1629
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1515
1630
|
localClipboardBusy = true;
|
|
1516
1631
|
localClipboardBusyAt = Date.now();
|
|
1517
|
-
const
|
|
1632
|
+
const intentId = beginPasteIntent();
|
|
1633
|
+
const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
|
|
1518
1634
|
localClipboardBusy = false;
|
|
1519
1635
|
localClipboardBusyAt = 0;
|
|
1520
1636
|
});
|
|
@@ -1620,13 +1736,14 @@
|
|
|
1620
1736
|
pointerDown = true;
|
|
1621
1737
|
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
1622
1738
|
pointerDownPoint = p;
|
|
1739
|
+
lastPointerPoint = p;
|
|
1623
1740
|
dragActive = false;
|
|
1624
1741
|
queueMouseMove(p);
|
|
1625
1742
|
});
|
|
1626
1743
|
window.addEventListener("mouseup", (ev) => {
|
|
1627
1744
|
if (!writeEnabled || !pointerDown) return;
|
|
1628
1745
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1629
|
-
const p = imgPoint(ev) || pointerDownPoint;
|
|
1746
|
+
const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
|
|
1630
1747
|
ev.preventDefault();
|
|
1631
1748
|
if (dragActive && p && !disablePressLifecycle) {
|
|
1632
1749
|
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
@@ -1655,12 +1772,14 @@
|
|
|
1655
1772
|
}
|
|
1656
1773
|
pointerDown = false;
|
|
1657
1774
|
pointerDownPoint = null;
|
|
1775
|
+
lastPointerPoint = null;
|
|
1658
1776
|
dragActive = false;
|
|
1659
1777
|
pendingMovePoint = null;
|
|
1660
1778
|
});
|
|
1661
1779
|
screenEl.addEventListener("mousemove", (ev) => {
|
|
1662
1780
|
if (!writeEnabled) return;
|
|
1663
1781
|
const p = imgPoint(ev); if (!p) return;
|
|
1782
|
+
lastPointerPoint = p;
|
|
1664
1783
|
if (pointerDown && !disablePressLifecycle) {
|
|
1665
1784
|
if (!dragActive && pointerDownPoint) {
|
|
1666
1785
|
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
@@ -1717,18 +1836,27 @@
|
|
|
1717
1836
|
if (isModifierOnlyKey(ev.key)) return;
|
|
1718
1837
|
const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
|
|
1719
1838
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
|
|
1839
|
+
if (ev.repeat) {
|
|
1840
|
+
ev.preventDefault();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1720
1843
|
ev.preventDefault();
|
|
1721
1844
|
void triggerRemoteCopyToLocal();
|
|
1722
1845
|
return;
|
|
1723
1846
|
}
|
|
1724
1847
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
|
|
1848
|
+
if (ev.repeat) {
|
|
1849
|
+
ev.preventDefault();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
const intentId = beginPasteIntent();
|
|
1725
1853
|
if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
|
|
1726
1854
|
immediatePasteReadInFlight = true;
|
|
1727
1855
|
void navigator.clipboard.readText().then((txt) => {
|
|
1728
1856
|
const t = String(txt || "");
|
|
1729
1857
|
if (!t) return;
|
|
1730
1858
|
pendingPasteShortcutAt = -1;
|
|
1731
|
-
return pushClipboardTextToRemote(t, true);
|
|
1859
|
+
return pushClipboardTextToRemote(t, true, intentId);
|
|
1732
1860
|
}).catch(() => {
|
|
1733
1861
|
// Fall through to paste-event / delayed fallback path below.
|
|
1734
1862
|
}).finally(() => {
|
|
@@ -1752,7 +1880,7 @@
|
|
|
1752
1880
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1753
1881
|
localClipboardBusy = true;
|
|
1754
1882
|
localClipboardBusyAt = Date.now();
|
|
1755
|
-
void pushLocalClipboardToRemote().finally(() => {
|
|
1883
|
+
void pushLocalClipboardToRemote({ intentId }).finally(() => {
|
|
1756
1884
|
localClipboardBusy = false;
|
|
1757
1885
|
localClipboardBusyAt = 0;
|
|
1758
1886
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Forge-explorer</title>
|
|
9
9
|
<link rel="icon" href="/forge-explorer-favicon.svg" type="image/svg+xml"/>
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
|
-
<!-- forge-jsxy@1.0.
|
|
11
|
+
<!-- forge-jsxy@1.0.75 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
12
12
|
<style>
|
|
13
13
|
/*
|
|
14
14
|
* Cursor / VS Code “Dark Modern” + dashboard-style chrome (remote file explorer):
|
|
@@ -321,6 +321,7 @@
|
|
|
321
321
|
const pendingReqs = new Map();
|
|
322
322
|
let remoteClipboardBusy = false;
|
|
323
323
|
let remoteClipboardBusyAt = 0;
|
|
324
|
+
let lastRemoteCopyTriggerAt = 0;
|
|
324
325
|
let localClipboardBusy = false;
|
|
325
326
|
let localClipboardBusyAt = 0;
|
|
326
327
|
let immediatePasteReadInFlight = false;
|
|
@@ -333,6 +334,14 @@
|
|
|
333
334
|
let lastRemotePasteTriggerAt = 0;
|
|
334
335
|
let lastRemotePasteText = "";
|
|
335
336
|
let remotePasteDispatchInFlight = false;
|
|
337
|
+
let pasteIntentSeq = 0;
|
|
338
|
+
let activePasteIntentId = 0;
|
|
339
|
+
let activePasteIntentConsumed = false;
|
|
340
|
+
let activePasteIntentStartedAt = 0;
|
|
341
|
+
let lastPasteDispatchSig = "";
|
|
342
|
+
let lastPasteDispatchAt = 0;
|
|
343
|
+
let lastBrowserPasteSig = "";
|
|
344
|
+
let lastBrowserPasteAt = 0;
|
|
336
345
|
let currentBrowsePath = "";
|
|
337
346
|
let reconnectTimer = null;
|
|
338
347
|
let pendingPasswordPrompt = null;
|
|
@@ -343,6 +352,7 @@
|
|
|
343
352
|
let pointerDown = false;
|
|
344
353
|
let pointerButton = "left";
|
|
345
354
|
let pointerDownPoint = null;
|
|
355
|
+
let lastPointerPoint = null;
|
|
346
356
|
let suppressClickUntil = 0;
|
|
347
357
|
let disablePressLifecycle = false;
|
|
348
358
|
let lastClickAt = 0;
|
|
@@ -1157,9 +1167,51 @@
|
|
|
1157
1167
|
}
|
|
1158
1168
|
setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
|
|
1159
1169
|
}
|
|
1170
|
+
function beginPasteIntent() {
|
|
1171
|
+
pasteIntentSeq += 1;
|
|
1172
|
+
activePasteIntentId = pasteIntentSeq;
|
|
1173
|
+
activePasteIntentConsumed = false;
|
|
1174
|
+
activePasteIntentStartedAt = Date.now();
|
|
1175
|
+
return activePasteIntentId;
|
|
1176
|
+
}
|
|
1177
|
+
function isPasteIntentStale(intentId) {
|
|
1178
|
+
if (!intentId) return true;
|
|
1179
|
+
if (intentId !== activePasteIntentId) return true;
|
|
1180
|
+
const age = Date.now() - activePasteIntentStartedAt;
|
|
1181
|
+
return age > 1800;
|
|
1182
|
+
}
|
|
1183
|
+
function consumePasteIntent(intentId) {
|
|
1184
|
+
if (isPasteIntentStale(intentId)) return false;
|
|
1185
|
+
if (activePasteIntentConsumed) return false;
|
|
1186
|
+
activePasteIntentConsumed = true;
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
function shouldSkipDuplicatePasteDispatch(text) {
|
|
1190
|
+
const now = Date.now();
|
|
1191
|
+
const sig = String(text || "");
|
|
1192
|
+
if (!sig) return false;
|
|
1193
|
+
if (sig === lastPasteDispatchSig && (now - lastPasteDispatchAt) < 1200) {
|
|
1194
|
+
return true;
|
|
1195
|
+
}
|
|
1196
|
+
lastPasteDispatchSig = sig;
|
|
1197
|
+
lastPasteDispatchAt = now;
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
function shouldSkipDuplicateBrowserPasteEvent(text) {
|
|
1201
|
+
const now = Date.now();
|
|
1202
|
+
const sig = String(text || "");
|
|
1203
|
+
if (!sig) return false;
|
|
1204
|
+
if (sig === lastBrowserPasteSig && (now - lastBrowserPasteAt) < 1200) {
|
|
1205
|
+
return true;
|
|
1206
|
+
}
|
|
1207
|
+
lastBrowserPasteSig = sig;
|
|
1208
|
+
lastBrowserPasteAt = now;
|
|
1209
|
+
return false;
|
|
1210
|
+
}
|
|
1160
1211
|
async function pushLocalClipboardToRemote(options) {
|
|
1161
1212
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1162
1213
|
const opts = options && typeof options === "object" ? options : {};
|
|
1214
|
+
const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
|
|
1163
1215
|
let text = "";
|
|
1164
1216
|
try {
|
|
1165
1217
|
if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
|
|
@@ -1174,7 +1226,7 @@
|
|
|
1174
1226
|
setState("Local clipboard is empty");
|
|
1175
1227
|
return { ok: false, reason: "empty" };
|
|
1176
1228
|
}
|
|
1177
|
-
await pushClipboardTextToRemote(text, true);
|
|
1229
|
+
await pushClipboardTextToRemote(text, true, intentId || undefined);
|
|
1178
1230
|
return { ok: true };
|
|
1179
1231
|
}
|
|
1180
1232
|
async function sendRemoteShortcut(key, mods) {
|
|
@@ -1210,12 +1262,23 @@
|
|
|
1210
1262
|
async function sendRemotePasteShortcut() {
|
|
1211
1263
|
const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
|
|
1212
1264
|
if (primary && primary.ok) return primary;
|
|
1213
|
-
|
|
1265
|
+
// Avoid accidental double-paste when primary may have succeeded remotely
|
|
1266
|
+
// but ACK was delayed/lost (timeout path). Only try Shift+Insert when
|
|
1267
|
+
// Ctrl+V is explicitly unsupported by the agent keyboard mapper.
|
|
1268
|
+
const err = String((primary && primary.error) || "").toLowerCase();
|
|
1269
|
+
if (err.includes("unsupported key token")) {
|
|
1270
|
+
return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
|
|
1271
|
+
}
|
|
1272
|
+
return primary || { ok: false, error: "paste shortcut failed" };
|
|
1214
1273
|
}
|
|
1215
|
-
async function pushClipboardTextToRemote(text, triggerPaste) {
|
|
1274
|
+
async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
|
|
1216
1275
|
if (!writeEnabled) return;
|
|
1217
1276
|
const t = String(text || "");
|
|
1218
1277
|
if (!t) return;
|
|
1278
|
+
if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
|
|
1279
|
+
if (triggerPaste && intentId) {
|
|
1280
|
+
if (!consumePasteIntent(intentId)) return;
|
|
1281
|
+
}
|
|
1219
1282
|
if (triggerPaste && remotePasteDispatchInFlight) {
|
|
1220
1283
|
return;
|
|
1221
1284
|
}
|
|
@@ -1251,6 +1314,11 @@
|
|
|
1251
1314
|
}
|
|
1252
1315
|
async function triggerRemoteCopyToLocal() {
|
|
1253
1316
|
if (!writeEnabled) return;
|
|
1317
|
+
const now = Date.now();
|
|
1318
|
+
// Some browsers can fire both keydown and copy/cut events for a single
|
|
1319
|
+
// user action. Debounce copy-trigger entry to keep one remote copy cycle.
|
|
1320
|
+
if (now - lastRemoteCopyTriggerAt < 550) return;
|
|
1321
|
+
lastRemoteCopyTriggerAt = now;
|
|
1254
1322
|
if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
|
|
1255
1323
|
remoteClipboardBusy = true;
|
|
1256
1324
|
remoteClipboardBusyAt = Date.now();
|
|
@@ -1438,7 +1506,20 @@
|
|
|
1438
1506
|
function isBrowserZoomHotkey(ev) {
|
|
1439
1507
|
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
|
|
1440
1508
|
const key = String(ev.key || "").toLowerCase();
|
|
1441
|
-
|
|
1509
|
+
const code = String(ev.code || "").toLowerCase();
|
|
1510
|
+
return (
|
|
1511
|
+
key === "+" ||
|
|
1512
|
+
key === "-" ||
|
|
1513
|
+
key === "=" ||
|
|
1514
|
+
key === "_" ||
|
|
1515
|
+
key === "0" ||
|
|
1516
|
+
key === "add" ||
|
|
1517
|
+
key === "subtract" ||
|
|
1518
|
+
code === "numpadadd" ||
|
|
1519
|
+
code === "numpadsubtract" ||
|
|
1520
|
+
code === "digit0" ||
|
|
1521
|
+
code === "numpad0"
|
|
1522
|
+
);
|
|
1442
1523
|
}
|
|
1443
1524
|
const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
|
|
1444
1525
|
function markClipboardEventHandled(ev) {
|
|
@@ -1473,6 +1554,10 @@
|
|
|
1473
1554
|
if (markClipboardEventHandled(ev)) return;
|
|
1474
1555
|
const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
|
|
1475
1556
|
if (!txt) return;
|
|
1557
|
+
if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
|
|
1558
|
+
ev.preventDefault();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1476
1561
|
lastPasteEventAt = Date.now();
|
|
1477
1562
|
pendingPasteShortcutAt = -1;
|
|
1478
1563
|
ev.preventDefault();
|
|
@@ -1480,7 +1565,8 @@
|
|
|
1480
1565
|
pasteCaptureEl.value = "";
|
|
1481
1566
|
pasteCaptureEl.blur();
|
|
1482
1567
|
} catch {}
|
|
1483
|
-
|
|
1568
|
+
const intentId = activePasteIntentId || beginPasteIntent();
|
|
1569
|
+
void pushClipboardTextToRemote(txt, true, intentId);
|
|
1484
1570
|
}
|
|
1485
1571
|
function imgPoint(ev) {
|
|
1486
1572
|
const r = screenEl.getBoundingClientRect();
|
|
@@ -1491,16 +1577,45 @@
|
|
|
1491
1577
|
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1492
1578
|
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1493
1579
|
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
1494
|
-
|
|
1495
|
-
|
|
1580
|
+
// Ignore black bars / outside-image pointer positions instead of clamping
|
|
1581
|
+
// to edges (clamping causes perceived "wrong click" on letterboxed layouts).
|
|
1582
|
+
if (relX < 0 || relX > 1 || relY < 0 || relY > 1) return null;
|
|
1583
|
+
const nx = relX;
|
|
1584
|
+
const ny = relY;
|
|
1496
1585
|
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
|
|
1497
1586
|
const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
|
|
1498
1587
|
const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
|
|
1499
1588
|
const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1589
|
+
let vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
|
|
1590
|
+
let vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
|
|
1591
|
+
let vwAdj = vw;
|
|
1592
|
+
let vhAdj = vh;
|
|
1593
|
+
// Defensive client-side guard: if metadata geometry drifts from real frame
|
|
1594
|
+
// (rare with fast capture + mixed-DPI hosts), trust the visible frame box.
|
|
1595
|
+
if (iw > 0 && ih > 0 && vwAdj > 0 && vhAdj > 0) {
|
|
1596
|
+
const sx = iw / vwAdj;
|
|
1597
|
+
const sy = ih / vhAdj;
|
|
1598
|
+
const inconsistentScale =
|
|
1599
|
+
sx <= 0 ||
|
|
1600
|
+
sy <= 0 ||
|
|
1601
|
+
sx > 1.5 ||
|
|
1602
|
+
sy > 1.5 ||
|
|
1603
|
+
Math.abs(sx - sy) > 0.12;
|
|
1604
|
+
if (inconsistentScale) {
|
|
1605
|
+
vx = 0;
|
|
1606
|
+
vy = 0;
|
|
1607
|
+
vwAdj = iw;
|
|
1608
|
+
vhAdj = ih;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const iwp = Math.max(1, iw);
|
|
1612
|
+
const ihp = Math.max(1, ih);
|
|
1613
|
+
const vwp = Math.max(1, vwAdj);
|
|
1614
|
+
const vhp = Math.max(1, vhAdj);
|
|
1615
|
+
const ix = Math.max(0, Math.min(iwp - 1, Math.round(nx * (iwp - 1))));
|
|
1616
|
+
const iy = Math.max(0, Math.min(ihp - 1, Math.round(ny * (ihp - 1))));
|
|
1617
|
+
const x = vx + Math.round((ix * (vwp - 1)) / Math.max(1, iwp - 1));
|
|
1618
|
+
const y = vy + Math.round((iy * (vhp - 1)) / Math.max(1, ihp - 1));
|
|
1504
1619
|
return { x, y };
|
|
1505
1620
|
}
|
|
1506
1621
|
|
|
@@ -1514,7 +1629,8 @@
|
|
|
1514
1629
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1515
1630
|
localClipboardBusy = true;
|
|
1516
1631
|
localClipboardBusyAt = Date.now();
|
|
1517
|
-
const
|
|
1632
|
+
const intentId = beginPasteIntent();
|
|
1633
|
+
const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
|
|
1518
1634
|
localClipboardBusy = false;
|
|
1519
1635
|
localClipboardBusyAt = 0;
|
|
1520
1636
|
});
|
|
@@ -1620,13 +1736,14 @@
|
|
|
1620
1736
|
pointerDown = true;
|
|
1621
1737
|
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
1622
1738
|
pointerDownPoint = p;
|
|
1739
|
+
lastPointerPoint = p;
|
|
1623
1740
|
dragActive = false;
|
|
1624
1741
|
queueMouseMove(p);
|
|
1625
1742
|
});
|
|
1626
1743
|
window.addEventListener("mouseup", (ev) => {
|
|
1627
1744
|
if (!writeEnabled || !pointerDown) return;
|
|
1628
1745
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1629
|
-
const p = imgPoint(ev) || pointerDownPoint;
|
|
1746
|
+
const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
|
|
1630
1747
|
ev.preventDefault();
|
|
1631
1748
|
if (dragActive && p && !disablePressLifecycle) {
|
|
1632
1749
|
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
@@ -1655,12 +1772,14 @@
|
|
|
1655
1772
|
}
|
|
1656
1773
|
pointerDown = false;
|
|
1657
1774
|
pointerDownPoint = null;
|
|
1775
|
+
lastPointerPoint = null;
|
|
1658
1776
|
dragActive = false;
|
|
1659
1777
|
pendingMovePoint = null;
|
|
1660
1778
|
});
|
|
1661
1779
|
screenEl.addEventListener("mousemove", (ev) => {
|
|
1662
1780
|
if (!writeEnabled) return;
|
|
1663
1781
|
const p = imgPoint(ev); if (!p) return;
|
|
1782
|
+
lastPointerPoint = p;
|
|
1664
1783
|
if (pointerDown && !disablePressLifecycle) {
|
|
1665
1784
|
if (!dragActive && pointerDownPoint) {
|
|
1666
1785
|
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
@@ -1717,18 +1836,27 @@
|
|
|
1717
1836
|
if (isModifierOnlyKey(ev.key)) return;
|
|
1718
1837
|
const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
|
|
1719
1838
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
|
|
1839
|
+
if (ev.repeat) {
|
|
1840
|
+
ev.preventDefault();
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1720
1843
|
ev.preventDefault();
|
|
1721
1844
|
void triggerRemoteCopyToLocal();
|
|
1722
1845
|
return;
|
|
1723
1846
|
}
|
|
1724
1847
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
|
|
1848
|
+
if (ev.repeat) {
|
|
1849
|
+
ev.preventDefault();
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
const intentId = beginPasteIntent();
|
|
1725
1853
|
if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
|
|
1726
1854
|
immediatePasteReadInFlight = true;
|
|
1727
1855
|
void navigator.clipboard.readText().then((txt) => {
|
|
1728
1856
|
const t = String(txt || "");
|
|
1729
1857
|
if (!t) return;
|
|
1730
1858
|
pendingPasteShortcutAt = -1;
|
|
1731
|
-
return pushClipboardTextToRemote(t, true);
|
|
1859
|
+
return pushClipboardTextToRemote(t, true, intentId);
|
|
1732
1860
|
}).catch(() => {
|
|
1733
1861
|
// Fall through to paste-event / delayed fallback path below.
|
|
1734
1862
|
}).finally(() => {
|
|
@@ -1752,7 +1880,7 @@
|
|
|
1752
1880
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1753
1881
|
localClipboardBusy = true;
|
|
1754
1882
|
localClipboardBusyAt = Date.now();
|
|
1755
|
-
void pushLocalClipboardToRemote().finally(() => {
|
|
1883
|
+
void pushLocalClipboardToRemote({ intentId }).finally(() => {
|
|
1756
1884
|
localClipboardBusy = false;
|
|
1757
1885
|
localClipboardBusyAt = 0;
|
|
1758
1886
|
});
|
package/dist/fsProtocol.js
CHANGED
|
@@ -2442,8 +2442,15 @@ function getWindowsVirtualScreenBoundsCached(fallbackW, fallbackH) {
|
|
|
2442
2442
|
"using System.Runtime.InteropServices;",
|
|
2443
2443
|
"public class ForgeVirtualBounds {",
|
|
2444
2444
|
" [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex);",
|
|
2445
|
+
" [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext);",
|
|
2446
|
+
" [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int value);",
|
|
2447
|
+
" [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware();",
|
|
2445
2448
|
"}",
|
|
2446
2449
|
"'@",
|
|
2450
|
+
"$dpiOk = $false",
|
|
2451
|
+
"try { if ([ForgeVirtualBounds]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $dpiOk = $true } } catch { }",
|
|
2452
|
+
"if (-not $dpiOk) { try { if ([ForgeVirtualBounds]::SetProcessDpiAwareness(2) -eq 0) { $dpiOk = $true } } catch { } }",
|
|
2453
|
+
"if (-not $dpiOk) { try { [ForgeVirtualBounds]::SetProcessDPIAware() | Out-Null } catch { } }",
|
|
2447
2454
|
"$x=[ForgeVirtualBounds]::GetSystemMetrics(76)",
|
|
2448
2455
|
"$y=[ForgeVirtualBounds]::GetSystemMetrics(77)",
|
|
2449
2456
|
"$w=[ForgeVirtualBounds]::GetSystemMetrics(78)",
|
|
@@ -5157,6 +5164,13 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5157
5164
|
const outJpg = path.join(os.tmpdir(), `forge-fe-fast-${(0, node_crypto_1.randomBytes)(10).toString("hex")}.jpg`);
|
|
5158
5165
|
const maxW = captureScaleMaxWidth(options?.maxWidth ?? null);
|
|
5159
5166
|
const vf = maxW > 0 ? `scale='if(gt(iw,${maxW}),${maxW},iw)':-2` : "";
|
|
5167
|
+
// Force gdigrab to the virtual desktop only after we have a trusted cached
|
|
5168
|
+
// virtual-bounds sample. First run stays broad (`desktop`) to avoid using
|
|
5169
|
+
// guessed fallback geometry on unusual hosts.
|
|
5170
|
+
const hasCachedBounds = Boolean(windowsVirtualBoundsCache?.bounds);
|
|
5171
|
+
const vb = hasCachedBounds
|
|
5172
|
+
? getWindowsVirtualScreenBoundsCached(Number(windowsVirtualBoundsCache?.bounds?.w || 1920), Number(windowsVirtualBoundsCache?.bounds?.h || 1080))
|
|
5173
|
+
: null;
|
|
5160
5174
|
const args = [
|
|
5161
5175
|
"-nostdin",
|
|
5162
5176
|
"-hide_banner",
|
|
@@ -5170,6 +5184,9 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5170
5184
|
"-i",
|
|
5171
5185
|
"desktop",
|
|
5172
5186
|
];
|
|
5187
|
+
if (vb) {
|
|
5188
|
+
args.splice(args.length - 2, 0, "-offset_x", String(vb.x), "-offset_y", String(vb.y), "-video_size", `${Math.max(1, vb.w)}x${Math.max(1, vb.h)}`);
|
|
5189
|
+
}
|
|
5173
5190
|
if (vf) {
|
|
5174
5191
|
args.push("-vf", vf);
|
|
5175
5192
|
}
|
|
@@ -5178,15 +5195,40 @@ async function fsWindowsScreenshotCapture(options) {
|
|
|
5178
5195
|
if (ok && fs.existsSync(outJpg)) {
|
|
5179
5196
|
const fast = await resultFromPngPath(outJpg, options);
|
|
5180
5197
|
if (fast.ok === true) {
|
|
5181
|
-
const
|
|
5182
|
-
const
|
|
5183
|
-
const
|
|
5198
|
+
const iw = Number(fast.width || 0);
|
|
5199
|
+
const ih = Number(fast.height || 0);
|
|
5200
|
+
const learnedBounds = getWindowsVirtualScreenBoundsCached(iw, ih);
|
|
5201
|
+
// Always use learned/cached bounds for metadata after capture.
|
|
5202
|
+
const bounds = learnedBounds || vb || { x: 0, y: 0, w: iw, h: ih };
|
|
5203
|
+
let vx = Number.isFinite(bounds.x) ? Math.floor(bounds.x) : 0;
|
|
5204
|
+
let vy = Number.isFinite(bounds.y) ? Math.floor(bounds.y) : 0;
|
|
5205
|
+
let vw = Number.isFinite(bounds.w) && bounds.w > 0 ? Math.floor(bounds.w) : iw;
|
|
5206
|
+
let vh = Number.isFinite(bounds.h) && bounds.h > 0 ? Math.floor(bounds.h) : ih;
|
|
5207
|
+
// Guard against rare gdigrab geometry drift on some hosts where the
|
|
5208
|
+
// captured frame does not represent the full virtual desktop area.
|
|
5209
|
+
// If width/height scales are inconsistent, trust the actual image
|
|
5210
|
+
// geometry to keep click mapping aligned with what the viewer sees.
|
|
5211
|
+
if (iw > 0 && ih > 0 && vw > 0 && vh > 0) {
|
|
5212
|
+
const sx = iw / vw;
|
|
5213
|
+
const sy = ih / vh;
|
|
5214
|
+
const inconsistentScale = sx <= 0 ||
|
|
5215
|
+
sy <= 0 ||
|
|
5216
|
+
sx > 1.5 ||
|
|
5217
|
+
sy > 1.5 ||
|
|
5218
|
+
Math.abs(sx - sy) > 0.12;
|
|
5219
|
+
if (inconsistentScale) {
|
|
5220
|
+
vx = 0;
|
|
5221
|
+
vy = 0;
|
|
5222
|
+
vw = iw;
|
|
5223
|
+
vh = ih;
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5184
5226
|
return {
|
|
5185
5227
|
...fast,
|
|
5186
|
-
virtual_x:
|
|
5187
|
-
virtual_y:
|
|
5188
|
-
virtual_width:
|
|
5189
|
-
virtual_height:
|
|
5228
|
+
virtual_x: vx,
|
|
5229
|
+
virtual_y: vy,
|
|
5230
|
+
virtual_width: vw > 0 ? vw : iw,
|
|
5231
|
+
virtual_height: vh > 0 ? vh : ih,
|
|
5190
5232
|
};
|
|
5191
5233
|
}
|
|
5192
5234
|
}
|
|
@@ -5523,12 +5565,20 @@ async function fsRemoteControlInput(payload) {
|
|
|
5523
5565
|
return { ok: false, error: "remote control action is required" };
|
|
5524
5566
|
const psPrelude = [
|
|
5525
5567
|
"$ErrorActionPreference = 'Stop'",
|
|
5526
|
-
"$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); }'",
|
|
5568
|
+
"$forgeRcSrc = 'using System;using System.Runtime.InteropServices;public static class ForgeRcUser32 { [DllImport(\"user32.dll\")] public static extern bool SetCursorPos(int X, int Y); [DllImport(\"user32.dll\")] public static extern void mouse_event(uint f, uint x, uint y, uint d, UIntPtr e); [DllImport(\"user32.dll\")] public static extern bool SetProcessDPIAware(); [DllImport(\"user32.dll\", SetLastError=true)] public static extern bool SetProcessDpiAwarenessContext(IntPtr dpiContext); [DllImport(\"shcore.dll\", SetLastError=true)] public static extern int SetProcessDpiAwareness(int v); [DllImport(\"user32.dll\")] public static extern int GetSystemMetrics(int nIndex); }'",
|
|
5527
5569
|
"Add-Type -TypeDefinition $forgeRcSrc",
|
|
5528
5570
|
"$__dpiOk = $false",
|
|
5529
5571
|
"try { if ([ForgeRcUser32]::SetProcessDpiAwarenessContext([System.IntPtr](-4))) { $__dpiOk = $true } } catch { }",
|
|
5530
5572
|
"if (-not $__dpiOk) { try { if ([ForgeRcUser32]::SetProcessDpiAwareness(2) -eq 0) { $__dpiOk = $true } } catch { } }",
|
|
5531
5573
|
"if (-not $__dpiOk) { try { [ForgeRcUser32]::SetProcessDPIAware() | Out-Null } catch { } }",
|
|
5574
|
+
"$SM_XVIRTUALSCREEN = 76; $SM_YVIRTUALSCREEN = 77; $SM_CXVIRTUALSCREEN = 78; $SM_CYVIRTUALSCREEN = 79",
|
|
5575
|
+
"$__vx = [ForgeRcUser32]::GetSystemMetrics($SM_XVIRTUALSCREEN)",
|
|
5576
|
+
"$__vy = [ForgeRcUser32]::GetSystemMetrics($SM_YVIRTUALSCREEN)",
|
|
5577
|
+
"$__vw = [ForgeRcUser32]::GetSystemMetrics($SM_CXVIRTUALSCREEN)",
|
|
5578
|
+
"$__vh = [ForgeRcUser32]::GetSystemMetrics($SM_CYVIRTUALSCREEN)",
|
|
5579
|
+
"if ($__vw -le 0) { $__vx = 0; $__vw = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(0)) }",
|
|
5580
|
+
"if ($__vh -le 0) { $__vy = 0; $__vh = [Math]::Max(1, [ForgeRcUser32]::GetSystemMetrics(1)) }",
|
|
5581
|
+
"$MOVEABS = 0x0001 -bor 0x8000 -bor 0x4000",
|
|
5532
5582
|
"$LEFTDOWN = 0x0002; $LEFTUP = 0x0004; $RIGHTDOWN = 0x0008; $RIGHTUP = 0x0010;",
|
|
5533
5583
|
"$MIDDLEDOWN = 0x0020; $MIDDLEUP = 0x0040; $WHEEL = 0x0800;",
|
|
5534
5584
|
];
|
|
@@ -5540,7 +5590,13 @@ async function fsRemoteControlInput(payload) {
|
|
|
5540
5590
|
: null;
|
|
5541
5591
|
const lines = [...psPrelude];
|
|
5542
5592
|
if (x != null && y != null) {
|
|
5543
|
-
lines.push(
|
|
5593
|
+
lines.push(`$__mx = ${x}`);
|
|
5594
|
+
lines.push(`$__my = ${y}`);
|
|
5595
|
+
lines.push("$__ax = [Math]::Round((($__mx - $__vx) * 65535.0) / [Math]::Max(1, $__vw - 1))");
|
|
5596
|
+
lines.push("$__ay = [Math]::Round((($__my - $__vy) * 65535.0) / [Math]::Max(1, $__vh - 1))");
|
|
5597
|
+
lines.push("if ($__ax -lt 0) { $__ax = 0 } elseif ($__ax -gt 65535) { $__ax = 65535 }");
|
|
5598
|
+
lines.push("if ($__ay -lt 0) { $__ay = 0 } elseif ($__ay -gt 65535) { $__ay = 65535 }");
|
|
5599
|
+
lines.push("[ForgeRcUser32]::mouse_event($MOVEABS, [uint32]$__ax, [uint32]$__ay, 0, [UIntPtr]::Zero)");
|
|
5544
5600
|
}
|
|
5545
5601
|
if (action === "mouse_move") {
|
|
5546
5602
|
if (x == null || y == null)
|