qapture2 0.7.9 → 0.8.2
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 +50 -12
- package/dist/bin/init.cjs +1 -1
- package/dist/{chunk-6UNSUNJS.cjs → chunk-IYQ5JZJX.cjs} +485 -227
- package/dist/{chunk-G6O4SIEH.js → chunk-JGSKPW7Y.js} +485 -227
- 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 */
|
|
@@ -1722,11 +1725,15 @@ var STR = {
|
|
|
1722
1725
|
sel_region: "Region selected",
|
|
1723
1726
|
sel_element: "Element selected",
|
|
1724
1727
|
capturing: "capturing screenshot\u2026",
|
|
1728
|
+
too_heavy: "This page is too big to redraw \u2014 the tab would freeze. Photograph it instead: one frame, and maps and charts come out right.",
|
|
1725
1729
|
no_shot: "no screenshot (location saved)",
|
|
1726
1730
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1727
1731
|
save_point: "Save point",
|
|
1732
|
+
save_next: "Save + next",
|
|
1733
|
+
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
1734
|
+
shot_reused: "{n} saved from this screenshot",
|
|
1728
1735
|
reselect: "Reselect",
|
|
1729
|
-
save_hint: "\u2318/Ctrl + Enter to save",
|
|
1736
|
+
save_hint: "\u2318/Ctrl + Enter to save \xB7 add Shift to keep the same screenshot",
|
|
1730
1737
|
login_with: "Log in with {field} + password.",
|
|
1731
1738
|
used_count: "{n}/{m} used",
|
|
1732
1739
|
used: "used",
|
|
@@ -1762,6 +1769,9 @@ var STR = {
|
|
|
1762
1769
|
sev_design: "Design",
|
|
1763
1770
|
sev_enhance: "Enhance",
|
|
1764
1771
|
status_open: "Open",
|
|
1772
|
+
status_sent: "Sent",
|
|
1773
|
+
check_fixes: "Check the {n} fixes",
|
|
1774
|
+
check_fixes_hint: "Walk back through everything the last export sent, one spot at a time, and say whether each is now what you asked for.",
|
|
1765
1775
|
status_verified: "Verified",
|
|
1766
1776
|
context_attached: "{n} runtime events attached",
|
|
1767
1777
|
start_walkthrough: "Start walkthrough",
|
|
@@ -1776,7 +1786,7 @@ var STR = {
|
|
|
1776
1786
|
no_evidence: "ticked, no capture",
|
|
1777
1787
|
expected_label: "Expected",
|
|
1778
1788
|
exact_label: "Pixel-exact shots",
|
|
1779
|
-
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.",
|
|
1780
1790
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1781
1791
|
redraw_action: "Use real screenshots",
|
|
1782
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.",
|
|
@@ -1785,7 +1795,7 @@ var STR = {
|
|
|
1785
1795
|
exact_off: "Back to redrawn screenshots",
|
|
1786
1796
|
exact_declined: "Staying on redrawn screenshots",
|
|
1787
1797
|
exact_turn_on: "Turn on",
|
|
1788
|
-
exact_unsupported: "Needs
|
|
1798
|
+
exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
|
|
1789
1799
|
sync_title: "Save to a folder",
|
|
1790
1800
|
sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
|
|
1791
1801
|
sync_choose: "Choose folder",
|
|
@@ -1930,11 +1940,15 @@ var STR = {
|
|
|
1930
1940
|
sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
|
|
1931
1941
|
sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
|
|
1932
1942
|
capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
|
|
1943
|
+
too_heavy: "\u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u0623\u0643\u0628\u0631 \u0645\u0646 \u0623\u0646 \u064A\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627 \u2014 \u0633\u064A\u062A\u062C\u0645\u0651\u062F \u0627\u0644\u062A\u0628\u0648\u064A\u0628. \u0635\u0648\u0651\u0631\u0647\u0627 \u0628\u062F\u0644 \u0630\u0644\u0643: \u0625\u0637\u0627\u0631 \u0648\u0627\u062D\u062F\u060C \u0648\u062A\u0638\u0647\u0631 \u0627\u0644\u062E\u0631\u0627\u0626\u0637 \u0648\u0627\u0644\u0631\u0633\u0648\u0645 \u0643\u0645\u0627 \u0647\u064A.",
|
|
1933
1944
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1934
1945
|
annotate_placeholder: "\u0645\u0627\u0630\u0627 \u062A\u0631\u064A\u062F \u0623\u0646 \u062A\u0641\u0639\u0644 \u0647\u0646\u0627\u061F (\u0625\u0636\u0627\u0641\u0629 / \u062D\u0630\u0641 / \u062A\u063A\u064A\u064A\u0631\u2026)",
|
|
1935
1946
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1947
|
+
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1948
|
+
save_next_hint: "\u0627\u062D\u0641\u0638 \u0647\u0630\u0647 \u0648\u062D\u062F\u0651\u062F \u062C\u0632\u0621\u064B\u0627 \u0622\u062E\u0631 \u0645\u0646 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629 \u2014 \u0628\u062F\u0648\u0646 \u0637\u0644\u0628 \u0625\u0630\u0646 \u062C\u062F\u064A\u062F.",
|
|
1949
|
+
shot_reused: "\u062D\u064F\u0641\u0638\u062A {n} \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1936
1950
|
reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
|
|
1937
|
-
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
|
|
1951
|
+
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638 \xB7 \u0623\u0636\u0641 Shift \u0644\u0644\u0628\u0642\u0627\u0621 \u0639\u0644\u0649 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1938
1952
|
login_with: "\u0633\u062C\u0651\u0644 \u0627\u0644\u062F\u062E\u0648\u0644 \u0628\u0640 {field} + \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631.",
|
|
1939
1953
|
used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
1940
1954
|
used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
@@ -1970,6 +1984,9 @@ var STR = {
|
|
|
1970
1984
|
sev_design: "Design",
|
|
1971
1985
|
sev_enhance: "Enhance",
|
|
1972
1986
|
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1987
|
+
status_sent: "\u0645\u064F\u0631\u0633\u064E\u0644",
|
|
1988
|
+
check_fixes: "\u062A\u062D\u0642\u0651\u0642 \u0645\u0646 {n} \u062A\u0639\u062F\u064A\u0644\u0627\u062A",
|
|
1989
|
+
check_fixes_hint: "\u0645\u0631\u0651 \u0639\u0644\u0649 \u0643\u0644 \u0645\u0627 \u0623\u0631\u0633\u0644\u0647 \u0627\u0644\u062A\u0635\u062F\u064A\u0631 \u0627\u0644\u0623\u062E\u064A\u0631\u060C \u0646\u0642\u0637\u0629 \u0646\u0642\u0637\u0629\u060C \u0648\u0642\u0644 \u0647\u0644 \u0635\u0627\u0631 \u0643\u0645\u0627 \u0637\u0644\u0628\u062A \u0623\u0645 \u0644\u0627.",
|
|
1973
1990
|
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1974
1991
|
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1975
1992
|
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
@@ -1984,7 +2001,7 @@ var STR = {
|
|
|
1984
2001
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1985
2002
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1986
2003
|
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. \
|
|
2004
|
+
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
2005
|
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
2006
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1990
2007
|
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 +2010,7 @@ var STR = {
|
|
|
1993
2010
|
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
2011
|
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
2012
|
exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
|
|
1996
|
-
exact_unsupported: "\u064A\u062A\u0637\u0644\u0628
|
|
2013
|
+
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
2014
|
sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
|
|
1998
2015
|
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
2016
|
sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
|
|
@@ -2298,7 +2315,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2298
2315
|
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2299
2316
|
const originX = tl.x - centres.tl.x * scaleX;
|
|
2300
2317
|
const originY = tl.y - centres.tl.y * scaleY;
|
|
2301
|
-
const
|
|
2318
|
+
const mapping = { scaleX, scaleY, originX, originY };
|
|
2302
2319
|
const farX = originX + vw * scaleX;
|
|
2303
2320
|
const farY = originY + vh * scaleY;
|
|
2304
2321
|
const slack = Math.max(4, frameW * 0.01);
|
|
@@ -2317,7 +2334,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2317
2334
|
verified++;
|
|
2318
2335
|
}
|
|
2319
2336
|
if (verified === 0) return null;
|
|
2320
|
-
return
|
|
2337
|
+
return mapping;
|
|
2321
2338
|
}
|
|
2322
2339
|
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2323
2340
|
const x0 = m.originX + rect.left * m.scaleX;
|
|
@@ -2329,73 +2346,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
|
|
|
2329
2346
|
if (sw < 1 || sh < 1) return null;
|
|
2330
2347
|
return { sx, sy, sw, sh };
|
|
2331
2348
|
}
|
|
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
2349
|
|
|
2345
2350
|
// src/lib/screenCapture.ts
|
|
2346
2351
|
var ASPECT_TOLERANCE = 0.08;
|
|
2347
2352
|
var CALIBRATION_SETTLE_MS = 220;
|
|
2348
2353
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2349
2354
|
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
2355
|
function isExactCaptureSupported() {
|
|
2356
2356
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2357
2357
|
const md = navigator.mediaDevices;
|
|
2358
2358
|
return !!md && typeof md.getDisplayMedia === "function";
|
|
2359
2359
|
}
|
|
2360
|
-
var
|
|
2361
|
-
var
|
|
2362
|
-
var
|
|
2360
|
+
var armed = false;
|
|
2361
|
+
var declined = false;
|
|
2362
|
+
var frozen = null;
|
|
2363
|
+
var lastMode = null;
|
|
2364
|
+
function armExactCapture() {
|
|
2365
|
+
if (!isExactCaptureSupported()) return false;
|
|
2366
|
+
armed = true;
|
|
2367
|
+
declined = false;
|
|
2368
|
+
return true;
|
|
2369
|
+
}
|
|
2370
|
+
function disarmExactCapture() {
|
|
2371
|
+
armed = false;
|
|
2372
|
+
releaseFrozenFrame();
|
|
2373
|
+
}
|
|
2374
|
+
function resetExactCaptureDecline() {
|
|
2375
|
+
declined = false;
|
|
2376
|
+
}
|
|
2363
2377
|
function getExactCaptureStatus() {
|
|
2364
2378
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2365
|
-
if (
|
|
2379
|
+
if (armed) return "live";
|
|
2366
2380
|
if (declined) return "declined";
|
|
2367
2381
|
return "idle";
|
|
2368
2382
|
}
|
|
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();
|
|
2383
|
+
function getFrozenFrame() {
|
|
2384
|
+
return frozen;
|
|
2396
2385
|
}
|
|
2397
|
-
function
|
|
2398
|
-
|
|
2386
|
+
function releaseFrozenFrame() {
|
|
2387
|
+
if (frozen) {
|
|
2388
|
+
frozen.canvas.width = 0;
|
|
2389
|
+
frozen.canvas.height = 0;
|
|
2390
|
+
}
|
|
2391
|
+
frozen = null;
|
|
2399
2392
|
}
|
|
2400
2393
|
function waitFor(test, timeoutMs) {
|
|
2401
2394
|
if (test()) return Promise.resolve(true);
|
|
@@ -2435,17 +2428,28 @@ function nextPresentedFrame(el) {
|
|
|
2435
2428
|
});
|
|
2436
2429
|
});
|
|
2437
2430
|
}
|
|
2438
|
-
function
|
|
2431
|
+
function looksLikeViewport(frameW, frameH) {
|
|
2439
2432
|
if (!frameW || !frameH) return false;
|
|
2440
2433
|
const vw = window.innerWidth;
|
|
2441
2434
|
const vh = window.innerHeight;
|
|
2442
2435
|
if (!vw || !vh) return false;
|
|
2443
|
-
|
|
2444
|
-
const viewAspect = vw / vh;
|
|
2445
|
-
return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
|
|
2436
|
+
return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
|
|
2446
2437
|
}
|
|
2447
|
-
async function
|
|
2448
|
-
|
|
2438
|
+
async function withOverlayHidden(fn) {
|
|
2439
|
+
const hosts = Array.from(
|
|
2440
|
+
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2441
|
+
);
|
|
2442
|
+
const previous = hosts.map((el) => el.style.visibility);
|
|
2443
|
+
for (const el of hosts) el.style.visibility = "hidden";
|
|
2444
|
+
try {
|
|
2445
|
+
return await fn();
|
|
2446
|
+
} finally {
|
|
2447
|
+
hosts.forEach((el, i) => {
|
|
2448
|
+
el.style.visibility = previous[i];
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
async function grabFrameCanvas(video, grabber) {
|
|
2449
2453
|
await nextPresentedFrame(video);
|
|
2450
2454
|
let source = video;
|
|
2451
2455
|
let w = video.videoWidth;
|
|
@@ -2476,11 +2480,10 @@ async function grabFrameCanvas() {
|
|
|
2476
2480
|
bitmap?.close?.();
|
|
2477
2481
|
return c;
|
|
2478
2482
|
}
|
|
2479
|
-
async function calibrate() {
|
|
2480
|
-
if (!video) return false;
|
|
2483
|
+
async function calibrate(video, grabber) {
|
|
2481
2484
|
const vw = window.innerWidth;
|
|
2482
2485
|
const vh = window.innerHeight;
|
|
2483
|
-
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return
|
|
2486
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
|
|
2484
2487
|
const card = document.createElement("div");
|
|
2485
2488
|
card.setAttribute("data-qa-calibration", "true");
|
|
2486
2489
|
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
@@ -2494,10 +2497,10 @@ async function calibrate() {
|
|
|
2494
2497
|
document.body.appendChild(card);
|
|
2495
2498
|
try {
|
|
2496
2499
|
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2497
|
-
const frame = await grabFrameCanvas();
|
|
2498
|
-
if (!frame) return
|
|
2500
|
+
const frame = await grabFrameCanvas(video, grabber);
|
|
2501
|
+
if (!frame) return null;
|
|
2499
2502
|
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2500
|
-
if (!ctx) return
|
|
2503
|
+
if (!ctx) return null;
|
|
2501
2504
|
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2502
2505
|
const found = deriveMapping(
|
|
2503
2506
|
findMarkerCandidates(data, frame.width, frame.height),
|
|
@@ -2506,20 +2509,40 @@ async function calibrate() {
|
|
|
2506
2509
|
frame.width,
|
|
2507
2510
|
frame.height
|
|
2508
2511
|
);
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
return true;
|
|
2512
|
+
frame.width = 0;
|
|
2513
|
+
frame.height = 0;
|
|
2514
|
+
return found;
|
|
2513
2515
|
} catch {
|
|
2514
|
-
return
|
|
2516
|
+
return null;
|
|
2515
2517
|
} finally {
|
|
2516
2518
|
card.remove();
|
|
2517
2519
|
}
|
|
2518
2520
|
}
|
|
2519
|
-
async function
|
|
2520
|
-
if (!isExactCaptureSupported()) return
|
|
2521
|
-
|
|
2522
|
-
|
|
2521
|
+
async function freezeViewport() {
|
|
2522
|
+
if (!isExactCaptureSupported()) return null;
|
|
2523
|
+
releaseFrozenFrame();
|
|
2524
|
+
let stream = null;
|
|
2525
|
+
let el = null;
|
|
2526
|
+
const release = () => {
|
|
2527
|
+
if (el) {
|
|
2528
|
+
try {
|
|
2529
|
+
el.pause();
|
|
2530
|
+
} catch {
|
|
2531
|
+
}
|
|
2532
|
+
el.srcObject = null;
|
|
2533
|
+
el.remove();
|
|
2534
|
+
el = null;
|
|
2535
|
+
}
|
|
2536
|
+
if (stream) {
|
|
2537
|
+
for (const t of stream.getTracks()) {
|
|
2538
|
+
try {
|
|
2539
|
+
t.stop();
|
|
2540
|
+
} catch {
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
stream = null;
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2523
2546
|
try {
|
|
2524
2547
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2525
2548
|
const constraints = {
|
|
@@ -2537,121 +2560,103 @@ async function startExactCapture() {
|
|
|
2537
2560
|
surfaceSwitching: "exclude",
|
|
2538
2561
|
systemAudio: "exclude"
|
|
2539
2562
|
};
|
|
2540
|
-
|
|
2541
|
-
const
|
|
2542
|
-
if (!
|
|
2543
|
-
for (const x of s.getTracks()) x.stop();
|
|
2563
|
+
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
2564
|
+
const track = stream.getVideoTracks()[0];
|
|
2565
|
+
if (!track) {
|
|
2544
2566
|
declined = true;
|
|
2545
|
-
return
|
|
2567
|
+
return null;
|
|
2546
2568
|
}
|
|
2547
|
-
const settings =
|
|
2569
|
+
const settings = track.getSettings();
|
|
2548
2570
|
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2549
|
-
|
|
2571
|
+
el = document.createElement("video");
|
|
2550
2572
|
el.setAttribute("data-qa-overlay", "true");
|
|
2551
2573
|
el.muted = true;
|
|
2552
2574
|
el.playsInline = true;
|
|
2553
2575
|
el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
2554
|
-
el.srcObject =
|
|
2576
|
+
el.srcObject = stream;
|
|
2555
2577
|
document.body.appendChild(el);
|
|
2556
2578
|
try {
|
|
2557
2579
|
await el.play();
|
|
2558
2580
|
} catch {
|
|
2559
2581
|
}
|
|
2560
|
-
const
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2563
|
-
|
|
2582
|
+
const video = el;
|
|
2583
|
+
const ready = await waitFor(
|
|
2584
|
+
() => video.videoWidth > 0 && video.videoHeight > 0,
|
|
2585
|
+
VIDEO_READY_TIMEOUT_MS
|
|
2586
|
+
);
|
|
2587
|
+
if (!ready) {
|
|
2564
2588
|
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;
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2572
2591
|
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
|
-
}
|
|
2592
|
+
const grabber = IC ? new IC(track) : null;
|
|
2593
|
+
const vw = window.innerWidth;
|
|
2594
|
+
const vh = window.innerHeight;
|
|
2595
|
+
const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
|
|
2596
|
+
const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
|
|
2597
|
+
if (mode === "surface" && !mapping) {
|
|
2598
|
+
declined = true;
|
|
2599
|
+
return null;
|
|
2585
2600
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2601
|
+
const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
|
|
2602
|
+
if (!raw) return null;
|
|
2603
|
+
let page;
|
|
2604
|
+
if (mode === "surface" && mapping) {
|
|
2605
|
+
const box = mapRectToFrame(
|
|
2606
|
+
{ left: 0, top: 0, width: vw, height: vh },
|
|
2607
|
+
mapping,
|
|
2608
|
+
raw.width,
|
|
2609
|
+
raw.height
|
|
2610
|
+
);
|
|
2611
|
+
if (!box) return null;
|
|
2612
|
+
page = document.createElement("canvas");
|
|
2613
|
+
page.width = box.sw;
|
|
2614
|
+
page.height = box.sh;
|
|
2615
|
+
const ctx = page.getContext("2d");
|
|
2616
|
+
if (!ctx) return null;
|
|
2617
|
+
ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
|
|
2618
|
+
raw.width = 0;
|
|
2619
|
+
raw.height = 0;
|
|
2620
|
+
} else {
|
|
2621
|
+
page = raw;
|
|
2622
|
+
}
|
|
2623
|
+
frozen = {
|
|
2624
|
+
canvas: page,
|
|
2625
|
+
mode,
|
|
2626
|
+
viewportWidth: vw,
|
|
2627
|
+
viewportHeight: vh,
|
|
2628
|
+
takenAt: Date.now()
|
|
2629
|
+
};
|
|
2630
|
+
lastMode = mode;
|
|
2631
|
+
return frozen;
|
|
2590
2632
|
} catch {
|
|
2591
2633
|
declined = true;
|
|
2592
|
-
|
|
2593
|
-
|
|
2634
|
+
return null;
|
|
2635
|
+
} finally {
|
|
2636
|
+
release();
|
|
2594
2637
|
}
|
|
2595
2638
|
}
|
|
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 {
|
|
2639
|
+
function cropFrozenRegion(rect) {
|
|
2640
|
+
const f = frozen;
|
|
2641
|
+
if (!f || !f.canvas.width || !f.canvas.height) return null;
|
|
2642
|
+
if (!f.viewportWidth || !f.viewportHeight) return null;
|
|
2643
|
+
if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
|
|
2651
2644
|
return null;
|
|
2652
|
-
} finally {
|
|
2653
|
-
if (bitmap && typeof bitmap.close === "function") bitmap.close();
|
|
2654
2645
|
}
|
|
2646
|
+
const scaleX = f.canvas.width / f.viewportWidth;
|
|
2647
|
+
const scaleY = f.canvas.height / f.viewportHeight;
|
|
2648
|
+
const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
|
|
2649
|
+
const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
|
|
2650
|
+
const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
|
|
2651
|
+
const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
|
|
2652
|
+
if (sw < 1 || sh < 1) return null;
|
|
2653
|
+
const out = document.createElement("canvas");
|
|
2654
|
+
out.width = sw;
|
|
2655
|
+
out.height = sh;
|
|
2656
|
+
const ctx = out.getContext("2d");
|
|
2657
|
+
if (!ctx) return null;
|
|
2658
|
+
ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2659
|
+
return out;
|
|
2655
2660
|
}
|
|
2656
2661
|
|
|
2657
2662
|
// src/lib/cssColors.ts
|
|
@@ -2795,12 +2800,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2795
2800
|
|
|
2796
2801
|
// src/lib/capture.ts
|
|
2797
2802
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2803
|
+
var CHUNK_TIMEOUT_MS = 8e3;
|
|
2798
2804
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
2799
2805
|
var MAX_SHOT_EDGE = 1800;
|
|
2800
2806
|
var WEBP_QUALITY = 0.92;
|
|
2801
2807
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2802
2808
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2803
2809
|
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2810
|
+
var TOO_HEAVY_NODES = 6e3;
|
|
2811
|
+
function documentWeight() {
|
|
2812
|
+
try {
|
|
2813
|
+
return document.getElementsByTagName("*").length;
|
|
2814
|
+
} catch {
|
|
2815
|
+
return 0;
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2804
2818
|
function withTimeout(promise, ms) {
|
|
2805
2819
|
let timer;
|
|
2806
2820
|
return Promise.race([
|
|
@@ -2862,8 +2876,13 @@ function fitToBudget(canvas) {
|
|
|
2862
2876
|
function canvasToBlob(canvas, type, quality) {
|
|
2863
2877
|
return new Promise((resolve) => {
|
|
2864
2878
|
if (canvas.toBlob) {
|
|
2865
|
-
|
|
2866
|
-
|
|
2879
|
+
try {
|
|
2880
|
+
canvas.toBlob((b) => resolve(b), type, quality);
|
|
2881
|
+
return;
|
|
2882
|
+
} catch {
|
|
2883
|
+
resolve(null);
|
|
2884
|
+
return;
|
|
2885
|
+
}
|
|
2867
2886
|
}
|
|
2868
2887
|
try {
|
|
2869
2888
|
const dataUrl = canvas.toDataURL(type, quality);
|
|
@@ -2890,20 +2909,6 @@ function shotExtension(blob) {
|
|
|
2890
2909
|
if (type === "image/jpeg") return "jpg";
|
|
2891
2910
|
return "png";
|
|
2892
2911
|
}
|
|
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
2912
|
function markStuckElements() {
|
|
2908
2913
|
let all;
|
|
2909
2914
|
try {
|
|
@@ -3064,7 +3069,9 @@ function planRender(rect, sx, sy, vw, vh) {
|
|
|
3064
3069
|
};
|
|
3065
3070
|
}
|
|
3066
3071
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
3067
|
-
const
|
|
3072
|
+
const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
|
|
3073
|
+
if (!mod) return null;
|
|
3074
|
+
const { default: html2canvas } = mod;
|
|
3068
3075
|
const { vw, vh } = viewportSize();
|
|
3069
3076
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
3070
3077
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -3083,7 +3090,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
3083
3090
|
height: plan.render.height,
|
|
3084
3091
|
scale,
|
|
3085
3092
|
useCORS: true,
|
|
3086
|
-
allowTaint: true
|
|
3093
|
+
// MUST stay false. `allowTaint: true` tells html2canvas to draw
|
|
3094
|
+
// cross-origin images it could not fetch with CORS -- which succeeds,
|
|
3095
|
+
// and TAINTS the canvas. A tainted canvas throws SecurityError from
|
|
3096
|
+
// toBlob/toDataURL, so the render finishes and then the encode dies,
|
|
3097
|
+
// and the tester gets "couldn't capture" with nothing to look at.
|
|
3098
|
+
//
|
|
3099
|
+
// That is one image away on any real page: a map tile, a news
|
|
3100
|
+
// thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
|
|
3101
|
+
// fail 100% of the time while a plain page worked, which is exactly
|
|
3102
|
+
// the "it works for you and not for me" this kept producing.
|
|
3103
|
+
//
|
|
3104
|
+
// False is strictly better: html2canvas SKIPS the images it cannot
|
|
3105
|
+
// read, so those come out blank and everything else comes out right.
|
|
3106
|
+
// A screenshot with two grey rectangles in it is worth having. A
|
|
3107
|
+
// SecurityError is not.
|
|
3108
|
+
allowTaint: false,
|
|
3087
3109
|
// The page's own background, never null — see resolvePageBackground().
|
|
3088
3110
|
// Passed straight to html2canvas's parser, so it gets the same
|
|
3089
3111
|
// CSS Color 4 treatment the document walk applies.
|
|
@@ -3118,20 +3140,24 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3118
3140
|
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3119
3141
|
const sx = scroll?.x ?? window.scrollX;
|
|
3120
3142
|
const sy = scroll?.y ?? window.scrollY;
|
|
3121
|
-
if (prefer !== "dom" &&
|
|
3143
|
+
if (prefer !== "dom" && getFrozenFrame()) {
|
|
3122
3144
|
const visible = intersectViewport(rect);
|
|
3123
3145
|
if (!visible) return { status: "empty" };
|
|
3124
3146
|
try {
|
|
3125
|
-
const canvas =
|
|
3147
|
+
const canvas = cropFrozenRegion(visible);
|
|
3126
3148
|
if (canvas) {
|
|
3127
3149
|
const blob = await encodeShot(canvas);
|
|
3128
3150
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3129
3151
|
}
|
|
3130
3152
|
} catch (err) {
|
|
3131
|
-
console.warn("[QA]
|
|
3153
|
+
console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
|
|
3132
3154
|
}
|
|
3133
3155
|
if (prefer === "exact") return { status: "failed" };
|
|
3134
3156
|
}
|
|
3157
|
+
const weight = documentWeight();
|
|
3158
|
+
if (weight > TOO_HEAVY_NODES) {
|
|
3159
|
+
return { status: "too-heavy", nodes: weight };
|
|
3160
|
+
}
|
|
3135
3161
|
try {
|
|
3136
3162
|
const canvas = await captureViaDom(rect, sx, sy);
|
|
3137
3163
|
if (!canvas) return { status: "failed" };
|
|
@@ -3189,6 +3215,12 @@ function formatEvent(ev, t0) {
|
|
|
3189
3215
|
}
|
|
3190
3216
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3191
3217
|
}
|
|
3218
|
+
function noteCheckLine(note, index) {
|
|
3219
|
+
const where = oneLine(note.route) || "/";
|
|
3220
|
+
const what = oneLine(note.description) || "(no description)";
|
|
3221
|
+
const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
|
|
3222
|
+
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3223
|
+
}
|
|
3192
3224
|
function noteToMarkdown(note, opts) {
|
|
3193
3225
|
const brand = opts?.brand ?? "Qapture";
|
|
3194
3226
|
const idx = opts?.index;
|
|
@@ -3246,6 +3278,14 @@ function noteToMarkdown(note, opts) {
|
|
|
3246
3278
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3247
3279
|
});
|
|
3248
3280
|
}
|
|
3281
|
+
if (idx != null) {
|
|
3282
|
+
lines.push("");
|
|
3283
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3284
|
+
lines.push("");
|
|
3285
|
+
lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
|
|
3286
|
+
lines.push("");
|
|
3287
|
+
lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
|
|
3288
|
+
}
|
|
3249
3289
|
const ctx = note.context;
|
|
3250
3290
|
if (ctx) {
|
|
3251
3291
|
const env = ctx.env;
|
|
@@ -3418,6 +3458,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
|
|
|
3418
3458
|
The tag sets your starting posture; **the words win.** A point tagged Design that says "this crashes" is a bug \u2014 treat it as one, and mention that you changed lane. People pick the wrong chip all the time; that is not a reason to do the wrong work.
|
|
3419
3459
|
|
|
3420
3460
|
And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
|
|
3461
|
+
);
|
|
3462
|
+
sections.push(
|
|
3463
|
+
`## You Are Being Graded On This
|
|
3464
|
+
|
|
3465
|
+
This archive is not a list of suggestions. Every point in \`notes.md\` is an acceptance test, and \`verify.md\` is the checklist those tests live in \u2014 one unticked box per point.
|
|
3466
|
+
|
|
3467
|
+
**What happens next.** The tester will be walked back through these same checks on the real page, in this order, one stop at a time, and asked about each: *is this now what I asked for?* Anything they say is still wrong comes back to you as round two with their new words attached, so nothing quietly disappears.
|
|
3468
|
+
|
|
3469
|
+
**What to do.** Do the work, then fill in \`verify.md\`: tick a box only when the check is true on a fresh load of the page named beside it, with the tester doing nothing extra. Where you could not do something, or you think it is the wrong thing to do, leave the box unticked and write one line saying why. An unticked box with a reason is a good answer. A ticked box that does not hold up is the only bad one \u2014 it costs the tester the trip to find out.
|
|
3470
|
+
|
|
3471
|
+
Hand \`verify.md\` back with the work.`
|
|
3421
3472
|
);
|
|
3422
3473
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3423
3474
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3565,6 +3616,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3565
3616
|
` : "";
|
|
3566
3617
|
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
3567
3618
|
zip.file("notes.md", notesMd);
|
|
3619
|
+
zip.file("verify.md", [
|
|
3620
|
+
`# ${brandLabel} \u2014 checks to satisfy`,
|
|
3621
|
+
"",
|
|
3622
|
+
`Exported: ${stamp}`,
|
|
3623
|
+
`Checks: ${notes.length}`,
|
|
3624
|
+
"",
|
|
3625
|
+
"Each line below is one thing the tester asked for. The full ask, the",
|
|
3626
|
+
"screenshot, the page and the element are in `notes.md` under the matching",
|
|
3627
|
+
'"Point" heading.',
|
|
3628
|
+
"",
|
|
3629
|
+
"**What is expected of you**",
|
|
3630
|
+
"",
|
|
3631
|
+
"1. Do the work in `notes.md`.",
|
|
3632
|
+
"2. Tick a box here only when the check is true on a fresh load of the page",
|
|
3633
|
+
" named beside it, with the tester doing nothing extra.",
|
|
3634
|
+
"3. Leave a box unticked and write one line under it saying why, if you",
|
|
3635
|
+
" could not do it or disagree with it. An unticked box with a reason is a",
|
|
3636
|
+
" good answer; a ticked box that is not true is the only bad one.",
|
|
3637
|
+
"4. Hand this file back with the work.",
|
|
3638
|
+
"",
|
|
3639
|
+
"The tester will then be walked back through these same checks on the real",
|
|
3640
|
+
"page, in this order, and asked about each one. Anything they say is still",
|
|
3641
|
+
"wrong comes back to you as round two, with their new words attached.",
|
|
3642
|
+
"",
|
|
3643
|
+
"---",
|
|
3644
|
+
"",
|
|
3645
|
+
...notes.map((n, i) => noteCheckLine(n, i + 1)),
|
|
3646
|
+
"",
|
|
3647
|
+
"---",
|
|
3648
|
+
"",
|
|
3649
|
+
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3650
|
+
""
|
|
3651
|
+
].join("\n"));
|
|
3568
3652
|
notes.forEach((n, i) => {
|
|
3569
3653
|
if (n.screenshot && shots) {
|
|
3570
3654
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4204,6 +4288,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
|
|
|
4204
4288
|
var LOGIN_KEY = "logins";
|
|
4205
4289
|
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
4206
4290
|
var EXACT_SHOTS_KEY = "exactShots";
|
|
4291
|
+
function exactShotsWanted(store) {
|
|
4292
|
+
return store.getItem(EXACT_SHOTS_KEY) !== "0";
|
|
4293
|
+
}
|
|
4207
4294
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4208
4295
|
var COMPACT_KEY = "compactCapture";
|
|
4209
4296
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
@@ -4218,11 +4305,12 @@ var PANEL_OPEN_KEY = "panelOpen";
|
|
|
4218
4305
|
var ACTIVE_TAB_KEY = "activeTab";
|
|
4219
4306
|
var WALK_KEY = "walk";
|
|
4220
4307
|
var SOFT_NAV_PROOF_MS = 700;
|
|
4308
|
+
var SIGNATURE_SEP = String.fromCharCode(0);
|
|
4221
4309
|
function pageSignature() {
|
|
4222
4310
|
if (typeof document === "undefined" || !document.body) return "";
|
|
4223
4311
|
const body = document.body;
|
|
4224
4312
|
const text = body.textContent || "";
|
|
4225
|
-
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(
|
|
4313
|
+
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
|
|
4226
4314
|
}
|
|
4227
4315
|
var GUIDE_SKIPPED_KEY = "guideSkipped";
|
|
4228
4316
|
var NOTICE_QUEUE_CAP = 3;
|
|
@@ -4280,8 +4368,12 @@ function QaProvider({
|
|
|
4280
4368
|
const [loginsUsed, setLoginsUsed] = useState(
|
|
4281
4369
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4282
4370
|
);
|
|
4283
|
-
const [exactStatus, setExactStatus] = useState(() =>
|
|
4371
|
+
const [exactStatus, setExactStatus] = useState(() => {
|
|
4372
|
+
if (exactShotsWanted(storage)) armExactCapture();
|
|
4373
|
+
return getExactCaptureStatus();
|
|
4374
|
+
});
|
|
4284
4375
|
const exactSupported = isExactCaptureSupported();
|
|
4376
|
+
const [frozenAt, setFrozenAt] = useState(null);
|
|
4285
4377
|
const [syncState, setSyncState] = useState(() => getFsSyncState());
|
|
4286
4378
|
const [syncTick, setSyncTick] = useState(0);
|
|
4287
4379
|
const [lastCampaign, setLastCampaign] = useState(
|
|
@@ -4782,20 +4874,26 @@ function QaProvider({
|
|
|
4782
4874
|
flushPendingDeletes();
|
|
4783
4875
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4784
4876
|
noticeTimers.current.clear();
|
|
4785
|
-
|
|
4877
|
+
releaseFrozenFrame();
|
|
4786
4878
|
};
|
|
4787
4879
|
}, [flushPendingDeletes]);
|
|
4788
4880
|
const startCapture = useCallback((prefill) => {
|
|
4789
4881
|
setIsOpen(false);
|
|
4790
4882
|
setCapturePrefill(prefill ?? "");
|
|
4791
4883
|
setCaptureActive(true);
|
|
4792
|
-
|
|
4884
|
+
setFrozenAt(null);
|
|
4885
|
+
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4793
4886
|
resetExactCaptureDecline();
|
|
4794
|
-
void
|
|
4887
|
+
void freezeViewport().then((frame) => {
|
|
4888
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4889
|
+
setExactStatus(getExactCaptureStatus());
|
|
4890
|
+
});
|
|
4795
4891
|
}
|
|
4796
4892
|
}, [storage]);
|
|
4797
4893
|
const endCapture = useCallback((reopen = true) => {
|
|
4798
4894
|
setCaptureActive(false);
|
|
4895
|
+
releaseFrozenFrame();
|
|
4896
|
+
setFrozenAt(null);
|
|
4799
4897
|
if (reopen) setIsOpen(true);
|
|
4800
4898
|
}, []);
|
|
4801
4899
|
const toggleGuide = useCallback((key) => {
|
|
@@ -4816,19 +4914,36 @@ function QaProvider({
|
|
|
4816
4914
|
return next;
|
|
4817
4915
|
});
|
|
4818
4916
|
}, [storage]);
|
|
4917
|
+
const stampHandedOff = useCallback(async (ids, at) => {
|
|
4918
|
+
if (!ids.length) return;
|
|
4919
|
+
const idSet = new Set(ids);
|
|
4920
|
+
const touched = [];
|
|
4921
|
+
applyNotes(
|
|
4922
|
+
(prev) => prev.map((n) => {
|
|
4923
|
+
if (!idSet.has(n.id)) return n;
|
|
4924
|
+
if ((n.status ?? "open") === "verified") return n;
|
|
4925
|
+
const next = { ...n, handedOffAt: at };
|
|
4926
|
+
touched.push(next);
|
|
4927
|
+
return next;
|
|
4928
|
+
})
|
|
4929
|
+
);
|
|
4930
|
+
for (const note of touched) await idb.put(note);
|
|
4931
|
+
}, [applyNotes, idb]);
|
|
4819
4932
|
const exportZipFn = useCallback(
|
|
4820
4933
|
async (filename) => {
|
|
4821
4934
|
if (!notes.length || isExporting) return;
|
|
4822
4935
|
setIsExporting(true);
|
|
4823
4936
|
try {
|
|
4824
|
-
|
|
4937
|
+
const stamp = nowIso();
|
|
4938
|
+
await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
|
|
4939
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
4825
4940
|
} catch (err) {
|
|
4826
4941
|
console.error("[QA] export failed", err);
|
|
4827
4942
|
} finally {
|
|
4828
4943
|
setIsExporting(false);
|
|
4829
4944
|
}
|
|
4830
4945
|
},
|
|
4831
|
-
[notes, isExporting, config, guideChecked, guideSkipped]
|
|
4946
|
+
[notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
|
|
4832
4947
|
);
|
|
4833
4948
|
const exportZipRef = useRef(async () => {
|
|
4834
4949
|
});
|
|
@@ -4836,16 +4951,32 @@ function QaProvider({
|
|
|
4836
4951
|
exportZipRef.current = exportZipFn;
|
|
4837
4952
|
}, [exportZipFn]);
|
|
4838
4953
|
const enableExactShots = useCallback(async () => {
|
|
4839
|
-
|
|
4840
|
-
const ok = await startExactCapture();
|
|
4954
|
+
const ok = armExactCapture();
|
|
4841
4955
|
setExactStatus(getExactCaptureStatus());
|
|
4842
4956
|
storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
|
|
4843
4957
|
notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
|
|
4844
4958
|
return ok;
|
|
4845
4959
|
}, [storage, notify, t]);
|
|
4960
|
+
const photographNow = useCallback(async () => {
|
|
4961
|
+
if (!armExactCapture()) {
|
|
4962
|
+
setExactStatus(getExactCaptureStatus());
|
|
4963
|
+
notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
|
|
4964
|
+
return false;
|
|
4965
|
+
}
|
|
4966
|
+
storage.setItem(EXACT_SHOTS_KEY, "1");
|
|
4967
|
+
const frame = await freezeViewport();
|
|
4968
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4969
|
+
setExactStatus(getExactCaptureStatus());
|
|
4970
|
+
notify(frame ? t("exact_on") : t("exact_declined"), {
|
|
4971
|
+
tone: frame ? "success" : "info",
|
|
4972
|
+
id: "exact-shots"
|
|
4973
|
+
});
|
|
4974
|
+
return !!frame;
|
|
4975
|
+
}, [storage, notify, t]);
|
|
4846
4976
|
const disableExactShots = useCallback(() => {
|
|
4847
|
-
|
|
4977
|
+
disarmExactCapture();
|
|
4848
4978
|
storage.setItem(EXACT_SHOTS_KEY, "0");
|
|
4979
|
+
setFrozenAt(null);
|
|
4849
4980
|
setExactStatus(getExactCaptureStatus());
|
|
4850
4981
|
}, [storage]);
|
|
4851
4982
|
const rememberCampaign = useCallback((next) => {
|
|
@@ -4998,6 +5129,7 @@ function QaProvider({
|
|
|
4998
5129
|
design: 0,
|
|
4999
5130
|
enhance: 0,
|
|
5000
5131
|
open: 0,
|
|
5132
|
+
sent: 0,
|
|
5001
5133
|
fixed: 0,
|
|
5002
5134
|
verified: 0,
|
|
5003
5135
|
thisPage: 0
|
|
@@ -5011,6 +5143,7 @@ function QaProvider({
|
|
|
5011
5143
|
if (status === "verified") counts.verified++;
|
|
5012
5144
|
else if (status === "fixed") counts.fixed++;
|
|
5013
5145
|
else counts.open++;
|
|
5146
|
+
if (n.handedOffAt && status !== "verified") counts.sent++;
|
|
5014
5147
|
if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
|
|
5015
5148
|
}
|
|
5016
5149
|
return counts;
|
|
@@ -5019,7 +5152,9 @@ function QaProvider({
|
|
|
5019
5152
|
const q = filter.query.trim().toLowerCase();
|
|
5020
5153
|
return notes.filter((n) => {
|
|
5021
5154
|
if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
|
|
5022
|
-
if (filter.status
|
|
5155
|
+
if (filter.status === "sent") {
|
|
5156
|
+
if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
|
|
5157
|
+
} else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
|
|
5023
5158
|
if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
|
|
5024
5159
|
if (!q) return true;
|
|
5025
5160
|
const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
|
|
@@ -5064,6 +5199,7 @@ function QaProvider({
|
|
|
5064
5199
|
const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
|
|
5065
5200
|
if (!blob) return { status: "unsupported" };
|
|
5066
5201
|
const outcome = await shareZipFile(blob, name, config.brand.label);
|
|
5202
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
5067
5203
|
if (outcome.status === "needs-gesture") {
|
|
5068
5204
|
setPendingShare({ blob, filename: name });
|
|
5069
5205
|
} else if (outcome.status === "unsupported") {
|
|
@@ -5075,7 +5211,7 @@ function QaProvider({
|
|
|
5075
5211
|
} finally {
|
|
5076
5212
|
setIsExporting(false);
|
|
5077
5213
|
}
|
|
5078
|
-
}, [notes, config, guideChecked, guideSkipped]);
|
|
5214
|
+
}, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
|
|
5079
5215
|
const sharePending = useCallback(async () => {
|
|
5080
5216
|
const pending = pendingShare;
|
|
5081
5217
|
if (!pending) return { status: "unsupported" };
|
|
@@ -5291,7 +5427,10 @@ function QaProvider({
|
|
|
5291
5427
|
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
|
|
5292
5428
|
} catch {
|
|
5293
5429
|
}
|
|
5294
|
-
if (which === "
|
|
5430
|
+
if (which === "verify") {
|
|
5431
|
+
setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
|
|
5432
|
+
startWalk("notes", 0);
|
|
5433
|
+
} else if (which === "retest") {
|
|
5295
5434
|
setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
|
|
5296
5435
|
startWalk("notes", 0);
|
|
5297
5436
|
} else if (which === "notes") {
|
|
@@ -5364,8 +5503,9 @@ function QaProvider({
|
|
|
5364
5503
|
gradeStep,
|
|
5365
5504
|
evidenceByStep,
|
|
5366
5505
|
// v0.4 — screenshots
|
|
5367
|
-
exactShots: { supported: exactSupported, status: exactStatus },
|
|
5506
|
+
exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
|
|
5368
5507
|
enableExactShots,
|
|
5508
|
+
photographNow,
|
|
5369
5509
|
disableExactShots,
|
|
5370
5510
|
// v0.4 — folder sync. syncTick is read here purely so this object is
|
|
5371
5511
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
@@ -7348,7 +7488,7 @@ function FilterChip({ chip }) {
|
|
|
7348
7488
|
);
|
|
7349
7489
|
}
|
|
7350
7490
|
function NoteFilterBar() {
|
|
7351
|
-
const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
|
|
7491
|
+
const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
|
|
7352
7492
|
const [confirmPrune, setConfirmPrune] = useState(false);
|
|
7353
7493
|
if (notes.length < 3) return null;
|
|
7354
7494
|
const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
|
|
@@ -7390,6 +7530,18 @@ function NoteFilterBar() {
|
|
|
7390
7530
|
tone: "accent",
|
|
7391
7531
|
onClick: sev("design")
|
|
7392
7532
|
},
|
|
7533
|
+
// Only ever shown once something has actually been sent. A chip reading
|
|
7534
|
+
// "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
|
|
7535
|
+
// morning after an export is the one thing the tester came back for.
|
|
7536
|
+
...noteCounts.sent > 0 ? [{
|
|
7537
|
+
key: "sent",
|
|
7538
|
+
label: t("status_sent"),
|
|
7539
|
+
count: noteCounts.sent,
|
|
7540
|
+
active: filter.status === "sent",
|
|
7541
|
+
icon: "Camera",
|
|
7542
|
+
tone: "accent",
|
|
7543
|
+
onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
|
|
7544
|
+
}] : [],
|
|
7393
7545
|
{
|
|
7394
7546
|
key: "open",
|
|
7395
7547
|
label: t("status_open"),
|
|
@@ -7458,6 +7610,24 @@ function NoteFilterBar() {
|
|
|
7458
7610
|
}
|
|
7459
7611
|
),
|
|
7460
7612
|
/* @__PURE__ */ jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsx(FilterChip, { chip }, chip.key)) }),
|
|
7613
|
+
noteCounts.sent > 0 && /* @__PURE__ */ jsxs(
|
|
7614
|
+
"button",
|
|
7615
|
+
{
|
|
7616
|
+
type: "button",
|
|
7617
|
+
"data-qa-verify-walk": "true",
|
|
7618
|
+
onClick: () => {
|
|
7619
|
+
setFilter({ status: "sent", severity: "all", thisPageOnly: false });
|
|
7620
|
+
startWalk("notes", 0);
|
|
7621
|
+
},
|
|
7622
|
+
title: t("check_fixes_hint"),
|
|
7623
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-xs qa-text-hi qa-focus-ring",
|
|
7624
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7625
|
+
children: [
|
|
7626
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
7627
|
+
t("check_fixes", { n: noteCounts.sent })
|
|
7628
|
+
]
|
|
7629
|
+
}
|
|
7630
|
+
),
|
|
7461
7631
|
showPrune && /* @__PURE__ */ jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxs("span", { className: "qa-text-mid", children: [
|
|
7462
7632
|
t("keep_only_retest_q", { n: others.length }),
|
|
7463
7633
|
" ",
|
|
@@ -9198,7 +9368,7 @@ function CaptureMode() {
|
|
|
9198
9368
|
compactCapture,
|
|
9199
9369
|
setCompactCapture,
|
|
9200
9370
|
exactShots,
|
|
9201
|
-
|
|
9371
|
+
photographNow,
|
|
9202
9372
|
capturePrefill
|
|
9203
9373
|
} = useQa();
|
|
9204
9374
|
const coarse = useCoarsePointer();
|
|
@@ -9212,10 +9382,13 @@ function CaptureMode() {
|
|
|
9212
9382
|
const [candidate, setCandidate] = useState(null);
|
|
9213
9383
|
const [regionMode, setRegionMode] = useState(false);
|
|
9214
9384
|
const [shot, setShot] = useState(null);
|
|
9385
|
+
const stillRef = useRef(null);
|
|
9386
|
+
const [notesFromThisShot, setNotesFromThisShot] = useState(0);
|
|
9215
9387
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9216
9388
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9217
9389
|
const [capturing, setCapturing] = useState(false);
|
|
9218
9390
|
const [captureError, setCaptureError] = useState(false);
|
|
9391
|
+
const [tooHeavy, setTooHeavy] = useState(false);
|
|
9219
9392
|
const [description, setDescription] = useState(capturePrefill);
|
|
9220
9393
|
const [severity, setSeverity] = useState("bug");
|
|
9221
9394
|
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
@@ -9245,6 +9418,7 @@ function CaptureMode() {
|
|
|
9245
9418
|
const runCapture = useCallback(async (rect) => {
|
|
9246
9419
|
setCapturing(true);
|
|
9247
9420
|
setCaptureError(false);
|
|
9421
|
+
setTooHeavy(false);
|
|
9248
9422
|
lockPageScroll();
|
|
9249
9423
|
try {
|
|
9250
9424
|
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
@@ -9255,6 +9429,7 @@ function CaptureMode() {
|
|
|
9255
9429
|
return;
|
|
9256
9430
|
}
|
|
9257
9431
|
setCaptureError(outcome.status === "failed");
|
|
9432
|
+
setTooHeavy(outcome.status === "too-heavy");
|
|
9258
9433
|
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
9259
9434
|
setShot(blob);
|
|
9260
9435
|
setShotUrl((old) => {
|
|
@@ -9504,7 +9679,29 @@ function CaptureMode() {
|
|
|
9504
9679
|
useEffect(() => () => {
|
|
9505
9680
|
unlockPageScroll();
|
|
9506
9681
|
}, []);
|
|
9507
|
-
|
|
9682
|
+
useEffect(() => {
|
|
9683
|
+
const host = stillRef.current;
|
|
9684
|
+
const frame = getFrozenFrame();
|
|
9685
|
+
if (!host || !frame) return;
|
|
9686
|
+
const canvas = frame.canvas;
|
|
9687
|
+
canvas.style.cssText = "display:block;width:100%;height:100%;";
|
|
9688
|
+
host.appendChild(canvas);
|
|
9689
|
+
return () => {
|
|
9690
|
+
if (canvas.parentNode === host) host.removeChild(canvas);
|
|
9691
|
+
canvas.removeAttribute("style");
|
|
9692
|
+
};
|
|
9693
|
+
}, [exactShots.frozenAt]);
|
|
9694
|
+
const resetForNextNote = useCallback(() => {
|
|
9695
|
+
setPhase("selecting");
|
|
9696
|
+
setSelection(null);
|
|
9697
|
+
setShot(null);
|
|
9698
|
+
setShotEngine(null);
|
|
9699
|
+
setDescription("");
|
|
9700
|
+
setSeverity("bug");
|
|
9701
|
+
setTargetForensics(void 0);
|
|
9702
|
+
setCaptureError(false);
|
|
9703
|
+
}, []);
|
|
9704
|
+
const save = async (keepGoing = false) => {
|
|
9508
9705
|
if (!selection || !description.trim()) return;
|
|
9509
9706
|
const target = {
|
|
9510
9707
|
kind: selection.kind,
|
|
@@ -9526,6 +9723,11 @@ function CaptureMode() {
|
|
|
9526
9723
|
severity,
|
|
9527
9724
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9528
9725
|
});
|
|
9726
|
+
if (keepGoing) {
|
|
9727
|
+
setNotesFromThisShot((n) => n + 1);
|
|
9728
|
+
resetForNextNote();
|
|
9729
|
+
return;
|
|
9730
|
+
}
|
|
9529
9731
|
endCapture();
|
|
9530
9732
|
};
|
|
9531
9733
|
const popStyleFor = useCallback((r, cardWidth = 340) => {
|
|
@@ -9562,6 +9764,16 @@ function CaptureMode() {
|
|
|
9562
9764
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
9563
9765
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
9564
9766
|
return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
9767
|
+
exactShots.frozenAt !== null && /* @__PURE__ */ jsx(
|
|
9768
|
+
"div",
|
|
9769
|
+
{
|
|
9770
|
+
ref: stillRef,
|
|
9771
|
+
"aria-hidden": "true",
|
|
9772
|
+
"data-qa-capture-still": "true",
|
|
9773
|
+
className: "qa-fixed qa-inset-0 qa-z-10089",
|
|
9774
|
+
style: { pointerEvents: "none", overflow: "hidden" }
|
|
9775
|
+
}
|
|
9776
|
+
),
|
|
9565
9777
|
/* @__PURE__ */ jsx(
|
|
9566
9778
|
"div",
|
|
9567
9779
|
{
|
|
@@ -9583,6 +9795,21 @@ function CaptureMode() {
|
|
|
9583
9795
|
{
|
|
9584
9796
|
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-border qa-border-subtle qa-bg-2 qa-px-4 qa-py-2 qa-text-sm qa-text-hi qa-elev-2",
|
|
9585
9797
|
children: [
|
|
9798
|
+
notesFromThisShot > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
9799
|
+
/* @__PURE__ */ jsxs(
|
|
9800
|
+
"span",
|
|
9801
|
+
{
|
|
9802
|
+
"data-qa-shot-reused": "true",
|
|
9803
|
+
className: "qa-flex qa-items-center qa-gap-1.5",
|
|
9804
|
+
style: { color: "var(--qa-success)" },
|
|
9805
|
+
children: [
|
|
9806
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 16 }),
|
|
9807
|
+
t("shot_reused", { n: String(notesFromThisShot) })
|
|
9808
|
+
]
|
|
9809
|
+
}
|
|
9810
|
+
),
|
|
9811
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "\xB7" })
|
|
9812
|
+
] }),
|
|
9586
9813
|
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
9587
9814
|
/* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
9588
9815
|
t("cap_click")
|
|
@@ -9620,7 +9847,7 @@ function CaptureMode() {
|
|
|
9620
9847
|
) : /* @__PURE__ */ jsxs(
|
|
9621
9848
|
"button",
|
|
9622
9849
|
{
|
|
9623
|
-
onClick: () => void
|
|
9850
|
+
onClick: () => void photographNow(),
|
|
9624
9851
|
title: t("exact_hint"),
|
|
9625
9852
|
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
9853
|
style: { background: "transparent", cursor: "pointer" },
|
|
@@ -10032,6 +10259,29 @@ function CaptureMode() {
|
|
|
10032
10259
|
)
|
|
10033
10260
|
]
|
|
10034
10261
|
}
|
|
10262
|
+
) : tooHeavy ? (
|
|
10263
|
+
// Not a failure to report — a refusal to explain, with the one
|
|
10264
|
+
// control that actually solves it.
|
|
10265
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3 qa-px-3 qa-text-center", children: [
|
|
10266
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
|
|
10267
|
+
exactShots.supported ? /* @__PURE__ */ jsxs(
|
|
10268
|
+
"button",
|
|
10269
|
+
{
|
|
10270
|
+
type: "button",
|
|
10271
|
+
onClick: () => void (async () => {
|
|
10272
|
+
const shot2 = await photographNow();
|
|
10273
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10274
|
+
})(),
|
|
10275
|
+
title: t("exact_hint"),
|
|
10276
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-hi qa-focus-ring",
|
|
10277
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10278
|
+
children: [
|
|
10279
|
+
/* @__PURE__ */ jsx(Icon, { name: "Camera", size: 13 }),
|
|
10280
|
+
t("redraw_action")
|
|
10281
|
+
]
|
|
10282
|
+
}
|
|
10283
|
+
) : /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
|
|
10284
|
+
] })
|
|
10035
10285
|
) : captureError ? (
|
|
10036
10286
|
// The render broke rather than being skipped — offer a retry
|
|
10037
10287
|
// against the same selection instead of a dead-end message.
|
|
@@ -10064,8 +10314,8 @@ function CaptureMode() {
|
|
|
10064
10314
|
{
|
|
10065
10315
|
type: "button",
|
|
10066
10316
|
onClick: () => void (async () => {
|
|
10067
|
-
const
|
|
10068
|
-
if (
|
|
10317
|
+
const shot2 = await photographNow();
|
|
10318
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10069
10319
|
})(),
|
|
10070
10320
|
title: t("exact_hint"),
|
|
10071
10321
|
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",
|
|
@@ -10138,7 +10388,7 @@ function CaptureMode() {
|
|
|
10138
10388
|
value: description,
|
|
10139
10389
|
onChange: (e) => setDescription(e.target.value),
|
|
10140
10390
|
onKeyDown: (e) => {
|
|
10141
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
10391
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
10142
10392
|
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
10143
10393
|
e.preventDefault();
|
|
10144
10394
|
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
@@ -10163,17 +10413,25 @@ function CaptureMode() {
|
|
|
10163
10413
|
]
|
|
10164
10414
|
}
|
|
10165
10415
|
),
|
|
10416
|
+
/* @__PURE__ */ jsxs(
|
|
10417
|
+
"button",
|
|
10418
|
+
{
|
|
10419
|
+
"data-qa-save-next": "true",
|
|
10420
|
+
onClick: () => void save(true),
|
|
10421
|
+
disabled: !description.trim(),
|
|
10422
|
+
title: t("save_next_hint"),
|
|
10423
|
+
className: "qa-tap qa-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-hi",
|
|
10424
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10425
|
+
children: [
|
|
10426
|
+
/* @__PURE__ */ jsx(Icon, { name: "Plus", size: 16 }),
|
|
10427
|
+
t("save_next")
|
|
10428
|
+
]
|
|
10429
|
+
}
|
|
10430
|
+
),
|
|
10166
10431
|
/* @__PURE__ */ jsx(
|
|
10167
10432
|
"button",
|
|
10168
10433
|
{
|
|
10169
|
-
onClick:
|
|
10170
|
-
setPhase("selecting");
|
|
10171
|
-
setSelection(null);
|
|
10172
|
-
setShot(null);
|
|
10173
|
-
setDescription("");
|
|
10174
|
-
setSeverity("bug");
|
|
10175
|
-
setTargetForensics(void 0);
|
|
10176
|
-
},
|
|
10434
|
+
onClick: resetForNextNote,
|
|
10177
10435
|
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
10178
10436
|
style: { background: "transparent", cursor: "pointer" },
|
|
10179
10437
|
children: t("reselect")
|
|
@@ -10389,5 +10647,5 @@ function Qapture({ config }) {
|
|
|
10389
10647
|
}
|
|
10390
10648
|
|
|
10391
10649
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10392
|
-
//# sourceMappingURL=chunk-
|
|
10393
|
-
//# sourceMappingURL=chunk-
|
|
10650
|
+
//# sourceMappingURL=chunk-JGSKPW7Y.js.map
|
|
10651
|
+
//# sourceMappingURL=chunk-JGSKPW7Y.js.map
|