qapture2 0.7.9 → 0.8.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/README.md +44 -12
- package/dist/{chunk-6UNSUNJS.cjs → chunk-E7OFH6EM.cjs} +233 -205
- package/dist/{chunk-G6O4SIEH.js → chunk-RJV6FTIB.js} +233 -205
- package/dist/index.cjs +5 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +3 -2
|
@@ -51,10 +51,10 @@ function openDB(dbName) {
|
|
|
51
51
|
dbCache.set(dbName, promise);
|
|
52
52
|
return promise;
|
|
53
53
|
}
|
|
54
|
-
function run(dbName, store,
|
|
54
|
+
function run(dbName, store, mode, fn) {
|
|
55
55
|
return openDB(dbName).then(
|
|
56
56
|
(db) => new Promise((resolve, reject) => {
|
|
57
|
-
const tx = db.transaction(store,
|
|
57
|
+
const tx = db.transaction(store, mode);
|
|
58
58
|
const s = tx.objectStore(store);
|
|
59
59
|
let result;
|
|
60
60
|
const req = fn(s);
|
|
@@ -509,6 +509,7 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
|
|
|
509
509
|
/* Z-index scale */
|
|
510
510
|
--qa-z-fab: 9990;
|
|
511
511
|
--qa-z-panel: 9995;
|
|
512
|
+
--qa-z-capture-still: 10089;
|
|
512
513
|
--qa-z-capture-dim: 10090;
|
|
513
514
|
--qa-z-capture-highlight: 10092;
|
|
514
515
|
--qa-z-capture-region-move: 10093;
|
|
@@ -558,6 +559,8 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
|
|
|
558
559
|
.qa-z-1 { z-index: 1; }
|
|
559
560
|
.qa-z-50 { z-index: 50; }
|
|
560
561
|
.qa-z-100 { z-index: 100; }
|
|
562
|
+
/* the frozen still, directly beneath the dim so the scrim tints it too */
|
|
563
|
+
.qa-z-10089 { z-index: var(--qa-z-capture-still); }
|
|
561
564
|
.qa-z-10090 { z-index: var(--qa-z-capture-dim); }
|
|
562
565
|
.qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
|
|
563
566
|
/* region-handle layering */
|
|
@@ -1776,7 +1779,7 @@ var STR = {
|
|
|
1776
1779
|
no_evidence: "ticked, no capture",
|
|
1777
1780
|
expected_label: "Expected",
|
|
1778
1781
|
exact_label: "Pixel-exact shots",
|
|
1779
|
-
exact_hint: "Screenshots become a real photo instead of a redraw.
|
|
1782
|
+
exact_hint: "Screenshots become a real photo instead of a redraw. Each capture asks to share this tab (or, in Safari and Firefox, this window), takes one frame, and hands the screen straight back \u2014 nothing keeps recording, and nothing leaves your device.",
|
|
1780
1783
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1781
1784
|
redraw_action: "Use real screenshots",
|
|
1782
1785
|
redraw_no_engine: "This browser cannot photograph the screen at all, so charts and canvases may come out wrong. Open the app on a desktop browser for true screenshots.",
|
|
@@ -1785,7 +1788,7 @@ var STR = {
|
|
|
1785
1788
|
exact_off: "Back to redrawn screenshots",
|
|
1786
1789
|
exact_declined: "Staying on redrawn screenshots",
|
|
1787
1790
|
exact_turn_on: "Turn on",
|
|
1788
|
-
exact_unsupported: "Needs
|
|
1791
|
+
exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
|
|
1789
1792
|
sync_title: "Save to a folder",
|
|
1790
1793
|
sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
|
|
1791
1794
|
sync_choose: "Choose folder",
|
|
@@ -1984,7 +1987,7 @@ var STR = {
|
|
|
1984
1987
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1985
1988
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1986
1989
|
exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
|
|
1987
|
-
exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \u0628\u062F\u0644 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0633\u0645. \
|
|
1990
|
+
exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \u0628\u062F\u0644 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0633\u0645. \u062A\u0637\u0644\u0628 \u0643\u0644 \u0644\u0642\u0637\u0629 \u0625\u0630\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u062A\u0628\u0648\u064A\u0628 (\u0623\u0648 \u0627\u0644\u0646\u0627\u0641\u0630\u0629 \u0641\u064A Safari \u0648Firefox)\u060C \u062A\u0623\u062E\u0630 \u0625\u0637\u0627\u0631\u064B\u0627 \u0648\u0627\u062D\u062F\u064B\u0627\u060C \u062B\u0645 \u062A\u0639\u064A\u062F \u0627\u0644\u0634\u0627\u0634\u0629 \u0641\u0648\u0631\u064B\u0627 \u2014 \u0644\u0627 \u062A\u0633\u062C\u064A\u0644 \u0645\u0633\u062A\u0645\u0631\u060C \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
|
|
1988
1991
|
redraw_notice: "\u0647\u0630\u0647 \u0625\u0639\u0627\u062F\u0629 \u0631\u0633\u0645 \u0644\u0644\u0635\u0641\u062D\u0629\u060C \u0648\u0644\u064A\u0633\u062A \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629.",
|
|
1989
1992
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1990
1993
|
redraw_no_engine: "\u0644\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u062A\u0635\u0648\u064A\u0631 \u0627\u0644\u0634\u0627\u0634\u0629 \u0625\u0637\u0644\u0627\u0642\u064B\u0627\u060C \u0644\u0630\u0627 \u0642\u062F \u062A\u0638\u0647\u0631 \u0627\u0644\u0645\u062E\u0637\u0637\u0627\u062A \u0648\u0627\u0644\u0644\u0648\u062D\u0627\u062A \u0628\u0634\u0643\u0644 \u062E\u0627\u0637\u0626. \u0627\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0639\u0644\u0649 \u0645\u062A\u0635\u0641\u062D \u0633\u0637\u062D \u0645\u0643\u062A\u0628 \u0644\u0644\u062D\u0635\u0648\u0644 \u0639\u0644\u0649 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629.",
|
|
@@ -1993,7 +1996,7 @@ var STR = {
|
|
|
1993
1996
|
exact_off: "\u0627\u0644\u0639\u0648\u062F\u0629 \u0625\u0644\u0649 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627",
|
|
1994
1997
|
exact_declined: "\u0633\u0646\u0628\u0642\u0649 \u0639\u0644\u0649 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627",
|
|
1995
1998
|
exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
|
|
1996
|
-
exact_unsupported: "\u064A\u062A\u0637\u0644\u0628
|
|
1999
|
+
exact_unsupported: "\u064A\u062A\u0637\u0644\u0628 \u0645\u062A\u0635\u0641\u062D \u0633\u0637\u062D \u0645\u0643\u062A\u0628 \u2014 \u0627\u0644\u0647\u0648\u0627\u062A\u0641 \u0644\u0627 \u062A\u0633\u062A\u0637\u064A\u0639 \u062A\u0635\u0648\u064A\u0631 \u0627\u0644\u0634\u0627\u0634\u0629",
|
|
1997
2000
|
sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
|
|
1998
2001
|
sync_hint: "\u062A\u064F\u0643\u062A\u0628 \u0643\u0644 \u0645\u0644\u0627\u062D\u0638\u0629 \u0639\u0644\u0649 \u0627\u0644\u0642\u0631\u0635 \u0644\u062D\u0638\u0629 \u062D\u0641\u0638\u0647\u0627 \u2014 \u0644\u0627 \u0634\u064A\u0621 \u064A\u0636\u064A\u0639 \u0625\u0630\u0627 \u062A\u0639\u0637\u0651\u0644 \u0627\u0644\u0645\u062A\u0635\u0641\u062D.",
|
|
1999
2002
|
sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
|
|
@@ -2298,7 +2301,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2298
2301
|
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2299
2302
|
const originX = tl.x - centres.tl.x * scaleX;
|
|
2300
2303
|
const originY = tl.y - centres.tl.y * scaleY;
|
|
2301
|
-
const
|
|
2304
|
+
const mapping = { scaleX, scaleY, originX, originY };
|
|
2302
2305
|
const farX = originX + vw * scaleX;
|
|
2303
2306
|
const farY = originY + vh * scaleY;
|
|
2304
2307
|
const slack = Math.max(4, frameW * 0.01);
|
|
@@ -2317,7 +2320,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2317
2320
|
verified++;
|
|
2318
2321
|
}
|
|
2319
2322
|
if (verified === 0) return null;
|
|
2320
|
-
return
|
|
2323
|
+
return mapping;
|
|
2321
2324
|
}
|
|
2322
2325
|
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2323
2326
|
const x0 = m.originX + rect.left * m.scaleX;
|
|
@@ -2329,73 +2332,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
|
|
|
2329
2332
|
if (sw < 1 || sh < 1) return null;
|
|
2330
2333
|
return { sx, sy, sw, sh };
|
|
2331
2334
|
}
|
|
2332
|
-
function environmentSignature() {
|
|
2333
|
-
if (typeof window === "undefined") return "";
|
|
2334
|
-
return [
|
|
2335
|
-
window.innerWidth,
|
|
2336
|
-
window.innerHeight,
|
|
2337
|
-
window.outerWidth,
|
|
2338
|
-
window.outerHeight,
|
|
2339
|
-
window.screenX,
|
|
2340
|
-
window.screenY,
|
|
2341
|
-
Math.round((window.devicePixelRatio || 1) * 100)
|
|
2342
|
-
].join("x");
|
|
2343
|
-
}
|
|
2344
2335
|
|
|
2345
2336
|
// src/lib/screenCapture.ts
|
|
2346
2337
|
var ASPECT_TOLERANCE = 0.08;
|
|
2347
2338
|
var CALIBRATION_SETTLE_MS = 220;
|
|
2348
2339
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2349
2340
|
var VIDEO_READY_TIMEOUT_MS = 4e3;
|
|
2350
|
-
var stream = null;
|
|
2351
|
-
var track = null;
|
|
2352
|
-
var video = null;
|
|
2353
|
-
var grabber = null;
|
|
2354
|
-
var declined = false;
|
|
2355
2341
|
function isExactCaptureSupported() {
|
|
2356
2342
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2357
2343
|
const md = navigator.mediaDevices;
|
|
2358
2344
|
return !!md && typeof md.getDisplayMedia === "function";
|
|
2359
2345
|
}
|
|
2360
|
-
var
|
|
2361
|
-
var
|
|
2362
|
-
var
|
|
2346
|
+
var armed = false;
|
|
2347
|
+
var declined = false;
|
|
2348
|
+
var frozen = null;
|
|
2349
|
+
var lastMode = null;
|
|
2350
|
+
function armExactCapture() {
|
|
2351
|
+
if (!isExactCaptureSupported()) return false;
|
|
2352
|
+
armed = true;
|
|
2353
|
+
declined = false;
|
|
2354
|
+
return true;
|
|
2355
|
+
}
|
|
2356
|
+
function disarmExactCapture() {
|
|
2357
|
+
armed = false;
|
|
2358
|
+
releaseFrozenFrame();
|
|
2359
|
+
}
|
|
2360
|
+
function resetExactCaptureDecline() {
|
|
2361
|
+
declined = false;
|
|
2362
|
+
}
|
|
2363
2363
|
function getExactCaptureStatus() {
|
|
2364
2364
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2365
|
-
if (
|
|
2365
|
+
if (armed) return "live";
|
|
2366
2366
|
if (declined) return "declined";
|
|
2367
2367
|
return "idle";
|
|
2368
2368
|
}
|
|
2369
|
-
function
|
|
2370
|
-
|
|
2371
|
-
try {
|
|
2372
|
-
video.pause();
|
|
2373
|
-
} catch {
|
|
2374
|
-
}
|
|
2375
|
-
video.srcObject = null;
|
|
2376
|
-
video.remove();
|
|
2377
|
-
}
|
|
2378
|
-
if (stream) {
|
|
2379
|
-
for (const t of stream.getTracks()) {
|
|
2380
|
-
try {
|
|
2381
|
-
t.stop();
|
|
2382
|
-
} catch {
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
video = null;
|
|
2387
|
-
stream = null;
|
|
2388
|
-
track = null;
|
|
2389
|
-
grabber = null;
|
|
2390
|
-
mode = "tab";
|
|
2391
|
-
mapping = null;
|
|
2392
|
-
mappingSignature = "";
|
|
2393
|
-
}
|
|
2394
|
-
function stopExactCapture() {
|
|
2395
|
-
teardown();
|
|
2369
|
+
function getFrozenFrame() {
|
|
2370
|
+
return frozen;
|
|
2396
2371
|
}
|
|
2397
|
-
function
|
|
2398
|
-
|
|
2372
|
+
function releaseFrozenFrame() {
|
|
2373
|
+
if (frozen) {
|
|
2374
|
+
frozen.canvas.width = 0;
|
|
2375
|
+
frozen.canvas.height = 0;
|
|
2376
|
+
}
|
|
2377
|
+
frozen = null;
|
|
2399
2378
|
}
|
|
2400
2379
|
function waitFor(test, timeoutMs) {
|
|
2401
2380
|
if (test()) return Promise.resolve(true);
|
|
@@ -2435,17 +2414,28 @@ function nextPresentedFrame(el) {
|
|
|
2435
2414
|
});
|
|
2436
2415
|
});
|
|
2437
2416
|
}
|
|
2438
|
-
function
|
|
2417
|
+
function looksLikeViewport(frameW, frameH) {
|
|
2439
2418
|
if (!frameW || !frameH) return false;
|
|
2440
2419
|
const vw = window.innerWidth;
|
|
2441
2420
|
const vh = window.innerHeight;
|
|
2442
2421
|
if (!vw || !vh) return false;
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2422
|
+
return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
|
|
2423
|
+
}
|
|
2424
|
+
async function withOverlayHidden(fn) {
|
|
2425
|
+
const hosts = Array.from(
|
|
2426
|
+
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2427
|
+
);
|
|
2428
|
+
const previous = hosts.map((el) => el.style.visibility);
|
|
2429
|
+
for (const el of hosts) el.style.visibility = "hidden";
|
|
2430
|
+
try {
|
|
2431
|
+
return await fn();
|
|
2432
|
+
} finally {
|
|
2433
|
+
hosts.forEach((el, i) => {
|
|
2434
|
+
el.style.visibility = previous[i];
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2446
2437
|
}
|
|
2447
|
-
async function grabFrameCanvas() {
|
|
2448
|
-
if (!video) return null;
|
|
2438
|
+
async function grabFrameCanvas(video, grabber) {
|
|
2449
2439
|
await nextPresentedFrame(video);
|
|
2450
2440
|
let source = video;
|
|
2451
2441
|
let w = video.videoWidth;
|
|
@@ -2476,11 +2466,10 @@ async function grabFrameCanvas() {
|
|
|
2476
2466
|
bitmap?.close?.();
|
|
2477
2467
|
return c;
|
|
2478
2468
|
}
|
|
2479
|
-
async function calibrate() {
|
|
2480
|
-
if (!video) return false;
|
|
2469
|
+
async function calibrate(video, grabber) {
|
|
2481
2470
|
const vw = window.innerWidth;
|
|
2482
2471
|
const vh = window.innerHeight;
|
|
2483
|
-
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return
|
|
2472
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
|
|
2484
2473
|
const card = document.createElement("div");
|
|
2485
2474
|
card.setAttribute("data-qa-calibration", "true");
|
|
2486
2475
|
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
@@ -2494,10 +2483,10 @@ async function calibrate() {
|
|
|
2494
2483
|
document.body.appendChild(card);
|
|
2495
2484
|
try {
|
|
2496
2485
|
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2497
|
-
const frame = await grabFrameCanvas();
|
|
2498
|
-
if (!frame) return
|
|
2486
|
+
const frame = await grabFrameCanvas(video, grabber);
|
|
2487
|
+
if (!frame) return null;
|
|
2499
2488
|
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2500
|
-
if (!ctx) return
|
|
2489
|
+
if (!ctx) return null;
|
|
2501
2490
|
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2502
2491
|
const found = deriveMapping(
|
|
2503
2492
|
findMarkerCandidates(data, frame.width, frame.height),
|
|
@@ -2506,20 +2495,40 @@ async function calibrate() {
|
|
|
2506
2495
|
frame.width,
|
|
2507
2496
|
frame.height
|
|
2508
2497
|
);
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
return true;
|
|
2498
|
+
frame.width = 0;
|
|
2499
|
+
frame.height = 0;
|
|
2500
|
+
return found;
|
|
2513
2501
|
} catch {
|
|
2514
|
-
return
|
|
2502
|
+
return null;
|
|
2515
2503
|
} finally {
|
|
2516
2504
|
card.remove();
|
|
2517
2505
|
}
|
|
2518
2506
|
}
|
|
2519
|
-
async function
|
|
2520
|
-
if (!isExactCaptureSupported()) return
|
|
2521
|
-
|
|
2522
|
-
|
|
2507
|
+
async function freezeViewport() {
|
|
2508
|
+
if (!isExactCaptureSupported()) return null;
|
|
2509
|
+
releaseFrozenFrame();
|
|
2510
|
+
let stream = null;
|
|
2511
|
+
let el = null;
|
|
2512
|
+
const release = () => {
|
|
2513
|
+
if (el) {
|
|
2514
|
+
try {
|
|
2515
|
+
el.pause();
|
|
2516
|
+
} catch {
|
|
2517
|
+
}
|
|
2518
|
+
el.srcObject = null;
|
|
2519
|
+
el.remove();
|
|
2520
|
+
el = null;
|
|
2521
|
+
}
|
|
2522
|
+
if (stream) {
|
|
2523
|
+
for (const t of stream.getTracks()) {
|
|
2524
|
+
try {
|
|
2525
|
+
t.stop();
|
|
2526
|
+
} catch {
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
stream = null;
|
|
2530
|
+
}
|
|
2531
|
+
};
|
|
2523
2532
|
try {
|
|
2524
2533
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2525
2534
|
const constraints = {
|
|
@@ -2537,121 +2546,103 @@ async function startExactCapture() {
|
|
|
2537
2546
|
surfaceSwitching: "exclude",
|
|
2538
2547
|
systemAudio: "exclude"
|
|
2539
2548
|
};
|
|
2540
|
-
|
|
2541
|
-
const
|
|
2542
|
-
if (!
|
|
2543
|
-
for (const x of s.getTracks()) x.stop();
|
|
2549
|
+
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
2550
|
+
const track = stream.getVideoTracks()[0];
|
|
2551
|
+
if (!track) {
|
|
2544
2552
|
declined = true;
|
|
2545
|
-
return
|
|
2553
|
+
return null;
|
|
2546
2554
|
}
|
|
2547
|
-
const settings =
|
|
2555
|
+
const settings = track.getSettings();
|
|
2548
2556
|
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2549
|
-
|
|
2557
|
+
el = document.createElement("video");
|
|
2550
2558
|
el.setAttribute("data-qa-overlay", "true");
|
|
2551
2559
|
el.muted = true;
|
|
2552
2560
|
el.playsInline = true;
|
|
2553
2561
|
el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
2554
|
-
el.srcObject =
|
|
2562
|
+
el.srcObject = stream;
|
|
2555
2563
|
document.body.appendChild(el);
|
|
2556
2564
|
try {
|
|
2557
2565
|
await el.play();
|
|
2558
2566
|
} catch {
|
|
2559
2567
|
}
|
|
2560
|
-
const
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2568
|
+
const video = el;
|
|
2569
|
+
const ready = await waitFor(
|
|
2570
|
+
() => video.videoWidth > 0 && video.videoHeight > 0,
|
|
2571
|
+
VIDEO_READY_TIMEOUT_MS
|
|
2572
|
+
);
|
|
2573
|
+
if (!ready) {
|
|
2564
2574
|
declined = true;
|
|
2565
|
-
return
|
|
2566
|
-
}
|
|
2567
|
-
if (!ready) return giveUp();
|
|
2568
|
-
const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
|
|
2569
|
-
stream = s;
|
|
2570
|
-
track = t;
|
|
2571
|
-
video = el;
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2572
2577
|
const IC = window.ImageCapture;
|
|
2573
|
-
grabber = IC ? new IC(
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
teardown();
|
|
2582
|
-
declined = true;
|
|
2583
|
-
return false;
|
|
2584
|
-
}
|
|
2578
|
+
const grabber = IC ? new IC(track) : null;
|
|
2579
|
+
const vw = window.innerWidth;
|
|
2580
|
+
const vh = window.innerHeight;
|
|
2581
|
+
const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
|
|
2582
|
+
const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
|
|
2583
|
+
if (mode === "surface" && !mapping) {
|
|
2584
|
+
declined = true;
|
|
2585
|
+
return null;
|
|
2585
2586
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2587
|
+
const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
|
|
2588
|
+
if (!raw) return null;
|
|
2589
|
+
let page;
|
|
2590
|
+
if (mode === "surface" && mapping) {
|
|
2591
|
+
const box = mapRectToFrame(
|
|
2592
|
+
{ left: 0, top: 0, width: vw, height: vh },
|
|
2593
|
+
mapping,
|
|
2594
|
+
raw.width,
|
|
2595
|
+
raw.height
|
|
2596
|
+
);
|
|
2597
|
+
if (!box) return null;
|
|
2598
|
+
page = document.createElement("canvas");
|
|
2599
|
+
page.width = box.sw;
|
|
2600
|
+
page.height = box.sh;
|
|
2601
|
+
const ctx = page.getContext("2d");
|
|
2602
|
+
if (!ctx) return null;
|
|
2603
|
+
ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
|
|
2604
|
+
raw.width = 0;
|
|
2605
|
+
raw.height = 0;
|
|
2606
|
+
} else {
|
|
2607
|
+
page = raw;
|
|
2608
|
+
}
|
|
2609
|
+
frozen = {
|
|
2610
|
+
canvas: page,
|
|
2611
|
+
mode,
|
|
2612
|
+
viewportWidth: vw,
|
|
2613
|
+
viewportHeight: vh,
|
|
2614
|
+
takenAt: Date.now()
|
|
2615
|
+
};
|
|
2616
|
+
lastMode = mode;
|
|
2617
|
+
return frozen;
|
|
2590
2618
|
} catch {
|
|
2591
2619
|
declined = true;
|
|
2592
|
-
|
|
2593
|
-
|
|
2620
|
+
return null;
|
|
2621
|
+
} finally {
|
|
2622
|
+
release();
|
|
2594
2623
|
}
|
|
2595
2624
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
if (
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
await nextPresentedFrame(video);
|
|
2604
|
-
let source;
|
|
2605
|
-
let frameW;
|
|
2606
|
-
let frameH;
|
|
2607
|
-
let bitmap = null;
|
|
2608
|
-
if (grabber) {
|
|
2609
|
-
try {
|
|
2610
|
-
bitmap = await grabber.grabFrame();
|
|
2611
|
-
source = bitmap;
|
|
2612
|
-
frameW = bitmap.width;
|
|
2613
|
-
frameH = bitmap.height;
|
|
2614
|
-
} catch {
|
|
2615
|
-
source = video;
|
|
2616
|
-
frameW = video.videoWidth;
|
|
2617
|
-
frameH = video.videoHeight;
|
|
2618
|
-
}
|
|
2619
|
-
} else {
|
|
2620
|
-
source = video;
|
|
2621
|
-
frameW = video.videoWidth;
|
|
2622
|
-
frameH = video.videoHeight;
|
|
2623
|
-
}
|
|
2624
|
-
try {
|
|
2625
|
-
let sx;
|
|
2626
|
-
let sy;
|
|
2627
|
-
let sw;
|
|
2628
|
-
let sh;
|
|
2629
|
-
if (mode === "surface") {
|
|
2630
|
-
if (!mapping) return null;
|
|
2631
|
-
const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
|
|
2632
|
-
if (!mapped) return null;
|
|
2633
|
-
({ sx, sy, sw, sh } = mapped);
|
|
2634
|
-
} else {
|
|
2635
|
-
if (!validateFrameMapping(frameW, frameH)) return null;
|
|
2636
|
-
const scaleX = frameW / window.innerWidth;
|
|
2637
|
-
const scaleY = frameH / window.innerHeight;
|
|
2638
|
-
sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
|
|
2639
|
-
sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
|
|
2640
|
-
sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
|
|
2641
|
-
sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
|
|
2642
|
-
}
|
|
2643
|
-
const canvas = document.createElement("canvas");
|
|
2644
|
-
canvas.width = sw;
|
|
2645
|
-
canvas.height = sh;
|
|
2646
|
-
const ctx = canvas.getContext("2d");
|
|
2647
|
-
if (!ctx) return null;
|
|
2648
|
-
ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2649
|
-
return canvas;
|
|
2650
|
-
} catch {
|
|
2625
|
+
function cropFrozenRegion(rect) {
|
|
2626
|
+
const f = frozen;
|
|
2627
|
+
if (!f || !f.canvas.width || !f.canvas.height) return null;
|
|
2628
|
+
if (!f.viewportWidth || !f.viewportHeight) return null;
|
|
2629
|
+
if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
|
|
2651
2630
|
return null;
|
|
2652
|
-
} finally {
|
|
2653
|
-
if (bitmap && typeof bitmap.close === "function") bitmap.close();
|
|
2654
2631
|
}
|
|
2632
|
+
const scaleX = f.canvas.width / f.viewportWidth;
|
|
2633
|
+
const scaleY = f.canvas.height / f.viewportHeight;
|
|
2634
|
+
const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
|
|
2635
|
+
const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
|
|
2636
|
+
const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
|
|
2637
|
+
const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
|
|
2638
|
+
if (sw < 1 || sh < 1) return null;
|
|
2639
|
+
const out = document.createElement("canvas");
|
|
2640
|
+
out.width = sw;
|
|
2641
|
+
out.height = sh;
|
|
2642
|
+
const ctx = out.getContext("2d");
|
|
2643
|
+
if (!ctx) return null;
|
|
2644
|
+
ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2645
|
+
return out;
|
|
2655
2646
|
}
|
|
2656
2647
|
|
|
2657
2648
|
// src/lib/cssColors.ts
|
|
@@ -2890,20 +2881,6 @@ function shotExtension(blob) {
|
|
|
2890
2881
|
if (type === "image/jpeg") return "jpg";
|
|
2891
2882
|
return "png";
|
|
2892
2883
|
}
|
|
2893
|
-
async function withOverlayHidden(fn) {
|
|
2894
|
-
const hosts = Array.from(
|
|
2895
|
-
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2896
|
-
);
|
|
2897
|
-
const previous = hosts.map((el) => el.style.visibility);
|
|
2898
|
-
for (const el of hosts) el.style.visibility = "hidden";
|
|
2899
|
-
try {
|
|
2900
|
-
return await fn();
|
|
2901
|
-
} finally {
|
|
2902
|
-
hosts.forEach((el, i) => {
|
|
2903
|
-
el.style.visibility = previous[i];
|
|
2904
|
-
});
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
2884
|
function markStuckElements() {
|
|
2908
2885
|
let all;
|
|
2909
2886
|
try {
|
|
@@ -3118,17 +3095,17 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3118
3095
|
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3119
3096
|
const sx = scroll?.x ?? window.scrollX;
|
|
3120
3097
|
const sy = scroll?.y ?? window.scrollY;
|
|
3121
|
-
if (prefer !== "dom" &&
|
|
3098
|
+
if (prefer !== "dom" && getFrozenFrame()) {
|
|
3122
3099
|
const visible = intersectViewport(rect);
|
|
3123
3100
|
if (!visible) return { status: "empty" };
|
|
3124
3101
|
try {
|
|
3125
|
-
const canvas =
|
|
3102
|
+
const canvas = cropFrozenRegion(visible);
|
|
3126
3103
|
if (canvas) {
|
|
3127
3104
|
const blob = await encodeShot(canvas);
|
|
3128
3105
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3129
3106
|
}
|
|
3130
3107
|
} catch (err) {
|
|
3131
|
-
console.warn("[QA]
|
|
3108
|
+
console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
|
|
3132
3109
|
}
|
|
3133
3110
|
if (prefer === "exact") return { status: "failed" };
|
|
3134
3111
|
}
|
|
@@ -4218,11 +4195,12 @@ var PANEL_OPEN_KEY = "panelOpen";
|
|
|
4218
4195
|
var ACTIVE_TAB_KEY = "activeTab";
|
|
4219
4196
|
var WALK_KEY = "walk";
|
|
4220
4197
|
var SOFT_NAV_PROOF_MS = 700;
|
|
4198
|
+
var SIGNATURE_SEP = String.fromCharCode(0);
|
|
4221
4199
|
function pageSignature() {
|
|
4222
4200
|
if (typeof document === "undefined" || !document.body) return "";
|
|
4223
4201
|
const body = document.body;
|
|
4224
4202
|
const text = body.textContent || "";
|
|
4225
|
-
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(
|
|
4203
|
+
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
|
|
4226
4204
|
}
|
|
4227
4205
|
var GUIDE_SKIPPED_KEY = "guideSkipped";
|
|
4228
4206
|
var NOTICE_QUEUE_CAP = 3;
|
|
@@ -4280,8 +4258,12 @@ function QaProvider({
|
|
|
4280
4258
|
const [loginsUsed, setLoginsUsed] = useState(
|
|
4281
4259
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4282
4260
|
);
|
|
4283
|
-
const [exactStatus, setExactStatus] = useState(() =>
|
|
4261
|
+
const [exactStatus, setExactStatus] = useState(() => {
|
|
4262
|
+
if (storage.getItem(EXACT_SHOTS_KEY) === "1") armExactCapture();
|
|
4263
|
+
return getExactCaptureStatus();
|
|
4264
|
+
});
|
|
4284
4265
|
const exactSupported = isExactCaptureSupported();
|
|
4266
|
+
const [frozenAt, setFrozenAt] = useState(null);
|
|
4285
4267
|
const [syncState, setSyncState] = useState(() => getFsSyncState());
|
|
4286
4268
|
const [syncTick, setSyncTick] = useState(0);
|
|
4287
4269
|
const [lastCampaign, setLastCampaign] = useState(
|
|
@@ -4782,20 +4764,26 @@ function QaProvider({
|
|
|
4782
4764
|
flushPendingDeletes();
|
|
4783
4765
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4784
4766
|
noticeTimers.current.clear();
|
|
4785
|
-
|
|
4767
|
+
releaseFrozenFrame();
|
|
4786
4768
|
};
|
|
4787
4769
|
}, [flushPendingDeletes]);
|
|
4788
4770
|
const startCapture = useCallback((prefill) => {
|
|
4789
4771
|
setIsOpen(false);
|
|
4790
4772
|
setCapturePrefill(prefill ?? "");
|
|
4791
4773
|
setCaptureActive(true);
|
|
4792
|
-
|
|
4774
|
+
setFrozenAt(null);
|
|
4775
|
+
if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported()) {
|
|
4793
4776
|
resetExactCaptureDecline();
|
|
4794
|
-
void
|
|
4777
|
+
void freezeViewport().then((frame) => {
|
|
4778
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4779
|
+
setExactStatus(getExactCaptureStatus());
|
|
4780
|
+
});
|
|
4795
4781
|
}
|
|
4796
4782
|
}, [storage]);
|
|
4797
4783
|
const endCapture = useCallback((reopen = true) => {
|
|
4798
4784
|
setCaptureActive(false);
|
|
4785
|
+
releaseFrozenFrame();
|
|
4786
|
+
setFrozenAt(null);
|
|
4799
4787
|
if (reopen) setIsOpen(true);
|
|
4800
4788
|
}, []);
|
|
4801
4789
|
const toggleGuide = useCallback((key) => {
|
|
@@ -4836,16 +4824,32 @@ function QaProvider({
|
|
|
4836
4824
|
exportZipRef.current = exportZipFn;
|
|
4837
4825
|
}, [exportZipFn]);
|
|
4838
4826
|
const enableExactShots = useCallback(async () => {
|
|
4839
|
-
|
|
4840
|
-
const ok = await startExactCapture();
|
|
4827
|
+
const ok = armExactCapture();
|
|
4841
4828
|
setExactStatus(getExactCaptureStatus());
|
|
4842
4829
|
storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
|
|
4843
4830
|
notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
|
|
4844
4831
|
return ok;
|
|
4845
4832
|
}, [storage, notify, t]);
|
|
4833
|
+
const photographNow = useCallback(async () => {
|
|
4834
|
+
if (!armExactCapture()) {
|
|
4835
|
+
setExactStatus(getExactCaptureStatus());
|
|
4836
|
+
notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
|
|
4837
|
+
return false;
|
|
4838
|
+
}
|
|
4839
|
+
storage.setItem(EXACT_SHOTS_KEY, "1");
|
|
4840
|
+
const frame = await freezeViewport();
|
|
4841
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4842
|
+
setExactStatus(getExactCaptureStatus());
|
|
4843
|
+
notify(frame ? t("exact_on") : t("exact_declined"), {
|
|
4844
|
+
tone: frame ? "success" : "info",
|
|
4845
|
+
id: "exact-shots"
|
|
4846
|
+
});
|
|
4847
|
+
return !!frame;
|
|
4848
|
+
}, [storage, notify, t]);
|
|
4846
4849
|
const disableExactShots = useCallback(() => {
|
|
4847
|
-
|
|
4850
|
+
disarmExactCapture();
|
|
4848
4851
|
storage.setItem(EXACT_SHOTS_KEY, "0");
|
|
4852
|
+
setFrozenAt(null);
|
|
4849
4853
|
setExactStatus(getExactCaptureStatus());
|
|
4850
4854
|
}, [storage]);
|
|
4851
4855
|
const rememberCampaign = useCallback((next) => {
|
|
@@ -5364,8 +5368,9 @@ function QaProvider({
|
|
|
5364
5368
|
gradeStep,
|
|
5365
5369
|
evidenceByStep,
|
|
5366
5370
|
// v0.4 — screenshots
|
|
5367
|
-
exactShots: { supported: exactSupported, status: exactStatus },
|
|
5371
|
+
exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
|
|
5368
5372
|
enableExactShots,
|
|
5373
|
+
photographNow,
|
|
5369
5374
|
disableExactShots,
|
|
5370
5375
|
// v0.4 — folder sync. syncTick is read here purely so this object is
|
|
5371
5376
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
@@ -9198,7 +9203,7 @@ function CaptureMode() {
|
|
|
9198
9203
|
compactCapture,
|
|
9199
9204
|
setCompactCapture,
|
|
9200
9205
|
exactShots,
|
|
9201
|
-
|
|
9206
|
+
photographNow,
|
|
9202
9207
|
capturePrefill
|
|
9203
9208
|
} = useQa();
|
|
9204
9209
|
const coarse = useCoarsePointer();
|
|
@@ -9212,6 +9217,7 @@ function CaptureMode() {
|
|
|
9212
9217
|
const [candidate, setCandidate] = useState(null);
|
|
9213
9218
|
const [regionMode, setRegionMode] = useState(false);
|
|
9214
9219
|
const [shot, setShot] = useState(null);
|
|
9220
|
+
const stillRef = useRef(null);
|
|
9215
9221
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9216
9222
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9217
9223
|
const [capturing, setCapturing] = useState(false);
|
|
@@ -9504,6 +9510,18 @@ function CaptureMode() {
|
|
|
9504
9510
|
useEffect(() => () => {
|
|
9505
9511
|
unlockPageScroll();
|
|
9506
9512
|
}, []);
|
|
9513
|
+
useEffect(() => {
|
|
9514
|
+
const host = stillRef.current;
|
|
9515
|
+
const frame = getFrozenFrame();
|
|
9516
|
+
if (!host || !frame) return;
|
|
9517
|
+
const canvas = frame.canvas;
|
|
9518
|
+
canvas.style.cssText = "display:block;width:100%;height:100%;";
|
|
9519
|
+
host.appendChild(canvas);
|
|
9520
|
+
return () => {
|
|
9521
|
+
if (canvas.parentNode === host) host.removeChild(canvas);
|
|
9522
|
+
canvas.removeAttribute("style");
|
|
9523
|
+
};
|
|
9524
|
+
}, [exactShots.frozenAt]);
|
|
9507
9525
|
const save = async () => {
|
|
9508
9526
|
if (!selection || !description.trim()) return;
|
|
9509
9527
|
const target = {
|
|
@@ -9562,6 +9580,16 @@ function CaptureMode() {
|
|
|
9562
9580
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
9563
9581
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
9564
9582
|
return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
9583
|
+
exactShots.frozenAt !== null && /* @__PURE__ */ jsx(
|
|
9584
|
+
"div",
|
|
9585
|
+
{
|
|
9586
|
+
ref: stillRef,
|
|
9587
|
+
"aria-hidden": "true",
|
|
9588
|
+
"data-qa-capture-still": "true",
|
|
9589
|
+
className: "qa-fixed qa-inset-0 qa-z-10089",
|
|
9590
|
+
style: { pointerEvents: "none", overflow: "hidden" }
|
|
9591
|
+
}
|
|
9592
|
+
),
|
|
9565
9593
|
/* @__PURE__ */ jsx(
|
|
9566
9594
|
"div",
|
|
9567
9595
|
{
|
|
@@ -9620,7 +9648,7 @@ function CaptureMode() {
|
|
|
9620
9648
|
) : /* @__PURE__ */ jsxs(
|
|
9621
9649
|
"button",
|
|
9622
9650
|
{
|
|
9623
|
-
onClick: () => void
|
|
9651
|
+
onClick: () => void photographNow(),
|
|
9624
9652
|
title: t("exact_hint"),
|
|
9625
9653
|
className: "qa-tap qa-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-11 qa-text-hi qa-hover-bg-white-15",
|
|
9626
9654
|
style: { background: "transparent", cursor: "pointer" },
|
|
@@ -10064,8 +10092,8 @@ function CaptureMode() {
|
|
|
10064
10092
|
{
|
|
10065
10093
|
type: "button",
|
|
10066
10094
|
onClick: () => void (async () => {
|
|
10067
|
-
const
|
|
10068
|
-
if (
|
|
10095
|
+
const shot2 = await photographNow();
|
|
10096
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10069
10097
|
})(),
|
|
10070
10098
|
title: t("exact_hint"),
|
|
10071
10099
|
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-subtle qa-px-1.5 qa-py-0.5 qa-text-10 qa-text-hi qa-focus-ring",
|
|
@@ -10389,5 +10417,5 @@ function Qapture({ config }) {
|
|
|
10389
10417
|
}
|
|
10390
10418
|
|
|
10391
10419
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10392
|
-
//# sourceMappingURL=chunk-
|
|
10393
|
-
//# sourceMappingURL=chunk-
|
|
10420
|
+
//# sourceMappingURL=chunk-RJV6FTIB.js.map
|
|
10421
|
+
//# sourceMappingURL=chunk-RJV6FTIB.js.map
|