qapture2 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,8 @@ 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",
1941
1949
  sync_title: "Save to a folder",
1942
1950
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1943
1951
  sync_choose: "Choose folder",
@@ -2175,6 +2183,8 @@ var STR = {
2175
2183
  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
2184
  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
2185
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
2186
+ 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.",
2187
+ 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",
2178
2188
  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
2189
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
2180
2190
  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 +2521,132 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2511
2521
  if (sw < 1 || sh < 1) return null;
2512
2522
  return { sx, sy, sw, sh };
2513
2523
  }
2524
+ function environmentSignature() {
2525
+ if (typeof window === "undefined") return "";
2526
+ return [
2527
+ window.innerWidth,
2528
+ window.innerHeight,
2529
+ window.outerWidth,
2530
+ window.outerHeight,
2531
+ window.screenX,
2532
+ window.screenY,
2533
+ Math.round((window.devicePixelRatio || 1) * 100)
2534
+ ].join("x");
2535
+ }
2536
+
2537
+ // src/lib/nativeShot.ts
2538
+ var DEFAULT_SHOT_PORT = 7017;
2539
+ var PROBE_TIMEOUT_MS = 700;
2540
+ var SHOT_TIMEOUT_MS = 9e3;
2541
+ var ABSENT_RECHECK_MS = 3e4;
2542
+ var configuredPort = DEFAULT_SHOT_PORT;
2543
+ function setShotPort(port) {
2544
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return;
2545
+ if (port === configuredPort) return;
2546
+ configuredPort = port;
2547
+ resetNativeShotProbe();
2548
+ }
2549
+ var cachedBase = null;
2550
+ var lastProbeAt = 0;
2551
+ var lastProbeResult = false;
2552
+ function baseUrl(port) {
2553
+ return `http://127.0.0.1:${port}`;
2554
+ }
2555
+ async function fetchWithTimeout(url, init, ms) {
2556
+ if (typeof fetch !== "function") return null;
2557
+ const ac = typeof AbortController === "function" ? new AbortController() : null;
2558
+ const timer = setTimeout(() => ac?.abort(), ms);
2559
+ try {
2560
+ return await fetch(url, { ...init, signal: ac?.signal, cache: "no-store" });
2561
+ } catch {
2562
+ return null;
2563
+ } finally {
2564
+ clearTimeout(timer);
2565
+ }
2566
+ }
2567
+ async function isNativeShotAvailable(port = configuredPort) {
2568
+ if (typeof window === "undefined") return false;
2569
+ const now2 = Date.now();
2570
+ if (lastProbeResult && cachedBase) return true;
2571
+ if (!lastProbeResult && now2 - lastProbeAt < ABSENT_RECHECK_MS) return false;
2572
+ lastProbeAt = now2;
2573
+ const res = await fetchWithTimeout(`${baseUrl(port)}/qapture/health`, { method: "GET" }, PROBE_TIMEOUT_MS);
2574
+ lastProbeResult = !!res && res.ok;
2575
+ cachedBase = lastProbeResult ? baseUrl(port) : null;
2576
+ return lastProbeResult;
2577
+ }
2578
+ function resetNativeShotProbe() {
2579
+ cachedBase = null;
2580
+ lastProbeAt = 0;
2581
+ lastProbeResult = false;
2582
+ }
2583
+ async function shootBrowserWindow(port = configuredPort) {
2584
+ if (typeof window === "undefined") return null;
2585
+ const base = cachedBase ?? baseUrl(port);
2586
+ const body = JSON.stringify({
2587
+ x: window.screenX,
2588
+ y: window.screenY,
2589
+ w: window.outerWidth,
2590
+ h: window.outerHeight
2591
+ });
2592
+ const res = await fetchWithTimeout(
2593
+ `${base}/qapture/shot`,
2594
+ { method: "POST", headers: { "content-type": "application/json" }, body },
2595
+ SHOT_TIMEOUT_MS
2596
+ );
2597
+ if (!res || !res.ok) return null;
2598
+ let payload;
2599
+ try {
2600
+ payload = await res.json();
2601
+ } catch {
2602
+ return null;
2603
+ }
2604
+ if (!payload.png) return null;
2605
+ return decodeToCanvas(payload.png);
2606
+ }
2607
+ function decodeToCanvas(dataUrl) {
2608
+ return new Promise((resolve) => {
2609
+ const img = new Image();
2610
+ img.onload = () => {
2611
+ const c = document.createElement("canvas");
2612
+ c.width = img.naturalWidth;
2613
+ c.height = img.naturalHeight;
2614
+ const ctx = c.getContext("2d", { willReadFrequently: true });
2615
+ if (!ctx) {
2616
+ resolve(null);
2617
+ return;
2618
+ }
2619
+ ctx.drawImage(img, 0, 0);
2620
+ resolve(c);
2621
+ };
2622
+ img.onerror = () => resolve(null);
2623
+ img.src = dataUrl;
2624
+ });
2625
+ }
2626
+ var cachedMapping = null;
2627
+ var cachedFor = "";
2628
+ function getCachedMapping() {
2629
+ if (!cachedMapping) return null;
2630
+ return environmentSignature() === cachedFor ? cachedMapping : null;
2631
+ }
2632
+ function cacheMapping(m) {
2633
+ cachedMapping = m;
2634
+ cachedFor = m ? environmentSignature() : "";
2635
+ }
2514
2636
 
