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.
@@ -166,6 +166,7 @@ function deleteQaDatabase(namespace) {
166
166
  // src/config/schema.ts
167
167
  var DEFAULTS = {
168
168
  namespace: "qapture",
169
+ shotPort: 7017,
169
170
  brandLabel: "Qapture",
170
171
  loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
171
172
  rtl: false,
@@ -336,6 +337,7 @@ function validateConfig(input) {
336
337
  return {
337
338
  config: {
338
339
  namespace: DEFAULTS.namespace,
340
+ shotPort: DEFAULTS.shotPort,
339
341
  brand: { label: DEFAULTS.brandLabel },
340
342
  loginField: { ...DEFAULTS.loginField },
341
343
  credentials: [],
@@ -358,6 +360,7 @@ function validateConfig(input) {
358
360
  return {
359
361
  config: {
360
362
  namespace: DEFAULTS.namespace,
363
+ shotPort: DEFAULTS.shotPort,
361
364
  brand: { label: DEFAULTS.brandLabel },
362
365
  loginField: { ...DEFAULTS.loginField },
363
366
  credentials: [],
@@ -377,6 +380,8 @@ function validateConfig(input) {
377
380
  }
378
381
  const raw = input;
379
382
  const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
383
+ const rawPort = Number(raw["shotPort"]);
384
+ const shotPort = Number.isInteger(rawPort) && rawPort > 0 && rawPort < 65536 ? rawPort : DEFAULTS.shotPort;
380
385
  if (raw["theme"] !== void 0) {
381
386
  warnings.push(
382
387
  '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.'
@@ -426,6 +431,7 @@ function validateConfig(input) {
426
431
  return {
427
432
  config: {
428
433
  namespace,
434
+ shotPort,
429
435
  brand: { label: brandLabel },
430
436
  loginField,
431
437
  credentials,
@@ -1931,6 +1937,8 @@ var STR = {
1931
1937
  exact_declined: "Staying on redrawn screenshots",
1932
1938
  exact_turn_on: "Turn on",
1933
1939
  exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
1940
+ 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.",
1941
+ exact_native_on: "Real screenshots on \u2014 local helper",
1934
1942
  sync_title: "Save to a folder",
1935
1943
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1936
1944
  sync_choose: "Choose folder",
@@ -2168,6 +2176,8 @@ var STR = {
2168
2176
  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",
2169
2177
  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",
2170
2178
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
2179
+ 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.",
2180
+ 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",
2171
2181
  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",
2172
2182
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
2173
2183
  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.",
@@ -2504,13 +2514,132 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2504
2514
  if (sw < 1 || sh < 1) return null;
2505
2515
  return { sx, sy, sw, sh };
2506
2516
  }
2517
+ function environmentSignature() {
2518
+ if (typeof window === "undefined") return "";
2519
+ return [
2520
+ window.innerWidth,
2521
+ window.innerHeight,
2522
+ window.outerWidth,
2523
+ window.outerHeight,
2524
+ window.screenX,
2525
+ window.screenY,
2526
+ Math.round((window.devicePixelRatio || 1) * 100)
2527
+ ].join("x");
2528
+ }
2529
+
2530
+ // src/lib/nativeShot.ts
2531
+ var DEFAULT_SHOT_PORT = 7017;
2532
+ var PROBE_TIMEOUT_MS = 700;
2533
+ var SHOT_TIMEOUT_MS = 9e3;
2534
+ var ABSENT_RECHECK_MS = 3e4;
2535
+ var configuredPort = DEFAULT_SHOT_PORT;
2536
+ function setShotPort(port) {
2537
+ if (!Number.isInteger(port) || port <= 0 || port >= 65536) return;
2538
+ if (port === configuredPort) return;
2539
+ configuredPort = port;
2540
+ resetNativeShotProbe();
2541
+ }
2542
+ var cachedBase = null;
2543
+ var lastProbeAt = 0;
2544
+ var lastProbeResult = false;
2545
+ function baseUrl(port) {
2546
+ return `http://127.0.0.1:${port}`;
2547
+ }
2548
+ async function fetchWithTimeout(url, init, ms) {
2549
+ if (typeof fetch !== "function") return null;
2550
+ const ac = typeof AbortController === "function" ? new AbortController() : null;
2551
+ const timer = setTimeout(() => ac?.abort(), ms);
2552
+ try {
2553
+ return await fetch(url, { ...init, signal: ac?.signal, cache: "no-store" });
2554
+ } catch {
2555
+ return null;
2556
+ } finally {
2557
+ clearTimeout(timer);
2558
+ }
2559
+ }
2560
+ async function isNativeShotAvailable(port = configuredPort) {
2561
+ if (typeof window === "undefined") return false;
2562
+ const now2 = Date.now();
2563
+ if (lastProbeResult && cachedBase) return true;
2564
+ if (!lastProbeResult && now2 - lastProbeAt < ABSENT_RECHECK_MS) return false;
2565
+ lastProbeAt = now2;
2566
+ const res = await fetchWithTimeout(`${baseUrl(port)}/qapture/health`, { method: "GET" }, PROBE_TIMEOUT_MS);
2567
+ lastProbeResult = !!res && res.ok;
2568
+ cachedBase = lastProbeResult ? baseUrl(port) : null;
2569
+ return lastProbeResult;
2570
+ }
2571
+ function resetNativeShotProbe() {
2572
+ cachedBase = null;
2573
+ lastProbeAt = 0;
2574
+ lastProbeResult = false;
2575
+ }
2576
+ async function shootBrowserWindow(port = configuredPort) {
2577
+ if (typeof window === "undefined") return null;
2578
+ const base = cachedBase ?? baseUrl(port);
2579
+ const body = JSON.stringify({
2580
+ x: window.screenX,
2581
+ y: window.screenY,
2582
+ w: window.outerWidth,
2583
+ h: window.outerHeight
2584
+ });
2585
+ const res = await fetchWithTimeout(
2586
+ `${base}/qapture/shot`,
2587
+ { method: "POST", headers: { "content-type": "application/json" }, body },
2588
+ SHOT_TIMEOUT_MS
2589
+ );
2590
+ if (!res || !res.ok) return null;
2591
+ let payload;
2592
+ try {
2593
+ payload = await res.json();
2594
+ } catch {
2595
+ return null;
2596
+ }
2597
+ if (!payload.png) return null;
2598
+ return decodeToCanvas(payload.png);
2599
+ }
2600
+ function decodeToCanvas(dataUrl) {
2601
+ return new Promise((resolve) => {
2602
+ const img = new Image();
2603
+ img.onload = () => {
2604
+ const c = document.createElement("canvas");
2605
+ c.width = img.naturalWidth;
2606
+ c.height = img.naturalHeight;
2607
+ const ctx = c.getContext("2d", { willReadFrequently: true });
2608
+ if (!ctx) {
2609
+ resolve(null);
2610
+ return;
2611
+ }
2612
+ ctx.drawImage(img, 0, 0);
2613
+ resolve(c);
2614
+ };
2615
+ img.onerror = () => resolve(null);
2616
+ img.src = dataUrl;
2617
+ });
2618
+ }
2619
+ var cachedMapping = null;
2620
+ var cachedFor = "";
2621
+ function getCachedMapping() {
2622
+ if (!cachedMapping) return null;
2623
+ return environmentSignature() === cachedFor ? cachedMapping : null;
2624
+ }
2625
+ function cacheMapping(m) {
2626
+ cachedMapping = m;
2627
+ cachedFor = m ? environmentSignature() : "";
2628
+ }
2507
2629
 
2508
2630
  // src/lib/screenCapture.ts
2509
2631
  var ASPECT_TOLERANCE = 0.08;
2510
2632
  var CALIBRATION_SETTLE_MS = 220;
2511
2633
  var FRESH_FRAME_TIMEOUT_MS = 500;
2512
2634
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2635
+ var nativeReady = false;
2636
+ async function refreshNativeAvailability(port) {
2637
+ if (typeof port === "number") setShotPort(port);
2638
+ nativeReady = await isNativeShotAvailable();
2639
+ return nativeReady;
2640
+ }
2513
2641
  function isExactCaptureSupported() {
2642
+ if (nativeReady) return true;
2514
2643
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2515
2644
  const md = navigator.mediaDevices;
2516
2645
  return !!md && typeof md.getDisplayMedia === "function";
@@ -2518,13 +2647,15 @@ function isExactCaptureSupported() {
2518
2647
  var armed = false;
2519
2648
  var declined = false;
2520
2649
  var frozen = null;
2521
- var lastMode = null;
2522
2650
  function armExactCapture() {
2523
2651
  if (!isExactCaptureSupported()) return false;
2524
2652
  armed = true;
2525
2653
  declined = false;
2526
2654
  return true;
2527
2655
  }
2656
+ function exactCaptureIsFree() {
2657
+ return nativeReady;
2658
+ }
2528
2659
  function disarmExactCapture() {
2529
2660
  armed = false;
2530
2661
  releaseFrozenFrame();
@@ -2533,6 +2664,7 @@ function resetExactCaptureDecline() {
2533
2664
  declined = false;
2534
2665
  }
2535
2666
  function getExactCaptureStatus() {
2667
+ if (nativeReady) return "native";
2536
2668
  if (!isExactCaptureSupported()) return "unsupported";
2537
2669
  if (armed) return "live";
2538
2670
  if (declined) return "declined";
@@ -2557,7 +2689,7 @@ function stillIsCurrent() {
2557
2689
  return true;
2558
2690
  }
2559
2691
  async function freezeOrReuse() {
2560
- if (stillIsCurrent()) return frozen;
2692
+ if (!nativeReady && stillIsCurrent()) return frozen;
2561
2693
  return freezeViewport();
2562
2694
  }
2563
2695
  function releaseFrozenFrame() {
@@ -2657,7 +2789,7 @@ async function grabFrameCanvas(video, grabber) {
2657
2789
  bitmap?.close?.();
2658
2790
  return c;
2659
2791
  }
2660
- async function calibrate(video, grabber) {
2792
+ async function calibrate(grabFrame) {
2661
2793
  const vw = window.innerWidth;
2662
2794
  const vh = window.innerHeight;
2663
2795
  if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
@@ -2674,7 +2806,7 @@ async function calibrate(video, grabber) {
2674
2806
  document.body.appendChild(card);
2675
2807
  try {
2676
2808
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2677
- const frame = await grabFrameCanvas(video, grabber);
2809
+ const frame = await grabFrame();
2678
2810
  if (!frame) return null;
2679
2811
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2680
2812
  if (!ctx) return null;
@@ -2696,8 +2828,69 @@ async function calibrate(video, grabber) {
2696
2828
  }
2697
2829
  }
2698
2830
  async function freezeViewport() {
2699
- if (!isExactCaptureSupported()) return null;
2700
2831
  releaseFrozenFrame();
2832
+ const native = await freezeViaNativeHelper();
2833
+ if (native) return native;
2834
+ if (!isExactCaptureSupported()) return null;
2835
+ return freezeViaDisplayMedia();
2836
+ }
2837
+ function adoptFrame(raw, mapping, mode, vw, vh) {
2838
+ let page;
2839
+ if (mapping) {
2840
+ const box = mapRectToFrame(
2841
+ { left: 0, top: 0, width: vw, height: vh },
2842
+ mapping,
2843
+ raw.width,
2844
+ raw.height
2845
+ );
2846
+ if (!box) return null;
2847
+ page = document.createElement("canvas");
2848
+ page.width = box.sw;
2849
+ page.height = box.sh;
2850
+ const ctx = page.getContext("2d");
2851
+ if (!ctx) return null;
2852
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2853
+ raw.width = 0;
2854
+ raw.height = 0;
2855
+ } else {
2856
+ page = raw;
2857
+ }
2858
+ frozen = {
2859
+ canvas: page,
2860
+ mode,
2861
+ viewportWidth: vw,
2862
+ viewportHeight: vh,
2863
+ takenAt: Date.now()
2864
+ };
2865
+ frozenAtScrollX = window.scrollX;
2866
+ frozenAtScrollY = window.scrollY;
2867
+ frozenAtPath = window.location.pathname + window.location.search;
2868
+ return frozen;
2869
+ }
2870
+ async function freezeViaNativeHelper() {
2871
+ if (typeof window === "undefined") return null;
2872
+ nativeReady = await isNativeShotAvailable();
2873
+ if (!nativeReady) return null;
2874
+ const vw = window.innerWidth;
2875
+ const vh = window.innerHeight;
2876
+ let mapping = getCachedMapping();
2877
+ if (!mapping) {
2878
+ mapping = await calibrate(() => shootBrowserWindow());
2879
+ if (!mapping) {
2880
+ cacheMapping(null);
2881
+ return null;
2882
+ }
2883
+ cacheMapping(mapping);
2884
+ }
2885
+ const raw = await withOverlayHidden(() => shootBrowserWindow());
2886
+ if (!raw) return null;
2887
+ const adopted = adoptFrame(raw, mapping, "native", vw, vh);
2888
+ if (!adopted) {
2889
+ cacheMapping(null);
2890
+ }
2891
+ return adopted;
2892
+ }
2893
+ async function freezeViaDisplayMedia() {
2701
2894
  let stream = null;
2702
2895
  let el = null;
2703
2896
  const release = () => {
@@ -2770,45 +2963,14 @@ async function freezeViewport() {
2770
2963
  const vw = window.innerWidth;
2771
2964
  const vh = window.innerHeight;
2772
2965
  const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2773
- const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
2966
+ const mapping = mode === "surface" ? await calibrate(() => grabFrameCanvas(video, grabber)) : null;
2774
2967
  if (mode === "surface" && !mapping) {
2775
2968
  declined = true;
2776
2969
  return null;
2777
2970
  }
2778
2971
  const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2779
2972
  if (!raw) return null;
2780
- let page;
2781
- if (mode === "surface" && mapping) {
2782
- const box = mapRectToFrame(
2783
- { left: 0, top: 0, width: vw, height: vh },
2784
- mapping,
2785
- raw.width,
2786
- raw.height
2787
- );
2788
- if (!box) return null;
2789
- page = document.createElement("canvas");
2790
- page.width = box.sw;
2791
- page.height = box.sh;
2792
- const ctx = page.getContext("2d");
2793
- if (!ctx) return null;
2794
- ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2795
- raw.width = 0;
2796
- raw.height = 0;
2797
- } else {
2798
- page = raw;
2799
- }
2800
- frozen = {
2801
- canvas: page,
2802
- mode,
2803
- viewportWidth: vw,
2804
- viewportHeight: vh,
2805
- takenAt: Date.now()
2806
- };
2807
- frozenAtScrollX = window.scrollX;
2808
- frozenAtScrollY = window.scrollY;
2809
- frozenAtPath = window.location.pathname + window.location.search;
2810
- lastMode = mode;
2811
- return frozen;
2973
+ return adoptFrame(raw, mode === "surface" ? mapping : null, mode, vw, vh);
2812
2974
  } catch {
2813
2975
  declined = true;
2814
2976
  return null;
@@ -3629,11 +3791,24 @@ function reproSpec(note, index) {
3629
3791
  }
3630
3792
 
3631
3793
  // src/lib/exportZip.ts
3632
- function safeName(name, stamp) {
3633
- const fallback = `qa-notes-${stamp.slice(0, 10)}`;
3794
+ function autoName(project, stamp) {
3795
+ const date = stamp.slice(0, 10);
3796
+ const time = stamp.slice(11, 16).replace(":", "");
3797
+ const slug = (project ?? "").trim().replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
3798
+ const when = time ? `${date}-${time}` : date;
3799
+ return slug ? `${slug}-qa-${when}` : `qa-notes-${when}`;
3800
+ }
3801
+ function exportProjectName(config) {
3802
+ const name = config?.preamble?.projectName;
3803
+ return typeof name === "string" && name.trim() ? name.trim() : void 0;
3804
+ }
3805
+ function safeName(name, stamp, project) {
3634
3806
  let base = (name ?? "").trim().replace(/\.zip$/i, "");
3635
3807
  base = base.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 80).trim();
3636
- return `${base || fallback}.zip`;
3808
+ return `${base || autoName(project, stamp)}.zip`;
3809
+ }
3810
+ function suggestedExportName(project, stamp) {
3811
+ return autoName(project, stamp);
3637
3812
  }
3638
3813
  function toStrings(val) {
3639
3814
  if (val == null) return [];
@@ -4011,14 +4186,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked,
4011
4186
  const url = URL.createObjectURL(blob);
4012
4187
  const a = document.createElement("a");
4013
4188
  a.href = url;
4014
- a.download = safeName(filename, stamp);
4189
+ a.download = safeName(filename, stamp, exportProjectName(config));
4015
4190
  document.body.appendChild(a);
4016
4191
  a.click();
4017
4192
  a.remove();
4018
4193
  setTimeout(() => URL.revokeObjectURL(url), 2e3);
4019
4194
  }
4020
- function exportFileName(filename, stamp) {
4021
- return safeName(filename, stamp);
4195
+ function exportFileName(filename, stamp, project) {
4196
+ return safeName(filename, stamp, project);
4022
4197
  }
4023
4198
 
4024
4199
  // src/lib/shareZip.ts
@@ -4716,7 +4891,23 @@ function QaProvider({
4716
4891
  if (exactShotsWanted(storage)) armExactCapture();
4717
4892
  return getExactCaptureStatus();
4718
4893
  });
4719
- const exactSupported = isExactCaptureSupported();
4894
+ const [exactSupported, setExactSupported] = useState(() => isExactCaptureSupported());
4895
+ useEffect(() => {
4896
+ let alive = true;
4897
+ const look = () => {
4898
+ void refreshNativeAvailability(config.shotPort).then(() => {
4899
+ if (!alive) return;
4900
+ setExactSupported(isExactCaptureSupported());
4901
+ setExactStatus(getExactCaptureStatus());
4902
+ });
4903
+ };
4904
+ look();
4905
+ window.addEventListener("focus", look);
4906
+ return () => {
4907
+ alive = false;
4908
+ window.removeEventListener("focus", look);
4909
+ };
4910
+ }, [config.shotPort]);
4720
4911
  const [frozenAt, setFrozenAt] = useState(null);
4721
4912
  const [syncState, setSyncState] = useState(() => getFsSyncState());
4722
4913
  const [syncTick, setSyncTick] = useState(0);
@@ -5242,7 +5433,7 @@ function QaProvider({
5242
5433
  setCapturePrefill(prefill ?? "");
5243
5434
  setCaptureActive(true);
5244
5435
  if (!stillIsCurrent()) setFrozenAt(null);
5245
- if (exactShotsWanted(storage) && isExactCaptureSupported()) {
5436
+ if (exactCaptureIsFree() || exactShotsWanted(storage) && isExactCaptureSupported()) {
5246
5437
  resetExactCaptureDecline();
5247
5438
  void freezeOrReuse().then((frame) => {
5248
5439
  setFrozenAt(frame?.takenAt ?? null);
@@ -5560,7 +5751,7 @@ function QaProvider({
5560
5751
  const shareExport = useCallback(async (filename) => {
5561
5752
  if (!notes.length) return { status: "unsupported" };
5562
5753
  const stamp = nowIso();
5563
- const name = exportFileName(filename, stamp);
5754
+ const name = exportFileName(filename, stamp, exportProjectName(config));
5564
5755
  setIsExporting(true);
5565
5756
  try {
5566
5757
  const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
@@ -5934,6 +6125,7 @@ function QaProvider({
5934
6125
  setSimpleMode,
5935
6126
  compactCapture,
5936
6127
  developerMode,
6128
+ projectName: exportProjectName(config),
5937
6129
  setDeveloperMode,
5938
6130
  setCompactCapture,
5939
6131
  exportZip: exportZipFn
@@ -8207,7 +8399,7 @@ function upgradeHint(latest) {
8207
8399
  }
8208
8400
 
8209
8401
  // src/version.ts
8210
- var QA_VERSION = "0.9.1" ;
8402
+ var QA_VERSION = "0.10.0" ;
8211
8403
  function Section({
8212
8404
  icon,
8213
8405
  title,
@@ -8445,8 +8637,11 @@ function SettingsSheet({ onClose }) {
8445
8637
  /* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8446
8638
  /* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8447
8639
  /* @__PURE__ */ jsxs(Section, { icon: "Camera", title: t("exact_label"), children: [
8448
- /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_hint") }),
8449
- !exactShots.supported ? /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: t("exact_unsupported") }) : exactShots.status === "live" ? /* @__PURE__ */ jsxs(
8640
+ /* @__PURE__ */ 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") }),
8641
+ exactShots.status === "native" ? /* @__PURE__ */ 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: [
8642
+ /* @__PURE__ */ jsx(Icon, { name: "CheckCircle2", size: 13 }),
8643
+ t("exact_native_on")
8644
+ ] }) : !exactShots.supported ? /* @__PURE__ */ jsx("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: t("exact_unsupported") }) : exactShots.status === "live" ? /* @__PURE__ */ jsxs(
8450
8645
  "button",
8451
8646
  {
8452
8647
  type: "button",
@@ -8820,8 +9015,8 @@ var TABS = ALL_TABS.filter((tab) => tab.key !== "guide" || GUIDE_TAB_ENABLED);
8820
9015
  function visibleTab(tab) {
8821
9016
  return tab === "guide" && !GUIDE_TAB_ENABLED ? "notes" : tab;
8822
9017
  }
8823
- function todayName() {
8824
- return `qa-notes-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
9018
+ function suggestName(project) {
9019
+ return suggestedExportName(project, (/* @__PURE__ */ new Date()).toISOString());
8825
9020
  }
8826
9021
  function panelReducer(state2, action) {
8827
9022
  switch (action.type) {
@@ -8883,7 +9078,8 @@ function QaPanel() {
8883
9078
  panelSide,
8884
9079
  setPanelSide,
8885
9080
  panelCollapsed,
8886
- setPanelCollapsed
9081
+ setPanelCollapsed,
9082
+ projectName
8887
9083
  } = useQa();
8888
9084
  const activeTab = visibleTab(storedTab);
8889
9085
  const [confirmClear, setConfirmClear] = useState(false);
@@ -9002,7 +9198,7 @@ function QaPanel() {
9002
9198
  const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
9003
9199
  if (phase === "hidden") return null;
9004
9200
  const openNaming = () => {
9005
- setFilename(todayName());
9201
+ setFilename(suggestName(projectName));
9006
9202
  setNaming(true);
9007
9203
  };
9008
9204
  const doExport = () => {
@@ -11055,7 +11251,7 @@ function CaptureMode() {
11055
11251
  }
11056
11252
  ),
11057
11253
  developerMode && /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
11058
- developerMode && /* @__PURE__ */ jsxs(
11254
+ /* @__PURE__ */ jsxs(
11059
11255
  "div",
11060
11256
  {
11061
11257
  role: "group",
@@ -11155,7 +11351,8 @@ function CaptureMode() {
11155
11351
  ] }),
11156
11352
  /* @__PURE__ */ jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
11157
11353
  t("save_hint"),
11158
- developerMode ? ` \xB7 ${t("severity_keys")}` : ""
11354
+ " \xB7 ",
11355
+ t("severity_keys")
11159
11356
  ] })
11160
11357
  ] })
11161
11358
  ]
@@ -11361,5 +11558,5 @@ function Qapture({ config }) {
11361
11558
  }
11362
11559
 
11363
11560
  export { Qapture, deleteQaDatabase, initQaStudio };
11364
- //# sourceMappingURL=chunk-HEHVZCSV.js.map
11365
- //# sourceMappingURL=chunk-HEHVZCSV.js.map
11561
+ //# sourceMappingURL=chunk-5W5FS7JY.js.map
11562
+ //# sourceMappingURL=chunk-5W5FS7JY.js.map