qapture2 0.9.1 → 0.10.1

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.
@@ -173,6 +173,7 @@ function deleteQaDatabase(namespace) {
173
173
  // src/config/schema.ts
174
174
  var DEFAULTS = {
175
175
  namespace: "qapture",
176
+ shotPort: 7017,
176
177
  brandLabel: "Qapture",
177
178
  loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
178
179
  rtl: false,
@@ -343,6 +344,7 @@ function validateConfig(input) {
343
344
  return {
344
345
  config: {
345
346
  namespace: DEFAULTS.namespace,
347
+ shotPort: DEFAULTS.shotPort,
346
348
  brand: { label: DEFAULTS.brandLabel },
347
349
  loginField: { ...DEFAULTS.loginField },
348
350
  credentials: [],
@@ -365,6 +367,7 @@ function validateConfig(input) {
365
367
  return {
366
368
  config: {
367
369
  namespace: DEFAULTS.namespace,
370
+ shotPort: DEFAULTS.shotPort,
368
371
  brand: { label: DEFAULTS.brandLabel },
369
372
  loginField: { ...DEFAULTS.loginField },
370
373
  credentials: [],
@@ -384,6 +387,8 @@ function validateConfig(input) {
384
387
  }
385
388
  const raw = input;
386
389
  const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
390
+ const rawPort = Number(raw["shotPort"]);
391
+ const shotPort = Number.isInteger(rawPort) && rawPort > 0 && rawPort < 65536 ? rawPort : DEFAULTS.shotPort;
387
392
  if (raw["theme"] !== void 0) {
388
393
  warnings.push(
389
394
  'theme: custom themes were removed in Qapture 0.3.0 \u2014 the widget now ships one fixed, self-contained design. The "theme" key is ignored; remove it from your qa.config to silence this warning.'
@@ -433,6 +438,7 @@ function validateConfig(input) {
433
438
  return {
434
439
  config: {
435
440
  namespace,
441
+ shotPort,
436
442
  brand: { label: brandLabel },
437
443
  loginField,
438
444
  credentials,
@@ -1938,6 +1944,10 @@ var STR = {
1938
1944
  exact_declined: "Staying on redrawn screenshots",
1939
1945
  exact_turn_on: "Turn on",
1940
1946
  exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
1947
+ exact_native_hint: "Screenshots are real photographs, taken by the qapture helper on this machine with the same engine as Cmd+Shift+4. It never asks permission and leaves nothing recording, so it costs no battery. Nothing leaves your device.",
1948
+ exact_native_on: "Real screenshots on \u2014 local helper",
1949
+ exact_helper_offer: "To stop being asked every time: run this next to your dev server and leave it going. macOS asks once for Screen Recording, then never again.",
1950
+ exact_helper_safari: "Safari cannot reach the local helper on an https site \u2014 it blocks loopback as mixed content, and no setting changes that. Open this site in Chrome for screenshots that never ask, or turn the option above off to stay on redrawn shots.",
1941
1951
  sync_title: "Save to a folder",
1942
1952
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1943
1953
  sync_choose: "Choose folder",
@@ -2175,6 +2185,10 @@ var STR = {
2175
2185
  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",
2176
2186
  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",
2177
2187
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
2188
+ exact_native_hint: "\u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0635\u0648\u0631 \u062D\u0642\u064A\u0642\u064A\u0629 \u064A\u0644\u062A\u0642\u0637\u0647\u0627 \u0645\u0633\u0627\u0639\u062F qapture \u0639\u0644\u0649 \u0647\u0630\u0627 \u0627\u0644\u062C\u0647\u0627\u0632 \u0628\u0627\u0644\u0645\u062D\u0631\u0643 \u0646\u0641\u0633\u0647 \u0627\u0644\u0630\u064A \u064A\u0633\u062A\u062E\u062F\u0645\u0647 Cmd+Shift+4. \u0644\u0627 \u064A\u0637\u0644\u0628 \u0625\u0630\u0646\u064B\u0627 \u0648\u0644\u0627 \u064A\u062A\u0631\u0643 \u0623\u064A \u062A\u0633\u062C\u064A\u0644 \u064A\u0639\u0645\u0644\u060C \u0641\u0644\u0627 \u064A\u0633\u062A\u0647\u0644\u0643 \u0627\u0644\u0628\u0637\u0627\u0631\u064A\u0629. \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
2189
+ exact_native_on: "\u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u062D\u0642\u064A\u0642\u064A\u0629 \u0645\u0641\u0639\u0651\u0644\u0629 \u2014 \u0627\u0644\u0645\u0633\u0627\u0639\u062F \u0627\u0644\u0645\u062D\u0644\u064A",
2190
+ exact_helper_offer: "\u0644\u0625\u064A\u0642\u0627\u0641 \u0627\u0644\u0633\u0624\u0627\u0644 \u0641\u064A \u0643\u0644 \u0645\u0631\u0629: \u0634\u063A\u0651\u0644 \u0647\u0630\u0627 \u0627\u0644\u0623\u0645\u0631 \u0628\u062C\u0627\u0646\u0628 \u062E\u0627\u062F\u0645 \u0627\u0644\u062A\u0637\u0648\u064A\u0631 \u0648\u0627\u062A\u0631\u0643\u0647 \u064A\u0639\u0645\u0644. \u064A\u0637\u0644\u0628 \u0645\u0627\u0643 \u0625\u0630\u0646 \u062A\u0633\u062C\u064A\u0644 \u0627\u0644\u0634\u0627\u0634\u0629 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629 \u0641\u0642\u0637.",
2191
+ exact_helper_safari: "\u0644\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 Safari \u0627\u0644\u0648\u0635\u0648\u0644 \u0625\u0644\u0649 \u0627\u0644\u0645\u0633\u0627\u0639\u062F \u0627\u0644\u0645\u062D\u0644\u064A \u0639\u0644\u0649 \u0645\u0648\u0642\u0639 https \u2014 \u0641\u0647\u0648 \u064A\u062D\u062C\u0628 \u0627\u0644\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0645\u062D\u0644\u064A \u0643\u0645\u062D\u062A\u0648\u0649 \u0645\u062E\u062A\u0644\u0637\u060C \u0648\u0644\u0627 \u064A\u063A\u064A\u0651\u0631 \u0630\u0644\u0643 \u0623\u064A \u0625\u0639\u062F\u0627\u062F. \u0627\u0641\u062A\u062D \u0627\u0644\u0645\u0648\u0642\u0639 \u0641\u064A Chrome \u0644\u0644\u062D\u0635\u0648\u0644 \u0639\u0644\u0649 \u0644\u0642\u0637\u0627\u062A \u0628\u0644\u0627 \u0633\u0624\u0627\u0644\u060C \u0623\u0648 \u0623\u0637\u0641\u0626 \u0627\u0644\u062E\u064A\u0627\u0631 \u0623\u0639\u0644\u0627\u0647 \u0644\u0644\u0628\u0642\u0627\u0621 \u0639\u0644\u0649 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627.",
2178
2192
  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",
2179
2193
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
2180
2194
  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.",
@@ -2511,13 +2525,156 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2511
2525
  if (sw < 1 || sh < 1) return null;
2512
2526
  return { sx, sy, sw, sh };
2513
2527
  }
2528
+ function environmentSignature() {
2529
+ if (typeof window === "undefined") return "";
2530
+ return [
2531
+ window.innerWidth,
2532
+ window.innerHeight,
2533
+ window.outerWidth,
2534
+ window.outerHeight,
2535
+ window.screenX,
2536
+ window.screenY,
2537
+ Math.round((window.devicePixelRatio || 1) * 100)
2538
+ ].join("x");
2539
+ }
2540
+
2541
+ // src/lib/nativeShot.ts
2542
+ var DEFAULT_SHOT_PORT = 7017;
2543
+ var PROBE_TIMEOUT_MS = 700;
2544
+ var SHOT_TIMEOUT_MS = 9e3;
2545
+ var ABSENT_RECHECK_MS = 3e4;
2546
+ var configuredPort = DEFAULT_SHOT_PORT;
2547
+ function helperReachableHere() {
2548
+ if (typeof window === "undefined") return false;
2549
+ if (window.location.protocol !== "https:") return true;
2550
+ const ua = navigator.userAgent;
2551
+ const isSafari = /Safari/.test(ua) && !/Chrome|Chromium|Edg|OPR/.test(ua);
2552
+ return !isSafari;
2553
+ }
2554
+ function setShotPort(port) {
2555
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return;
2556
+ if (port === configuredPort) return;
2557
+ configuredPort = port;
2558
+ resetNativeShotProbe();
2559
+ }
2560
+ var cachedBase = null;
2561
+ var lastProbeAt = 0;
2562
+ var lastProbeResult = false;
2563
+ function baseUrl(port) {
2564
+ return `http://127.0.0.1:${port}`;
2565
+ }
2566
+ async function fetchWithTimeout(url, init, ms) {
2567
+ if (typeof fetch !== "function") return null;
2568
+ const ac = typeof AbortController === "function" ? new AbortController() : null;
2569
+ const timer = setTimeout(() => ac?.abort(), ms);
2570
+ try {
2571
+ return await fetch(url, { ...init, signal: ac?.signal, cache: "no-store" });
2572
+ } catch {
2573
+ return null;
2574
+ } finally {
2575
+ clearTimeout(timer);
2576
+ }
2577
+ }
2578
+ async function isNativeShotAvailable(port = configuredPort) {
2579
+ if (typeof window === "undefined") return false;
2580
+ if (!helperReachableHere()) return false;
2581
+ const now2 = Date.now();
2582
+ if (lastProbeResult && cachedBase) return true;
2583
+ if (!lastProbeResult && now2 - lastProbeAt < ABSENT_RECHECK_MS) return false;
2584
+ lastProbeAt = now2;
2585
+ const res = await fetchWithTimeout(`${baseUrl(port)}/qapture/health`, { method: "GET" }, PROBE_TIMEOUT_MS);
2586
+ lastProbeResult = !!res && res.ok;
2587
+ cachedBase = lastProbeResult ? baseUrl(port) : null;
2588
+ return lastProbeResult;
2589
+ }
2590
+ function helperStartCommand(port = configuredPort) {
2591
+ const flags = [];
2592
+ if (port !== DEFAULT_SHOT_PORT) flags.push(`--port ${port}`);
2593
+ if (typeof window !== "undefined") {
2594
+ const origin = window.location.origin;
2595
+ const local = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:|$)/.test(origin);
2596
+ if (!local) flags.push(`--allow ${origin}`);
2597
+ }
2598
+ return `npx qapture2 shots${flags.length ? " " + flags.join(" ") : ""}`;
2599
+ }
2600
+ function resetNativeShotProbe() {
2601
+ cachedBase = null;
2602
+ lastProbeAt = 0;
2603
+ lastProbeResult = false;
2604
+ }
2605
+ async function shootBrowserWindow(port = configuredPort) {
2606
+ if (typeof window === "undefined") return null;
2607
+ const base = cachedBase ?? baseUrl(port);
2608
+ const body = JSON.stringify({
2609
+ x: window.screenX,
2610
+ y: window.screenY,
2611
+ w: window.outerWidth,
2612
+ h: window.outerHeight
2613
+ });
2614
+ const res = await fetchWithTimeout(
2615
+ `${base}/qapture/shot`,
2616
+ { method: "POST", headers: { "content-type": "application/json" }, body },
2617
+ SHOT_TIMEOUT_MS
2618
+ );
2619
+ if (!res || !res.ok) return null;
2620
+ let payload;
2621
+ try {
2622
+ payload = await res.json();
2623
+ } catch {
2624
+ return null;
2625
+ }
2626
+ if (!payload.png) return null;
2627
+ return decodeToCanvas(payload.png);
2628
+ }
2629
+ function decodeToCanvas(dataUrl) {
2630
+ return new Promise((resolve) => {
2631
+ const img = new Image();
2632
+ img.onload = () => {
2633
+ const c = document.createElement("canvas");
2634
+ c.width = img.naturalWidth;
2635
+ c.height = img.naturalHeight;
2636
+ const ctx = c.getContext("2d", { willReadFrequently: true });
2637
+ if (!ctx) {
2638
+ resolve(null);
2639
+ return;
2640
+ }
2641
+ ctx.drawImage(img, 0, 0);
2642
+ resolve(c);
2643
+ };
2644
+ img.onerror = () => resolve(null);
2645
+ img.src = dataUrl;
2646
+ });
2647
+ }
2648
+ var cachedMapping = null;
2649
+ var cachedFor = "";
2650
+ function getCachedMapping() {
2651
+ if (!cachedMapping) return null;
2652
+ return environmentSignature() === cachedFor ? cachedMapping : null;
2653
+ }
2654
+ function cacheMapping(m) {
2655
+ cachedMapping = m;
2656
+ cachedFor = m ? environmentSignature() : "";
2657
+ }
2514
2658
 
2515
2659
  // src/lib/screenCapture.ts
2516
2660
  var ASPECT_TOLERANCE = 0.08;
2517
2661
  var CALIBRATION_SETTLE_MS = 220;
2518
2662
  var FRESH_FRAME_TIMEOUT_MS = 500;
2519
2663
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2664
+ var nativeReady = false;
2665
+ function isHelperReachableHere() {
2666
+ return helperReachableHere();
2667
+ }
2668
+ function getHelperCommand() {
2669
+ return helperStartCommand();
2670
+ }
2671
+ async function refreshNativeAvailability(port) {
2672
+ if (typeof port === "number") setShotPort(port);
2673
+ nativeReady = await isNativeShotAvailable();
2674
+ return nativeReady;
2675
+ }
2520
2676
  function isExactCaptureSupported() {
2677
+ if (nativeReady) return true;
2521
2678
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2522
2679
  const md = navigator.mediaDevices;
2523
2680
  return !!md && typeof md.getDisplayMedia === "function";
@@ -2525,13 +2682,15 @@ function isExactCaptureSupported() {
2525
2682
  var armed = false;
2526
2683
  var declined = false;
2527
2684
  var frozen = null;
2528
- var lastMode = null;
2529
2685
  function armExactCapture() {
2530
2686
  if (!isExactCaptureSupported()) return false;
2531
2687
  armed = true;
2532
2688
  declined = false;
2533
2689
  return true;
2534
2690
  }
2691
+ function exactCaptureIsFree() {
2692
+ return nativeReady;
2693
+ }
2535
2694
  function disarmExactCapture() {
2536
2695
  armed = false;
2537
2696
  releaseFrozenFrame();
@@ -2540,6 +2699,7 @@ function resetExactCaptureDecline() {
2540
2699
  declined = false;
2541
2700
  }
2542
2701
  function getExactCaptureStatus() {
2702
+ if (nativeReady) return "native";
2543
2703
  if (!isExactCaptureSupported()) return "unsupported";
2544
2704
  if (armed) return "live";
2545
2705
  if (declined) return "declined";
@@ -2564,7 +2724,7 @@ function stillIsCurrent() {
2564
2724
  return true;
2565
2725
  }
2566
2726
  async function freezeOrReuse() {
2567
- if (stillIsCurrent()) return frozen;
2727
+ if (!nativeReady && stillIsCurrent()) return frozen;
2568
2728
  return freezeViewport();
2569
2729
  }
2570
2730
  function releaseFrozenFrame() {
@@ -2664,7 +2824,7 @@ async function grabFrameCanvas(video, grabber) {
2664
2824
  bitmap?.close?.();
2665
2825
  return c;
2666
2826
  }
2667
- async function calibrate(video, grabber) {
2827
+ async function calibrate(grabFrame) {
2668
2828
  const vw = window.innerWidth;
2669
2829
  const vh = window.innerHeight;
2670
2830
  if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
@@ -2681,7 +2841,7 @@ async function calibrate(video, grabber) {
2681
2841
  document.body.appendChild(card);
2682
2842
  try {
2683
2843
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2684
- const frame = await grabFrameCanvas(video, grabber);
2844
+ const frame = await grabFrame();
2685
2845
  if (!frame) return null;
2686
2846
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2687
2847
  if (!ctx) return null;
@@ -2703,8 +2863,69 @@ async function calibrate(video, grabber) {
2703
2863
  }
2704
2864
  }
2705
2865
  async function freezeViewport() {
2706
- if (!isExactCaptureSupported()) return null;
2707
2866
  releaseFrozenFrame();
2867
+ const native = await freezeViaNativeHelper();
2868
+ if (native) return native;
2869
+ if (!isExactCaptureSupported()) return null;
2870
+ return freezeViaDisplayMedia();
2871
+ }
2872
+ function adoptFrame(raw, mapping, mode, vw, vh) {
2873
+ let page;
2874
+ if (mapping) {
2875
+ const box = mapRectToFrame(
2876
+ { left: 0, top: 0, width: vw, height: vh },
2877
+ mapping,
2878
+ raw.width,
2879
+ raw.height
2880
+ );
2881
+ if (!box) return null;
2882
+ page = document.createElement("canvas");
2883
+ page.width = box.sw;
2884
+ page.height = box.sh;
2885
+ const ctx = page.getContext("2d");
2886
+ if (!ctx) return null;
2887
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2888
+ raw.width = 0;
2889
+ raw.height = 0;
2890
+ } else {
2891
+ page = raw;
2892
+ }
2893
+ frozen = {
2894
+ canvas: page,
2895
+ mode,
2896
+ viewportWidth: vw,
2897
+ viewportHeight: vh,
2898
+ takenAt: Date.now()
2899
+ };
2900
+ frozenAtScrollX = window.scrollX;
2901
+ frozenAtScrollY = window.scrollY;
2902
+ frozenAtPath = window.location.pathname + window.location.search;
2903
+ return frozen;
2904
+ }
2905
+ async function freezeViaNativeHelper() {
2906
+ if (typeof window === "undefined") return null;
2907
+ nativeReady = await isNativeShotAvailable();
2908
+ if (!nativeReady) return null;
2909
+ const vw = window.innerWidth;
2910
+ const vh = window.innerHeight;
2911
+ let mapping = getCachedMapping();
2912
+ if (!mapping) {
2913
+ mapping = await calibrate(() => shootBrowserWindow());
2914
+ if (!mapping) {
2915
+ cacheMapping(null);
2916
+ return null;
2917
+ }
2918
+ cacheMapping(mapping);
2919
+ }
2920
+ const raw = await withOverlayHidden(() => shootBrowserWindow());
2921
+ if (!raw) return null;
2922
+ const adopted = adoptFrame(raw, mapping, "native", vw, vh);
2923
+ if (!adopted) {
2924
+ cacheMapping(null);
2925
+ }
2926
+ return adopted;
2927
+ }
2928
+ async function freezeViaDisplayMedia() {
2708
2929
  let stream = null;
2709
2930
  let el = null;
2710
2931
  const release = () => {
@@ -2777,45 +2998,14 @@ async function freezeViewport() {
2777
2998
  const vw = window.innerWidth;
2778
2999
  const vh = window.innerHeight;
2779
3000
  const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2780
- const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
3001
+ const mapping = mode === "surface" ? await calibrate(() => grabFrameCanvas(video, grabber)) : null;
2781
3002
  if (mode === "surface" && !mapping) {
2782
3003
  declined = true;
2783
3004
  return null;
2784
3005
  }
2785
3006
  const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2786
3007
  if (!raw) return null;
2787
- let page;
2788
- if (mode === "surface" && mapping) {
2789
- const box = mapRectToFrame(
2790
- { left: 0, top: 0, width: vw, height: vh },
2791
- mapping,
2792
- raw.width,
2793
- raw.height
2794
- );
2795
- if (!box) return null;
2796
- page = document.createElement("canvas");
2797
- page.width = box.sw;
2798
- page.height = box.sh;
2799
- const ctx = page.getContext("2d");
2800
- if (!ctx) return null;
2801
- ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2802
- raw.width = 0;
2803
- raw.height = 0;
2804
- } else {
2805
- page = raw;
2806
- }
2807
- frozen = {
2808
- canvas: page,
2809
- mode,
2810
- viewportWidth: vw,
2811
- viewportHeight: vh,
2812
- takenAt: Date.now()
2813
- };
2814
- frozenAtScrollX = window.scrollX;
2815
- frozenAtScrollY = window.scrollY;
2816
- frozenAtPath = window.location.pathname + window.location.search;
2817
- lastMode = mode;
2818
- return frozen;
3008
+ return adoptFrame(raw, mode === "surface" ? mapping : null, mode, vw, vh);
2819
3009
  } catch {
2820
3010
  declined = true;
2821
3011
  return null;
@@ -3636,11 +3826,24 @@ function reproSpec(note, index) {
3636
3826
  }
3637
3827
 
3638
3828
  // src/lib/exportZip.ts
3639
- function safeName(name, stamp) {
3640
- const fallback = `qa-notes-${stamp.slice(0, 10)}`;
3829
+ function autoName(project, stamp) {
3830
+ const date = stamp.slice(0, 10);
3831
+ const time = stamp.slice(11, 16).replace(":", "");
3832
+ const slug = (project ?? "").trim().replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3833
+ const when = time ? `${date}-${time}` : date;
3834
+ return slug ? `${slug}-qa-${when}` : `qa-notes-${when}`;
3835
+ }
3836
+ function exportProjectName(config) {
3837
+ const name = config?.preamble?.projectName;
3838
+ return typeof name === "string" && name.trim() ? name.trim() : void 0;
3839
+ }
3840
+ function safeName(name, stamp, project) {
3641
3841
  let base = (name ?? "").trim().replace(/\.zip$/i, "");
3642
3842
  base = base.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 80).trim();
3643
- return `${base || fallback}.zip`;
3843
+ return `${base || autoName(project, stamp)}.zip`;
3844
+ }
3845
+ function suggestedExportName(project, stamp) {
3846
+ return autoName(project, stamp);
3644
3847
  }
3645
3848
  function toStrings(val) {
3646
3849
  if (val == null) return [];
@@ -4018,14 +4221,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked,
4018
4221
  const url = URL.createObjectURL(blob);
4019
4222
  const a = document.createElement("a");
4020
4223
  a.href = url;
4021
- a.download = safeName(filename, stamp);
4224
+ a.download = safeName(filename, stamp, exportProjectName(config));
4022
4225
  document.body.appendChild(a);
4023
4226
  a.click();
4024
4227
  a.remove();
4025
4228
  setTimeout(() => URL.revokeObjectURL(url), 2e3);
4026
4229
  }
4027
- function exportFileName(filename, stamp) {
4028
- return safeName(filename, stamp);
4230
+ function exportFileName(filename, stamp, project) {
4231
+ return safeName(filename, stamp, project);
4029
4232
  }
4030
4233
 
4031
4234
  // src/lib/shareZip.ts
@@ -4723,7 +4926,23 @@ function QaProvider({
4723
4926
  if (exactShotsWanted(storage)) armExactCapture();
4724
4927
  return getExactCaptureStatus();
4725
4928
  });
4726
- const exactSupported = isExactCaptureSupported();
4929
+ const [exactSupported, setExactSupported] = React.useState(() => isExactCaptureSupported());
4930
+ React.useEffect(() => {
4931
+ let alive = true;
4932
+ const look = () => {
4933
+ void refreshNativeAvailability(config.shotPort).then(() => {
4934
+ if (!alive) return;
4935
+ setExactSupported(isExactCaptureSupported());
4936
+ setExactStatus(getExactCaptureStatus());
4937
+ });
4938
+ };
4939
+ look();
4940
+ window.addEventListener("focus", look);
4941
+ return () => {
4942
+ alive = false;
4943
+ window.removeEventListener("focus", look);
4944
+ };
4945
+ }, [config.shotPort]);
4727
4946
  const [frozenAt, setFrozenAt] = React.useState(null);
4728
4947
  const [syncState, setSyncState] = React.useState(() => getFsSyncState());
4729
4948
  const [syncTick, setSyncTick] = React.useState(0);
@@ -5249,7 +5468,7 @@ function QaProvider({
5249
5468
  setCapturePrefill(prefill ?? "");
5250
5469
  setCaptureActive(true);
5251
5470
  if (!stillIsCurrent()) setFrozenAt(null);
5252
- if (exactShotsWanted(storage) && isExactCaptureSupported()) {
5471
+ if (exactCaptureIsFree() || exactShotsWanted(storage) && isExactCaptureSupported()) {
5253
5472
  resetExactCaptureDecline();
5254
5473
  void freezeOrReuse().then((frame) => {
5255
5474
  setFrozenAt(frame?.takenAt ?? null);
@@ -5567,7 +5786,7 @@ function QaProvider({
5567
5786
  const shareExport = React.useCallback(async (filename) => {
5568
5787
  if (!notes.length) return { status: "unsupported" };
5569
5788
  const stamp = nowIso();
5570
- const name = exportFileName(filename, stamp);
5789
+ const name = exportFileName(filename, stamp, exportProjectName(config));
5571
5790
  setIsExporting(true);
5572
5791
  try {
5573
5792
  const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
@@ -5878,7 +6097,13 @@ function QaProvider({
5878
6097
  gradeStep,
5879
6098
  evidenceByStep,
5880
6099
  // v0.4 — screenshots
5881
- exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
6100
+ exactShots: {
6101
+ supported: exactSupported,
6102
+ status: exactStatus,
6103
+ frozenAt,
6104
+ helperReachable: isHelperReachableHere(),
6105
+ helperCommand: getHelperCommand()
6106
+ },
5882
6107
  enableExactShots,
5883
6108
  photographNow,
5884
6109
  disableExactShots,
@@ -5941,6 +6166,7 @@ function QaProvider({
5941
6166
  setSimpleMode,
5942
6167
  compactCapture,
5943
6168
  developerMode,
6169
+ projectName: exportProjectName(config),
5944
6170
  setDeveloperMode,
5945
6171
  setCompactCapture,
5946
6172
  exportZip: exportZipFn
@@ -8214,7 +8440,7 @@ function upgradeHint(latest) {
8214
8440
  }
8215
8441
 
8216
8442
  // src/version.ts
8217
- var QA_VERSION = "0.9.1" ;
8443
+ var QA_VERSION = "0.10.1" ;
8218
8444
  function Section({
8219
8445
  icon,
8220
8446
  title,
@@ -8452,8 +8678,11 @@ function SettingsSheet({ onClose }) {
8452
8678
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8453
8679
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8454
8680
  /* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Camera", title: t("exact_label"), children: [
8455
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_hint") }),
8456
- !exactShots.supported ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: t("exact_unsupported") }) : exactShots.status === "live" ? /* @__PURE__ */ jsxRuntime.jsxs(
8681
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: exactShots.status === "native" ? t("exact_native_hint") : t("exact_hint") }),
8682
+ exactShots.status === "native" ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-inline-flex qa-items-center qa-gap-1.5 qa-text-xs qa-font-semibold qa-text-success", children: [
8683
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 13 }),
8684
+ t("exact_native_on")
8685
+ ] }) : !exactShots.supported ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: t("exact_unsupported") }) : exactShots.status === "live" ? /* @__PURE__ */ jsxRuntime.jsxs(
8457
8686
  "button",
8458
8687
  {
8459
8688
  type: "button",
@@ -8478,7 +8707,18 @@ function SettingsSheet({ onClose }) {
8478
8707
  t("exact_turn_on")
8479
8708
  ]
8480
8709
  }
8481
- )
8710
+ ),
8711
+ exactShots.status !== "native" && (exactShots.helperReachable ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-2", children: [
8712
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-mb-1 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_helper_offer") }),
8713
+ /* @__PURE__ */ jsxRuntime.jsx(
8714
+ "code",
8715
+ {
8716
+ className: "qa-block qa-rounded-lg qa-bg-2 qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-10 qa-text-hi",
8717
+ style: { userSelect: "all", wordBreak: "break-all" },
8718
+ children: exactShots.helperCommand
8719
+ }
8720
+ )
8721
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-mt-2 qa-text-10 qa-text-mid qa-leading-relaxed", children: t("exact_helper_safari") }))
8482
8722
  ] }),
8483
8723
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8484
8724
  /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
@@ -8827,8 +9067,8 @@ var TABS = ALL_TABS.filter((tab) => tab.key !== "guide" || GUIDE_TAB_ENABLED);
8827
9067
  function visibleTab(tab) {
8828
9068
  return tab === "guide" && !GUIDE_TAB_ENABLED ? "notes" : tab;
8829
9069
  }
8830
- function todayName() {
8831
- return `qa-notes-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
9070
+ function suggestName(project) {
9071
+ return suggestedExportName(project, (/* @__PURE__ */ new Date()).toISOString());
8832
9072
  }
8833
9073
  function panelReducer(state2, action) {
8834
9074
  switch (action.type) {
@@ -8890,7 +9130,8 @@ function QaPanel() {
8890
9130
  panelSide,
8891
9131
  setPanelSide,
8892
9132
  panelCollapsed,
8893
- setPanelCollapsed
9133
+ setPanelCollapsed,
9134
+ projectName
8894
9135
  } = useQa();
8895
9136
  const activeTab = visibleTab(storedTab);
8896
9137
  const [confirmClear, setConfirmClear] = React.useState(false);
@@ -9009,7 +9250,7 @@ function QaPanel() {
9009
9250
  const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
9010
9251
  if (phase === "hidden") return null;
9011
9252
  const openNaming = () => {
9012
- setFilename(todayName());
9253
+ setFilename(suggestName(projectName));
9013
9254
  setNaming(true);
9014
9255
  };
9015
9256
  const doExport = () => {
@@ -11062,7 +11303,7 @@ function CaptureMode() {
11062
11303
  }
11063
11304
  ),
11064
11305
  developerMode && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
11065
- developerMode && /* @__PURE__ */ jsxRuntime.jsxs(
11306
+ /* @__PURE__ */ jsxRuntime.jsxs(
11066
11307
  "div",
11067
11308
  {
11068
11309
  role: "group",
@@ -11162,7 +11403,8 @@ function CaptureMode() {
11162
11403
  ] }),
11163
11404
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
11164
11405
  t("save_hint"),
11165
- developerMode ? ` \xB7 ${t("severity_keys")}` : ""
11406
+ " \xB7 ",
11407
+ t("severity_keys")
11166
11408
  ] })
11167
11409
  ] })
11168
11410
  ]
@@ -11370,5 +11612,5 @@ function Qapture({ config }) {
11370
11612
  exports.Qapture = Qapture;
11371
11613
  exports.deleteQaDatabase = deleteQaDatabase;
11372
11614
  exports.initQaStudio = initQaStudio;
11373
- //# sourceMappingURL=chunk-J77SGE5E.cjs.map
11374
- //# sourceMappingURL=chunk-J77SGE5E.cjs.map
11615
+ //# sourceMappingURL=chunk-PEPHZGI2.cjs.map
11616
+ //# sourceMappingURL=chunk-PEPHZGI2.cjs.map
package/dist/index.cjs CHANGED
@@ -1,24 +1,24 @@
1
1
  'use strict';
2
2
 
3
- var chunkJ77SGE5E_cjs = require('./chunk-J77SGE5E.cjs');
3
+ var chunkPEPHZGI2_cjs = require('./chunk-PEPHZGI2.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "QaStudio", {
8
8
  enumerable: true,
9
- get: function () { return chunkJ77SGE5E_cjs.Qapture; }
9
+ get: function () { return chunkPEPHZGI2_cjs.Qapture; }
10
10
  });
11
11
  Object.defineProperty(exports, "Qapture", {
12
12
  enumerable: true,
13
- get: function () { return chunkJ77SGE5E_cjs.Qapture; }
13
+ get: function () { return chunkPEPHZGI2_cjs.Qapture; }
14
14
  });
15
15
  Object.defineProperty(exports, "deleteQaDatabase", {
16
16
  enumerable: true,
17
- get: function () { return chunkJ77SGE5E_cjs.deleteQaDatabase; }
17
+ get: function () { return chunkPEPHZGI2_cjs.deleteQaDatabase; }
18
18
  });
19
19
  Object.defineProperty(exports, "initQaStudio", {
20
20
  enumerable: true,
21
- get: function () { return chunkJ77SGE5E_cjs.initQaStudio; }
21
+ get: function () { return chunkPEPHZGI2_cjs.initQaStudio; }
22
22
  });
23
23
  //# sourceMappingURL=index.cjs.map
24
24
  //# sourceMappingURL=index.cjs.map
package/dist/index.d.cts CHANGED
@@ -88,6 +88,11 @@ type QaPreamble = {
88
88
  type QaConfig = {
89
89
  /** Storage + DB namespace. Defaults to 'qapture'. */
90
90
  namespace?: string;
91
+ /**
92
+ * Where `npx qapture2 shots` is listening, when you started it on a port
93
+ * other than the default 7017. Only needed if 7017 was already taken.
94
+ */
95
+ shotPort?: number;
91
96
  /**
92
97
  * Override any subset of the colour palette.
93
98
  *
@@ -175,6 +180,7 @@ type QaConfig = {
175
180
  };
176
181
  type ResolvedConfig = {
177
182
  namespace: string;
183
+ shotPort: number;
178
184
  brand: {
179
185
  label: string;
180
186
  };
package/dist/index.d.ts CHANGED
@@ -88,6 +88,11 @@ type QaPreamble = {
88
88
  type QaConfig = {
89
89
  /** Storage + DB namespace. Defaults to 'qapture'. */
90
90
  namespace?: string;
91
+ /**
92
+ * Where `npx qapture2 shots` is listening, when you started it on a port
93
+ * other than the default 7017. Only needed if 7017 was already taken.
94
+ */
95
+ shotPort?: number;
91
96
  /**
92
97
  * Override any subset of the colour palette.
93
98
  *
@@ -175,6 +180,7 @@ type QaConfig = {
175
180
  };
176
181
  type ResolvedConfig = {
177
182
  namespace: string;
183
+ shotPort: number;
178
184
  brand: {
179
185
  label: string;
180
186
  };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Qapture as QaStudio, Qapture, deleteQaDatabase, initQaStudio } from './chunk-HEHVZCSV.js';
1
+ export { Qapture as QaStudio, Qapture, deleteQaDatabase, initQaStudio } from './chunk-F66JBHJG.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map