2515
2637
  // src/lib/screenCapture.ts
2516
2638
  var ASPECT_TOLERANCE = 0.08;
2517
2639
  var CALIBRATION_SETTLE_MS = 220;
2518
2640
  var FRESH_FRAME_TIMEOUT_MS = 500;
2519
2641
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2642
+ var nativeReady = false;
2643
+ async function refreshNativeAvailability(port) {
2644
+ if (typeof port === "number") setShotPort(port);
2645
+ nativeReady = await isNativeShotAvailable();
2646
+ return nativeReady;
2647
+ }
2520
2648
  function isExactCaptureSupported() {
2649
+ if (nativeReady) return true;
2521
2650
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2522
2651
  const md = navigator.mediaDevices;
2523
2652
  return !!md && typeof md.getDisplayMedia === "function";
@@ -2525,13 +2654,15 @@ function isExactCaptureSupported() {
2525
2654
  var armed = false;
2526
2655
  var declined = false;
2527
2656
  var frozen = null;
2528
- var lastMode = null;
2529
2657
  function armExactCapture() {
2530
2658
  if (!isExactCaptureSupported()) return false;
2531
2659
  armed = true;
2532
2660
  declined = false;
2533
2661
  return true;
2534
2662
  }
2663
+ function exactCaptureIsFree() {
2664
+ return nativeReady;
2665
+ }
2535
2666
  function disarmExactCapture() {
2536
2667
  armed = false;
2537
2668
  releaseFrozenFrame();
@@ -2540,6 +2671,7 @@ function resetExactCaptureDecline() {
2540
2671
  declined = false;
2541
2672
  }
2542
2673
  function getExactCaptureStatus() {
2674
+ if (nativeReady) return "native";
2543
2675
  if (!isExactCaptureSupported()) return "unsupported";
2544
2676
  if (armed) return "live";
2545
2677
  if (declined) return "declined";
@@ -2564,7 +2696,7 @@ function stillIsCurrent() {
2564
2696
  return true;
2565
2697
  }
2566
2698
  async function freezeOrReuse() {
2567
- if (stillIsCurrent()) return frozen;
2699
+ if (!nativeReady && stillIsCurrent()) return frozen;
2568
2700
  return freezeViewport();
2569
2701
  }
2570
2702
  function releaseFrozenFrame() {
@@ -2664,7 +2796,7 @@ async function grabFrameCanvas(video, grabber) {
2664
2796
  bitmap?.close?.();
2665
2797
  return c;
2666
2798
  }
2667
- async function calibrate(video, grabber) {
2799
+ async function calibrate(grabFrame) {
2668
2800
  const vw = window.innerWidth;
2669
2801
  const vh = window.innerHeight;
2670
2802
  if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
@@ -2681,7 +2813,7 @@ async function calibrate(video, grabber) {
2681
2813
  document.body.appendChild(card);
2682
2814
  try {
2683
2815
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2684
- const frame = await grabFrameCanvas(video, grabber);
2816
+ const frame = await grabFrame();
2685
2817
  if (!frame) return null;
2686
2818
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2687
2819
  if (!ctx) return null;
@@ -2703,8 +2835,69 @@ async function calibrate(video, grabber) {
2703
2835
  }
2704
2836
  }
2705
2837
  async function freezeViewport() {
2706
- if (!isExactCaptureSupported()) return null;
2707
2838
  releaseFrozenFrame();
2839
+ const native = await freezeViaNativeHelper();
2840
+ if (native) return native;
2841
+ if (!isExactCaptureSupported()) return null;
2842
+ return freezeViaDisplayMedia();
2843
+ }
2844
+ function adoptFrame(raw, mapping, mode, vw, vh) {
2845
+ let page;
2846
+ if (mapping) {
2847
+ const box = mapRectToFrame(
2848
+ { left: 0, top: 0, width: vw, height: vh },
2849
+ mapping,
2850
+ raw.width,
2851
+ raw.height
2852
+ );
2853
+ if (!box) return null;
2854
+ page = document.createElement("canvas");
2855
+ page.width = box.sw;
2856
+ page.height = box.sh;
2857
+ const ctx = page.getContext("2d");
2858
+ if (!ctx) return null;
2859
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2860
+ raw.width = 0;
2861
+ raw.height = 0;
2862
+ } else {
2863
+ page = raw;
2864
+ }
2865
+ frozen = {
2866
+ canvas: page,
2867
+ mode,
2868
+ viewportWidth: vw,
2869
+ viewportHeight: vh,
2870
+ takenAt: Date.now()
2871
+ };
2872
+ frozenAtScrollX = window.scrollX;
2873
+ frozenAtScrollY = window.scrollY;
2874
+ frozenAtPath = window.location.pathname + window.location.search;
2875
+ return frozen;
2876
+ }
2877
+ async function freezeViaNativeHelper() {
2878
+ if (typeof window === "undefined") return null;
2879
+ nativeReady = await isNativeShotAvailable();
2880
+ if (!nativeReady) return null;
2881
+ const vw = window.innerWidth;
2882
+ const vh = window.innerHeight;
2883
+ let mapping = getCachedMapping();
2884
+ if (!mapping) {
2885
+ mapping = await calibrate(() => shootBrowserWindow());
2886
+ if (!mapping) {
2887
+ cacheMapping(null);
2888
+ return null;
2889
+ }
2890
+ cacheMapping(mapping);
2891
+ }
2892
+ const raw = await withOverlayHidden(() => shootBrowserWindow());
2893
+ if (!raw) return null;
2894
+ const adopted = adoptFrame(raw, mapping, "native", vw, vh);
2895
+ if (!adopted) {
2896
+ cacheMapping(null);
2897
+ }
2898
+ return adopted;
2899
+ }
2900
+ async function freezeViaDisplayMedia() {
2708
2901
  let stream = null;
2709
2902
  let el = null;
2710
2903
  const release = () => {
@@ -2777,45 +2970,14 @@ async function freezeViewport() {
2777
2970
  const vw = window.innerWidth;
2778
2971
  const vh = window.innerHeight;
2779
2972
  const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2780
- const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
2973
+ const mapping = mode === "surface" ? await calibrate(() => grabFrameCanvas(video, grabber)) : null;
2781
2974
  if (mode === "surface" && !mapping) {
2782
2975
  declined = true;
2783
2976
  return null;
2784
2977
  }
2785
2978
  const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2786
2979
  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;
2980
+ return adoptFrame(raw, mode === "surface" ? mapping : null, mode, vw, vh);
2819
2981
  } catch {
2820
2982
  declined = true;
2821
2983
  return null;
@@ -3636,11 +3798,24 @@ function reproSpec(note, index) {
3636
3798
  }
3637
3799
 
3638
3800
  // src/lib/exportZip.ts
3639
- function safeName(name, stamp) {
3640
- const fallback = `qa-notes-${stamp.slice(0, 10)}`;
3801
+ function autoName(project, stamp) {
3802
+ const date = stamp.slice(0, 10);
3803
+ const time = stamp.slice(11, 16).replace(":", "");
3804
+ const slug = (project ?? "").trim().replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3805
+ const when = time ? `${date}-${time}` : date;
3806
+ return slug ? `${slug}-qa-${when}` : `qa-notes-${when}`;
3807
+ }
3808
+ function exportProjectName(config) {
3809
+ const name = config?.preamble?.projectName;
3810
+ return typeof name === "string" && name.trim() ? name.trim() : void 0;
3811
+ }
3812
+ function safeName(name, stamp, project) {
3641
3813
  let base = (name ?? "").trim().replace(/\.zip$/i, "");
3642
3814
  base = base.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 80).trim();
3643
- return `${base || fallback}.zip`;
3815
+ return `${base || autoName(project, stamp)}.zip`;
3816
+ }
3817
+ function suggestedExportName(project, stamp) {
3818
+ return autoName(project, stamp);
3644
3819
  }
3645
3820
  function toStrings(val) {
3646
3821
  if (val == null) return [];
@@ -4018,14 +4193,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked,
4018
4193
  const url = URL.createObjectURL(blob);
4019
4194
  const a = document.createElement("a");
4020
4195
  a.href = url;
4021
- a.download = safeName(filename, stamp);
4196
+ a.download = safeName(filename, stamp, exportProjectName(config));
4022
4197
  document.body.appendChild(a);
4023
4198
  a.click();
4024
4199
  a.remove();
4025
4200
  setTimeout(() => URL.revokeObjectURL(url), 2e3);
4026
4201
  }
4027
- function exportFileName(filename, stamp) {
4028
- return safeName(filename, stamp);
4202
+ function exportFileName(filename, stamp, project) {
4203
+ return safeName(filename, stamp, project);
4029
4204
  }
4030
4205
 
4031
4206
  // src/lib/shareZip.ts
@@ -4723,7 +4898,23 @@ function QaProvider({
4723
4898
  if (exactShotsWanted(storage)) armExactCapture();
4724
4899
  return getExactCaptureStatus();
4725
4900
  });
4726
- const exactSupported = isExactCaptureSupported();
4901
+ const [exactSupported, setExactSupported] = React.useState(() => isExactCaptureSupported());
4902
+ React.useEffect(() => {
4903
+ let alive = true;
4904
+ const look = () => {
4905
+ void refreshNativeAvailability(config.shotPort).then(() => {
4906
+ if (!alive) return;
4907
+ setExactSupported(isExactCaptureSupported());
4908
+ setExactStatus(getExactCaptureStatus());
4909
+ });
4910
+ };
4911
+ look();
4912
+ window.addEventListener("focus", look);
4913
+ return () => {
4914
+ alive = false;
4915
+ window.removeEventListener("focus", look);
4916
+ };
4917
+ }, [config.shotPort]);
4727
4918
  const [frozenAt, setFrozenAt] = React.useState(null);
4728
4919
  const [syncState, setSyncState] = React.useState(() => getFsSyncState());
4729
4920
  const [syncTick, setSyncTick] = React.useState(0);
@@ -5249,7 +5440,7 @@ function QaProvider({
5249
5440
  setCapturePrefill(prefill ?? "");
5250
5441
  setCaptureActive(true);
5251
5442
  if (!stillIsCurrent()) setFrozenAt(null);
5252
- if (exactShotsWanted(storage) && isExactCaptureSupported()) {
5443
+ if (exactCaptureIsFree() || exactShotsWanted(storage) && isExactCaptureSupported()) {
5253
5444
  resetExactCaptureDecline();
5254
5445
  void freezeOrReuse().then((frame) => {
5255
5446
  setFrozenAt(frame?.takenAt ?? null);
@@ -5567,7 +5758,7 @@ function QaProvider({
5567
5758
  const shareExport = React.useCallback(async (filename) => {
5568
5759
  if (!notes.length) return { status: "unsupported" };
5569
5760
  const stamp = nowIso();
5570
- const name = exportFileName(filename, stamp);
5761
+ const name = exportFileName(filename, stamp, exportProjectName(config));
5571
5762
  setIsExporting(true);
5572
5763
  try {
5573
5764
  const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
@@ -5941,6 +6132,7 @@ function QaProvider({
5941
6132
  setSimpleMode,
5942
6133
  compactCapture,
5943
6134
  developerMode,
6135
+ projectName: exportProjectName(config),
5944
6136
  setDeveloperMode,
5945
6137
  setCompactCapture,
5946
6138
  exportZip: exportZipFn
@@ -8214,7 +8406,7 @@ function upgradeHint(latest) {
8214
8406
  }
8215
8407
 
8216
8408
  // src/version.ts
8217
- var QA_VERSION = "0.9.1" ;
8409
+ var QA_VERSION = "0.10.0" ;
8218
8410
  function Section({
8219
8411
  icon,
8220
8412
  title,
@@ -8452,8 +8644,11 @@ function SettingsSheet({ onClose }) {
8452
8644
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8453
8645
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8454
8646
  /* @__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(
8647
+ /* @__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") }),
8648
+ 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: [
8649
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 13 }),
8650
+ t("exact_native_on")
8651
+ ] }) : !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
8652
  "button",
8458
8653
  {
8459
8654
  type: "button",
@@ -8827,8 +9022,8 @@ var TABS = ALL_TABS.filter((tab) => tab.key !== "guide" || GUIDE_TAB_ENABLED);
8827
9022
  function visibleTab(tab) {
8828
9023
  return tab === "guide" && !GUIDE_TAB_ENABLED ? "notes" : tab;
8829
9024
  }
8830
- function todayName() {
8831
- return `qa-notes-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
9025
+ function suggestName(project) {
9026
+ return suggestedExportName(project, (/* @__PURE__ */ new Date()).toISOString());
8832
9027
  }
8833
9028
  function panelReducer(state2, action) {
8834
9029
  switch (action.type) {
@@ -8890,7 +9085,8 @@ function QaPanel() {
8890
9085
  panelSide,
8891
9086
  setPanelSide,
8892
9087
  panelCollapsed,
8893
- setPanelCollapsed
9088
+ setPanelCollapsed,
9089
+ projectName
8894
9090
  } = useQa();
8895
9091
  const activeTab = visibleTab(storedTab);
8896
9092
  const [confirmClear, setConfirmClear] = React.useState(false);
@@ -9009,7 +9205,7 @@ function QaPanel() {
9009
9205
  const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
9010
9206
  if (phase === "hidden") return null;
9011
9207
  const openNaming = () => {
9012
- setFilename(todayName());
9208
+ setFilename(suggestName(projectName));
9013
9209
  setNaming(true);
9014
9210
  };
9015
9211
  const doExport = () => {
@@ -11062,7 +11258,7 @@ function CaptureMode() {
11062
11258
  }
11063
11259
  ),
11064
11260
  developerMode && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
11065
- developerMode && /* @__PURE__ */ jsxRuntime.jsxs(
11261
+ /* @__PURE__ */ jsxRuntime.jsxs(
11066
11262
  "div",
11067
11263
  {
11068
11264
  role: "group",
@@ -11162,7 +11358,8 @@ function CaptureMode() {
11162
11358
  ] }),
11163
11359
  /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
11164
11360
  t("save_hint"),
11165
- developerMode ? ` \xB7 ${t("severity_keys")}` : ""
11361
+ " \xB7 ",
11362
+ t("severity_keys")
11166
11363
  ] })
11167
11364
  ] })
11168
11365
  ]
@@ -11370,5 +11567,5 @@ function Qapture({ config }) {
11370
11567
  exports.Qapture = Qapture;
11371
11568
  exports.deleteQaDatabase = deleteQaDatabase;
11372
11569
  exports.initQaStudio = initQaStudio;
11373
- //# sourceMappingURL=chunk-J77SGE5E.cjs.map
11374
- //# sourceMappingURL=chunk-J77SGE5E.cjs.map
11570
+ //# sourceMappingURL=chunk-R6WVHVXO.cjs.map
11571
+ //# sourceMappingURL=chunk-R6WVHVXO.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 chunkR6WVHVXO_cjs = require('./chunk-R6WVHVXO.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 chunkR6WVHVXO_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 chunkR6WVHVXO_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 chunkR6WVHVXO_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 chunkR6WVHVXO_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-5W5FS7JY.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/next.cjs CHANGED
@@ -1,21 +1,21 @@
1
1
  "use client";
2
2
  'use strict';
3
3
 
4
- var chunkJ77SGE5E_cjs = require('./chunk-J77SGE5E.cjs');
4
+ var chunkR6WVHVXO_cjs = require('./chunk-R6WVHVXO.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "QaStudio", {
9
9
  enumerable: true,
10
- get: function () { return chunkJ77SGE5E_cjs.Qapture; }
10
+ get: function () { return chunkR6WVHVXO_cjs.Qapture; }
11
11
  });
12
12
  Object.defineProperty(exports, "Qapture", {
13
13
  enumerable: true,
14
- get: function () { return chunkJ77SGE5E_cjs.Qapture; }
14
+ get: function () { return chunkR6WVHVXO_cjs.Qapture; }
15
15
  });
16
16
  Object.defineProperty(exports, "initQaStudio", {
17
17
  enumerable: true,
18
- get: function () { return chunkJ77SGE5E_cjs.initQaStudio; }
18
+ get: function () { return chunkR6WVHVXO_cjs.initQaStudio; }
19
19
  });
20
20
  //# sourceMappingURL=next.cjs.map
21
21
  //# sourceMappingURL=next.cjs.map
package/dist/next.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-HEHVZCSV.js';
2
+ export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-5W5FS7JY.js';
3
3
  //# sourceMappingURL=next.js.map
4
4
  //# sourceMappingURL=next.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkJ77SGE5E_cjs = require('./chunk-J77SGE5E.cjs');
3
+ var chunkR6WVHVXO_cjs = require('./chunk-R6WVHVXO.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 chunkJ77SGE5E_cjs.initQaStudio; }
41
+ get: function () { return chunkR6WVHVXO_cjs.initQaStudio; }
42
42
  });
43
43
  //# sourceMappingURL=standalone.cjs.map
44
44
  //# sourceMappingURL=standalone.cjs.map
@@ -1,4 +1,4 @@
1
- export { initQaStudio } from './chunk-HEHVZCSV.js';
1
+ export { initQaStudio } from './chunk-5W5FS7JY.js';
2
2
 
3
3
  // src/standalone.ts
4
4
  if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {