forge-jsxy 1.0.74 → 1.0.76
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.
|
@@ -218,6 +218,7 @@
|
|
|
218
218
|
<strong class="brand">Remote</strong>
|
|
219
219
|
<button id="modeBtn" class="alt">View Only</button>
|
|
220
220
|
<button id="cameraBtn" class="alt">Camera: Off</button>
|
|
221
|
+
<button id="qualityBtn" class="alt">Quality: Text</button>
|
|
221
222
|
<button id="copyFromPcBtn" class="alt">Copy <- PC</button>
|
|
222
223
|
<button id="pasteToPcBtn" class="alt">Paste -> PC</button>
|
|
223
224
|
<button id="refreshBtn" class="alt">Refresh</button>
|
|
@@ -225,7 +226,7 @@
|
|
|
225
226
|
<button id="disconnectBtn" class="warn">Disconnect</button>
|
|
226
227
|
<span class="spacer"></span>
|
|
227
228
|
<span class="state hint">Shortcuts: Ctrl/Cmd+C (PC -> Local), Ctrl/Cmd+V (Local -> PC)</span>
|
|
228
|
-
<span class="state" id="streamStats">Tier: - · Frame: - · Capture: -</span>
|
|
229
|
+
<span class="state" id="streamStats">Q: - · Tier: - · Frame: - · Capture: -</span>
|
|
229
230
|
<span class="state" id="fpsState">FPS: 0.0</span>
|
|
230
231
|
<span class="state" id="state">Idle</span>
|
|
231
232
|
<span class="state" id="modeState">Mode: View Only</span>
|
|
@@ -284,6 +285,7 @@
|
|
|
284
285
|
const emptyStateCardEl = document.getElementById("emptyStateCard");
|
|
285
286
|
const modeBtn = document.getElementById("modeBtn");
|
|
286
287
|
const cameraBtn = document.getElementById("cameraBtn");
|
|
288
|
+
const qualityBtn = document.getElementById("qualityBtn");
|
|
287
289
|
const copyFromPcBtn = document.getElementById("copyFromPcBtn");
|
|
288
290
|
const pasteToPcBtn = document.getElementById("pasteToPcBtn");
|
|
289
291
|
const wrapEl = document.getElementById("screenWrap");
|
|
@@ -321,6 +323,7 @@
|
|
|
321
323
|
const pendingReqs = new Map();
|
|
322
324
|
let remoteClipboardBusy = false;
|
|
323
325
|
let remoteClipboardBusyAt = 0;
|
|
326
|
+
let lastRemoteCopyTriggerAt = 0;
|
|
324
327
|
let localClipboardBusy = false;
|
|
325
328
|
let localClipboardBusyAt = 0;
|
|
326
329
|
let immediatePasteReadInFlight = false;
|
|
@@ -333,6 +336,14 @@
|
|
|
333
336
|
let lastRemotePasteTriggerAt = 0;
|
|
334
337
|
let lastRemotePasteText = "";
|
|
335
338
|
let remotePasteDispatchInFlight = false;
|
|
339
|
+
let pasteIntentSeq = 0;
|
|
340
|
+
let activePasteIntentId = 0;
|
|
341
|
+
let activePasteIntentConsumed = false;
|
|
342
|
+
let activePasteIntentStartedAt = 0;
|
|
343
|
+
let lastPasteDispatchSig = "";
|
|
344
|
+
let lastPasteDispatchAt = 0;
|
|
345
|
+
let lastBrowserPasteSig = "";
|
|
346
|
+
let lastBrowserPasteAt = 0;
|
|
336
347
|
let currentBrowsePath = "";
|
|
337
348
|
let reconnectTimer = null;
|
|
338
349
|
let pendingPasswordPrompt = null;
|
|
@@ -343,6 +354,7 @@
|
|
|
343
354
|
let pointerDown = false;
|
|
344
355
|
let pointerButton = "left";
|
|
345
356
|
let pointerDownPoint = null;
|
|
357
|
+
let lastPointerPoint = null;
|
|
346
358
|
let suppressClickUntil = 0;
|
|
347
359
|
let disablePressLifecycle = false;
|
|
348
360
|
let lastClickAt = 0;
|
|
@@ -361,7 +373,9 @@
|
|
|
361
373
|
let lastShotStartedAt = 0;
|
|
362
374
|
let streamFastStreak = 0;
|
|
363
375
|
let streamSlowStreak = 0;
|
|
364
|
-
let streamTier =
|
|
376
|
+
let streamTier = 0;
|
|
377
|
+
let qualityMode = "max";
|
|
378
|
+
let lastInteractionAt = 0;
|
|
365
379
|
let legacyShotMode = false;
|
|
366
380
|
let shotFailureStreak = 0;
|
|
367
381
|
let fpsFrames = 0;
|
|
@@ -372,15 +386,59 @@
|
|
|
372
386
|
let shotTimeoutTimer = null;
|
|
373
387
|
let lastFrameBytes = 0;
|
|
374
388
|
let lastCaptureMs = 0;
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
389
|
+
const STREAM_TUNING_PRESETS = {
|
|
390
|
+
balanced: [
|
|
391
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
392
|
+
{ maxBytes: 4_200_000, maxWidth: 2880 },
|
|
393
|
+
{ maxBytes: 3_600_000, maxWidth: 2560 },
|
|
394
|
+
{ maxBytes: 3_000_000, maxWidth: 2360 },
|
|
395
|
+
{ maxBytes: 2_400_000, maxWidth: 2160 },
|
|
396
|
+
{ maxBytes: 1_800_000, maxWidth: 1920 },
|
|
397
|
+
{ maxBytes: 1_250_000, maxWidth: 1680 },
|
|
398
|
+
{ maxBytes: 900_000, maxWidth: 1440 },
|
|
399
|
+
],
|
|
400
|
+
text: [
|
|
401
|
+
{ maxBytes: 5_000_000, maxWidth: 3200 },
|
|
402
|
+
{ maxBytes: 4_300_000, maxWidth: 3000 },
|
|
403
|
+
{ maxBytes: 3_700_000, maxWidth: 2800 },
|
|
404
|
+
{ maxBytes: 3_200_000, maxWidth: 2560 },
|
|
405
|
+
{ maxBytes: 2_700_000, maxWidth: 2360 },
|
|
406
|
+
],
|
|
407
|
+
max: [
|
|
408
|
+
{ maxBytes: 10_500_000, maxWidth: 0 },
|
|
409
|
+
{ maxBytes: 9_000_000, maxWidth: 0 },
|
|
410
|
+
{ maxBytes: 7_500_000, maxWidth: 3200 },
|
|
411
|
+
],
|
|
412
|
+
};
|
|
413
|
+
function currentStreamTuning() {
|
|
414
|
+
return STREAM_TUNING_PRESETS[qualityMode] || STREAM_TUNING_PRESETS.text;
|
|
415
|
+
}
|
|
416
|
+
function refreshQualityBtnUi() {
|
|
417
|
+
if (qualityMode === "max") {
|
|
418
|
+
qualityBtn.textContent = "Quality: Max";
|
|
419
|
+
qualityBtn.className = "warn";
|
|
420
|
+
} else if (qualityMode === "balanced") {
|
|
421
|
+
qualityBtn.textContent = "Quality: Balanced";
|
|
422
|
+
qualityBtn.className = "alt";
|
|
423
|
+
} else {
|
|
424
|
+
qualityBtn.textContent = "Quality: Text";
|
|
425
|
+
qualityBtn.className = "alt";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function rotateQualityMode() {
|
|
429
|
+
qualityMode = qualityMode === "balanced" ? "text" : (qualityMode === "text" ? "max" : "balanced");
|
|
430
|
+
const m = currentStreamTuning();
|
|
431
|
+
streamTier = Math.max(0, Math.min(m.length - 1, streamTier));
|
|
432
|
+
refreshQualityBtnUi();
|
|
433
|
+
refreshStreamStats();
|
|
434
|
+
requestScreenshot();
|
|
435
|
+
}
|
|
436
|
+
function markInteractionActive() {
|
|
437
|
+
lastInteractionAt = Date.now();
|
|
438
|
+
}
|
|
439
|
+
function isInteractionActive() {
|
|
440
|
+
return (Date.now() - lastInteractionAt) < 1200;
|
|
441
|
+
}
|
|
384
442
|
|
|
385
443
|
function setState(t) { stateEl.textContent = t; }
|
|
386
444
|
function refreshCameraBtnUi() {
|
|
@@ -402,29 +460,34 @@
|
|
|
402
460
|
return Math.round(n / 1024) + "KB";
|
|
403
461
|
}
|
|
404
462
|
function refreshStreamStats() {
|
|
405
|
-
const
|
|
463
|
+
const tuning = currentStreamTuning();
|
|
464
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
406
465
|
streamStatsEl.textContent =
|
|
407
|
-
"
|
|
466
|
+
"Q: " + qualityMode.toUpperCase() +
|
|
467
|
+
" · Tier: " + (streamTier + 1) + "/" + tuning.length +
|
|
408
468
|
" · Frame: " + kb(lastFrameBytes) +
|
|
409
469
|
" · Capture: " + (lastCaptureMs > 0 ? (Math.round(lastCaptureMs) + "ms") : "-") +
|
|
410
470
|
" · Cap: " + kb(prof.maxBytes);
|
|
411
471
|
}
|
|
412
472
|
function tuneRemoteStreamProfile(latencyMs, captureMs, frameBytes, targetBytes) {
|
|
473
|
+
const tuning = currentStreamTuning();
|
|
413
474
|
const ms = Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : 0;
|
|
414
475
|
const capMs = Number.isFinite(Number(captureMs)) ? Number(captureMs) : 0;
|
|
415
476
|
const fb = Number.isFinite(Number(frameBytes)) ? Number(frameBytes) : 0;
|
|
416
477
|
const tb = Number.isFinite(Number(targetBytes)) ? Number(targetBytes) : 0;
|
|
417
|
-
|
|
478
|
+
const interacting = isInteractionActive();
|
|
479
|
+
const minFps = qualityMode === "max" ? 1.0 : 3.0;
|
|
480
|
+
if (fpsCurrent > 0 && fpsCurrent < minFps) {
|
|
418
481
|
fpsLowStreak += 1;
|
|
419
482
|
fpsHighStreak = 0;
|
|
420
|
-
if (fpsLowStreak >=
|
|
483
|
+
if (fpsLowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
421
484
|
streamTier += 1;
|
|
422
485
|
fpsLowStreak = 0;
|
|
423
486
|
}
|
|
424
|
-
} else if (fpsCurrent >= 5
|
|
487
|
+
} else if (fpsCurrent >= (qualityMode === "max" ? 1.6 : 4.5)) {
|
|
425
488
|
fpsHighStreak += 1;
|
|
426
489
|
fpsLowStreak = 0;
|
|
427
|
-
if (fpsHighStreak >=
|
|
490
|
+
if (fpsHighStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
428
491
|
streamTier -= 1;
|
|
429
492
|
fpsHighStreak = 0;
|
|
430
493
|
}
|
|
@@ -433,13 +496,13 @@
|
|
|
433
496
|
fpsHighStreak = 0;
|
|
434
497
|
}
|
|
435
498
|
const overload =
|
|
436
|
-
ms >
|
|
437
|
-
capMs >
|
|
438
|
-
(tb > 0 && fb > tb * 0.
|
|
499
|
+
ms > (interacting ? 360 : 520) ||
|
|
500
|
+
capMs > (interacting ? 380 : 560) ||
|
|
501
|
+
(tb > 0 && fb > tb * (interacting ? 0.995 : 1.03));
|
|
439
502
|
if (overload) {
|
|
440
503
|
streamSlowStreak += 1;
|
|
441
504
|
streamFastStreak = 0;
|
|
442
|
-
if (streamSlowStreak >=
|
|
505
|
+
if (streamSlowStreak >= (interacting ? 3 : 5) && streamTier < tuning.length - 1) {
|
|
443
506
|
streamTier += 1;
|
|
444
507
|
streamSlowStreak = 0;
|
|
445
508
|
}
|
|
@@ -447,13 +510,13 @@
|
|
|
447
510
|
}
|
|
448
511
|
const healthy =
|
|
449
512
|
ms > 0 &&
|
|
450
|
-
ms <
|
|
451
|
-
(capMs <= 0 || capMs <
|
|
452
|
-
(tb <= 0 || fb <= tb * 0.
|
|
513
|
+
ms < (interacting ? 300 : 420) &&
|
|
514
|
+
(capMs <= 0 || capMs < (interacting ? 260 : 340)) &&
|
|
515
|
+
(tb <= 0 || fb <= tb * (interacting ? 0.93 : 0.96));
|
|
453
516
|
if (healthy) {
|
|
454
517
|
streamFastStreak += 1;
|
|
455
518
|
streamSlowStreak = 0;
|
|
456
|
-
if (streamFastStreak >=
|
|
519
|
+
if (streamFastStreak >= (interacting ? 5 : 3) && streamTier > 0) {
|
|
457
520
|
streamTier -= 1;
|
|
458
521
|
streamFastStreak = 0;
|
|
459
522
|
}
|
|
@@ -474,8 +537,19 @@
|
|
|
474
537
|
fpsLastAt = now;
|
|
475
538
|
}
|
|
476
539
|
function currentShotIntervalMs() {
|
|
477
|
-
const
|
|
478
|
-
|
|
540
|
+
const interacting = isInteractionActive();
|
|
541
|
+
const tuning = currentStreamTuning();
|
|
542
|
+
const idx = Math.max(0, Math.min(tuning.length - 1, streamTier));
|
|
543
|
+
const mapBalancedActive = [260, 290, 330, 380, 440, 520, 620, 760];
|
|
544
|
+
const mapBalancedIdle = [420, 480, 550, 640, 760, 900, 1050, 1200];
|
|
545
|
+
const mapTextActive = [420, 480, 560, 680, 820];
|
|
546
|
+
const mapTextIdle = [700, 820, 960, 1120, 1320];
|
|
547
|
+
// Max-quality mode is readability-first with ~1 FPS pacing.
|
|
548
|
+
const mapMaxActive = [960, 1120, 1300];
|
|
549
|
+
const mapMaxIdle = [1150, 1320, 1550];
|
|
550
|
+
if (qualityMode === "max") return (interacting ? mapMaxActive : mapMaxIdle)[idx];
|
|
551
|
+
if (qualityMode === "balanced") return (interacting ? mapBalancedActive : mapBalancedIdle)[idx];
|
|
552
|
+
return (interacting ? mapTextActive : mapTextIdle)[idx];
|
|
479
553
|
}
|
|
480
554
|
function clearShotTimeout() {
|
|
481
555
|
if (shotTimeoutTimer) {
|
|
@@ -485,10 +559,11 @@
|
|
|
485
559
|
}
|
|
486
560
|
function armShotTimeout() {
|
|
487
561
|
clearShotTimeout();
|
|
562
|
+
const t = qualityMode === "max" ? 6500 : 3000;
|
|
488
563
|
shotTimeoutTimer = setTimeout(() => {
|
|
489
564
|
inflightShot = false;
|
|
490
565
|
scheduleNextShot(currentShotIntervalMs() + 80);
|
|
491
|
-
},
|
|
566
|
+
}, t);
|
|
492
567
|
}
|
|
493
568
|
function parseVersion(v) {
|
|
494
569
|
return String(v || "")
|
|
@@ -906,7 +981,8 @@
|
|
|
906
981
|
if (msg.ok && msg.b64) {
|
|
907
982
|
shotFailureStreak = 0;
|
|
908
983
|
if (lastShotStartedAt > 0) {
|
|
909
|
-
const
|
|
984
|
+
const tuning = currentStreamTuning();
|
|
985
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
910
986
|
lastFrameBytes = Number.isFinite(Number(msg.bytes)) ? Math.max(0, Number(msg.bytes)) : 0;
|
|
911
987
|
lastCaptureMs = Number.isFinite(Number(msg.capture_ms)) ? Math.max(0, Number(msg.capture_ms)) : 0;
|
|
912
988
|
tuneRemoteStreamProfile(
|
|
@@ -1041,7 +1117,7 @@
|
|
|
1041
1117
|
fpsHighStreak = 0;
|
|
1042
1118
|
lastFrameBytes = 0;
|
|
1043
1119
|
lastCaptureMs = 0;
|
|
1044
|
-
streamStatsEl.textContent = "Tier: - · Frame: - · Capture: -";
|
|
1120
|
+
streamStatsEl.textContent = "Q: - · Tier: - · Frame: - · Capture: -";
|
|
1045
1121
|
fpsStateEl.textContent = "FPS: 0.0";
|
|
1046
1122
|
modeBtn.textContent = "View Only";
|
|
1047
1123
|
modeStateEl.textContent = "Mode: View Only";
|
|
@@ -1074,7 +1150,8 @@
|
|
|
1074
1150
|
if (inflightShot) return;
|
|
1075
1151
|
inflightShot = true;
|
|
1076
1152
|
lastShotStartedAt = Date.now();
|
|
1077
|
-
const
|
|
1153
|
+
const tuning = currentStreamTuning();
|
|
1154
|
+
const prof = tuning[Math.max(0, Math.min(tuning.length - 1, streamTier))];
|
|
1078
1155
|
if (!hasFrame) showEmptyState("Requesting screenshot frame...", false);
|
|
1079
1156
|
const payload = {
|
|
1080
1157
|
type: "fs_screenshot",
|
|
@@ -1157,9 +1234,51 @@
|
|
|
1157
1234
|
}
|
|
1158
1235
|
setState(String(hintText || "Press Ctrl/Cmd+V once to send local clipboard to PC"));
|
|
1159
1236
|
}
|
|
1237
|
+
function beginPasteIntent() {
|
|
1238
|
+
pasteIntentSeq += 1;
|
|
1239
|
+
activePasteIntentId = pasteIntentSeq;
|
|
1240
|
+
activePasteIntentConsumed = false;
|
|
1241
|
+
activePasteIntentStartedAt = Date.now();
|
|
1242
|
+
return activePasteIntentId;
|
|
1243
|
+
}
|
|
1244
|
+
function isPasteIntentStale(intentId) {
|
|
1245
|
+
if (!intentId) return true;
|
|
1246
|
+
if (intentId !== activePasteIntentId) return true;
|
|
1247
|
+
const age = Date.now() - activePasteIntentStartedAt;
|
|
1248
|
+
return age > 1800;
|
|
1249
|
+
}
|
|
1250
|
+
function consumePasteIntent(intentId) {
|
|
1251
|
+
if (isPasteIntentStale(intentId)) return false;
|
|
1252
|
+
if (activePasteIntentConsumed) return false;
|
|
1253
|
+
activePasteIntentConsumed = true;
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
function shouldSkipDuplicatePasteDispatch(text) {
|
|
1257
|
+
const now = Date.now();
|
|
1258
|
+
const sig = String(text || "");
|
|
1259
|
+
if (!sig) return false;
|
|
1260
|
+
if (sig === lastPasteDispatchSig && (now - lastPasteDispatchAt) < 1200) {
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
lastPasteDispatchSig = sig;
|
|
1264
|
+
lastPasteDispatchAt = now;
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
function shouldSkipDuplicateBrowserPasteEvent(text) {
|
|
1268
|
+
const now = Date.now();
|
|
1269
|
+
const sig = String(text || "");
|
|
1270
|
+
if (!sig) return false;
|
|
1271
|
+
if (sig === lastBrowserPasteSig && (now - lastBrowserPasteAt) < 1200) {
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
lastBrowserPasteSig = sig;
|
|
1275
|
+
lastBrowserPasteAt = now;
|
|
1276
|
+
return false;
|
|
1277
|
+
}
|
|
1160
1278
|
async function pushLocalClipboardToRemote(options) {
|
|
1161
1279
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1162
1280
|
const opts = options && typeof options === "object" ? options : {};
|
|
1281
|
+
const intentId = Number.isFinite(Number(opts.intentId)) ? Number(opts.intentId) : 0;
|
|
1163
1282
|
let text = "";
|
|
1164
1283
|
try {
|
|
1165
1284
|
if (!navigator.clipboard || typeof navigator.clipboard.readText !== "function") throw new Error("clipboard api unavailable");
|
|
@@ -1174,7 +1293,7 @@
|
|
|
1174
1293
|
setState("Local clipboard is empty");
|
|
1175
1294
|
return { ok: false, reason: "empty" };
|
|
1176
1295
|
}
|
|
1177
|
-
await pushClipboardTextToRemote(text, true);
|
|
1296
|
+
await pushClipboardTextToRemote(text, true, intentId || undefined);
|
|
1178
1297
|
return { ok: true };
|
|
1179
1298
|
}
|
|
1180
1299
|
async function sendRemoteShortcut(key, mods) {
|
|
@@ -1210,12 +1329,23 @@
|
|
|
1210
1329
|
async function sendRemotePasteShortcut() {
|
|
1211
1330
|
const primary = await sendRemoteShortcutWithRetry("v", { ctrl: true }, 1, 120);
|
|
1212
1331
|
if (primary && primary.ok) return primary;
|
|
1213
|
-
|
|
1332
|
+
// Avoid accidental double-paste when primary may have succeeded remotely
|
|
1333
|
+
// but ACK was delayed/lost (timeout path). Only try Shift+Insert when
|
|
1334
|
+
// Ctrl+V is explicitly unsupported by the agent keyboard mapper.
|
|
1335
|
+
const err = String((primary && primary.error) || "").toLowerCase();
|
|
1336
|
+
if (err.includes("unsupported key token")) {
|
|
1337
|
+
return sendRemoteShortcutWithRetry("insert", { shift: true }, 1, 120);
|
|
1338
|
+
}
|
|
1339
|
+
return primary || { ok: false, error: "paste shortcut failed" };
|
|
1214
1340
|
}
|
|
1215
|
-
async function pushClipboardTextToRemote(text, triggerPaste) {
|
|
1341
|
+
async function pushClipboardTextToRemote(text, triggerPaste, intentId) {
|
|
1216
1342
|
if (!writeEnabled) return;
|
|
1217
1343
|
const t = String(text || "");
|
|
1218
1344
|
if (!t) return;
|
|
1345
|
+
if (triggerPaste && shouldSkipDuplicatePasteDispatch(t)) return;
|
|
1346
|
+
if (triggerPaste && intentId) {
|
|
1347
|
+
if (!consumePasteIntent(intentId)) return;
|
|
1348
|
+
}
|
|
1219
1349
|
if (triggerPaste && remotePasteDispatchInFlight) {
|
|
1220
1350
|
return;
|
|
1221
1351
|
}
|
|
@@ -1251,6 +1381,11 @@
|
|
|
1251
1381
|
}
|
|
1252
1382
|
async function triggerRemoteCopyToLocal() {
|
|
1253
1383
|
if (!writeEnabled) return;
|
|
1384
|
+
const now = Date.now();
|
|
1385
|
+
// Some browsers can fire both keydown and copy/cut events for a single
|
|
1386
|
+
// user action. Debounce copy-trigger entry to keep one remote copy cycle.
|
|
1387
|
+
if (now - lastRemoteCopyTriggerAt < 550) return;
|
|
1388
|
+
lastRemoteCopyTriggerAt = now;
|
|
1254
1389
|
if (remoteClipboardBusy && (Date.now() - remoteClipboardBusyAt) < 2200) return;
|
|
1255
1390
|
remoteClipboardBusy = true;
|
|
1256
1391
|
remoteClipboardBusyAt = Date.now();
|
|
@@ -1438,7 +1573,20 @@
|
|
|
1438
1573
|
function isBrowserZoomHotkey(ev) {
|
|
1439
1574
|
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey) return false;
|
|
1440
1575
|
const key = String(ev.key || "").toLowerCase();
|
|
1441
|
-
|
|
1576
|
+
const code = String(ev.code || "").toLowerCase();
|
|
1577
|
+
return (
|
|
1578
|
+
key === "+" ||
|
|
1579
|
+
key === "-" ||
|
|
1580
|
+
key === "=" ||
|
|
1581
|
+
key === "_" ||
|
|
1582
|
+
key === "0" ||
|
|
1583
|
+
key === "add" ||
|
|
1584
|
+
key === "subtract" ||
|
|
1585
|
+
code === "numpadadd" ||
|
|
1586
|
+
code === "numpadsubtract" ||
|
|
1587
|
+
code === "digit0" ||
|
|
1588
|
+
code === "numpad0"
|
|
1589
|
+
);
|
|
1442
1590
|
}
|
|
1443
1591
|
const CLIPBOARD_EVENT_HANDLED_KEY = "__forgeClipboardHandled";
|
|
1444
1592
|
function markClipboardEventHandled(ev) {
|
|
@@ -1473,6 +1621,10 @@
|
|
|
1473
1621
|
if (markClipboardEventHandled(ev)) return;
|
|
1474
1622
|
const txt = String((ev.clipboardData && ev.clipboardData.getData && ev.clipboardData.getData("text/plain")) || "");
|
|
1475
1623
|
if (!txt) return;
|
|
1624
|
+
if (shouldSkipDuplicateBrowserPasteEvent(txt)) {
|
|
1625
|
+
ev.preventDefault();
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1476
1628
|
lastPasteEventAt = Date.now();
|
|
1477
1629
|
pendingPasteShortcutAt = -1;
|
|
1478
1630
|
ev.preventDefault();
|
|
@@ -1480,7 +1632,8 @@
|
|
|
1480
1632
|
pasteCaptureEl.value = "";
|
|
1481
1633
|
pasteCaptureEl.blur();
|
|
1482
1634
|
} catch {}
|
|
1483
|
-
|
|
1635
|
+
const intentId = activePasteIntentId || beginPasteIntent();
|
|
1636
|
+
void pushClipboardTextToRemote(txt, true, intentId);
|
|
1484
1637
|
}
|
|
1485
1638
|
function imgPoint(ev) {
|
|
1486
1639
|
const r = screenEl.getBoundingClientRect();
|
|
@@ -1491,30 +1644,62 @@
|
|
|
1491
1644
|
// Using rect coordinates directly keeps mapping stable across browser zoom in/out.
|
|
1492
1645
|
const relX = (ev.clientX - r.left) / Math.max(1, r.width);
|
|
1493
1646
|
const relY = (ev.clientY - r.top) / Math.max(1, r.height);
|
|
1494
|
-
|
|
1495
|
-
|
|
1647
|
+
// Ignore black bars / outside-image pointer positions instead of clamping
|
|
1648
|
+
// to edges (clamping causes perceived "wrong click" on letterboxed layouts).
|
|
1649
|
+
if (relX < 0 || relX > 1 || relY < 0 || relY > 1) return null;
|
|
1650
|
+
const nx = relX;
|
|
1651
|
+
const ny = relY;
|
|
1496
1652
|
const iw = Number(lastFrameMeta && lastFrameMeta.imageWidth) || naturalW;
|
|
1497
1653
|
const ih = Number(lastFrameMeta && lastFrameMeta.imageHeight) || naturalH;
|
|
1498
1654
|
const vw = Number(lastFrameMeta && lastFrameMeta.virtualWidth) || iw;
|
|
1499
1655
|
const vh = Number(lastFrameMeta && lastFrameMeta.virtualHeight) || ih;
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1656
|
+
let vx = Number(lastFrameMeta && lastFrameMeta.virtualX) || 0;
|
|
1657
|
+
let vy = Number(lastFrameMeta && lastFrameMeta.virtualY) || 0;
|
|
1658
|
+
let vwAdj = vw;
|
|
1659
|
+
let vhAdj = vh;
|
|
1660
|
+
// Defensive client-side guard: if metadata geometry drifts from real frame
|
|
1661
|
+
// (rare with fast capture + mixed-DPI hosts), trust the visible frame box.
|
|
1662
|
+
if (iw > 0 && ih > 0 && vwAdj > 0 && vhAdj > 0) {
|
|
1663
|
+
const sx = iw / vwAdj;
|
|
1664
|
+
const sy = ih / vhAdj;
|
|
1665
|
+
const inconsistentScale =
|
|
1666
|
+
sx <= 0 ||
|
|
1667
|
+
sy <= 0 ||
|
|
1668
|
+
sx > 1.5 ||
|
|
1669
|
+
sy > 1.5 ||
|
|
1670
|
+
Math.abs(sx - sy) > 0.12;
|
|
1671
|
+
if (inconsistentScale) {
|
|
1672
|
+
vx = 0;
|
|
1673
|
+
vy = 0;
|
|
1674
|
+
vwAdj = iw;
|
|
1675
|
+
vhAdj = ih;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
const iwp = Math.max(1, iw);
|
|
1679
|
+
const ihp = Math.max(1, ih);
|
|
1680
|
+
const vwp = Math.max(1, vwAdj);
|
|
1681
|
+
const vhp = Math.max(1, vhAdj);
|
|
1682
|
+
const ix = Math.max(0, Math.min(iwp - 1, Math.round(nx * (iwp - 1))));
|
|
1683
|
+
const iy = Math.max(0, Math.min(ihp - 1, Math.round(ny * (ihp - 1))));
|
|
1684
|
+
const x = vx + Math.round((ix * (vwp - 1)) / Math.max(1, iwp - 1));
|
|
1685
|
+
const y = vy + Math.round((iy * (vhp - 1)) / Math.max(1, ihp - 1));
|
|
1504
1686
|
return { x, y };
|
|
1505
1687
|
}
|
|
1506
1688
|
|
|
1507
1689
|
document.getElementById("disconnectBtn").addEventListener("click", disconnect);
|
|
1508
1690
|
copyFromPcBtn.addEventListener("click", async () => {
|
|
1509
1691
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1692
|
+
markInteractionActive();
|
|
1510
1693
|
void triggerRemoteCopyToLocal();
|
|
1511
1694
|
});
|
|
1512
1695
|
pasteToPcBtn.addEventListener("click", async () => {
|
|
1513
1696
|
if (!writeEnabled) { setState("Enable Write Only mode for clipboard"); return; }
|
|
1697
|
+
markInteractionActive();
|
|
1514
1698
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1515
1699
|
localClipboardBusy = true;
|
|
1516
1700
|
localClipboardBusyAt = Date.now();
|
|
1517
|
-
const
|
|
1701
|
+
const intentId = beginPasteIntent();
|
|
1702
|
+
const r = await pushLocalClipboardToRemote({ silentReadFailure: true, intentId }).finally(() => {
|
|
1518
1703
|
localClipboardBusy = false;
|
|
1519
1704
|
localClipboardBusyAt = 0;
|
|
1520
1705
|
});
|
|
@@ -1554,6 +1739,9 @@
|
|
|
1554
1739
|
refreshCameraBtnUi();
|
|
1555
1740
|
requestScreenshot();
|
|
1556
1741
|
});
|
|
1742
|
+
qualityBtn.addEventListener("click", () => {
|
|
1743
|
+
rotateQualityMode();
|
|
1744
|
+
});
|
|
1557
1745
|
filePullBtn.addEventListener("click", async () => {
|
|
1558
1746
|
if (!writeEnabled) { setState("Enable Write Only mode for file fetch"); return; }
|
|
1559
1747
|
const p = String(filePullPath.value || "").trim();
|
|
@@ -1617,22 +1805,26 @@
|
|
|
1617
1805
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1618
1806
|
const p = imgPoint(ev); if (!p) return;
|
|
1619
1807
|
ev.preventDefault();
|
|
1808
|
+
markInteractionActive();
|
|
1620
1809
|
pointerDown = true;
|
|
1621
1810
|
pointerButton = ev.button === 2 ? "right" : (ev.button === 1 ? "middle" : "left");
|
|
1622
1811
|
pointerDownPoint = p;
|
|
1812
|
+
lastPointerPoint = p;
|
|
1623
1813
|
dragActive = false;
|
|
1624
1814
|
queueMouseMove(p);
|
|
1625
1815
|
});
|
|
1626
1816
|
window.addEventListener("mouseup", (ev) => {
|
|
1627
1817
|
if (!writeEnabled || !pointerDown) return;
|
|
1628
1818
|
if (ev.button !== 0 && ev.button !== 1 && ev.button !== 2) return;
|
|
1629
|
-
const p = imgPoint(ev) || pointerDownPoint;
|
|
1819
|
+
const p = imgPoint(ev) || lastPointerPoint || pointerDownPoint;
|
|
1630
1820
|
ev.preventDefault();
|
|
1631
1821
|
if (dragActive && p && !disablePressLifecycle) {
|
|
1822
|
+
markInteractionActive();
|
|
1632
1823
|
sendRemoteInput({ action: "mouse_up", button: pointerButton, x: p.x, y: p.y });
|
|
1633
1824
|
suppressClickUntil = Date.now() + 220;
|
|
1634
1825
|
requestScreenshot();
|
|
1635
1826
|
} else if (p && Date.now() >= suppressClickUntil) {
|
|
1827
|
+
markInteractionActive();
|
|
1636
1828
|
const now = Date.now();
|
|
1637
1829
|
let clickCount = 1;
|
|
1638
1830
|
if (
|
|
@@ -1655,12 +1847,14 @@
|
|
|
1655
1847
|
}
|
|
1656
1848
|
pointerDown = false;
|
|
1657
1849
|
pointerDownPoint = null;
|
|
1850
|
+
lastPointerPoint = null;
|
|
1658
1851
|
dragActive = false;
|
|
1659
1852
|
pendingMovePoint = null;
|
|
1660
1853
|
});
|
|
1661
1854
|
screenEl.addEventListener("mousemove", (ev) => {
|
|
1662
1855
|
if (!writeEnabled) return;
|
|
1663
1856
|
const p = imgPoint(ev); if (!p) return;
|
|
1857
|
+
lastPointerPoint = p;
|
|
1664
1858
|
if (pointerDown && !disablePressLifecycle) {
|
|
1665
1859
|
if (!dragActive && pointerDownPoint) {
|
|
1666
1860
|
const dx = Math.abs(p.x - pointerDownPoint.x);
|
|
@@ -1671,6 +1865,7 @@
|
|
|
1671
1865
|
}
|
|
1672
1866
|
}
|
|
1673
1867
|
if (dragActive) {
|
|
1868
|
+
markInteractionActive();
|
|
1674
1869
|
ev.preventDefault();
|
|
1675
1870
|
queueMouseMove(p);
|
|
1676
1871
|
return;
|
|
@@ -1695,6 +1890,7 @@
|
|
|
1695
1890
|
wrapEl.addEventListener("wheel", (ev) => {
|
|
1696
1891
|
if (ev.ctrlKey || ev.metaKey) return; // Keep browser zoom (ctrl/cmd + wheel) working.
|
|
1697
1892
|
if (!writeEnabled) return;
|
|
1893
|
+
markInteractionActive();
|
|
1698
1894
|
ev.preventDefault();
|
|
1699
1895
|
const p = imgPoint(ev);
|
|
1700
1896
|
let dy = Number(ev.deltaY || 0);
|
|
@@ -1717,18 +1913,29 @@
|
|
|
1717
1913
|
if (isModifierOnlyKey(ev.key)) return;
|
|
1718
1914
|
const ctrlOrMeta = ev.ctrlKey || ev.metaKey;
|
|
1719
1915
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "c") {
|
|
1916
|
+
if (ev.repeat) {
|
|
1917
|
+
ev.preventDefault();
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1720
1920
|
ev.preventDefault();
|
|
1921
|
+
markInteractionActive();
|
|
1721
1922
|
void triggerRemoteCopyToLocal();
|
|
1722
1923
|
return;
|
|
1723
1924
|
}
|
|
1724
1925
|
if (ctrlOrMeta && !ev.shiftKey && !ev.altKey && String(ev.key || "").toLowerCase() === "v") {
|
|
1926
|
+
if (ev.repeat) {
|
|
1927
|
+
ev.preventDefault();
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const intentId = beginPasteIntent();
|
|
1931
|
+
markInteractionActive();
|
|
1725
1932
|
if (!immediatePasteReadInFlight && navigator.clipboard && typeof navigator.clipboard.readText === "function") {
|
|
1726
1933
|
immediatePasteReadInFlight = true;
|
|
1727
1934
|
void navigator.clipboard.readText().then((txt) => {
|
|
1728
1935
|
const t = String(txt || "");
|
|
1729
1936
|
if (!t) return;
|
|
1730
1937
|
pendingPasteShortcutAt = -1;
|
|
1731
|
-
return pushClipboardTextToRemote(t, true);
|
|
1938
|
+
return pushClipboardTextToRemote(t, true, intentId);
|
|
1732
1939
|
}).catch(() => {
|
|
1733
1940
|
// Fall through to paste-event / delayed fallback path below.
|
|
1734
1941
|
}).finally(() => {
|
|
@@ -1752,7 +1959,7 @@
|
|
|
1752
1959
|
if (localClipboardBusy && (Date.now() - localClipboardBusyAt) < 1600) return;
|
|
1753
1960
|
localClipboardBusy = true;
|
|
1754
1961
|
localClipboardBusyAt = Date.now();
|
|
1755
|
-
void pushLocalClipboardToRemote().finally(() => {
|
|
1962
|
+
void pushLocalClipboardToRemote({ intentId }).finally(() => {
|
|
1756
1963
|
localClipboardBusy = false;
|
|
1757
1964
|
localClipboardBusyAt = 0;
|
|
1758
1965
|
});
|
|
@@ -1761,6 +1968,7 @@
|
|
|
1761
1968
|
}
|
|
1762
1969
|
if (isTypingTarget(document.activeElement)) return;
|
|
1763
1970
|
ev.preventDefault();
|
|
1971
|
+
markInteractionActive();
|
|
1764
1972
|
sendRemoteInput({ action: "key", key: ev.key, ctrl: ev.ctrlKey, alt: ev.altKey, shift: ev.shiftKey, meta: ev.metaKey });
|
|
1765
1973
|
});
|
|
1766
1974
|
window.addEventListener("copy", onClipboardCopyOrCut);
|
|
@@ -1780,6 +1988,7 @@
|
|
|
1780
1988
|
|
|
1781
1989
|
refreshWriteModeEligibilityUi();
|
|
1782
1990
|
refreshCameraBtnUi();
|
|
1991
|
+
refreshQualityBtnUi();
|
|
1783
1992
|
refreshStreamStats();
|
|
1784
1993
|
updateWriteControls();
|
|
1785
1994
|
connect();
|
|
@@ -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.76 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):
|