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
|
@@ -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 */
|
|
@@ -1729,11 +1732,15 @@ var STR = {
|
|
|
1729
1732
|
sel_region: "Region selected",
|
|
1730
1733
|
sel_element: "Element selected",
|
|
1731
1734
|
capturing: "capturing screenshot\u2026",
|
|
1735
|
+
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.",
|
|
1732
1736
|
no_shot: "no screenshot (location saved)",
|
|
1733
1737
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1734
1738
|
save_point: "Save point",
|
|
1739
|
+
save_next: "Save + next",
|
|
1740
|
+
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
1741
|
+
shot_reused: "{n} saved from this screenshot",
|
|
1735
1742
|
reselect: "Reselect",
|
|
1736
|
-
save_hint: "\u2318/Ctrl + Enter to save",
|
|
1743
|
+
save_hint: "\u2318/Ctrl + Enter to save \xB7 add Shift to keep the same screenshot",
|
|
1737
1744
|
login_with: "Log in with {field} + password.",
|
|
1738
1745
|
used_count: "{n}/{m} used",
|
|
1739
1746
|
used: "used",
|
|
@@ -1769,6 +1776,9 @@ var STR = {
|
|
|
1769
1776
|
sev_design: "Design",
|
|
1770
1777
|
sev_enhance: "Enhance",
|
|
1771
1778
|
status_open: "Open",
|
|
1779
|
+
status_sent: "Sent",
|
|
1780
|
+
check_fixes: "Check the {n} fixes",
|
|
1781
|
+
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.",
|
|
1772
1782
|
status_verified: "Verified",
|
|
1773
1783
|
context_attached: "{n} runtime events attached",
|
|
1774
1784
|
start_walkthrough: "Start walkthrough",
|
|
@@ -1783,7 +1793,7 @@ var STR = {
|
|
|
1783
1793
|
no_evidence: "ticked, no capture",
|
|
1784
1794
|
expected_label: "Expected",
|
|
1785
1795
|
exact_label: "Pixel-exact shots",
|
|
1786
|
-
exact_hint: "Screenshots become a real photo instead of a redraw.
|
|
1796
|
+
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
1797
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1788
1798
|
redraw_action: "Use real screenshots",
|
|
1789
1799
|
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 +1802,7 @@ var STR = {
|
|
|
1792
1802
|
exact_off: "Back to redrawn screenshots",
|
|
1793
1803
|
exact_declined: "Staying on redrawn screenshots",
|
|
1794
1804
|
exact_turn_on: "Turn on",
|
|
1795
|
-
exact_unsupported: "Needs
|
|
1805
|
+
exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
|
|
1796
1806
|
sync_title: "Save to a folder",
|
|
1797
1807
|
sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
|
|
1798
1808
|
sync_choose: "Choose folder",
|
|
@@ -1937,11 +1947,15 @@ var STR = {
|
|
|
1937
1947
|
sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
|
|
1938
1948
|
sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
|
|
1939
1949
|
capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
|
|
1950
|
+
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.",
|
|
1940
1951
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1941
1952
|
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)",
|
|
1942
1953
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1954
|
+
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1955
|
+
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.",
|
|
1956
|
+
shot_reused: "\u062D\u064F\u0641\u0638\u062A {n} \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0644\u0642\u0637\u0629",
|
|
1943
1957
|
reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
|
|
1944
|
-
save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
|
|
1958
|
+
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",
|
|
1945
1959
|
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.",
|
|
1946
1960
|
used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
1947
1961
|
used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
|
|
@@ -1977,6 +1991,9 @@ var STR = {
|
|
|
1977
1991
|
sev_design: "Design",
|
|
1978
1992
|
sev_enhance: "Enhance",
|
|
1979
1993
|
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1994
|
+
status_sent: "\u0645\u064F\u0631\u0633\u064E\u0644",
|
|
1995
|
+
check_fixes: "\u062A\u062D\u0642\u0651\u0642 \u0645\u0646 {n} \u062A\u0639\u062F\u064A\u0644\u0627\u062A",
|
|
1996
|
+
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.",
|
|
1980
1997
|
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1981
1998
|
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1982
1999
|
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
@@ -1991,7 +2008,7 @@ var STR = {
|
|
|
1991
2008
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1992
2009
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1993
2010
|
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. \
|
|
2011
|
+
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
2012
|
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
2013
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1997
2014
|
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 +2017,7 @@ var STR = {
|
|
|
2000
2017
|
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
2018
|
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
2019
|
exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
|
|
2003
|
-
exact_unsupported: "\u064A\u062A\u0637\u0644\u0628
|
|
2020
|
+
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
2021
|
sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
|
|
2005
2022
|
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
2023
|
sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
|
|
@@ -2305,7 +2322,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2305
2322
|
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2306
2323
|
const originX = tl.x - centres.tl.x * scaleX;
|
|
2307
2324
|
const originY = tl.y - centres.tl.y * scaleY;
|
|
2308
|
-
const
|
|
2325
|
+
const mapping = { scaleX, scaleY, originX, originY };
|
|
2309
2326
|
const farX = originX + vw * scaleX;
|
|
2310
2327
|
const farY = originY + vh * scaleY;
|
|
2311
2328
|
const slack = Math.max(4, frameW * 0.01);
|
|
@@ -2324,7 +2341,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
|
2324
2341
|
verified++;
|
|
2325
2342
|
}
|
|
2326
2343
|
if (verified === 0) return null;
|
|
2327
|
-
return
|
|
2344
|
+
return mapping;
|
|
2328
2345
|
}
|
|
2329
2346
|
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2330
2347
|
const x0 = m.originX + rect.left * m.scaleX;
|
|
@@ -2336,73 +2353,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
|
|
|
2336
2353
|
if (sw < 1 || sh < 1) return null;
|
|
2337
2354
|
return { sx, sy, sw, sh };
|
|
2338
2355
|
}
|
|
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
2356
|
|
|
2352
2357
|
// src/lib/screenCapture.ts
|
|
2353
2358
|
var ASPECT_TOLERANCE = 0.08;
|
|
2354
2359
|
var CALIBRATION_SETTLE_MS = 220;
|
|
2355
2360
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2356
2361
|
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
2362
|
function isExactCaptureSupported() {
|
|
2363
2363
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2364
2364
|
const md = navigator.mediaDevices;
|
|
2365
2365
|
return !!md && typeof md.getDisplayMedia === "function";
|
|
2366
2366
|
}
|
|
2367
|
-
var
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
2367
|
+
var armed = false;
|
|
2368
|
+
var declined = false;
|
|
2369
|
+
var frozen = null;
|
|
2370
|
+
var lastMode = null;
|
|
2371
|
+
function armExactCapture() {
|
|
2372
|
+
if (!isExactCaptureSupported()) return false;
|
|
2373
|
+
armed = true;
|
|
2374
|
+
declined = false;
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2377
|
+
function disarmExactCapture() {
|
|
2378
|
+
armed = false;
|
|
2379
|
+
releaseFrozenFrame();
|
|
2380
|
+
}
|
|
2381
|
+
function resetExactCaptureDecline() {
|
|
2382
|
+
declined = false;
|
|
2383
|
+
}
|
|
2370
2384
|
function getExactCaptureStatus() {
|
|
2371
2385
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2372
|
-
if (
|
|
2386
|
+
if (armed) return "live";
|
|
2373
2387
|
if (declined) return "declined";
|
|
2374
2388
|
return "idle";
|
|
2375
2389
|
}
|
|
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();
|
|
2390
|
+
function getFrozenFrame() {
|
|
2391
|
+
return frozen;
|
|
2403
2392
|
}
|
|
2404
|
-
function
|
|
2405
|
-
|
|
2393
|
+
function releaseFrozenFrame() {
|
|
2394
|
+
if (frozen) {
|
|
2395
|
+
frozen.canvas.width = 0;
|
|
2396
|
+
frozen.canvas.height = 0;
|
|
2397
|
+
}
|
|
2398
|
+
frozen = null;
|
|
2406
2399
|
}
|
|
2407
2400
|
function waitFor(test, timeoutMs) {
|
|
2408
2401
|
if (test()) return Promise.resolve(true);
|
|
@@ -2442,17 +2435,28 @@ function nextPresentedFrame(el) {
|
|
|
2442
2435
|
});
|
|
2443
2436
|
});
|
|
2444
2437
|
}
|
|
2445
|
-
function
|
|
2438
|
+
function looksLikeViewport(frameW, frameH) {
|
|
2446
2439
|
if (!frameW || !frameH) return false;
|
|
2447
2440
|
const vw = window.innerWidth;
|
|
2448
2441
|
const vh = window.innerHeight;
|
|
2449
2442
|
if (!vw || !vh) return false;
|
|
2450
|
-
|
|
2451
|
-
const viewAspect = vw / vh;
|
|
2452
|
-
return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
|
|
2443
|
+
return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
|
|
2453
2444
|
}
|
|
2454
|
-
async function
|
|
2455
|
-
|
|
2445
|
+
async function withOverlayHidden(fn) {
|
|
2446
|
+
const hosts = Array.from(
|
|
2447
|
+
document.querySelectorAll("body > [data-qa-overlay]")
|
|
2448
|
+
);
|
|
2449
|
+
const previous = hosts.map((el) => el.style.visibility);
|
|
2450
|
+
for (const el of hosts) el.style.visibility = "hidden";
|
|
2451
|
+
try {
|
|
2452
|
+
return await fn();
|
|
2453
|
+
} finally {
|
|
2454
|
+
hosts.forEach((el, i) => {
|
|
2455
|
+
el.style.visibility = previous[i];
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
async function grabFrameCanvas(video, grabber) {
|
|
2456
2460
|
await nextPresentedFrame(video);
|
|
2457
2461
|
let source = video;
|
|
2458
2462
|
let w = video.videoWidth;
|
|
@@ -2483,11 +2487,10 @@ async function grabFrameCanvas() {
|
|
|
2483
2487
|
bitmap?.close?.();
|
|
2484
2488
|
return c;
|
|
2485
2489
|
}
|
|
2486
|
-
async function calibrate() {
|
|
2487
|
-
if (!video) return false;
|
|
2490
|
+
async function calibrate(video, grabber) {
|
|
2488
2491
|
const vw = window.innerWidth;
|
|
2489
2492
|
const vh = window.innerHeight;
|
|
2490
|
-
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return
|
|
2493
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
|
|
2491
2494
|
const card = document.createElement("div");
|
|
2492
2495
|
card.setAttribute("data-qa-calibration", "true");
|
|
2493
2496
|
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
@@ -2501,10 +2504,10 @@ async function calibrate() {
|
|
|
2501
2504
|
document.body.appendChild(card);
|
|
2502
2505
|
try {
|
|
2503
2506
|
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2504
|
-
const frame = await grabFrameCanvas();
|
|
2505
|
-
if (!frame) return
|
|
2507
|
+
const frame = await grabFrameCanvas(video, grabber);
|
|
2508
|
+
if (!frame) return null;
|
|
2506
2509
|
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2507
|
-
if (!ctx) return
|
|
2510
|
+
if (!ctx) return null;
|
|
2508
2511
|
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2509
2512
|
const found = deriveMapping(
|
|
2510
2513
|
findMarkerCandidates(data, frame.width, frame.height),
|
|
@@ -2513,20 +2516,40 @@ async function calibrate() {
|
|
|
2513
2516
|
frame.width,
|
|
2514
2517
|
frame.height
|
|
2515
2518
|
);
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
return true;
|
|
2519
|
+
frame.width = 0;
|
|
2520
|
+
frame.height = 0;
|
|
2521
|
+
return found;
|
|
2520
2522
|
} catch {
|
|
2521
|
-
return
|
|
2523
|
+
return null;
|
|
2522
2524
|
} finally {
|
|
2523
2525
|
card.remove();
|
|
2524
2526
|
}
|
|
2525
2527
|
}
|
|
2526
|
-
async function
|
|
2527
|
-
if (!isExactCaptureSupported()) return
|
|
2528
|
-
|
|
2529
|
-
|
|
2528
|
+
async function freezeViewport() {
|
|
2529
|
+
if (!isExactCaptureSupported()) return null;
|
|
2530
|
+
releaseFrozenFrame();
|
|
2531
|
+
let stream = null;
|
|
2532
|
+
let el = null;
|
|
2533
|
+
const release = () => {
|
|
2534
|
+
if (el) {
|
|
2535
|
+
try {
|
|
2536
|
+
el.pause();
|
|
2537
|
+
} catch {
|
|
2538
|
+
}
|
|
2539
|
+
el.srcObject = null;
|
|
2540
|
+
el.remove();
|
|
2541
|
+
el = null;
|
|
2542
|
+
}
|
|
2543
|
+
if (stream) {
|
|
2544
|
+
for (const t of stream.getTracks()) {
|
|
2545
|
+
try {
|
|
2546
|
+
t.stop();
|
|
2547
|
+
} catch {
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
stream = null;
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2530
2553
|
try {
|
|
2531
2554
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
2532
2555
|
const constraints = {
|
|
@@ -2544,121 +2567,103 @@ async function startExactCapture() {
|
|
|
2544
2567
|
surfaceSwitching: "exclude",
|
|
2545
2568
|
systemAudio: "exclude"
|
|
2546
2569
|
};
|
|
2547
|
-
|
|
2548
|
-
const
|
|
2549
|
-
if (!
|
|
2550
|
-
for (const x of s.getTracks()) x.stop();
|
|
2570
|
+
stream = await navigator.mediaDevices.getDisplayMedia(constraints);
|
|
2571
|
+
const track = stream.getVideoTracks()[0];
|
|
2572
|
+
if (!track) {
|
|
2551
2573
|
declined = true;
|
|
2552
|
-
return
|
|
2574
|
+
return null;
|
|
2553
2575
|
}
|
|
2554
|
-
const settings =
|
|
2576
|
+
const settings = track.getSettings();
|
|
2555
2577
|
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2556
|
-
|
|
2578
|
+
el = document.createElement("video");
|
|
2557
2579
|
el.setAttribute("data-qa-overlay", "true");
|
|
2558
2580
|
el.muted = true;
|
|
2559
2581
|
el.playsInline = true;
|
|
2560
2582
|
el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
2561
|
-
el.srcObject =
|
|
2583
|
+
el.srcObject = stream;
|
|
2562
2584
|
document.body.appendChild(el);
|
|
2563
2585
|
try {
|
|
2564
2586
|
await el.play();
|
|
2565
2587
|
} catch {
|
|
2566
2588
|
}
|
|
2567
|
-
const
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
|
|
2589
|
+
const video = el;
|
|
2590
|
+
const ready = await waitFor(
|
|
2591
|
+
() => video.videoWidth > 0 && video.videoHeight > 0,
|
|
2592
|
+
VIDEO_READY_TIMEOUT_MS
|
|
2593
|
+
);
|
|
2594
|
+
if (!ready) {
|
|
2571
2595
|
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;
|
|
2596
|
+
return null;
|
|
2597
|
+
}
|
|
2579
2598
|
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
|
-
}
|
|
2599
|
+
const grabber = IC ? new IC(track) : null;
|
|
2600
|
+
const vw = window.innerWidth;
|
|
2601
|
+
const vh = window.innerHeight;
|
|
2602
|
+
const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
|
|
2603
|
+
const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
|
|
2604
|
+
if (mode === "surface" && !mapping) {
|
|
2605
|
+
declined = true;
|
|
2606
|
+
return null;
|
|
2592
2607
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2608
|
+
const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
|
|
2609
|
+
if (!raw) return null;
|
|
2610
|
+
let page;
|
|
2611
|
+
if (mode === "surface" && mapping) {
|
|
2612
|
+
const box = mapRectToFrame(
|
|
2613
|
+
{ left: 0, top: 0, width: vw, height: vh },
|
|
2614
|
+
mapping,
|
|
2615
|
+
raw.width,
|
|
2616
|
+
raw.height
|
|
2617
|
+
);
|
|
2618
|
+
if (!box) return null;
|
|
2619
|
+
page = document.createElement("canvas");
|
|
2620
|
+
page.width = box.sw;
|
|
2621
|
+
page.height = box.sh;
|
|
2622
|
+
const ctx = page.getContext("2d");
|
|
2623
|
+
if (!ctx) return null;
|
|
2624
|
+
ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
|
|
2625
|
+
raw.width = 0;
|
|
2626
|
+
raw.height = 0;
|
|
2627
|
+
} else {
|
|
2628
|
+
page = raw;
|
|
2629
|
+
}
|
|
2630
|
+
frozen = {
|
|
2631
|
+
canvas: page,
|
|
2632
|
+
mode,
|
|
2633
|
+
viewportWidth: vw,
|
|
2634
|
+
viewportHeight: vh,
|
|
2635
|
+
takenAt: Date.now()
|
|
2636
|
+
};
|
|
2637
|
+
lastMode = mode;
|
|
2638
|
+
return frozen;
|
|
2597
2639
|
} catch {
|
|
2598
2640
|
declined = true;
|
|
2599
|
-
|
|
2600
|
-
|
|
2641
|
+
return null;
|
|
2642
|
+
} finally {
|
|
2643
|
+
release();
|
|
2601
2644
|
}
|
|
2602
2645
|
}
|
|
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 {
|
|
2646
|
+
function cropFrozenRegion(rect) {
|
|
2647
|
+
const f = frozen;
|
|
2648
|
+
if (!f || !f.canvas.width || !f.canvas.height) return null;
|
|
2649
|
+
if (!f.viewportWidth || !f.viewportHeight) return null;
|
|
2650
|
+
if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
|
|
2658
2651
|
return null;
|
|
2659
|
-
} finally {
|
|
2660
|
-
if (bitmap && typeof bitmap.close === "function") bitmap.close();
|
|
2661
2652
|
}
|
|
2653
|
+
const scaleX = f.canvas.width / f.viewportWidth;
|
|
2654
|
+
const scaleY = f.canvas.height / f.viewportHeight;
|
|
2655
|
+
const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
|
|
2656
|
+
const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
|
|
2657
|
+
const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
|
|
2658
|
+
const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
|
|
2659
|
+
if (sw < 1 || sh < 1) return null;
|
|
2660
|
+
const out = document.createElement("canvas");
|
|
2661
|
+
out.width = sw;
|
|
2662
|
+
out.height = sh;
|
|
2663
|
+
const ctx = out.getContext("2d");
|
|
2664
|
+
if (!ctx) return null;
|
|
2665
|
+
ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2666
|
+
return out;
|
|
2662
2667
|
}
|
|
2663
2668
|
|
|
2664
2669
|
// src/lib/cssColors.ts
|
|
@@ -2802,12 +2807,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2802
2807
|
|
|
2803
2808
|
// src/lib/capture.ts
|
|
2804
2809
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2810
|
+
var CHUNK_TIMEOUT_MS = 8e3;
|
|
2805
2811
|
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
2806
2812
|
var MAX_SHOT_EDGE = 1800;
|
|
2807
2813
|
var WEBP_QUALITY = 0.92;
|
|
2808
2814
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2809
2815
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2810
2816
|
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2817
|
+
var TOO_HEAVY_NODES = 6e3;
|
|
2818
|
+
function documentWeight() {
|
|
2819
|
+
try {
|
|
2820
|
+
return document.getElementsByTagName("*").length;
|
|
2821
|
+
} catch {
|
|
2822
|
+
return 0;
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2811
2825
|
function withTimeout(promise, ms) {
|
|
2812
2826
|
let timer;
|
|
2813
2827
|
return Promise.race([
|
|
@@ -2869,8 +2883,13 @@ function fitToBudget(canvas) {
|
|
|
2869
2883
|
function canvasToBlob(canvas, type, quality) {
|
|
2870
2884
|
return new Promise((resolve) => {
|
|
2871
2885
|
if (canvas.toBlob) {
|
|
2872
|
-
|
|
2873
|
-
|
|
2886
|
+
try {
|
|
2887
|
+
canvas.toBlob((b) => resolve(b), type, quality);
|
|
2888
|
+
return;
|
|
2889
|
+
} catch {
|
|
2890
|
+
resolve(null);
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2874
2893
|
}
|
|
2875
2894
|
try {
|
|
2876
2895
|
const dataUrl = canvas.toDataURL(type, quality);
|
|
@@ -2897,20 +2916,6 @@ function shotExtension(blob) {
|
|
|
2897
2916
|
if (type === "image/jpeg") return "jpg";
|
|
2898
2917
|
return "png";
|
|
2899
2918
|
}
|
|
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
2919
|
function markStuckElements() {
|
|
2915
2920
|
let all;
|
|
2916
2921
|
try {
|
|
@@ -3071,7 +3076,9 @@ function planRender(rect, sx, sy, vw, vh) {
|
|
|
3071
3076
|
};
|
|
3072
3077
|
}
|
|
3073
3078
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
3074
|
-
const
|
|
3079
|
+
const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
|
|
3080
|
+
if (!mod) return null;
|
|
3081
|
+
const { default: html2canvas } = mod;
|
|
3075
3082
|
const { vw, vh } = viewportSize();
|
|
3076
3083
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
3077
3084
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
@@ -3090,7 +3097,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
3090
3097
|
height: plan.render.height,
|
|
3091
3098
|
scale,
|
|
3092
3099
|
useCORS: true,
|
|
3093
|
-
allowTaint: true
|
|
3100
|
+
// MUST stay false. `allowTaint: true` tells html2canvas to draw
|
|
3101
|
+
// cross-origin images it could not fetch with CORS -- which succeeds,
|
|
3102
|
+
// and TAINTS the canvas. A tainted canvas throws SecurityError from
|
|
3103
|
+
// toBlob/toDataURL, so the render finishes and then the encode dies,
|
|
3104
|
+
// and the tester gets "couldn't capture" with nothing to look at.
|
|
3105
|
+
//
|
|
3106
|
+
// That is one image away on any real page: a map tile, a news
|
|
3107
|
+
// thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
|
|
3108
|
+
// fail 100% of the time while a plain page worked, which is exactly
|
|
3109
|
+
// the "it works for you and not for me" this kept producing.
|
|
3110
|
+
//
|
|
3111
|
+
// False is strictly better: html2canvas SKIPS the images it cannot
|
|
3112
|
+
// read, so those come out blank and everything else comes out right.
|
|
3113
|
+
// A screenshot with two grey rectangles in it is worth having. A
|
|
3114
|
+
// SecurityError is not.
|
|
3115
|
+
allowTaint: false,
|
|
3094
3116
|
// The page's own background, never null — see resolvePageBackground().
|
|
3095
3117
|
// Passed straight to html2canvas's parser, so it gets the same
|
|
3096
3118
|
// CSS Color 4 treatment the document walk applies.
|
|
@@ -3125,20 +3147,24 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3125
3147
|
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3126
3148
|
const sx = scroll?.x ?? window.scrollX;
|
|
3127
3149
|
const sy = scroll?.y ?? window.scrollY;
|
|
3128
|
-
if (prefer !== "dom" &&
|
|
3150
|
+
if (prefer !== "dom" && getFrozenFrame()) {
|
|
3129
3151
|
const visible = intersectViewport(rect);
|
|
3130
3152
|
if (!visible) return { status: "empty" };
|
|
3131
3153
|
try {
|
|
3132
|
-
const canvas =
|
|
3154
|
+
const canvas = cropFrozenRegion(visible);
|
|
3133
3155
|
if (canvas) {
|
|
3134
3156
|
const blob = await encodeShot(canvas);
|
|
3135
3157
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3136
3158
|
}
|
|
3137
3159
|
} catch (err) {
|
|
3138
|
-
console.warn("[QA]
|
|
3160
|
+
console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
|
|
3139
3161
|
}
|
|
3140
3162
|
if (prefer === "exact") return { status: "failed" };
|
|
3141
3163
|
}
|
|
3164
|
+
const weight = documentWeight();
|
|
3165
|
+
if (weight > TOO_HEAVY_NODES) {
|
|
3166
|
+
return { status: "too-heavy", nodes: weight };
|
|
3167
|
+
}
|
|
3142
3168
|
try {
|
|
3143
3169
|
const canvas = await captureViaDom(rect, sx, sy);
|
|
3144
3170
|
if (!canvas) return { status: "failed" };
|
|
@@ -3196,6 +3222,12 @@ function formatEvent(ev, t0) {
|
|
|
3196
3222
|
}
|
|
3197
3223
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3198
3224
|
}
|
|
3225
|
+
function noteCheckLine(note, index) {
|
|
3226
|
+
const where = oneLine(note.route) || "/";
|
|
3227
|
+
const what = oneLine(note.description) || "(no description)";
|
|
3228
|
+
const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
|
|
3229
|
+
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3230
|
+
}
|
|
3199
3231
|
function noteToMarkdown(note, opts) {
|
|
3200
3232
|
const brand = opts?.brand ?? "Qapture";
|
|
3201
3233
|
const idx = opts?.index;
|
|
@@ -3253,6 +3285,14 @@ function noteToMarkdown(note, opts) {
|
|
|
3253
3285
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3254
3286
|
});
|
|
3255
3287
|
}
|
|
3288
|
+
if (idx != null) {
|
|
3289
|
+
lines.push("");
|
|
3290
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3291
|
+
lines.push("");
|
|
3292
|
+
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?*");
|
|
3293
|
+
lines.push("");
|
|
3294
|
+
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\`.`);
|
|
3295
|
+
}
|
|
3256
3296
|
const ctx = note.context;
|
|
3257
3297
|
if (ctx) {
|
|
3258
3298
|
const env = ctx.env;
|
|
@@ -3425,6 +3465,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
|
|
|
3425
3465
|
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.
|
|
3426
3466
|
|
|
3427
3467
|
And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
|
|
3468
|
+
);
|
|
3469
|
+
sections.push(
|
|
3470
|
+
`## You Are Being Graded On This
|
|
3471
|
+
|
|
3472
|
+
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.
|
|
3473
|
+
|
|
3474
|
+
**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.
|
|
3475
|
+
|
|
3476
|
+
**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.
|
|
3477
|
+
|
|
3478
|
+
Hand \`verify.md\` back with the work.`
|
|
3428
3479
|
);
|
|
3429
3480
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3430
3481
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3572,6 +3623,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3572
3623
|
` : "";
|
|
3573
3624
|
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
3574
3625
|
zip.file("notes.md", notesMd);
|
|
3626
|
+
zip.file("verify.md", [
|
|
3627
|
+
`# ${brandLabel} \u2014 checks to satisfy`,
|
|
3628
|
+
"",
|
|
3629
|
+
`Exported: ${stamp}`,
|
|
3630
|
+
`Checks: ${notes.length}`,
|
|
3631
|
+
"",
|
|
3632
|
+
"Each line below is one thing the tester asked for. The full ask, the",
|
|
3633
|
+
"screenshot, the page and the element are in `notes.md` under the matching",
|
|
3634
|
+
'"Point" heading.',
|
|
3635
|
+
"",
|
|
3636
|
+
"**What is expected of you**",
|
|
3637
|
+
"",
|
|
3638
|
+
"1. Do the work in `notes.md`.",
|
|
3639
|
+
"2. Tick a box here only when the check is true on a fresh load of the page",
|
|
3640
|
+
" named beside it, with the tester doing nothing extra.",
|
|
3641
|
+
"3. Leave a box unticked and write one line under it saying why, if you",
|
|
3642
|
+
" could not do it or disagree with it. An unticked box with a reason is a",
|
|
3643
|
+
" good answer; a ticked box that is not true is the only bad one.",
|
|
3644
|
+
"4. Hand this file back with the work.",
|
|
3645
|
+
"",
|
|
3646
|
+
"The tester will then be walked back through these same checks on the real",
|
|
3647
|
+
"page, in this order, and asked about each one. Anything they say is still",
|
|
3648
|
+
"wrong comes back to you as round two, with their new words attached.",
|
|
3649
|
+
"",
|
|
3650
|
+
"---",
|
|
3651
|
+
"",
|
|
3652
|
+
...notes.map((n, i) => noteCheckLine(n, i + 1)),
|
|
3653
|
+
"",
|
|
3654
|
+
"---",
|
|
3655
|
+
"",
|
|
3656
|
+
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3657
|
+
""
|
|
3658
|
+
].join("\n"));
|
|
3575
3659
|
notes.forEach((n, i) => {
|
|
3576
3660
|
if (n.screenshot && shots) {
|
|
3577
3661
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4211,6 +4295,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
|
|
|
4211
4295
|
var LOGIN_KEY = "logins";
|
|
4212
4296
|
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
4213
4297
|
var EXACT_SHOTS_KEY = "exactShots";
|
|
4298
|
+
function exactShotsWanted(store) {
|
|
4299
|
+
return store.getItem(EXACT_SHOTS_KEY) !== "0";
|
|
4300
|
+
}
|
|
4214
4301
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4215
4302
|
var COMPACT_KEY = "compactCapture";
|
|
4216
4303
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
@@ -4225,11 +4312,12 @@ var PANEL_OPEN_KEY = "panelOpen";
|
|
|
4225
4312
|
var ACTIVE_TAB_KEY = "activeTab";
|
|
4226
4313
|
var WALK_KEY = "walk";
|
|
4227
4314
|
var SOFT_NAV_PROOF_MS = 700;
|
|
4315
|
+
var SIGNATURE_SEP = String.fromCharCode(0);
|
|
4228
4316
|
function pageSignature() {
|
|
4229
4317
|
if (typeof document === "undefined" || !document.body) return "";
|
|
4230
4318
|
const body = document.body;
|
|
4231
4319
|
const text = body.textContent || "";
|
|
4232
|
-
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(
|
|
4320
|
+
return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
|
|
4233
4321
|
}
|
|
4234
4322
|
var GUIDE_SKIPPED_KEY = "guideSkipped";
|
|
4235
4323
|
var NOTICE_QUEUE_CAP = 3;
|
|
@@ -4287,8 +4375,12 @@ function QaProvider({
|
|
|
4287
4375
|
const [loginsUsed, setLoginsUsed] = React.useState(
|
|
4288
4376
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
4289
4377
|
);
|
|
4290
|
-
const [exactStatus, setExactStatus] = React.useState(() =>
|
|
4378
|
+
const [exactStatus, setExactStatus] = React.useState(() => {
|
|
4379
|
+
if (exactShotsWanted(storage)) armExactCapture();
|
|
4380
|
+
return getExactCaptureStatus();
|
|
4381
|
+
});
|
|
4291
4382
|
const exactSupported = isExactCaptureSupported();
|
|
4383
|
+
const [frozenAt, setFrozenAt] = React.useState(null);
|
|
4292
4384
|
const [syncState, setSyncState] = React.useState(() => getFsSyncState());
|
|
4293
4385
|
const [syncTick, setSyncTick] = React.useState(0);
|
|
4294
4386
|
const [lastCampaign, setLastCampaign] = React.useState(
|
|
@@ -4789,20 +4881,26 @@ function QaProvider({
|
|
|
4789
4881
|
flushPendingDeletes();
|
|
4790
4882
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4791
4883
|
noticeTimers.current.clear();
|
|
4792
|
-
|
|
4884
|
+
releaseFrozenFrame();
|
|
4793
4885
|
};
|
|
4794
4886
|
}, [flushPendingDeletes]);
|
|
4795
4887
|
const startCapture = React.useCallback((prefill) => {
|
|
4796
4888
|
setIsOpen(false);
|
|
4797
4889
|
setCapturePrefill(prefill ?? "");
|
|
4798
4890
|
setCaptureActive(true);
|
|
4799
|
-
|
|
4891
|
+
setFrozenAt(null);
|
|
4892
|
+
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4800
4893
|
resetExactCaptureDecline();
|
|
4801
|
-
void
|
|
4894
|
+
void freezeViewport().then((frame) => {
|
|
4895
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4896
|
+
setExactStatus(getExactCaptureStatus());
|
|
4897
|
+
});
|
|
4802
4898
|
}
|
|
4803
4899
|
}, [storage]);
|
|
4804
4900
|
const endCapture = React.useCallback((reopen = true) => {
|
|
4805
4901
|
setCaptureActive(false);
|
|
4902
|
+
releaseFrozenFrame();
|
|
4903
|
+
setFrozenAt(null);
|
|
4806
4904
|
if (reopen) setIsOpen(true);
|
|
4807
4905
|
}, []);
|
|
4808
4906
|
const toggleGuide = React.useCallback((key) => {
|
|
@@ -4823,19 +4921,36 @@ function QaProvider({
|
|
|
4823
4921
|
return next;
|
|
4824
4922
|
});
|
|
4825
4923
|
}, [storage]);
|
|
4924
|
+
const stampHandedOff = React.useCallback(async (ids, at) => {
|
|
4925
|
+
if (!ids.length) return;
|
|
4926
|
+
const idSet = new Set(ids);
|
|
4927
|
+
const touched = [];
|
|
4928
|
+
applyNotes(
|
|
4929
|
+
(prev) => prev.map((n) => {
|
|
4930
|
+
if (!idSet.has(n.id)) return n;
|
|
4931
|
+
if ((n.status ?? "open") === "verified") return n;
|
|
4932
|
+
const next = { ...n, handedOffAt: at };
|
|
4933
|
+
touched.push(next);
|
|
4934
|
+
return next;
|
|
4935
|
+
})
|
|
4936
|
+
);
|
|
4937
|
+
for (const note of touched) await idb.put(note);
|
|
4938
|
+
}, [applyNotes, idb]);
|
|
4826
4939
|
const exportZipFn = React.useCallback(
|
|
4827
4940
|
async (filename) => {
|
|
4828
4941
|
if (!notes.length || isExporting) return;
|
|
4829
4942
|
setIsExporting(true);
|
|
4830
4943
|
try {
|
|
4831
|
-
|
|
4944
|
+
const stamp = nowIso();
|
|
4945
|
+
await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
|
|
4946
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
4832
4947
|
} catch (err) {
|
|
4833
4948
|
console.error("[QA] export failed", err);
|
|
4834
4949
|
} finally {
|
|
4835
4950
|
setIsExporting(false);
|
|
4836
4951
|
}
|
|
4837
4952
|
},
|
|
4838
|
-
[notes, isExporting, config, guideChecked, guideSkipped]
|
|
4953
|
+
[notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
|
|
4839
4954
|
);
|
|
4840
4955
|
const exportZipRef = React.useRef(async () => {
|
|
4841
4956
|
});
|
|
@@ -4843,16 +4958,32 @@ function QaProvider({
|
|
|
4843
4958
|
exportZipRef.current = exportZipFn;
|
|
4844
4959
|
}, [exportZipFn]);
|
|
4845
4960
|
const enableExactShots = React.useCallback(async () => {
|
|
4846
|
-
|
|
4847
|
-
const ok = await startExactCapture();
|
|
4961
|
+
const ok = armExactCapture();
|
|
4848
4962
|
setExactStatus(getExactCaptureStatus());
|
|
4849
4963
|
storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
|
|
4850
4964
|
notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
|
|
4851
4965
|
return ok;
|
|
4852
4966
|
}, [storage, notify, t]);
|
|
4967
|
+
const photographNow = React.useCallback(async () => {
|
|
4968
|
+
if (!armExactCapture()) {
|
|
4969
|
+
setExactStatus(getExactCaptureStatus());
|
|
4970
|
+
notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
|
|
4971
|
+
return false;
|
|
4972
|
+
}
|
|
4973
|
+
storage.setItem(EXACT_SHOTS_KEY, "1");
|
|
4974
|
+
const frame = await freezeViewport();
|
|
4975
|
+
setFrozenAt(frame?.takenAt ?? null);
|
|
4976
|
+
setExactStatus(getExactCaptureStatus());
|
|
4977
|
+
notify(frame ? t("exact_on") : t("exact_declined"), {
|
|
4978
|
+
tone: frame ? "success" : "info",
|
|
4979
|
+
id: "exact-shots"
|
|
4980
|
+
});
|
|
4981
|
+
return !!frame;
|
|
4982
|
+
}, [storage, notify, t]);
|
|
4853
4983
|
const disableExactShots = React.useCallback(() => {
|
|
4854
|
-
|
|
4984
|
+
disarmExactCapture();
|
|
4855
4985
|
storage.setItem(EXACT_SHOTS_KEY, "0");
|
|
4986
|
+
setFrozenAt(null);
|
|
4856
4987
|
setExactStatus(getExactCaptureStatus());
|
|
4857
4988
|
}, [storage]);
|
|
4858
4989
|
const rememberCampaign = React.useCallback((next) => {
|
|
@@ -5005,6 +5136,7 @@ function QaProvider({
|
|
|
5005
5136
|
design: 0,
|
|
5006
5137
|
enhance: 0,
|
|
5007
5138
|
open: 0,
|
|
5139
|
+
sent: 0,
|
|
5008
5140
|
fixed: 0,
|
|
5009
5141
|
verified: 0,
|
|
5010
5142
|
thisPage: 0
|
|
@@ -5018,6 +5150,7 @@ function QaProvider({
|
|
|
5018
5150
|
if (status === "verified") counts.verified++;
|
|
5019
5151
|
else if (status === "fixed") counts.fixed++;
|
|
5020
5152
|
else counts.open++;
|
|
5153
|
+
if (n.handedOffAt && status !== "verified") counts.sent++;
|
|
5021
5154
|
if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
|
|
5022
5155
|
}
|
|
5023
5156
|
return counts;
|
|
@@ -5026,7 +5159,9 @@ function QaProvider({
|
|
|
5026
5159
|
const q = filter.query.trim().toLowerCase();
|
|
5027
5160
|
return notes.filter((n) => {
|
|
5028
5161
|
if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
|
|
5029
|
-
if (filter.status
|
|
5162
|
+
if (filter.status === "sent") {
|
|
5163
|
+
if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
|
|
5164
|
+
} else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
|
|
5030
5165
|
if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
|
|
5031
5166
|
if (!q) return true;
|
|
5032
5167
|
const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
|
|
@@ -5071,6 +5206,7 @@ function QaProvider({
|
|
|
5071
5206
|
const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
|
|
5072
5207
|
if (!blob) return { status: "unsupported" };
|
|
5073
5208
|
const outcome = await shareZipFile(blob, name, config.brand.label);
|
|
5209
|
+
await stampHandedOff(notes.map((n) => n.id), stamp);
|
|
5074
5210
|
if (outcome.status === "needs-gesture") {
|
|
5075
5211
|
setPendingShare({ blob, filename: name });
|
|
5076
5212
|
} else if (outcome.status === "unsupported") {
|
|
@@ -5082,7 +5218,7 @@ function QaProvider({
|
|
|
5082
5218
|
} finally {
|
|
5083
5219
|
setIsExporting(false);
|
|
5084
5220
|
}
|
|
5085
|
-
}, [notes, config, guideChecked, guideSkipped]);
|
|
5221
|
+
}, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
|
|
5086
5222
|
const sharePending = React.useCallback(async () => {
|
|
5087
5223
|
const pending = pendingShare;
|
|
5088
5224
|
if (!pending) return { status: "unsupported" };
|
|
@@ -5298,7 +5434,10 @@ function QaProvider({
|
|
|
5298
5434
|
window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
|
|
5299
5435
|
} catch {
|
|
5300
5436
|
}
|
|
5301
|
-
if (which === "
|
|
5437
|
+
if (which === "verify") {
|
|
5438
|
+
setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
|
|
5439
|
+
startWalk("notes", 0);
|
|
5440
|
+
} else if (which === "retest") {
|
|
5302
5441
|
setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
|
|
5303
5442
|
startWalk("notes", 0);
|
|
5304
5443
|
} else if (which === "notes") {
|
|
@@ -5371,8 +5510,9 @@ function QaProvider({
|
|
|
5371
5510
|
gradeStep,
|
|
5372
5511
|
evidenceByStep,
|
|
5373
5512
|
// v0.4 — screenshots
|
|
5374
|
-
exactShots: { supported: exactSupported, status: exactStatus },
|
|
5513
|
+
exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
|
|
5375
5514
|
enableExactShots,
|
|
5515
|
+
photographNow,
|
|
5376
5516
|
disableExactShots,
|
|
5377
5517
|
// v0.4 — folder sync. syncTick is read here purely so this object is
|
|
5378
5518
|
// rebuilt when fsSync's module state changes (path/campaign live outside
|
|
@@ -7355,7 +7495,7 @@ function FilterChip({ chip }) {
|
|
|
7355
7495
|
);
|
|
7356
7496
|
}
|
|
7357
7497
|
function NoteFilterBar() {
|
|
7358
|
-
const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
|
|
7498
|
+
const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
|
|
7359
7499
|
const [confirmPrune, setConfirmPrune] = React.useState(false);
|
|
7360
7500
|
if (notes.length < 3) return null;
|
|
7361
7501
|
const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
|
|
@@ -7397,6 +7537,18 @@ function NoteFilterBar() {
|
|
|
7397
7537
|
tone: "accent",
|
|
7398
7538
|
onClick: sev("design")
|
|
7399
7539
|
},
|
|
7540
|
+
// Only ever shown once something has actually been sent. A chip reading
|
|
7541
|
+
// "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
|
|
7542
|
+
// morning after an export is the one thing the tester came back for.
|
|
7543
|
+
...noteCounts.sent > 0 ? [{
|
|
7544
|
+
key: "sent",
|
|
7545
|
+
label: t("status_sent"),
|
|
7546
|
+
count: noteCounts.sent,
|
|
7547
|
+
active: filter.status === "sent",
|
|
7548
|
+
icon: "Camera",
|
|
7549
|
+
tone: "accent",
|
|
7550
|
+
onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
|
|
7551
|
+
}] : [],
|
|
7400
7552
|
{
|
|
7401
7553
|
key: "open",
|
|
7402
7554
|
label: t("status_open"),
|
|
@@ -7465,6 +7617,24 @@ function NoteFilterBar() {
|
|
|
7465
7617
|
}
|
|
7466
7618
|
),
|
|
7467
7619
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(FilterChip, { chip }, chip.key)) }),
|
|
7620
|
+
noteCounts.sent > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7621
|
+
"button",
|
|
7622
|
+
{
|
|
7623
|
+
type: "button",
|
|
7624
|
+
"data-qa-verify-walk": "true",
|
|
7625
|
+
onClick: () => {
|
|
7626
|
+
setFilter({ status: "sent", severity: "all", thisPageOnly: false });
|
|
7627
|
+
startWalk("notes", 0);
|
|
7628
|
+
},
|
|
7629
|
+
title: t("check_fixes_hint"),
|
|
7630
|
+
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",
|
|
7631
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
7632
|
+
children: [
|
|
7633
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
|
|
7634
|
+
t("check_fixes", { n: noteCounts.sent })
|
|
7635
|
+
]
|
|
7636
|
+
}
|
|
7637
|
+
),
|
|
7468
7638
|
showPrune && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-mid", children: [
|
|
7469
7639
|
t("keep_only_retest_q", { n: others.length }),
|
|
7470
7640
|
" ",
|
|
@@ -9205,7 +9375,7 @@ function CaptureMode() {
|
|
|
9205
9375
|
compactCapture,
|
|
9206
9376
|
setCompactCapture,
|
|
9207
9377
|
exactShots,
|
|
9208
|
-
|
|
9378
|
+
photographNow,
|
|
9209
9379
|
capturePrefill
|
|
9210
9380
|
} = useQa();
|
|
9211
9381
|
const coarse = useCoarsePointer();
|
|
@@ -9219,10 +9389,13 @@ function CaptureMode() {
|
|
|
9219
9389
|
const [candidate, setCandidate] = React.useState(null);
|
|
9220
9390
|
const [regionMode, setRegionMode] = React.useState(false);
|
|
9221
9391
|
const [shot, setShot] = React.useState(null);
|
|
9392
|
+
const stillRef = React.useRef(null);
|
|
9393
|
+
const [notesFromThisShot, setNotesFromThisShot] = React.useState(0);
|
|
9222
9394
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
9223
9395
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
9224
9396
|
const [capturing, setCapturing] = React.useState(false);
|
|
9225
9397
|
const [captureError, setCaptureError] = React.useState(false);
|
|
9398
|
+
const [tooHeavy, setTooHeavy] = React.useState(false);
|
|
9226
9399
|
const [description, setDescription] = React.useState(capturePrefill);
|
|
9227
9400
|
const [severity, setSeverity] = React.useState("bug");
|
|
9228
9401
|
const [targetForensics, setTargetForensics] = React.useState(void 0);
|
|
@@ -9252,6 +9425,7 @@ function CaptureMode() {
|
|
|
9252
9425
|
const runCapture = React.useCallback(async (rect) => {
|
|
9253
9426
|
setCapturing(true);
|
|
9254
9427
|
setCaptureError(false);
|
|
9428
|
+
setTooHeavy(false);
|
|
9255
9429
|
lockPageScroll();
|
|
9256
9430
|
try {
|
|
9257
9431
|
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
@@ -9262,6 +9436,7 @@ function CaptureMode() {
|
|
|
9262
9436
|
return;
|
|
9263
9437
|
}
|
|
9264
9438
|
setCaptureError(outcome.status === "failed");
|
|
9439
|
+
setTooHeavy(outcome.status === "too-heavy");
|
|
9265
9440
|
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
9266
9441
|
setShot(blob);
|
|
9267
9442
|
setShotUrl((old) => {
|
|
@@ -9511,7 +9686,29 @@ function CaptureMode() {
|
|
|
9511
9686
|
React.useEffect(() => () => {
|
|
9512
9687
|
unlockPageScroll();
|
|
9513
9688
|
}, []);
|
|
9514
|
-
|
|
9689
|
+
React.useEffect(() => {
|
|
9690
|
+
const host = stillRef.current;
|
|
9691
|
+
const frame = getFrozenFrame();
|
|
9692
|
+
if (!host || !frame) return;
|
|
9693
|
+
const canvas = frame.canvas;
|
|
9694
|
+
canvas.style.cssText = "display:block;width:100%;height:100%;";
|
|
9695
|
+
host.appendChild(canvas);
|
|
9696
|
+
return () => {
|
|
9697
|
+
if (canvas.parentNode === host) host.removeChild(canvas);
|
|
9698
|
+
canvas.removeAttribute("style");
|
|
9699
|
+
};
|
|
9700
|
+
}, [exactShots.frozenAt]);
|
|
9701
|
+
const resetForNextNote = React.useCallback(() => {
|
|
9702
|
+
setPhase("selecting");
|
|
9703
|
+
setSelection(null);
|
|
9704
|
+
setShot(null);
|
|
9705
|
+
setShotEngine(null);
|
|
9706
|
+
setDescription("");
|
|
9707
|
+
setSeverity("bug");
|
|
9708
|
+
setTargetForensics(void 0);
|
|
9709
|
+
setCaptureError(false);
|
|
9710
|
+
}, []);
|
|
9711
|
+
const save = async (keepGoing = false) => {
|
|
9515
9712
|
if (!selection || !description.trim()) return;
|
|
9516
9713
|
const target = {
|
|
9517
9714
|
kind: selection.kind,
|
|
@@ -9533,6 +9730,11 @@ function CaptureMode() {
|
|
|
9533
9730
|
severity,
|
|
9534
9731
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9535
9732
|
});
|
|
9733
|
+
if (keepGoing) {
|
|
9734
|
+
setNotesFromThisShot((n) => n + 1);
|
|
9735
|
+
resetForNextNote();
|
|
9736
|
+
return;
|
|
9737
|
+
}
|
|
9536
9738
|
endCapture();
|
|
9537
9739
|
};
|
|
9538
9740
|
const popStyleFor = React.useCallback((r, cardWidth = 340) => {
|
|
@@ -9569,6 +9771,16 @@ function CaptureMode() {
|
|
|
9569
9771
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
9570
9772
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
9571
9773
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
9774
|
+
exactShots.frozenAt !== null && /* @__PURE__ */ jsxRuntime.jsx(
|
|
9775
|
+
"div",
|
|
9776
|
+
{
|
|
9777
|
+
ref: stillRef,
|
|
9778
|
+
"aria-hidden": "true",
|
|
9779
|
+
"data-qa-capture-still": "true",
|
|
9780
|
+
className: "qa-fixed qa-inset-0 qa-z-10089",
|
|
9781
|
+
style: { pointerEvents: "none", overflow: "hidden" }
|
|
9782
|
+
}
|
|
9783
|
+
),
|
|
9572
9784
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9573
9785
|
"div",
|
|
9574
9786
|
{
|
|
@@ -9590,6 +9802,21 @@ function CaptureMode() {
|
|
|
9590
9802
|
{
|
|
9591
9803
|
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",
|
|
9592
9804
|
children: [
|
|
9805
|
+
notesFromThisShot > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9806
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9807
|
+
"span",
|
|
9808
|
+
{
|
|
9809
|
+
"data-qa-shot-reused": "true",
|
|
9810
|
+
className: "qa-flex qa-items-center qa-gap-1.5",
|
|
9811
|
+
style: { color: "var(--qa-success)" },
|
|
9812
|
+
children: [
|
|
9813
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 16 }),
|
|
9814
|
+
t("shot_reused", { n: String(notesFromThisShot) })
|
|
9815
|
+
]
|
|
9816
|
+
}
|
|
9817
|
+
),
|
|
9818
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "\xB7" })
|
|
9819
|
+
] }),
|
|
9593
9820
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
9594
9821
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
9595
9822
|
t("cap_click")
|
|
@@ -9627,7 +9854,7 @@ function CaptureMode() {
|
|
|
9627
9854
|
) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
9628
9855
|
"button",
|
|
9629
9856
|
{
|
|
9630
|
-
onClick: () => void
|
|
9857
|
+
onClick: () => void photographNow(),
|
|
9631
9858
|
title: t("exact_hint"),
|
|
9632
9859
|
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
9860
|
style: { background: "transparent", cursor: "pointer" },
|
|
@@ -10039,6 +10266,29 @@ function CaptureMode() {
|
|
|
10039
10266
|
)
|
|
10040
10267
|
]
|
|
10041
10268
|
}
|
|
10269
|
+
) : tooHeavy ? (
|
|
10270
|
+
// Not a failure to report — a refusal to explain, with the one
|
|
10271
|
+
// control that actually solves it.
|
|
10272
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3 qa-px-3 qa-text-center", children: [
|
|
10273
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
|
|
10274
|
+
exactShots.supported ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10275
|
+
"button",
|
|
10276
|
+
{
|
|
10277
|
+
type: "button",
|
|
10278
|
+
onClick: () => void (async () => {
|
|
10279
|
+
const shot2 = await photographNow();
|
|
10280
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10281
|
+
})(),
|
|
10282
|
+
title: t("exact_hint"),
|
|
10283
|
+
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",
|
|
10284
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10285
|
+
children: [
|
|
10286
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 13 }),
|
|
10287
|
+
t("redraw_action")
|
|
10288
|
+
]
|
|
10289
|
+
}
|
|
10290
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
|
|
10291
|
+
] })
|
|
10042
10292
|
) : captureError ? (
|
|
10043
10293
|
// The render broke rather than being skipped — offer a retry
|
|
10044
10294
|
// against the same selection instead of a dead-end message.
|
|
@@ -10071,8 +10321,8 @@ function CaptureMode() {
|
|
|
10071
10321
|
{
|
|
10072
10322
|
type: "button",
|
|
10073
10323
|
onClick: () => void (async () => {
|
|
10074
|
-
const
|
|
10075
|
-
if (
|
|
10324
|
+
const shot2 = await photographNow();
|
|
10325
|
+
if (shot2 && selection) void runCapture(selection.rect);
|
|
10076
10326
|
})(),
|
|
10077
10327
|
title: t("exact_hint"),
|
|
10078
10328
|
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",
|
|
@@ -10145,7 +10395,7 @@ function CaptureMode() {
|
|
|
10145
10395
|
value: description,
|
|
10146
10396
|
onChange: (e) => setDescription(e.target.value),
|
|
10147
10397
|
onKeyDown: (e) => {
|
|
10148
|
-
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
10398
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
10149
10399
|
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
10150
10400
|
e.preventDefault();
|
|
10151
10401
|
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
@@ -10170,17 +10420,25 @@ function CaptureMode() {
|
|
|
10170
10420
|
]
|
|
10171
10421
|
}
|
|
10172
10422
|
),
|
|
10423
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10424
|
+
"button",
|
|
10425
|
+
{
|
|
10426
|
+
"data-qa-save-next": "true",
|
|
10427
|
+
onClick: () => void save(true),
|
|
10428
|
+
disabled: !description.trim(),
|
|
10429
|
+
title: t("save_next_hint"),
|
|
10430
|
+
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",
|
|
10431
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
10432
|
+
children: [
|
|
10433
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 16 }),
|
|
10434
|
+
t("save_next")
|
|
10435
|
+
]
|
|
10436
|
+
}
|
|
10437
|
+
),
|
|
10173
10438
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
10174
10439
|
"button",
|
|
10175
10440
|
{
|
|
10176
|
-
onClick:
|
|
10177
|
-
setPhase("selecting");
|
|
10178
|
-
setSelection(null);
|
|
10179
|
-
setShot(null);
|
|
10180
|
-
setDescription("");
|
|
10181
|
-
setSeverity("bug");
|
|
10182
|
-
setTargetForensics(void 0);
|
|
10183
|
-
},
|
|
10441
|
+
onClick: resetForNextNote,
|
|
10184
10442
|
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
10185
10443
|
style: { background: "transparent", cursor: "pointer" },
|
|
10186
10444
|
children: t("reselect")
|
|
@@ -10398,5 +10656,5 @@ function Qapture({ config }) {
|
|
|
10398
10656
|
exports.Qapture = Qapture;
|
|
10399
10657
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10400
10658
|
exports.initQaStudio = initQaStudio;
|
|
10401
|
-
//# sourceMappingURL=chunk-
|
|
10402
|
-
//# sourceMappingURL=chunk-
|
|
10659
|
+
//# sourceMappingURL=chunk-IYQ5JZJX.cjs.map
|
|
10660
|
+
//# sourceMappingURL=chunk-IYQ5JZJX.cjs.map
|