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
|
@@ -58,10 +58,10 @@ function openDB(dbName) {
|
|
|
58
58
|
dbCache.set(dbName, promise);
|
|
59
59
|
return promise;
|
|
60
60
|
}
|
|
61
|
-
function run(dbName, store,
|
|
61
|
+
function run(dbName, store, mode, fn) {
|
|
62
62
|
return openDB(dbName).then(
|
|
63
63
|
(db) => new Promise((resolve, reject) => {
|
|
64
|
-
const tx = db.transaction(store,
|
|
64
|
+
const tx = db.transaction(store, mode);
|
|
65
65
|
const s = tx.objectStore(store);
|
|
66
66
|
let result;
|
|
67
67
|
const req = fn(s);
|
|
@@ -516,6 +516,7 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
|
|
|
516
516
|
/* Z-index scale */
|
|
517
517
|
--qa-z-fab: 9990;
|
|
518
518
|
--qa-z-panel: 9995;
|
|
519
|
+
--qa-z-capture-still: 10089;
|
|
519
520
|
--qa-z-capture-dim: 10090;
|
|
520
521
|
--qa-z-capture-highlight: 10092;
|
|
521
522
|
--qa-z-capture-region-move: 10093;
|
|
@@ -565,6 +566,8 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
|
|
|
565
566
|
.qa-z-1 { z-index: 1; }
|
|
566
567
|
.qa-z-50 { z-index: 50; }
|
|
567
568
|
.qa-z-100 { z-index: 100; }
|
|
569
|
+
/* the frozen still, directly beneath the dim so the scrim tints it too */
|
|
570
|
+
.qa-z-10089 { z-index: var(--qa-z-capture-still); }
|
|
568
571
|
.qa-z-10090 { z-index: var(--qa-z-capture-dim); }
|
|
569
572
|
.qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
|
|
570
573
|
/* region-handle layering */
|
|
@@ -1783,7 +1786,7 @@ var STR = {
|
|
|
1783
1786
|
no_evidence: "ticked, no capture",
|
|
1784
1787
|
expected_label: "Expected",
|
|
1785
1788
|
exact_label: "Pixel-exact shots",
|
|
1786
|
-
exact_hint: "Screenshots become a real photo instead of a redraw.
|
|
1789
|
+
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.",
|
|
1787
1790
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1788
1791
|
redraw_action: "Use real screenshots",
|
|
1789
1792
|
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.",
|
|
@@ -1792,7 +1795,7 @@ var STR = {
|
|
|
1792
1795
|
exact_off: "Back to redrawn screenshots",
|
|
1793
1796
|
exact_declined: "Staying on redrawn screenshots",
|
|
1794
1797
|
exact_turn_on: "Turn on",
|
|
1795
|
-
exact_unsupported: "Needs
|
|
1798
|
+
exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
|
|
1796
1799
|
sync_title: "Save to a folder",
|
|
1797
1800
|
sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
|
|
1798
1801
|
sync_choose: "Choose folder",
|
|
@@ -1991,7 +1994,7 @@ var STR = {
|
|
|
1991
1994
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1992
1995
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1993
1996
|
exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
|
|
1994
|
-
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. \
|
|
1997
|
+
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.",
|
|
1995
1998
|
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.",
|
|
1996
1999
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1997
2000
|
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.",
|
|
@@ -2000,7 +2003,7 @@ var STR = {
|
|
|
2000
2003
|
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",
|
|
2001
2004
|
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",
|
|
2002
2005
|
exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
|
|
2003
|
-
exact_unsupported: "\u064A\u062A\u0637\u0644\u0628
|
|
2006
|
+
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",
|
|
2004
2007
|
sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
|
|
2005
2008
|
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.",
|
|
2006
2009
|
sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
|
|
@@ -2305,7 +2308,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2305
2308
|
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2306
2309
|
const originX = tl.x - centres.tl.x * scaleX;
|
|
2307
2310
|
const originY = tl.y - centres.tl.y * scaleY;
|
|
2308
|
-
const
|
|
2311
|
+
const mapping = { scaleX, scaleY, originX, originY };
|
|
2309
2312
|
const farX = originX + vw * scaleX;
|
|
2310
2313
|
const farY = originY + vh * scaleY;
|
|
2311
2314
|
const slack = Math.max(4, frameW * 0.01);
|
|
@@ -2324,7 +2327,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2324
2327
|
verified++;
|
|
2325
2328
|
}
|
|
2326
2329
|
if (verified === 0) return null;
|
|
2327
|
-
return
|
|
2330
|
+
return mapping;
|
|
2328
2331
|
}
|
|
2329
2332
|
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2330
2333
|
const x0 = m.originX + rect.left * m.scaleX;
|
|
@@ -2336,73 +2339,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
|
|
|
2336
2339
|
if (sw < 1 || sh < 1) return null;
|
|
2337
2340
|
return { sx, sy, sw, sh };
|
|
2338
2341
|
}
|
|
2339
|
-
function environmentSignature() {
|
|
2340
|
-
if (typeof window === "undefined") return "";
|
|
2341
|
-
return [
|
|
2342
|
-
window.innerWidth,
|
|
2343
|
-
window.innerHeight,
|
|
2344
|
-
window.outerWidth,
|
|
2345
|
-
window.outerHeight,
|
|
2346
|
-
window.screenX,
|
|
2347
|
-
window.screenY,
|
|
2348
|
-
Math.round((window.devicePixelRatio || 1) * 100)
|
|
2349
|
-
].join("x");
|
|
2350
|
-
}
|
|
2351
2342
|
|
|
2352
2343
|
// src/lib/screenCapture.ts
|
|
2353
2344
|
var ASPECT_TOLERANCE = 0.08;
|
|
2354
2345
|
var CALIBRATION_SETTLE_MS = 220;
|
|
2355
2346
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2356
2347
|
var VIDEO_READY_TIMEOUT_MS = 4e3;
|
|
2357
|
-
var stream = null;
|
|
2358
|
-
var track = null;
|
|
2359
|
-
var video = null;
|
|
2360
|
-
var grabber = null;
|
|
2361
|
-
var declined = false;
|
|
2362
2348
|
function isExactCaptureSupported() {
|
|
2363
2349
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2364
2350
|
const md = navigator.mediaDevices;
|
|
2365
2351
|
return !!md && typeof md.getDisplayMedia === "function";
|
|
2366
2352
|
}
|
|
2367
|
-
var
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
2353
|
+
var armed = false;
|
|
2354
|
+
var declined = false;
|
|
2355
|
+
var frozen = null;
|
|
2356
|
+
var lastMode = null;
|
|
2357
|
+
function armExactCapture() {
|
|
2358
|
+
if (!isExactCaptureSupported()) return false;
|
|
2359
|
+
armed = true;
|
|
2360
|
+
declined = false;
|
|
2361
|
+
return true;
|
|
2362
|
+
}
|
|
2363
|
+
function disarmExactCapture() {
|
|
2364
|
+
armed = false;
|
|
2365
|
+
releaseFrozenFrame();
|
|
2366
|
+
}
|
|
2367
|
+
function resetExactCaptureDecline() {
|
|
2368
|
+
declined = false;
|
|
2369
|
+
}
|
|
2370
2370
|
function getExactCaptureStatus() {
|
|
2371
2371
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2372
|
-
if (
|
|
2372
|
+
if (armed) return "live";
|
|
2373
2373
|
if (declined) return "declined";
|
|
2374
2374
|
return "idle";
|
|
2375
2375
|
}
|
|
2376
|
-
function
|
|
2377
|
-
|
|
2378
|
-
try {
|
|
2379
|
-
video.pause();
|
|
2380
|
-
} catch {
|
|
2381
|
-
}
|
|
2382
|
-
video.srcObject = null;
|
|
2383
|
-
video.remove();
|
|
2384
|
-
}
|
|
2385
|
-
if (stream) {
|
|
2386
|
-
for (const t of stream.getTracks()) {
|
|
2387
|
-
try {
|
|
2388
|
-
t.stop();
|
|
2389
|
-
} catch {
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
video = null;
|
|
2394
|
-
stream = null;
|
|
2395
|
-
track = null;
|
|
2396
|
-
grabber = null;
|
|
2397
|
-
mode = "tab";
|
|
2398
|
-
mapping = null;
|
|
2399
|
-
mappingSignature = "";
|
|
2400
|
-
}
|
|
2401
|
-
function stopExactCapture() {
|
|
2402
|
-
teardown();
|
|
2376
|
+
function getFrozenFrame() {
|
|
2377
|
+
return frozen;
|
|
2403
2378
|
}
|
|
2404
|
-
function
|
|
2405
|
-
|
|
2379
|
+
function releaseFrozenFrame() {
|
|
2380
|
+
if (frozen) {
|
|
2381
|
+
frozen.canvas.width = 0;
|
|
2382
|
+
frozen.canvas.height = 0;
|
|
2383
|
+
}
|
|
2384
|
+
frozen = null;
|
|
2406
2385
|
}
|
|
2407
2386
|
function waitFor(test, timeoutMs) {
|
|
2408
2387
|
if (test()) return Promise.resolve(true);
|
|
@@ -2442,17 +2421,28 @@ function nextPresentedFrame(el) {
|
|
|
2442
2421
|
});
|
|
2443
2422
|
});
|
|
2444
2423
|
}
|
|
2445
|
-
function
|
|
2424
|
+
function looksLikeViewport(frameW, frameH) {
|
|
2446
2425
|
if (!frameW || !frameH) return false;
|
|
2447
2426
|
const vw = window.innerWidth;
|
|
2448
2427
|
const vh = window.innerHeight;
|
|
2449
2428
|
if (!vw || !vh) return false;
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2429
|
+
return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
|
|
2430
|
+
}
|
|
2431
|
+
async function withOverlayHidden(fn) {
|
|
2432
|
+
const hosts = Array.from(
|
|
2433
|
+
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2434
|
+
);
|
|
2435
|
+
const previous = hosts.map((el) => el.style.visibility);
|
|
2436
|
+
for (const el of hosts) el.style.visibility = "hidden";
|
|
2437
|
+
try {
|
|
2438
|
+
return await fn();
|
|
2439
|
+
} finally {
|
|
2440
|
+
hosts.forEach((el, i) => {
|
|
2441
|
+
el.style.visibility = previous[i];
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2453
2444
|
}
|
|
2454
|
-
async function grabFrameCanvas() {
|
|
2455
|
-
if (!video) return null;
|
|
2445
|
+
async function grabFrameCanvas(video, grabber) {
|
|
2456
2446
|
await nextPresentedFrame(video);
|
|
2457
2447
|
let source = video;
|
|
2458
2448
|
let w = video.videoWidth;
|
|
@@ -2483,11 +2473,10 @@ async function grabFrameCanvas() {
|
|
|
2483
2473
|
bitmap?.close?.();
|
|
2484
2474
|
return c;
|
|
2485
2475
|
}
|
|
2486
|
-
async function calibrate() {
|
|
2487
|
-
if (!video) return false;
|
|
2476
|
+
async function calibrate(video, grabber) {
|
|
2488
2477
|
const vw = window.innerWidth;
|
|
2489
2478
|
const vh = window.innerHeight;
|
|
2490
|
-
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return
|
|
2479
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
|
|
2491
2480
|
const card = document.createElement("div");
|
|
2492
2481
|
card.setAttribute("data-qa-calibration", "true");
|
|
2493
2482
|
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
@@ -2501,10 +2490,10 @@ async function calibrate() {
|
|
|
2501
2490
|
document.body.appendChild(card);
|
|
2502
2491
|
try {
|
|
2503
2492
|
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2504
|
-
const frame = await grabFrameCanvas();
|
|
2505
|
-
if (!frame) return
|
|
2493
|
+
const frame = await grabFrameCanvas(video, grabber);
|
|
2494
|
+
if (!frame) return null;
|
|
2506
2495
|
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2507
|
-
if (!ctx) return
|
|
2496
|
+
if (!ctx) return null;
|
|
2508
2497
|
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2509
2498
|
const found = deriveMapping(
|
|
2510
2499
|
findMarkerCandidates(data, frame.width, frame.height),
|
|
@@ -2513,20 +2502,40 @@ async function calibrate() {
|
|
|
2513
2502
|
frame.width,
|
|
2514
2503
|
frame.height
|
|
2515
2504
|
);
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
return true;
|
|
2505
|
+
frame.width = 0;
|
|
2506
|
+
frame.height = 0;
|
|
2507
|
+
return found;
|
|
2520
2508
|
} catch {
|
|
2521
|
-
return
|
|
2509
|
+
return null;
|
|
2522
2510
|
} finally {
|
|
2523
2511
|
card.remove();
|
|
2524
2512
|
}
|
|
2525
2513
|
}
|
|
2526
|
-
async function
|
|
2527
|
-
if (!isExactCaptureSupported()) return
|
|
2528
|
-
|
|
2529
|
-
|
|
2514
|
+
async function freezeViewport() {
|
|
2515
|
+
if (!isExactCaptureSupported()) return null;
|
|
2516
|
+
releaseFrozenFrame();
|
|
2517
|
+
let stream = null;
|
|
2518
|
+
let el = null;
|
|
2519
|
+
const release = () => {
|
|
2520
|
+
if (el) {
|
|
2521
|
+
try {
|
|
2522
|
+
el.pause();
|
|
2523
|
+
} catch {
|
|
2524
|
+
}
|
|
2525
|
+
el.srcObject = null;
|
|
2526
|
+
el.remove();
|
|
2527
|
+
el = null;
|
|
2528
|
+
}
|
|
2529
|
+
if (stream) {
|
|
2530
|
+
for (const t of stream.getTracks()) {
|
|
2531
|
+
try {
|
|
2532
|
+
t.stop();
|
|
2533
|
+
} catch {
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
stream = null;
|
|
2537
|
+
}
|
|
2538
|
+
};
|
|
2530
2539
|
try {
|
|
2531
2540
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2532
2541
|
const constraints = {
|
|
@@ -2544,121 +2553,103 @@ async function startExactCapture() {
|
|
|
2544
2553
|
surfaceSwitching: "exclude",
|
|
2545
2554
|
systemAudio: "exclude"
|
|
2546
2555
|
};
|
|
2547
|
-
|
|
2548
|
-
const
|
|
2549
|
-
if (!
|
|
2550
|
-
for (const x of s.getTracks()) x.stop();
|
|
2556
|
+
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
2557
|
+
const track = stream.getVideoTracks()[0];
|
|
2558
|
+
if (!track) {
|
|
2551
2559
|
declined = true;
|
|
2552
|
-
return
|
|
2560
|
+
return null;
|
|
2553
2561
|
}
|
|
2554
|
-
const settings =
|
|
2562
|
+
const settings = track.getSettings();
|
|
2555
2563
|
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2556
|
-
|
|
2564
|
+
el = document.createElement("video");
|
|
2557
2565
|
el.setAttribute("data-qa-overlay", "true");
|
|
2558
2566
|
el.muted = true;
|
|
2559
2567
|
el.playsInline = true;
|
|
2560
2568
|
el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
2561
|
-
el.srcObject =
|
|
2569
|
+
el.srcObject = stream;
|
|
2562
2570
|
document.body.appendChild(el);
|
|
2563
2571
|
try {
|
|
2564
2572
|
await el.play();
|
|
2565
2573
|
} catch {
|
|
2566
2574
|
}
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
|
|
2575
|
+
const video = el;
|
|
2576
|
+
const ready = await waitFor(
|
|
2577
|
+
() => video.videoWidth > 0 && video.videoHeight > 0,
|
|
2578
|
+
VIDEO_READY_TIMEOUT_MS
|
|
2579
|
+
);
|
|
2580
|
+
if (!ready) {
|
|
2571
2581
|
declined = true;
|
|
2572
|
-
return
|
|
2573
|
-
}
|
|
2574
|
-
if (!ready) return giveUp();
|
|
2575
|
-
const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
|
|
2576
|
-
stream = s;
|
|
2577
|
-
track = t;
|
|
2578
|
-
video = el;
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2579
2584
|
const IC = window.ImageCapture;
|
|
2580
|
-
grabber = IC ? new IC(
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
teardown();
|
|
2589
|
-
declined = true;
|
|
2590
|
-
return false;
|
|
2591
|
-
}
|
|
2585
|
+
const grabber = IC ? new IC(track) : null;
|
|
2586
|
+
const vw = window.innerWidth;
|
|
2587
|
+
const vh = window.innerHeight;
|
|
2588
|
+
const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
|
|
2589
|
+
const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
|
|
2590
|
+
if (mode === "surface" && !mapping) {
|
|
2591
|
+
declined = true;
|
|
2592
|
+
return null;
|
|
2592
2593
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2594
|
+
const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
|
|
2595
|
+
if (!raw) return null;
|
|
2596
|
+
let page;
|
|
2597
|
+
if (mode === "surface" && mapping) {
|
|
2598
|
+
const box = mapRectToFrame(
|
|
2599
|
+
{ left: 0, top: 0, width: vw, height: vh },
|
|
2600
|
+
mapping,
|
|
2601
|
+
raw.width,
|
|
2602
|
+
raw.height
|
|
2603
|
+
);
|
|
2604
|
+
if (!box) return null;
|
|
2605
|
+
page = document.createElement("canvas");
|
|
2606
|
+
page.width = box.sw;
|
|
2607
|
+
page.height = box.sh;
|
|
2608
|
+
const ctx = page.getContext("2d");
|
|
2609
|
+
if (!ctx) return null;
|
|
2610
|
+
ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
|
|
2611
|
+
raw.width = 0;
|
|
2612
|
+
raw.height = 0;
|
|
2613
|
+
} else {
|
|
2614
|
+
page = raw;
|
|
2615
|
+
}
|
|
2616
|
+
frozen = {
|
|
2617
|
+
canvas: page,
|
|
2618
|
+
mode,
|
|
2619
|
+
viewportWidth: vw,
|
|
2620
|
+
viewportHeight: vh,
|
|
2621
|
+
takenAt: Date.now()
|
|
2622
|
+
};
|
|
2623
|
+
lastMode = mode;
|
|
2624
|
+
return frozen;
|
|
2597
2625
|
} catch {
|
|
2598
2626
|
declined = true;
|
|
2599
|
-
|
|
2600
|
-
|
|
2627
|
+
return null;
|
|
2628
|
+
} finally {
|
|
2629
|
+
release();
|
|
2601
2630
|
}
|
|
2602
2631
|
}
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
if (
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
}
|
|
2609
|
-
}
|
|
2610
|
-
await nextPresentedFrame(video);
|
|
2611
|
-
let source;
|
|
2612
|
-
let frameW;
|
|
2613
|
-
let frameH;
|
|
2614
|
-
let bitmap = null;
|
|
2615
|
-
if (grabber) {
|
|
2616
|
-
try {
|
|
2617
|
-
bitmap = await grabber.grabFrame();
|
|
2618
|
-
source = bitmap;
|
|
2619
|
-
frameW = bitmap.width;
|
|
2620
|
-
frameH = bitmap.height;
|
|
2621
|
-
} catch {
|
|
2622
|
-
source = video;
|
|
2623
|
-
frameW = video.videoWidth;
|
|
2624
|
-
frameH = video.videoHeight;
|
|
2625
|
-
}
|
|
2626
|
-
} else {
|
|
2627
|
-
source = video;
|
|
2628
|
-
frameW = video.videoWidth;
|
|
2629
|
-
frameH = video.videoHeight;
|
|
2630
|
-
}
|
|
2631
|
-
try {
|
|
2632
|
-
let sx;
|
|
2633
|
-
let sy;
|
|
2634
|
-
let sw;
|
|
2635
|
-
let sh;
|
|
2636
|
-
if (mode === "surface") {
|
|
2637
|
-
if (!mapping) return null;
|
|
2638
|
-
const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
|
|
2639
|
-
if (!mapped) return null;
|
|
2640
|
-
({ sx, sy, sw, sh } = mapped);
|
|
2641
|
-
} else {
|
|
2642
|
-
if (!validateFrameMapping(frameW, frameH)) return null;
|
|
2643
|
-
const scaleX = frameW / window.innerWidth;
|
|
2644
|
-
const scaleY = frameH / window.innerHeight;
|
|
2645
|
-
sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
|
|
2646
|
-
sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
|
|
2647
|
-
sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
|
|
2648
|
-
sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
|
|
2649
|
-
}
|
|
2650
|
-
const canvas = document.createElement("canvas");
|
|
2651
|
-
canvas.width = sw;
|
|
2652
|
-
canvas.height = sh;
|
|
2653
|
-
const ctx = canvas.getContext("2d");
|
|
2654
|
-
if (!ctx) return null;
|
|
2655
|
-
ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2656
|
-
return canvas;
|
|
2657
|
-
} catch {
|
|
2632
|
+
function cropFrozenRegion(rect) {
|
|
2633
|
+
const f = frozen;
|
|
2634
|
+
if (!f || !f.canvas.width || !f.canvas.height) return null;
|
|
2635
|
+
if (!f.viewportWidth || !f.viewportHeight) return null;
|
|
2636
|
+
if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
|
|
2658
2637
|
return null;
|
|
2659
|
-
} finally {
|
|
2660
|
-
if (bitmap && typeof bitmap.close === "function") bitmap.close();
|
|
2661
2638
|
}
|
|
2639
|
+
const scaleX = f.canvas.width / f.viewportWidth;
|
|
2640
|
+
const scaleY = f.canvas.height / f.viewportHeight;
|
|
2641
|
+
const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
|
|
2642
|
+
const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
|
|
2643
|
+
const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
|
|
2644
|
+
const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
|
|
2645
|
+
if (sw < 1 || sh < 1) return null;
|
|
2646
|
+
const out = document.createElement("canvas");
|
|
2647
|
+
out.width = sw;
|
|
2648
|
+
out.height = sh;
|
|
2649
|
+
const ctx = out.getContext("2d");
|
|
2650
|
+
if (!ctx) return null;
|
|
2651
|
+
ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2652
|
+
return out;
|
|
2662
2653
|
}
|
|
2663
2654
|
|
|
2664
2655
|
// src/lib/cssColors.ts
|
|
@@ -2897,20 +2888,6 @@ function shotExtension(blob) {
|
|
|
2897
2888
|
if (type === "image/jpeg") return "jpg";
|
|
2898
2889
|
return "png";
|
|
2899
2890
|
}
|
|
2900
|
-
async function withOverlayHidden(fn) {
|
|
2901
|
-
const hosts = Array.from(
|
|
2902
|
-
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2903
|
-
);
|
|
2904
|
-
const previous = hosts.map((el) => el.style.visibility);
|
|
2905
|
-
for (const el of hosts) el.style.visibility = "hidden";
|
|
2906
|
-
try {
|
|
2907
|
-
return await fn();
|
|
2908
|
-
} finally {
|
|
2909
|
-
hosts.forEach((el, i) => {
|
|
2910
|
-
el.style.visibility = previous[i];
|
|
2911
|
-
});
|
|
2912
|
-
}
|
|
2913
|
-
}
|
|
2914
2891
|
function markStuckElements() {
|
|
2915
2892
|
let all;
|
|
2916
2893
|
try {
|
|
@@ -3125,17 +3102,17 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3125
3102
|
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3126
3103
|
const sx = scroll?.x ?? window.scrollX;
|
|
3127
3104
|
const sy = scroll?.y ?? window.scrollY;
|
|
3128
|
-
if (prefer !== "dom" &&
|
|
3105
|
+
if (prefer !== "dom" && getFrozenFrame()) {
|
|
3129
3106
|
const visible = intersectViewport(rect);
|
|
3130
3107
|
if (!visible) return { status: "empty" };
|
|
3131
3108
|
try {
|
|
3132
|
-
const canvas =
|
|
3109
|
+
const canvas = cropFrozenRegion(visible);
|
|
3133
3110
|
if (canvas) {
|
|
3134
3111
|
const blob = await encodeShot(canvas);
|
|
3135
3112
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3136
3113
|
}
|
|
3137
3114
|
} catch (err) {
|
|
3138
|
-
console.warn("[QA]
|
|
3115
|
+
console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
|
|
3139
3116
|
}
|
|
3140
3117
|
if (prefer === "exact") return { status: "failed" };
|
|
3141
3118
|
}
|
|
@@ -4225,11 +4202,12 @@ var PANEL_OPEN_KEY = "panelOpen";
|
|
|
4225
4202
|
var ACTIVE_TAB_KEY = "activeTab";
|
|
4226
4203
|
var WALK_KEY = "walk";
|
|
4227
4204
|
var SOFT_NAV_PROOF_MS = 700;
|
|
4205
|
+
var SIGNATURE_SEP = String.fromCharCode(0);
|
|
4228
4206
|
function pageSignature() {
|
|
4229
4207
|
if (typeof document === "undefined" || !document.body) return "";
|
|
4230
4208
|
const body = document.body;
|
|
4231
4209
|
const text = body.textContent || "";
|
|
4232
|
-
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(
|
|
4210
|
+
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
|
|
4233
4211
|
}
|
|
4234
4212
|
var GUIDE_SKIPPED_KEY = "guideSkipped";
|
|
4235
4213
|
var NOTICE_QUEUE_CAP = 3;
|
|
@@ -4287,8 +4265,12 @@ function QaProvider({
|
|
|
4287
4265
|
const [loginsUsed, setLoginsUsed] = React.useState(
|
|
4288
4266
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4289
4267
|
);
|
|
4290
|
-
const [exactStatus, setExactStatus] = React.useState(() =>
|
|
4268
|
+
const [exactStatus, setExactStatus] = React.useState(() => {
|
|
4269
|
+
if (storage.getItem(EXACT_SHOTS_KEY) === "1") armExactCapture();
|
|
4270
|
+
return getExactCaptureStatus();
|
|
4271
|
+
});
|
|
4291
4272
|
const exactSupported = isExactCaptureSupported();
|
|
4273
|
+
const [frozenAt, setFrozenAt] = React.useState(null);
|
|
4292
4274
|
const [syncState, setSyncState] = React.useState(() => getFsSyncState());
|
|
4293
4275
|
const [syncTick, setSyncTick] = React.useState(0);
|
|
4294
4276
|
const [lastCampaign, setLastCampaign] = React.useState(
|
|
@@ -4789,20 +4771,26 @@ function QaProvider({
|
|
|
4789
4771
|
flushPendingDeletes();
|
|
4790
4772
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4791
4773
|
noticeTimers.current.clear();
|
|
4792
|
-
|
|
4774
|
+
releaseFrozenFrame();
|
|
4793
4775
|
};
|
|
4794
4776
|
}, [flushPendingDeletes]);
|
|
4795
4777
|
const startCapture = React.useCallback((prefill) => {
|
|
4796
4778
|
setIsOpen(false);
|
|
4797
4779
|
setCapturePrefill(prefill ?? "");
|
|
4798
4780
|
setCaptureActive(true);
|
|
4799
|
-
|
|
4781
|
+
setFrozenAt(null);
|
|
4782
|
+
if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported()) {
|
|
4800
4783
|
resetExactCaptureDecline();
|
|
4801
|
-
void
|
|
4784
|
+
void freezeViewport().then((frame) => {
|
|
4785
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4786
|
+
setExactStatus(getExactCaptureStatus());
|
|
4787
|
+
});
|
|
4802
4788
|
}
|
|
4803
4789
|
}, [storage]);
|
|
4804
4790
|
const endCapture = React.useCallback((reopen = true) => {
|
|
4805
4791
|
setCaptureActive(false);
|
|
4792
|
+
releaseFrozenFrame();
|
|
4793
|
+
setFrozenAt(null);
|
|
4806
4794
|
if (reopen) setIsOpen(true);
|
|
4807
4795
|
}, []);
|
|
4808
4796
|
const toggleGuide = React.useCallback((key) => {
|
|
@@ -4843,16 +4831,32 @@ function QaProvider({
|
|
|
4843
4831
|
exportZipRef.current = exportZipFn;
|
|
4844
4832
|
}, [exportZipFn]);
|
|
4845
4833
|
const enableExactShots = React.useCallback(async () => {
|
|
4846
|
-
|
|
4847
|
-
const ok = await startExactCapture();
|
|
4834
|
+
const ok = armExactCapture();
|
|
4848
4835
|
setExactStatus(getExactCaptureStatus());
|
|
4849
4836
|
storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
|
|
4850
4837
|
notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
|
|
4851
4838
|
return ok;
|
|
4852
4839
|
}, [storage, notify, t]);
|
|
4840
|
+
const photographNow = React.useCallback(async () => {
|
|
4841
|
+
if (!armExactCapture()) {
|
|
4842
|
+
setExactStatus(getExactCaptureStatus());
|
|
4843
|
+
notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
|
|
4844
|
+
return false;
|
|
4845
|
+
}
|
|
4846
|
+
storage.setItem(EXACT_SHOTS_KEY, "1");
|
|
4847
|
+
const frame = await freezeViewport();
|
|
4848
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4849
|
+
setExactStatus(getExactCaptureStatus());
|
|
4850
|
+
notify(frame ? t("exact_on") : t("exact_declined"), {
|
|
4851
|
+
tone: frame ? "success" : "info",
|
|
4852
|
+
id: "exact-shots"
|
|
4853
|
+
});
|
|
4854
|
+
return !!frame;
|
|
4855
|
+
}, [storage, notify, t]);
|
|
4853
4856
|
const disableExactShots = React.useCallback(() => {
|
|
4854
|
-
|
|
4857
|
+
disarmExactCapture();
|
|
4855
4858
|
storage.setItem(EXACT_SHOTS_KEY, "0");
|
|
4859
|
+
setFrozenAt(null);
|
|
4856
4860
|
setExactStatus(getExactCaptureStatus());
|
|
4857
4861
|
}, [storage]);
|
|
4858
4862
|
const rememberCampaign = React.useCallback((next) => {
|
|
@@ -5371,8 +5375,9 @@ function QaProvider({
|
|
|
5371
5375
|
gradeStep,
|
|
5372
5376
|
evidenceByStep,
|
|
5373
5377
|
// v0.4 — screenshots
|
|
5374
|
-
exactShots: { supported: exactSupported, status: exactStatus },
|
|
5378
|
+
exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
|
|
5375
5379
|
enableExactShots,
|
|
5380
|
+
photographNow,
|
|
5376
5381
|
disableExactShots,
|
|
5377
5382
|
// v0.4 — folder sync. syncTick is read here purely so this object is
|
|
5378
5383
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
@@ -9205,7 +9210,7 @@ function CaptureMode() {
|
|
|
9205
9210
|
compactCapture,
|
|
9206
9211
|
setCompactCapture,
|
|
9207
9212
|
exactShots,
|
|
9208
|
-
|
|
9213
|
+
photographNow,
|
|
9209
9214
|
capturePrefill
|
|
9210
9215
|
} = useQa();
|
|
9211
9216
|
const coarse = useCoarsePointer();
|
|
@@ -9219,6 +9224,7 @@ function CaptureMode() {
|
|
|
9219
9224
|
const [candidate, setCandidate] = React.useState(null);
|
|
9220
9225
|
const [regionMode, setRegionMode] = React.useState(false);
|
|
9221
9226
|
const [shot, setShot] = React.useState(null);
|
|
9227
|
+
const stillRef = React.useRef(null);
|
|
9222
9228
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
9223
9229
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
9224
9230
|
const [capturing, setCapturing] = React.useState(false);
|
|
@@ -9511,6 +9517,18 @@ function CaptureMode() {
|
|
|
9511
9517
|
React.useEffect(() => () => {
|
|
9512
9518
|
unlockPageScroll();
|
|
9513
9519
|
}, []);
|
|
9520
|
+
React.useEffect(() => {
|
|
9521
|
+
const host = stillRef.current;
|
|
9522
|
+
const frame = getFrozenFrame();
|
|
9523
|
+
if (!host || !frame) return;
|
|
9524
|
+
const canvas = frame.canvas;
|
|
9525
|
+
canvas.style.cssText = "display:block;width:100%;height:100%;";
|
|
9526
|
+
host.appendChild(canvas);
|
|
9527
|
+
return () => {
|
|
9528
|
+
if (canvas.parentNode === host) host.removeChild(canvas);
|
|
9529
|
+
canvas.removeAttribute("style");
|
|
9530
|
+
};
|
|
9531
|
+
}, [exactShots.frozenAt]);
|
|
9514
9532
|
const save = async () => {
|
|
9515
9533
|
if (!selection || !description.trim()) return;
|
|
9516
9534
|
const target = {
|
|
@@ -9569,6 +9587,16 @@ function CaptureMode() {
|
|
|
9569
9587
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
9570
9588
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
9571
9589
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
9590
|
+
exactShots.frozenAt !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9591
|
+
"div",
|
|
9592
|
+
{
|
|
9593
|
+
ref: stillRef,
|
|
9594
|
+
"aria-hidden": "true",
|
|
9595
|
+
"data-qa-capture-still": "true",
|
|
9596
|
+
className: "qa-fixed qa-inset-0 qa-z-10089",
|
|
9597
|
+
style: { pointerEvents: "none", overflow: "hidden" }
|
|
9598
|
+
}
|
|
9599
|
+
),
|
|
9572
9600
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9573
9601
|
"div",
|
|
9574
9602
|
{
|
|
@@ -9627,7 +9655,7 @@ function CaptureMode() {
|
|
|
9627
9655
|
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9628
9656
|
"button",
|
|
9629
9657
|
{
|
|
9630
|
-
onClick: () => void
|
|
9658
|
+
onClick: () => void photographNow(),
|
|
9631
9659
|
title: t("exact_hint"),
|
|
9632
9660
|
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",
|
|
9633
9661
|
style: { background: "transparent", cursor: "pointer" },
|
|
@@ -10071,8 +10099,8 @@ function CaptureMode() {
|
|
|
10071
10099
|
{
|
|
10072
10100
|
type: "button",
|
|
10073
10101
|
onClick: () => void (async () => {
|
|
10074
|
-
const
|
|
10075
|
-
if (
|
|
10102
|
+
const shot2 = await photographNow();
|
|
10103
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10076
10104
|
})(),
|
|
10077
10105
|
title: t("exact_hint"),
|
|
10078
10106
|
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",
|
|
@@ -10398,5 +10426,5 @@ function Qapture({ config }) {
|
|
|
10398
10426
|
exports.Qapture = Qapture;
|
|
10399
10427
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10400
10428
|
exports.initQaStudio = initQaStudio;
|
|
10401
|
-
//# sourceMappingURL=chunk-
|
|
10402
|
-
//# sourceMappingURL=chunk-
|
|
10429
|
+
//# sourceMappingURL=chunk-E7OFH6EM.cjs.map
|
|
10430
|
+
//# sourceMappingURL=chunk-E7OFH6EM.cjs.map
|