qapture2 0.7.4 → 0.7.6
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 +16 -7
- package/dist/{chunk-7WHQBMVP.cjs → chunk-CNNNWLTX.cjs} +321 -66
- package/dist/{chunk-ZAWR5VJR.js → chunk-NM3IDVEL.js} +321 -66
- 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
package/README.md
CHANGED
|
@@ -353,13 +353,22 @@ v0.7.3 a redrawn capture says so under the preview and offers a one-tap **Use
|
|
|
353
353
|
real screenshots**, which grants tab-share and re-shoots the same selection
|
|
354
354
|
with the exact engine below.
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
356
|
+
**From v0.7.6 this works in Safari and Firefox too.** They have no tab capture,
|
|
357
|
+
but they can share a *window*, and Qapture works out where your page sits
|
|
358
|
+
inside that frame — by measuring, not by guessing at toolbar heights. On the
|
|
359
|
+
first capture the page is covered for a fifth of a second by a dark card with
|
|
360
|
+
four coloured corners; the engine photographs it, finds the corners and solves
|
|
361
|
+
for the mapping. That dark flash is the measurement. If it can't verify the
|
|
362
|
+
result it refuses the grant and falls back to redrawing, because a screenshot
|
|
363
|
+
of the wrong pixels is worse than no screenshot.
|
|
364
|
+
|
|
365
|
+
Chromium still takes the direct path (`preferCurrentTab`), unchanged and still
|
|
366
|
+
0.0px. Only iOS/iPadOS have no Screen Capture API at all, and there the card
|
|
367
|
+
says so.
|
|
368
|
+
|
|
369
|
+
**If your app draws anything with inline SVG, a `<canvas>`, or video, turn real
|
|
370
|
+
screenshots on.** A redraw will render an SVG chart as bare outlines and a
|
|
371
|
+
canvas as a blank box, every time, in any tool built this way.
|
|
363
372
|
|
|
364
373
|
### `exact` — opt-in, pixel-for-pixel
|
|
365
374
|
|
|
@@ -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, mode2, 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, mode2);
|
|
65
65
|
const s = tx.objectStore(store);
|
|
66
66
|
let result;
|
|
67
67
|
const req = fn(s);
|
|
@@ -1781,10 +1781,11 @@ var STR = {
|
|
|
1781
1781
|
no_evidence: "ticked, no capture",
|
|
1782
1782
|
expected_label: "Expected",
|
|
1783
1783
|
exact_label: "Pixel-exact shots",
|
|
1784
|
-
exact_hint: "Screenshots become a real photo
|
|
1784
|
+
exact_hint: "Screenshots become a real photo instead of a redraw. Asks once to share this tab (or, in Safari and Firefox, this window); nothing leaves your device.",
|
|
1785
1785
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1786
1786
|
redraw_action: "Use real screenshots",
|
|
1787
|
-
redraw_no_engine: "This browser cannot photograph
|
|
1787
|
+
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.",
|
|
1788
|
+
fab_title: "Qapture \u2014 click to open, drag to move it out of the way",
|
|
1788
1789
|
exact_on: "Pixel-exact screenshots on",
|
|
1789
1790
|
exact_off: "Back to redrawn screenshots",
|
|
1790
1791
|
exact_declined: "Staying on redrawn screenshots",
|
|
@@ -1978,10 +1979,11 @@ var STR = {
|
|
|
1978
1979
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1979
1980
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1980
1981
|
exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
|
|
1981
|
-
exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \
|
|
1982
|
+
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. \u064A\u064F\u0637\u0644\u0628 \u0627\u0644\u0625\u0630\u0646 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629 \u0644\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 \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
|
|
1982
1983
|
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.",
|
|
1983
1984
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1984
|
-
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\
|
|
1985
|
+
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.",
|
|
1986
|
+
fab_title: "Qapture \u2014 \u0627\u0636\u063A\u0637 \u0644\u0644\u0641\u062A\u062D\u060C \u0648\u0627\u0633\u062D\u0628 \u0644\u062A\u062D\u0631\u064A\u0643\u0647 \u0645\u0646 \u0645\u0643\u0627\u0646\u0647",
|
|
1985
1987
|
exact_on: "\u062A\u0645 \u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629",
|
|
1986
1988
|
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",
|
|
1987
1989
|
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",
|
|
@@ -2180,8 +2182,168 @@ function computeCoverage(journey, guideChecked, skipped) {
|
|
|
2180
2182
|
};
|
|
2181
2183
|
}
|
|
2182
2184
|
|
|
2185
|
+
// src/lib/frameCalibration.ts
|
|
2186
|
+
var MARKER_SIZE = 28;
|
|
2187
|
+
var MARKERS = [
|
|
2188
|
+
{ key: "tl", color: [255, 0, 0] },
|
|
2189
|
+
{ key: "tr", color: [0, 255, 0] },
|
|
2190
|
+
{ key: "bl", color: [0, 0, 255] },
|
|
2191
|
+
{ key: "br", color: [255, 0, 255] }
|
|
2192
|
+
];
|
|
2193
|
+
function markerCentres(vw, vh) {
|
|
2194
|
+
const h = MARKER_SIZE / 2;
|
|
2195
|
+
return {
|
|
2196
|
+
tl: { x: h, y: h },
|
|
2197
|
+
tr: { x: vw - h, y: h },
|
|
2198
|
+
bl: { x: h, y: vh - h },
|
|
2199
|
+
br: { x: vw - h, y: vh - h }
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function dist2(r, g, b, c) {
|
|
2203
|
+
const dr = r - c[0];
|
|
2204
|
+
const dg = g - c[1];
|
|
2205
|
+
const db = b - c[2];
|
|
2206
|
+
return dr * dr + dg * dg + db * db;
|
|
2207
|
+
}
|
|
2208
|
+
var COLOR_TOLERANCE_2 = 90 * 90;
|
|
2209
|
+
var CLUSTER_CELL = 8;
|
|
2210
|
+
var MAX_CANDIDATES = 4;
|
|
2211
|
+
function findMarkerCandidates(data, width, height) {
|
|
2212
|
+
const cols = Math.ceil(width / CLUSTER_CELL);
|
|
2213
|
+
const rows = Math.ceil(height / CLUSTER_CELL);
|
|
2214
|
+
const cells = {};
|
|
2215
|
+
for (const m of MARKERS) cells[m.key] = new Int32Array(cols * rows);
|
|
2216
|
+
for (let y = 0; y < height; y++) {
|
|
2217
|
+
const row = y * width * 4;
|
|
2218
|
+
const cy = y / CLUSTER_CELL | 0;
|
|
2219
|
+
for (let x = 0; x < width; x++) {
|
|
2220
|
+
const i = row + x * 4;
|
|
2221
|
+
if (data[i + 3] < 128) continue;
|
|
2222
|
+
const r = data[i];
|
|
2223
|
+
const g = data[i + 1];
|
|
2224
|
+
const b = data[i + 2];
|
|
2225
|
+
if (Math.max(r, g, b) - Math.min(r, g, b) < 60) continue;
|
|
2226
|
+
for (const m of MARKERS) {
|
|
2227
|
+
if (dist2(r, g, b, m.color) <= COLOR_TOLERANCE_2) {
|
|
2228
|
+
cells[m.key][cy * cols + (x / CLUSTER_CELL | 0)]++;
|
|
2229
|
+
break;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
const out = {};
|
|
2235
|
+
for (const m of MARKERS) {
|
|
2236
|
+
const grid = cells[m.key];
|
|
2237
|
+
const seen = new Uint8Array(cols * rows);
|
|
2238
|
+
const blobs = [];
|
|
2239
|
+
for (let start = 0; start < grid.length; start++) {
|
|
2240
|
+
if (!grid[start] || seen[start]) continue;
|
|
2241
|
+
const queue = [start];
|
|
2242
|
+
seen[start] = 1;
|
|
2243
|
+
let sumX = 0;
|
|
2244
|
+
let sumY = 0;
|
|
2245
|
+
let total = 0;
|
|
2246
|
+
while (queue.length) {
|
|
2247
|
+
const idx = queue.pop();
|
|
2248
|
+
const n = grid[idx];
|
|
2249
|
+
const cx = idx % cols;
|
|
2250
|
+
const cy = idx / cols | 0;
|
|
2251
|
+
sumX += (cx * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
|
|
2252
|
+
sumY += (cy * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
|
|
2253
|
+
total += n;
|
|
2254
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2255
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2256
|
+
const nx = cx + dx;
|
|
2257
|
+
const ny = cy + dy;
|
|
2258
|
+
if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
|
|
2259
|
+
const nIdx = ny * cols + nx;
|
|
2260
|
+
if (seen[nIdx] || !grid[nIdx]) continue;
|
|
2261
|
+
seen[nIdx] = 1;
|
|
2262
|
+
queue.push(nIdx);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (total < 16) continue;
|
|
2267
|
+
blobs.push({ x: sumX / total, y: sumY / total, count: total });
|
|
2268
|
+
}
|
|
2269
|
+
blobs.sort((a, b) => b.count - a.count);
|
|
2270
|
+
out[m.key] = blobs.slice(0, MAX_CANDIDATES);
|
|
2271
|
+
}
|
|
2272
|
+
return out;
|
|
2273
|
+
}
|
|
2274
|
+
var VERIFY_TOLERANCE = 0.02;
|
|
2275
|
+
var SQUARENESS_TOLERANCE = 0.05;
|
|
2276
|
+
function deriveMapping(candidates, vw, vh, frameW, frameH) {
|
|
2277
|
+
const tls = candidates.tl ?? [];
|
|
2278
|
+
const brs = candidates.br ?? [];
|
|
2279
|
+
for (const tl of tls) {
|
|
2280
|
+
for (const br of brs) {
|
|
2281
|
+
const m = solveFrom(tl, br, candidates, vw, vh, frameW, frameH);
|
|
2282
|
+
if (m) return m;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
return null;
|
|
2286
|
+
}
|
|
2287
|
+
function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
2288
|
+
if (!(vw > MARKER_SIZE) || !(vh > MARKER_SIZE)) return null;
|
|
2289
|
+
const centres = markerCentres(vw, vh);
|
|
2290
|
+
const spanCssX = centres.br.x - centres.tl.x;
|
|
2291
|
+
const spanCssY = centres.br.y - centres.tl.y;
|
|
2292
|
+
if (spanCssX <= 0 || spanCssY <= 0) return null;
|
|
2293
|
+
const scaleX = (br.x - tl.x) / spanCssX;
|
|
2294
|
+
const scaleY = (br.y - tl.y) / spanCssY;
|
|
2295
|
+
if (!(scaleX > 0.05) || !(scaleY > 0.05)) return null;
|
|
2296
|
+
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return null;
|
|
2297
|
+
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2298
|
+
const originX = tl.x - centres.tl.x * scaleX;
|
|
2299
|
+
const originY = tl.y - centres.tl.y * scaleY;
|
|
2300
|
+
const mapping2 = { scaleX, scaleY, originX, originY };
|
|
2301
|
+
const farX = originX + vw * scaleX;
|
|
2302
|
+
const farY = originY + vh * scaleY;
|
|
2303
|
+
const slack = Math.max(4, frameW * 0.01);
|
|
2304
|
+
if (originX < -slack || originY < -slack || farX > frameW + slack || farY > frameH + slack) {
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
const tolerance = Math.max(6, Math.max(frameW, frameH) * VERIFY_TOLERANCE);
|
|
2308
|
+
let verified = 0;
|
|
2309
|
+
for (const key of ["tr", "bl"]) {
|
|
2310
|
+
const hits = candidates[key] ?? [];
|
|
2311
|
+
if (!hits.length) continue;
|
|
2312
|
+
const want = centres[key];
|
|
2313
|
+
const px = originX + want.x * scaleX;
|
|
2314
|
+
const py = originY + want.y * scaleY;
|
|
2315
|
+
if (!hits.some((h) => Math.hypot(px - h.x, py - h.y) <= tolerance)) return null;
|
|
2316
|
+
verified++;
|
|
2317
|
+
}
|
|
2318
|
+
if (verified === 0) return null;
|
|
2319
|
+
return mapping2;
|
|
2320
|
+
}
|
|
2321
|
+
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2322
|
+
const x0 = m.originX + rect.left * m.scaleX;
|
|
2323
|
+
const y0 = m.originY + rect.top * m.scaleY;
|
|
2324
|
+
const sx = Math.max(0, Math.min(frameW, Math.round(x0)));
|
|
2325
|
+
const sy = Math.max(0, Math.min(frameH, Math.round(y0)));
|
|
2326
|
+
const sw = Math.min(frameW - sx, Math.round(rect.width * m.scaleX));
|
|
2327
|
+
const sh = Math.min(frameH - sy, Math.round(rect.height * m.scaleY));
|
|
2328
|
+
if (sw < 1 || sh < 1) return null;
|
|
2329
|
+
return { sx, sy, sw, sh };
|
|
2330
|
+
}
|
|
2331
|
+
function environmentSignature() {
|
|
2332
|
+
if (typeof window === "undefined") return "";
|
|
2333
|
+
return [
|
|
2334
|
+
window.innerWidth,
|
|
2335
|
+
window.innerHeight,
|
|
2336
|
+
window.outerWidth,
|
|
2337
|
+
window.outerHeight,
|
|
2338
|
+
window.screenX,
|
|
2339
|
+
window.screenY,
|
|
2340
|
+
Math.round((window.devicePixelRatio || 1) * 100)
|
|
2341
|
+
].join("x");
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2183
2344
|
// src/lib/screenCapture.ts
|
|
2184
2345
|
var ASPECT_TOLERANCE = 0.08;
|
|
2346
|
+
var CALIBRATION_SETTLE_MS = 220;
|
|
2185
2347
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2186
2348
|
var VIDEO_READY_TIMEOUT_MS = 4e3;
|
|
2187
2349
|
var stream = null;
|
|
@@ -2189,22 +2351,14 @@ var track = null;
|
|
|
2189
2351
|
var video = null;
|
|
2190
2352
|
var grabber = null;
|
|
2191
2353
|
var declined = false;
|
|
2192
|
-
function supportsPreferCurrentTab() {
|
|
2193
|
-
const nav = navigator;
|
|
2194
|
-
const brands = nav.userAgentData?.brands;
|
|
2195
|
-
if (Array.isArray(brands) && brands.length) {
|
|
2196
|
-
return brands.some((b) => /chromium/i.test(b.brand));
|
|
2197
|
-
}
|
|
2198
|
-
const ua = navigator.userAgent || "";
|
|
2199
|
-
if (/CriOS|FxiOS|EdgiOS|Firefox/i.test(ua)) return false;
|
|
2200
|
-
return /Chrome|Chromium|Edg\//i.test(ua);
|
|
2201
|
-
}
|
|
2202
2354
|
function isExactCaptureSupported() {
|
|
2203
2355
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2204
2356
|
const md = navigator.mediaDevices;
|
|
2205
|
-
|
|
2206
|
-
return supportsPreferCurrentTab();
|
|
2357
|
+
return !!md && typeof md.getDisplayMedia === "function";
|
|
2207
2358
|
}
|
|
2359
|
+
var mode = "tab";
|
|
2360
|
+
var mapping = null;
|
|
2361
|
+
var mappingSignature = "";
|
|
2208
2362
|
function getExactCaptureStatus() {
|
|
2209
2363
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2210
2364
|
if (track && track.readyState === "live") return "live";
|
|
@@ -2232,6 +2386,9 @@ function teardown() {
|
|
|
2232
2386
|
stream = null;
|
|
2233
2387
|
track = null;
|
|
2234
2388
|
grabber = null;
|
|
2389
|
+
mode = "tab";
|
|
2390
|
+
mapping = null;
|
|
2391
|
+
mappingSignature = "";
|
|
2235
2392
|
}
|
|
2236
2393
|
function stopExactCapture() {
|
|
2237
2394
|
teardown();
|
|
@@ -2286,6 +2443,78 @@ function validateFrameMapping(frameW, frameH) {
|
|
|
2286
2443
|
const viewAspect = vw / vh;
|
|
2287
2444
|
return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
|
|
2288
2445
|
}
|
|
2446
|
+
async function grabFrameCanvas() {
|
|
2447
|
+
if (!video) return null;
|
|
2448
|
+
await nextPresentedFrame(video);
|
|
2449
|
+
let source = video;
|
|
2450
|
+
let w = video.videoWidth;
|
|
2451
|
+
let h = video.videoHeight;
|
|
2452
|
+
let bitmap = null;
|
|
2453
|
+
if (grabber) {
|
|
2454
|
+
try {
|
|
2455
|
+
bitmap = await grabber.grabFrame();
|
|
2456
|
+
source = bitmap;
|
|
2457
|
+
w = bitmap.width;
|
|
2458
|
+
h = bitmap.height;
|
|
2459
|
+
} catch {
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
if (!w || !h) {
|
|
2463
|
+
bitmap?.close?.();
|
|
2464
|
+
return null;
|
|
2465
|
+
}
|
|
2466
|
+
const c = document.createElement("canvas");
|
|
2467
|
+
c.width = w;
|
|
2468
|
+
c.height = h;
|
|
2469
|
+
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
2470
|
+
if (!ctx) {
|
|
2471
|
+
bitmap?.close?.();
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
ctx.drawImage(source, 0, 0);
|
|
2475
|
+
bitmap?.close?.();
|
|
2476
|
+
return c;
|
|
2477
|
+
}
|
|
2478
|
+
async function calibrate() {
|
|
2479
|
+
if (!video) return false;
|
|
2480
|
+
const vw = window.innerWidth;
|
|
2481
|
+
const vh = window.innerHeight;
|
|
2482
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
|
|
2483
|
+
const card = document.createElement("div");
|
|
2484
|
+
card.setAttribute("data-qa-calibration", "true");
|
|
2485
|
+
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
2486
|
+
const centres = markerCentres(vw, vh);
|
|
2487
|
+
for (const m of MARKERS) {
|
|
2488
|
+
const dot = document.createElement("div");
|
|
2489
|
+
const c = centres[m.key];
|
|
2490
|
+
dot.style.cssText = `position:absolute;width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;left:${c.x - MARKER_SIZE / 2}px;top:${c.y - MARKER_SIZE / 2}px;background:rgb(${m.color[0]},${m.color[1]},${m.color[2]});`;
|
|
2491
|
+
card.appendChild(dot);
|
|
2492
|
+
}
|
|
2493
|
+
document.body.appendChild(card);
|
|
2494
|
+
try {
|
|
2495
|
+
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2496
|
+
const frame = await grabFrameCanvas();
|
|
2497
|
+
if (!frame) return false;
|
|
2498
|
+
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2499
|
+
if (!ctx) return false;
|
|
2500
|
+
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2501
|
+
const found = deriveMapping(
|
|
2502
|
+
findMarkerCandidates(data, frame.width, frame.height),
|
|
2503
|
+
vw,
|
|
2504
|
+
vh,
|
|
2505
|
+
frame.width,
|
|
2506
|
+
frame.height
|
|
2507
|
+
);
|
|
2508
|
+
if (!found) return false;
|
|
2509
|
+
mapping = found;
|
|
2510
|
+
mappingSignature = environmentSignature();
|
|
2511
|
+
return true;
|
|
2512
|
+
} catch {
|
|
2513
|
+
return false;
|
|
2514
|
+
} finally {
|
|
2515
|
+
card.remove();
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2289
2518
|
async function startExactCapture() {
|
|
2290
2519
|
if (!isExactCaptureSupported()) return false;
|
|
2291
2520
|
if (track && track.readyState === "live" && video) return true;
|
|
@@ -2315,11 +2544,7 @@ async function startExactCapture() {
|
|
|
2315
2544
|
return false;
|
|
2316
2545
|
}
|
|
2317
2546
|
const settings = t.getSettings();
|
|
2318
|
-
|
|
2319
|
-
for (const x of s.getTracks()) x.stop();
|
|
2320
|
-
declined = true;
|
|
2321
|
-
return false;
|
|
2322
|
-
}
|
|
2547
|
+
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2323
2548
|
const el = document.createElement("video");
|
|
2324
2549
|
el.setAttribute("data-qa-overlay", "true");
|
|
2325
2550
|
el.muted = true;
|
|
@@ -2332,17 +2557,31 @@ async function startExactCapture() {
|
|
|
2332
2557
|
} catch {
|
|
2333
2558
|
}
|
|
2334
2559
|
const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
|
|
2335
|
-
|
|
2560
|
+
const giveUp = () => {
|
|
2336
2561
|
el.remove();
|
|
2337
2562
|
for (const x of s.getTracks()) x.stop();
|
|
2338
2563
|
declined = true;
|
|
2339
2564
|
return false;
|
|
2340
|
-
}
|
|
2565
|
+
};
|
|
2566
|
+
if (!ready) return giveUp();
|
|
2567
|
+
const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
|
|
2341
2568
|
stream = s;
|
|
2342
2569
|
track = t;
|
|
2343
2570
|
video = el;
|
|
2344
2571
|
const IC = window.ImageCapture;
|
|
2345
2572
|
grabber = IC ? new IC(t) : null;
|
|
2573
|
+
if (isDirect) {
|
|
2574
|
+
mode = "tab";
|
|
2575
|
+
mapping = null;
|
|
2576
|
+
mappingSignature = "";
|
|
2577
|
+
} else {
|
|
2578
|
+
mode = "surface";
|
|
2579
|
+
if (!await calibrate()) {
|
|
2580
|
+
teardown();
|
|
2581
|
+
declined = true;
|
|
2582
|
+
return false;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2346
2585
|
t.addEventListener("ended", () => {
|
|
2347
2586
|
teardown();
|
|
2348
2587
|
});
|
|
@@ -2355,6 +2594,11 @@ async function startExactCapture() {
|
|
|
2355
2594
|
}
|
|
2356
2595
|
async function grabExactRegion(rect) {
|
|
2357
2596
|
if (!track || track.readyState !== "live" || !video) return null;
|
|
2597
|
+
if (mode === "surface") {
|
|
2598
|
+
if (!mapping || environmentSignature() !== mappingSignature) {
|
|
2599
|
+
if (!await calibrate()) return null;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2358
2602
|
await nextPresentedFrame(video);
|
|
2359
2603
|
let source;
|
|
2360
2604
|
let frameW;
|
|
@@ -2377,13 +2621,24 @@ async function grabExactRegion(rect) {
|
|
|
2377
2621
|
frameH = video.videoHeight;
|
|
2378
2622
|
}
|
|
2379
2623
|
try {
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2624
|
+
let sx;
|
|
2625
|
+
let sy;
|
|
2626
|
+
let sw;
|
|
2627
|
+
let sh;
|
|
2628
|
+
if (mode === "surface") {
|
|
2629
|
+
if (!mapping) return null;
|
|
2630
|
+
const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
|
|
2631
|
+
if (!mapped) return null;
|
|
2632
|
+
({ sx, sy, sw, sh } = mapped);
|
|
2633
|
+
} else {
|
|
2634
|
+
if (!validateFrameMapping(frameW, frameH)) return null;
|
|
2635
|
+
const scaleX = frameW / window.innerWidth;
|
|
2636
|
+
const scaleY = frameH / window.innerHeight;
|
|
2637
|
+
sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
|
|
2638
|
+
sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
|
|
2639
|
+
sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
|
|
2640
|
+
sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
|
|
2641
|
+
}
|
|
2387
2642
|
const canvas = document.createElement("canvas");
|
|
2388
2643
|
canvas.width = sw;
|
|
2389
2644
|
canvas.height = sh;
|
|
@@ -5306,29 +5561,6 @@ function Icon({
|
|
|
5306
5561
|
function renderSvgElement(tag, attrs, key) {
|
|
5307
5562
|
return React__default.default.createElement(tag, { key, ...attrs });
|
|
5308
5563
|
}
|
|
5309
|
-
function isCoarsePointer() {
|
|
5310
|
-
if (typeof window === "undefined") return false;
|
|
5311
|
-
try {
|
|
5312
|
-
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
|
|
5313
|
-
} catch {
|
|
5314
|
-
}
|
|
5315
|
-
return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
|
|
5316
|
-
}
|
|
5317
|
-
function useCoarsePointer() {
|
|
5318
|
-
const [coarse, setCoarse] = React.useState(() => isCoarsePointer());
|
|
5319
|
-
React.useEffect(() => {
|
|
5320
|
-
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
5321
|
-
const mq = window.matchMedia("(pointer: coarse)");
|
|
5322
|
-
const on = () => setCoarse(isCoarsePointer());
|
|
5323
|
-
if (mq.addEventListener) mq.addEventListener("change", on);
|
|
5324
|
-
else if (mq.addListener) mq.addListener(on);
|
|
5325
|
-
return () => {
|
|
5326
|
-
if (mq.removeEventListener) mq.removeEventListener("change", on);
|
|
5327
|
-
else if (mq.removeListener) mq.removeListener(on);
|
|
5328
|
-
};
|
|
5329
|
-
}, []);
|
|
5330
|
-
return coarse;
|
|
5331
|
-
}
|
|
5332
5564
|
var DEFAULT_LEFT = "calc(1.25rem + env(safe-area-inset-left))";
|
|
5333
5565
|
var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
|
|
5334
5566
|
var FAB_SIZE_PX = 56;
|
|
@@ -5385,8 +5617,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5385
5617
|
};
|
|
5386
5618
|
}
|
|
5387
5619
|
function QaFab() {
|
|
5388
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace } = useQa();
|
|
5389
|
-
const coarse = useCoarsePointer();
|
|
5620
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
5390
5621
|
const [pos, setPos] = React.useState(() => loadFabPos(namespace));
|
|
5391
5622
|
const dragRef = React.useRef(null);
|
|
5392
5623
|
const didDragRef = React.useRef(false);
|
|
@@ -5404,6 +5635,7 @@ function QaFab() {
|
|
|
5404
5635
|
if (captureActive) return null;
|
|
5405
5636
|
const onPointerDown = (e) => {
|
|
5406
5637
|
if (dragRef.current) return;
|
|
5638
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
5407
5639
|
didDragRef.current = false;
|
|
5408
5640
|
const target = e.currentTarget;
|
|
5409
5641
|
const rect = target.getBoundingClientRect();
|
|
@@ -5465,7 +5697,7 @@ function QaFab() {
|
|
|
5465
5697
|
}
|
|
5466
5698
|
setIsOpen(!isOpen);
|
|
5467
5699
|
};
|
|
5468
|
-
const applied =
|
|
5700
|
+
const applied = pos ? clampFabPos(pos) : null;
|
|
5469
5701
|
const fabStyle = {
|
|
5470
5702
|
left: applied ? `${applied.left}px` : DEFAULT_LEFT,
|
|
5471
5703
|
bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
|
|
@@ -5480,13 +5712,13 @@ function QaFab() {
|
|
|
5480
5712
|
"data-qa-overlay": "true",
|
|
5481
5713
|
dir: "ltr",
|
|
5482
5714
|
onClick: handleClick,
|
|
5483
|
-
onPointerDown
|
|
5484
|
-
onPointerMove
|
|
5485
|
-
onPointerUp
|
|
5486
|
-
onPointerCancel
|
|
5715
|
+
onPointerDown,
|
|
5716
|
+
onPointerMove,
|
|
5717
|
+
onPointerUp,
|
|
5718
|
+
onPointerCancel,
|
|
5487
5719
|
"aria-label": "Qapture \u2014 testing notes",
|
|
5488
|
-
title: "
|
|
5489
|
-
className:
|
|
5720
|
+
title: t("fab_title"),
|
|
5721
|
+
className: "qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-bg-accent qa-elev-2 qa-print-hidden qa-fab-btn qa-touch-none",
|
|
5490
5722
|
style: fabStyle,
|
|
5491
5723
|
children: [
|
|
5492
5724
|
!isOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -7803,6 +8035,29 @@ function GuideSection() {
|
|
|
7803
8035
|
))
|
|
7804
8036
|
] });
|
|
7805
8037
|
}
|
|
8038
|
+
function isCoarsePointer() {
|
|
8039
|
+
if (typeof window === "undefined") return false;
|
|
8040
|
+
try {
|
|
8041
|
+
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
|
|
8042
|
+
} catch {
|
|
8043
|
+
}
|
|
8044
|
+
return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
|
|
8045
|
+
}
|
|
8046
|
+
function useCoarsePointer() {
|
|
8047
|
+
const [coarse, setCoarse] = React.useState(() => isCoarsePointer());
|
|
8048
|
+
React.useEffect(() => {
|
|
8049
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
8050
|
+
const mq = window.matchMedia("(pointer: coarse)");
|
|
8051
|
+
const on = () => setCoarse(isCoarsePointer());
|
|
8052
|
+
if (mq.addEventListener) mq.addEventListener("change", on);
|
|
8053
|
+
else if (mq.addListener) mq.addListener(on);
|
|
8054
|
+
return () => {
|
|
8055
|
+
if (mq.removeEventListener) mq.removeEventListener("change", on);
|
|
8056
|
+
else if (mq.removeListener) mq.removeListener(on);
|
|
8057
|
+
};
|
|
8058
|
+
}, []);
|
|
8059
|
+
return coarse;
|
|
8060
|
+
}
|
|
7806
8061
|
var TABS = [
|
|
7807
8062
|
{ key: "notes", labelKey: "tab_notes", icon: "StickyNote" },
|
|
7808
8063
|
{ key: "logins", labelKey: "tab_logins", icon: "KeyRound" },
|
|
@@ -10099,5 +10354,5 @@ function Qapture({ config }) {
|
|
|
10099
10354
|
exports.Qapture = Qapture;
|
|
10100
10355
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10101
10356
|
exports.initQaStudio = initQaStudio;
|
|
10102
|
-
//# sourceMappingURL=chunk-
|
|
10103
|
-
//# sourceMappingURL=chunk-
|
|
10357
|
+
//# sourceMappingURL=chunk-CNNNWLTX.cjs.map
|
|
10358
|
+
//# sourceMappingURL=chunk-CNNNWLTX.cjs.map
|
|
@@ -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, mode2, 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, mode2);
|
|
58
58
|
const s = tx.objectStore(store);
|
|
59
59
|
let result;
|
|
60
60
|
const req = fn(s);
|
|
@@ -1774,10 +1774,11 @@ var STR = {
|
|
|
1774
1774
|
no_evidence: "ticked, no capture",
|
|
1775
1775
|
expected_label: "Expected",
|
|
1776
1776
|
exact_label: "Pixel-exact shots",
|
|
1777
|
-
exact_hint: "Screenshots become a real photo
|
|
1777
|
+
exact_hint: "Screenshots become a real photo instead of a redraw. Asks once to share this tab (or, in Safari and Firefox, this window); nothing leaves your device.",
|
|
1778
1778
|
redraw_notice: "This is a redraw of the page, not a real photo.",
|
|
1779
1779
|
redraw_action: "Use real screenshots",
|
|
1780
|
-
redraw_no_engine: "This browser cannot photograph
|
|
1780
|
+
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.",
|
|
1781
|
+
fab_title: "Qapture \u2014 click to open, drag to move it out of the way",
|
|
1781
1782
|
exact_on: "Pixel-exact screenshots on",
|
|
1782
1783
|
exact_off: "Back to redrawn screenshots",
|
|
1783
1784
|
exact_declined: "Staying on redrawn screenshots",
|
|
@@ -1971,10 +1972,11 @@ var STR = {
|
|
|
1971
1972
|
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1972
1973
|
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
|
|
1973
1974
|
exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
|
|
1974
|
-
exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \
|
|
1975
|
+
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. \u064A\u064F\u0637\u0644\u0628 \u0627\u0644\u0625\u0630\u0646 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629 \u0644\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 \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
|
|
1975
1976
|
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.",
|
|
1976
1977
|
redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
|
|
1977
|
-
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\
|
|
1978
|
+
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.",
|
|
1979
|
+
fab_title: "Qapture \u2014 \u0627\u0636\u063A\u0637 \u0644\u0644\u0641\u062A\u062D\u060C \u0648\u0627\u0633\u062D\u0628 \u0644\u062A\u062D\u0631\u064A\u0643\u0647 \u0645\u0646 \u0645\u0643\u0627\u0646\u0647",
|
|
1978
1980
|
exact_on: "\u062A\u0645 \u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629",
|
|
1979
1981
|
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",
|
|
1980
1982
|
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",
|
|
@@ -2173,8 +2175,168 @@ function computeCoverage(journey, guideChecked, skipped) {
|
|
|
2173
2175
|
};
|
|
2174
2176
|
}
|
|
2175
2177
|
|
|
2178
|
+
// src/lib/frameCalibration.ts
|
|
2179
|
+
var MARKER_SIZE = 28;
|
|
2180
|
+
var MARKERS = [
|
|
2181
|
+
{ key: "tl", color: [255, 0, 0] },
|
|
2182
|
+
{ key: "tr", color: [0, 255, 0] },
|
|
2183
|
+
{ key: "bl", color: [0, 0, 255] },
|
|
2184
|
+
{ key: "br", color: [255, 0, 255] }
|
|
2185
|
+
];
|
|
2186
|
+
function markerCentres(vw, vh) {
|
|
2187
|
+
const h = MARKER_SIZE / 2;
|
|
2188
|
+
return {
|
|
2189
|
+
tl: { x: h, y: h },
|
|
2190
|
+
tr: { x: vw - h, y: h },
|
|
2191
|
+
bl: { x: h, y: vh - h },
|
|
2192
|
+
br: { x: vw - h, y: vh - h }
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
function dist2(r, g, b, c) {
|
|
2196
|
+
const dr = r - c[0];
|
|
2197
|
+
const dg = g - c[1];
|
|
2198
|
+
const db = b - c[2];
|
|
2199
|
+
return dr * dr + dg * dg + db * db;
|
|
2200
|
+
}
|
|
2201
|
+
var COLOR_TOLERANCE_2 = 90 * 90;
|
|
2202
|
+
var CLUSTER_CELL = 8;
|
|
2203
|
+
var MAX_CANDIDATES = 4;
|
|
2204
|
+
function findMarkerCandidates(data, width, height) {
|
|
2205
|
+
const cols = Math.ceil(width / CLUSTER_CELL);
|
|
2206
|
+
const rows = Math.ceil(height / CLUSTER_CELL);
|
|
2207
|
+
const cells = {};
|
|
2208
|
+
for (const m of MARKERS) cells[m.key] = new Int32Array(cols * rows);
|
|
2209
|
+
for (let y = 0; y < height; y++) {
|
|
2210
|
+
const row = y * width * 4;
|
|
2211
|
+
const cy = y / CLUSTER_CELL | 0;
|
|
2212
|
+
for (let x = 0; x < width; x++) {
|
|
2213
|
+
const i = row + x * 4;
|
|
2214
|
+
if (data[i + 3] < 128) continue;
|
|
2215
|
+
const r = data[i];
|
|
2216
|
+
const g = data[i + 1];
|
|
2217
|
+
const b = data[i + 2];
|
|
2218
|
+
if (Math.max(r, g, b) - Math.min(r, g, b) < 60) continue;
|
|
2219
|
+
for (const m of MARKERS) {
|
|
2220
|
+
if (dist2(r, g, b, m.color) <= COLOR_TOLERANCE_2) {
|
|
2221
|
+
cells[m.key][cy * cols + (x / CLUSTER_CELL | 0)]++;
|
|
2222
|
+
break;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
const out = {};
|
|
2228
|
+
for (const m of MARKERS) {
|
|
2229
|
+
const grid = cells[m.key];
|
|
2230
|
+
const seen = new Uint8Array(cols * rows);
|
|
2231
|
+
const blobs = [];
|
|
2232
|
+
for (let start = 0; start < grid.length; start++) {
|
|
2233
|
+
if (!grid[start] || seen[start]) continue;
|
|
2234
|
+
const queue = [start];
|
|
2235
|
+
seen[start] = 1;
|
|
2236
|
+
let sumX = 0;
|
|
2237
|
+
let sumY = 0;
|
|
2238
|
+
let total = 0;
|
|
2239
|
+
while (queue.length) {
|
|
2240
|
+
const idx = queue.pop();
|
|
2241
|
+
const n = grid[idx];
|
|
2242
|
+
const cx = idx % cols;
|
|
2243
|
+
const cy = idx / cols | 0;
|
|
2244
|
+
sumX += (cx * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
|
|
2245
|
+
sumY += (cy * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
|
|
2246
|
+
total += n;
|
|
2247
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
2248
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
2249
|
+
const nx = cx + dx;
|
|
2250
|
+
const ny = cy + dy;
|
|
2251
|
+
if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
|
|
2252
|
+
const nIdx = ny * cols + nx;
|
|
2253
|
+
if (seen[nIdx] || !grid[nIdx]) continue;
|
|
2254
|
+
seen[nIdx] = 1;
|
|
2255
|
+
queue.push(nIdx);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
if (total < 16) continue;
|
|
2260
|
+
blobs.push({ x: sumX / total, y: sumY / total, count: total });
|
|
2261
|
+
}
|
|
2262
|
+
blobs.sort((a, b) => b.count - a.count);
|
|
2263
|
+
out[m.key] = blobs.slice(0, MAX_CANDIDATES);
|
|
2264
|
+
}
|
|
2265
|
+
return out;
|
|
2266
|
+
}
|
|
2267
|
+
var VERIFY_TOLERANCE = 0.02;
|
|
2268
|
+
var SQUARENESS_TOLERANCE = 0.05;
|
|
2269
|
+
function deriveMapping(candidates, vw, vh, frameW, frameH) {
|
|
2270
|
+
const tls = candidates.tl ?? [];
|
|
2271
|
+
const brs = candidates.br ?? [];
|
|
2272
|
+
for (const tl of tls) {
|
|
2273
|
+
for (const br of brs) {
|
|
2274
|
+
const m = solveFrom(tl, br, candidates, vw, vh, frameW, frameH);
|
|
2275
|
+
if (m) return m;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
|
|
2281
|
+
if (!(vw > MARKER_SIZE) || !(vh > MARKER_SIZE)) return null;
|
|
2282
|
+
const centres = markerCentres(vw, vh);
|
|
2283
|
+
const spanCssX = centres.br.x - centres.tl.x;
|
|
2284
|
+
const spanCssY = centres.br.y - centres.tl.y;
|
|
2285
|
+
if (spanCssX <= 0 || spanCssY <= 0) return null;
|
|
2286
|
+
const scaleX = (br.x - tl.x) / spanCssX;
|
|
2287
|
+
const scaleY = (br.y - tl.y) / spanCssY;
|
|
2288
|
+
if (!(scaleX > 0.05) || !(scaleY > 0.05)) return null;
|
|
2289
|
+
if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return null;
|
|
2290
|
+
if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
|
|
2291
|
+
const originX = tl.x - centres.tl.x * scaleX;
|
|
2292
|
+
const originY = tl.y - centres.tl.y * scaleY;
|
|
2293
|
+
const mapping2 = { scaleX, scaleY, originX, originY };
|
|
2294
|
+
const farX = originX + vw * scaleX;
|
|
2295
|
+
const farY = originY + vh * scaleY;
|
|
2296
|
+
const slack = Math.max(4, frameW * 0.01);
|
|
2297
|
+
if (originX < -slack || originY < -slack || farX > frameW + slack || farY > frameH + slack) {
|
|
2298
|
+
return null;
|
|
2299
|
+
}
|
|
2300
|
+
const tolerance = Math.max(6, Math.max(frameW, frameH) * VERIFY_TOLERANCE);
|
|
2301
|
+
let verified = 0;
|
|
2302
|
+
for (const key of ["tr", "bl"]) {
|
|
2303
|
+
const hits = candidates[key] ?? [];
|
|
2304
|
+
if (!hits.length) continue;
|
|
2305
|
+
const want = centres[key];
|
|
2306
|
+
const px = originX + want.x * scaleX;
|
|
2307
|
+
const py = originY + want.y * scaleY;
|
|
2308
|
+
if (!hits.some((h) => Math.hypot(px - h.x, py - h.y) <= tolerance)) return null;
|
|
2309
|
+
verified++;
|
|
2310
|
+
}
|
|
2311
|
+
if (verified === 0) return null;
|
|
2312
|
+
return mapping2;
|
|
2313
|
+
}
|
|
2314
|
+
function mapRectToFrame(rect, m, frameW, frameH) {
|
|
2315
|
+
const x0 = m.originX + rect.left * m.scaleX;
|
|
2316
|
+
const y0 = m.originY + rect.top * m.scaleY;
|
|
2317
|
+
const sx = Math.max(0, Math.min(frameW, Math.round(x0)));
|
|
2318
|
+
const sy = Math.max(0, Math.min(frameH, Math.round(y0)));
|
|
2319
|
+
const sw = Math.min(frameW - sx, Math.round(rect.width * m.scaleX));
|
|
2320
|
+
const sh = Math.min(frameH - sy, Math.round(rect.height * m.scaleY));
|
|
2321
|
+
if (sw < 1 || sh < 1) return null;
|
|
2322
|
+
return { sx, sy, sw, sh };
|
|
2323
|
+
}
|
|
2324
|
+
function environmentSignature() {
|
|
2325
|
+
if (typeof window === "undefined") return "";
|
|
2326
|
+
return [
|
|
2327
|
+
window.innerWidth,
|
|
2328
|
+
window.innerHeight,
|
|
2329
|
+
window.outerWidth,
|
|
2330
|
+
window.outerHeight,
|
|
2331
|
+
window.screenX,
|
|
2332
|
+
window.screenY,
|
|
2333
|
+
Math.round((window.devicePixelRatio || 1) * 100)
|
|
2334
|
+
].join("x");
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2176
2337
|
// src/lib/screenCapture.ts
|
|
2177
2338
|
var ASPECT_TOLERANCE = 0.08;
|
|
2339
|
+
var CALIBRATION_SETTLE_MS = 220;
|
|
2178
2340
|
var FRESH_FRAME_TIMEOUT_MS = 500;
|
|
2179
2341
|
var VIDEO_READY_TIMEOUT_MS = 4e3;
|
|
2180
2342
|
var stream = null;
|
|
@@ -2182,22 +2344,14 @@ var track = null;
|
|
|
2182
2344
|
var video = null;
|
|
2183
2345
|
var grabber = null;
|
|
2184
2346
|
var declined = false;
|
|
2185
|
-
function supportsPreferCurrentTab() {
|
|
2186
|
-
const nav = navigator;
|
|
2187
|
-
const brands = nav.userAgentData?.brands;
|
|
2188
|
-
if (Array.isArray(brands) && brands.length) {
|
|
2189
|
-
return brands.some((b) => /chromium/i.test(b.brand));
|
|
2190
|
-
}
|
|
2191
|
-
const ua = navigator.userAgent || "";
|
|
2192
|
-
if (/CriOS|FxiOS|EdgiOS|Firefox/i.test(ua)) return false;
|
|
2193
|
-
return /Chrome|Chromium|Edg\//i.test(ua);
|
|
2194
|
-
}
|
|
2195
2347
|
function isExactCaptureSupported() {
|
|
2196
2348
|
if (typeof navigator === "undefined" || typeof document === "undefined") return false;
|
|
2197
2349
|
const md = navigator.mediaDevices;
|
|
2198
|
-
|
|
2199
|
-
return supportsPreferCurrentTab();
|
|
2350
|
+
return !!md && typeof md.getDisplayMedia === "function";
|
|
2200
2351
|
}
|
|
2352
|
+
var mode = "tab";
|
|
2353
|
+
var mapping = null;
|
|
2354
|
+
var mappingSignature = "";
|
|
2201
2355
|
function getExactCaptureStatus() {
|
|
2202
2356
|
if (!isExactCaptureSupported()) return "unsupported";
|
|
2203
2357
|
if (track && track.readyState === "live") return "live";
|
|
@@ -2225,6 +2379,9 @@ function teardown() {
|
|
|
2225
2379
|
stream = null;
|
|
2226
2380
|
track = null;
|
|
2227
2381
|
grabber = null;
|
|
2382
|
+
mode = "tab";
|
|
2383
|
+
mapping = null;
|
|
2384
|
+
mappingSignature = "";
|
|
2228
2385
|
}
|
|
2229
2386
|
function stopExactCapture() {
|
|
2230
2387
|
teardown();
|
|
@@ -2279,6 +2436,78 @@ function validateFrameMapping(frameW, frameH) {
|
|
|
2279
2436
|
const viewAspect = vw / vh;
|
|
2280
2437
|
return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
|
|
2281
2438
|
}
|
|
2439
|
+
async function grabFrameCanvas() {
|
|
2440
|
+
if (!video) return null;
|
|
2441
|
+
await nextPresentedFrame(video);
|
|
2442
|
+
let source = video;
|
|
2443
|
+
let w = video.videoWidth;
|
|
2444
|
+
let h = video.videoHeight;
|
|
2445
|
+
let bitmap = null;
|
|
2446
|
+
if (grabber) {
|
|
2447
|
+
try {
|
|
2448
|
+
bitmap = await grabber.grabFrame();
|
|
2449
|
+
source = bitmap;
|
|
2450
|
+
w = bitmap.width;
|
|
2451
|
+
h = bitmap.height;
|
|
2452
|
+
} catch {
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
if (!w || !h) {
|
|
2456
|
+
bitmap?.close?.();
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
const c = document.createElement("canvas");
|
|
2460
|
+
c.width = w;
|
|
2461
|
+
c.height = h;
|
|
2462
|
+
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
2463
|
+
if (!ctx) {
|
|
2464
|
+
bitmap?.close?.();
|
|
2465
|
+
return null;
|
|
2466
|
+
}
|
|
2467
|
+
ctx.drawImage(source, 0, 0);
|
|
2468
|
+
bitmap?.close?.();
|
|
2469
|
+
return c;
|
|
2470
|
+
}
|
|
2471
|
+
async function calibrate() {
|
|
2472
|
+
if (!video) return false;
|
|
2473
|
+
const vw = window.innerWidth;
|
|
2474
|
+
const vh = window.innerHeight;
|
|
2475
|
+
if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
|
|
2476
|
+
const card = document.createElement("div");
|
|
2477
|
+
card.setAttribute("data-qa-calibration", "true");
|
|
2478
|
+
card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
|
|
2479
|
+
const centres = markerCentres(vw, vh);
|
|
2480
|
+
for (const m of MARKERS) {
|
|
2481
|
+
const dot = document.createElement("div");
|
|
2482
|
+
const c = centres[m.key];
|
|
2483
|
+
dot.style.cssText = `position:absolute;width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;left:${c.x - MARKER_SIZE / 2}px;top:${c.y - MARKER_SIZE / 2}px;background:rgb(${m.color[0]},${m.color[1]},${m.color[2]});`;
|
|
2484
|
+
card.appendChild(dot);
|
|
2485
|
+
}
|
|
2486
|
+
document.body.appendChild(card);
|
|
2487
|
+
try {
|
|
2488
|
+
await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
|
|
2489
|
+
const frame = await grabFrameCanvas();
|
|
2490
|
+
if (!frame) return false;
|
|
2491
|
+
const ctx = frame.getContext("2d", { willReadFrequently: true });
|
|
2492
|
+
if (!ctx) return false;
|
|
2493
|
+
const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
|
|
2494
|
+
const found = deriveMapping(
|
|
2495
|
+
findMarkerCandidates(data, frame.width, frame.height),
|
|
2496
|
+
vw,
|
|
2497
|
+
vh,
|
|
2498
|
+
frame.width,
|
|
2499
|
+
frame.height
|
|
2500
|
+
);
|
|
2501
|
+
if (!found) return false;
|
|
2502
|
+
mapping = found;
|
|
2503
|
+
mappingSignature = environmentSignature();
|
|
2504
|
+
return true;
|
|
2505
|
+
} catch {
|
|
2506
|
+
return false;
|
|
2507
|
+
} finally {
|
|
2508
|
+
card.remove();
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2282
2511
|
async function startExactCapture() {
|
|
2283
2512
|
if (!isExactCaptureSupported()) return false;
|
|
2284
2513
|
if (track && track.readyState === "live" && video) return true;
|
|
@@ -2308,11 +2537,7 @@ async function startExactCapture() {
|
|
|
2308
2537
|
return false;
|
|
2309
2538
|
}
|
|
2310
2539
|
const settings = t.getSettings();
|
|
2311
|
-
|
|
2312
|
-
for (const x of s.getTracks()) x.stop();
|
|
2313
|
-
declined = true;
|
|
2314
|
-
return false;
|
|
2315
|
-
}
|
|
2540
|
+
const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
|
|
2316
2541
|
const el = document.createElement("video");
|
|
2317
2542
|
el.setAttribute("data-qa-overlay", "true");
|
|
2318
2543
|
el.muted = true;
|
|
@@ -2325,17 +2550,31 @@ async function startExactCapture() {
|
|
|
2325
2550
|
} catch {
|
|
2326
2551
|
}
|
|
2327
2552
|
const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
|
|
2328
|
-
|
|
2553
|
+
const giveUp = () => {
|
|
2329
2554
|
el.remove();
|
|
2330
2555
|
for (const x of s.getTracks()) x.stop();
|
|
2331
2556
|
declined = true;
|
|
2332
2557
|
return false;
|
|
2333
|
-
}
|
|
2558
|
+
};
|
|
2559
|
+
if (!ready) return giveUp();
|
|
2560
|
+
const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
|
|
2334
2561
|
stream = s;
|
|
2335
2562
|
track = t;
|
|
2336
2563
|
video = el;
|
|
2337
2564
|
const IC = window.ImageCapture;
|
|
2338
2565
|
grabber = IC ? new IC(t) : null;
|
|
2566
|
+
if (isDirect) {
|
|
2567
|
+
mode = "tab";
|
|
2568
|
+
mapping = null;
|
|
2569
|
+
mappingSignature = "";
|
|
2570
|
+
} else {
|
|
2571
|
+
mode = "surface";
|
|
2572
|
+
if (!await calibrate()) {
|
|
2573
|
+
teardown();
|
|
2574
|
+
declined = true;
|
|
2575
|
+
return false;
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2339
2578
|
t.addEventListener("ended", () => {
|
|
2340
2579
|
teardown();
|
|
2341
2580
|
});
|
|
@@ -2348,6 +2587,11 @@ async function startExactCapture() {
|
|
|
2348
2587
|
}
|
|
2349
2588
|
async function grabExactRegion(rect) {
|
|
2350
2589
|
if (!track || track.readyState !== "live" || !video) return null;
|
|
2590
|
+
if (mode === "surface") {
|
|
2591
|
+
if (!mapping || environmentSignature() !== mappingSignature) {
|
|
2592
|
+
if (!await calibrate()) return null;
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2351
2595
|
await nextPresentedFrame(video);
|
|
2352
2596
|
let source;
|
|
2353
2597
|
let frameW;
|
|
@@ -2370,13 +2614,24 @@ async function grabExactRegion(rect) {
|
|
|
2370
2614
|
frameH = video.videoHeight;
|
|
2371
2615
|
}
|
|
2372
2616
|
try {
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2617
|
+
let sx;
|
|
2618
|
+
let sy;
|
|
2619
|
+
let sw;
|
|
2620
|
+
let sh;
|
|
2621
|
+
if (mode === "surface") {
|
|
2622
|
+
if (!mapping) return null;
|
|
2623
|
+
const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
|
|
2624
|
+
if (!mapped) return null;
|
|
2625
|
+
({ sx, sy, sw, sh } = mapped);
|
|
2626
|
+
} else {
|
|
2627
|
+
if (!validateFrameMapping(frameW, frameH)) return null;
|
|
2628
|
+
const scaleX = frameW / window.innerWidth;
|
|
2629
|
+
const scaleY = frameH / window.innerHeight;
|
|
2630
|
+
sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
|
|
2631
|
+
sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
|
|
2632
|
+
sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
|
|
2633
|
+
sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
|
|
2634
|
+
}
|
|
2380
2635
|
const canvas = document.createElement("canvas");
|
|
2381
2636
|
canvas.width = sw;
|
|
2382
2637
|
canvas.height = sh;
|
|
@@ -5299,29 +5554,6 @@ function Icon({
|
|
|
5299
5554
|
function renderSvgElement(tag, attrs, key) {
|
|
5300
5555
|
return React.createElement(tag, { key, ...attrs });
|
|
5301
5556
|
}
|
|
5302
|
-
function isCoarsePointer() {
|
|
5303
|
-
if (typeof window === "undefined") return false;
|
|
5304
|
-
try {
|
|
5305
|
-
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
|
|
5306
|
-
} catch {
|
|
5307
|
-
}
|
|
5308
|
-
return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
|
|
5309
|
-
}
|
|
5310
|
-
function useCoarsePointer() {
|
|
5311
|
-
const [coarse, setCoarse] = useState(() => isCoarsePointer());
|
|
5312
|
-
useEffect(() => {
|
|
5313
|
-
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
5314
|
-
const mq = window.matchMedia("(pointer: coarse)");
|
|
5315
|
-
const on = () => setCoarse(isCoarsePointer());
|
|
5316
|
-
if (mq.addEventListener) mq.addEventListener("change", on);
|
|
5317
|
-
else if (mq.addListener) mq.addListener(on);
|
|
5318
|
-
return () => {
|
|
5319
|
-
if (mq.removeEventListener) mq.removeEventListener("change", on);
|
|
5320
|
-
else if (mq.removeListener) mq.removeListener(on);
|
|
5321
|
-
};
|
|
5322
|
-
}, []);
|
|
5323
|
-
return coarse;
|
|
5324
|
-
}
|
|
5325
5557
|
var DEFAULT_LEFT = "calc(1.25rem + env(safe-area-inset-left))";
|
|
5326
5558
|
var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
|
|
5327
5559
|
var FAB_SIZE_PX = 56;
|
|
@@ -5378,8 +5610,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5378
5610
|
};
|
|
5379
5611
|
}
|
|
5380
5612
|
function QaFab() {
|
|
5381
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace } = useQa();
|
|
5382
|
-
const coarse = useCoarsePointer();
|
|
5613
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
5383
5614
|
const [pos, setPos] = useState(() => loadFabPos(namespace));
|
|
5384
5615
|
const dragRef = useRef(null);
|
|
5385
5616
|
const didDragRef = useRef(false);
|
|
@@ -5397,6 +5628,7 @@ function QaFab() {
|
|
|
5397
5628
|
if (captureActive) return null;
|
|
5398
5629
|
const onPointerDown = (e) => {
|
|
5399
5630
|
if (dragRef.current) return;
|
|
5631
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
5400
5632
|
didDragRef.current = false;
|
|
5401
5633
|
const target = e.currentTarget;
|
|
5402
5634
|
const rect = target.getBoundingClientRect();
|
|
@@ -5458,7 +5690,7 @@ function QaFab() {
|
|
|
5458
5690
|
}
|
|
5459
5691
|
setIsOpen(!isOpen);
|
|
5460
5692
|
};
|
|
5461
|
-
const applied =
|
|
5693
|
+
const applied = pos ? clampFabPos(pos) : null;
|
|
5462
5694
|
const fabStyle = {
|
|
5463
5695
|
left: applied ? `${applied.left}px` : DEFAULT_LEFT,
|
|
5464
5696
|
bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
|
|
@@ -5473,13 +5705,13 @@ function QaFab() {
|
|
|
5473
5705
|
"data-qa-overlay": "true",
|
|
5474
5706
|
dir: "ltr",
|
|
5475
5707
|
onClick: handleClick,
|
|
5476
|
-
onPointerDown
|
|
5477
|
-
onPointerMove
|
|
5478
|
-
onPointerUp
|
|
5479
|
-
onPointerCancel
|
|
5708
|
+
onPointerDown,
|
|
5709
|
+
onPointerMove,
|
|
5710
|
+
onPointerUp,
|
|
5711
|
+
onPointerCancel,
|
|
5480
5712
|
"aria-label": "Qapture \u2014 testing notes",
|
|
5481
|
-
title: "
|
|
5482
|
-
className:
|
|
5713
|
+
title: t("fab_title"),
|
|
5714
|
+
className: "qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-bg-accent qa-elev-2 qa-print-hidden qa-fab-btn qa-touch-none",
|
|
5483
5715
|
style: fabStyle,
|
|
5484
5716
|
children: [
|
|
5485
5717
|
!isOpen && /* @__PURE__ */ jsx(
|
|
@@ -7796,6 +8028,29 @@ function GuideSection() {
|
|
|
7796
8028
|
))
|
|
7797
8029
|
] });
|
|
7798
8030
|
}
|
|
8031
|
+
function isCoarsePointer() {
|
|
8032
|
+
if (typeof window === "undefined") return false;
|
|
8033
|
+
try {
|
|
8034
|
+
if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
|
|
8035
|
+
} catch {
|
|
8036
|
+
}
|
|
8037
|
+
return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
|
|
8038
|
+
}
|
|
8039
|
+
function useCoarsePointer() {
|
|
8040
|
+
const [coarse, setCoarse] = useState(() => isCoarsePointer());
|
|
8041
|
+
useEffect(() => {
|
|
8042
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
8043
|
+
const mq = window.matchMedia("(pointer: coarse)");
|
|
8044
|
+
const on = () => setCoarse(isCoarsePointer());
|
|
8045
|
+
if (mq.addEventListener) mq.addEventListener("change", on);
|
|
8046
|
+
else if (mq.addListener) mq.addListener(on);
|
|
8047
|
+
return () => {
|
|
8048
|
+
if (mq.removeEventListener) mq.removeEventListener("change", on);
|
|
8049
|
+
else if (mq.removeListener) mq.removeListener(on);
|
|
8050
|
+
};
|
|
8051
|
+
}, []);
|
|
8052
|
+
return coarse;
|
|
8053
|
+
}
|
|
7799
8054
|
var TABS = [
|
|
7800
8055
|
{ key: "notes", labelKey: "tab_notes", icon: "StickyNote" },
|
|
7801
8056
|
{ key: "logins", labelKey: "tab_logins", icon: "KeyRound" },
|
|
@@ -10090,5 +10345,5 @@ function Qapture({ config }) {
|
|
|
10090
10345
|
}
|
|
10091
10346
|
|
|
10092
10347
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10093
|
-
//# sourceMappingURL=chunk-
|
|
10094
|
-
//# sourceMappingURL=chunk-
|
|
10348
|
+
//# sourceMappingURL=chunk-NM3IDVEL.js.map
|
|
10349
|
+
//# sourceMappingURL=chunk-NM3IDVEL.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkCNNNWLTX_cjs = require('./chunk-CNNNWLTX.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, "QaStudio", {
|
|
8
8
|
enumerable: true,
|
|
9
|
-
get: function () { return
|
|
9
|
+
get: function () { return chunkCNNNWLTX_cjs.Qapture; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "Qapture", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkCNNNWLTX_cjs.Qapture; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "deleteQaDatabase", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkCNNNWLTX_cjs.deleteQaDatabase; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "initQaStudio", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
21
|
+
get: function () { return chunkCNNNWLTX_cjs.initQaStudio; }
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=index.cjs.map
|
|
24
24
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
package/dist/next.cjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var chunkCNNNWLTX_cjs = require('./chunk-CNNNWLTX.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Object.defineProperty(exports, "QaStudio", {
|
|
9
9
|
enumerable: true,
|
|
10
|
-
get: function () { return
|
|
10
|
+
get: function () { return chunkCNNNWLTX_cjs.Qapture; }
|
|
11
11
|
});
|
|
12
12
|
Object.defineProperty(exports, "Qapture", {
|
|
13
13
|
enumerable: true,
|
|
14
|
-
get: function () { return
|
|
14
|
+
get: function () { return chunkCNNNWLTX_cjs.Qapture; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "initQaStudio", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
18
|
+
get: function () { return chunkCNNNWLTX_cjs.initQaStudio; }
|
|
19
19
|
});
|
|
20
20
|
//# sourceMappingURL=next.cjs.map
|
|
21
21
|
//# sourceMappingURL=next.cjs.map
|
package/dist/next.js
CHANGED
package/dist/standalone.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkCNNNWLTX_cjs = require('./chunk-CNNNWLTX.cjs');
|
|
4
4
|
|
|
5
5
|
// src/standalone.ts
|
|
6
6
|
if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
|
|
@@ -38,7 +38,7 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
|
|
|
38
38
|
|
|
39
39
|
Object.defineProperty(exports, "initQaStudio", {
|
|
40
40
|
enumerable: true,
|
|
41
|
-
get: function () { return
|
|
41
|
+
get: function () { return chunkCNNNWLTX_cjs.initQaStudio; }
|
|
42
42
|
});
|
|
43
43
|
//# sourceMappingURL=standalone.cjs.map
|
|
44
44
|
//# sourceMappingURL=standalone.cjs.map
|
package/dist/standalone.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qapture2",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.6",
|
|
4
4
|
"description": "Drop-in, AI-aware in-browser QA capture widget. A tester annotates the live app (element/region + auto-screenshot + note), tracks a graded testing journey, and exports a ZIP your own coding agent reads. Ships zero AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -86,6 +86,7 @@
|
|
|
86
86
|
"selector-smoke": "node scripts/selector-smoke.mjs",
|
|
87
87
|
"capture-timeout-smoke": "node scripts/capture-timeout-smoke.mjs",
|
|
88
88
|
"fs-sync-smoke": "node scripts/fs-sync-smoke.mjs",
|
|
89
|
+
"frame-calibration-smoke": "node scripts/frame-calibration-smoke.mjs",
|
|
89
90
|
"standalone-smoke": "node scripts/standalone-smoke.mjs",
|
|
90
91
|
"browser-test": "node scripts/browser-test.mjs",
|
|
91
92
|
"capture-accuracy-test": "node scripts/capture-accuracy-test.mjs",
|
|
@@ -94,7 +95,7 @@
|
|
|
94
95
|
"loop-features-test": "node scripts/loop-features-test.mjs",
|
|
95
96
|
"walk-test": "node scripts/walk-test.mjs",
|
|
96
97
|
"modern-css-test": "node scripts/modern-css-test.mjs",
|
|
97
|
-
"verify": "npm run typecheck && npm run typecheck:bin && npm run build && npm run smoke && npm run export-smoke && npm run cli-smoke && npm run cli-detectors-smoke && npm run selector-smoke && npm run capture-timeout-smoke && npm run fs-sync-smoke && npm run standalone-smoke",
|
|
98
|
+
"verify": "npm run typecheck && npm run typecheck:bin && npm run build && npm run smoke && npm run export-smoke && npm run cli-smoke && npm run cli-detectors-smoke && npm run selector-smoke && npm run capture-timeout-smoke && npm run fs-sync-smoke && npm run frame-calibration-smoke && npm run standalone-smoke",
|
|
98
99
|
"play": "npm --prefix playground run dev"
|
|
99
100
|
},
|
|
100
101
|
"engines": {
|