@ai-matrx/capture 0.5.3 → 0.5.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — @ai-matrx/capture
2
2
 
3
+ ## 0.5.4 — 2026-09-01
4
+
5
+ - Persisted-media editing now has an explicit fresh-byte port (`resolveEditBlob`). The package creates and owns an editor-private object URL instead of reusing a viewer URL that the host byte cache may evict and revoke while the decoded viewer frame remains visible.
6
+ - Both camera chromes revoke the editor-private URL on every close/save/unmount path and surface a visible retry remedy when byte preparation fails. Exact fail-first coverage reproduces a stale viewer URL and proves the editor consumes fresh bytes.
7
+
3
8
  ## 0.5.3 — 2026-09-01
4
9
 
5
10
  - `ImageEditSheet` now keeps Save, crop presets, rotate, and flip unavailable while a newly baked bitmap is decoding, then enables them from the image's real `load` event. A transformed image can no longer hit the zero-dimension silent-return race and leave the editor stuck with no persistence or error.
package/dist/react.cjs CHANGED
@@ -727,6 +727,16 @@ function invalidateMedia(key) {
727
727
  if (entry?.revoke && entry.url) URL.revokeObjectURL(entry.url);
728
728
  notify(key);
729
729
  }
730
+ async function prepareMediaForEdit(item) {
731
+ if (item.resolveEditBlob) {
732
+ const blob = await item.resolveEditBlob();
733
+ if (blob.size === 0) throw new Error("media snapshot was empty");
734
+ return { url: URL.createObjectURL(blob), revoke: true };
735
+ }
736
+ const url = item.src ?? getMediaUrl(item);
737
+ if (!url) throw new Error("media item has no editable source");
738
+ return { url, revoke: false };
739
+ }
730
740
  function useMediaUrl(item) {
731
741
  const [, bump] = (0, import_react2.useState)(0);
732
742
  const key = item?.key ?? null;
@@ -1604,6 +1614,10 @@ function CameraCapture({
1604
1614
  const [optionsOpen, setOptionsOpen] = (0, import_react5.useState)(false);
1605
1615
  const [viewerKey, setViewerKey] = (0, import_react5.useState)(null);
1606
1616
  const [editTarget, setEditTarget] = (0, import_react5.useState)(null);
1617
+ const [editPreparing, setEditPreparing] = (0, import_react5.useState)(false);
1618
+ const [editPreparationError, setEditPreparationError] = (0, import_react5.useState)(
1619
+ null
1620
+ );
1607
1621
  const [gridOn, setGridOn] = (0, import_react5.useState)(false);
1608
1622
  const [timerSetting, setTimerSetting] = (0, import_react5.useState)(0);
1609
1623
  const [aspect, setAspect] = (0, import_react5.useState)("full");
@@ -1628,6 +1642,12 @@ function CameraCapture({
1628
1642
  (0, import_react5.useEffect)(() => {
1629
1643
  onReviewOpenChange?.(reviewOpen);
1630
1644
  }, [reviewOpen, onReviewOpenChange]);
1645
+ (0, import_react5.useEffect)(
1646
+ () => () => {
1647
+ if (editTarget?.revoke) URL.revokeObjectURL(editTarget.url);
1648
+ },
1649
+ [editTarget]
1650
+ );
1631
1651
  const clearCountdown = (0, import_react5.useCallback)(() => {
1632
1652
  if (countdownRef.current) clearInterval(countdownRef.current);
1633
1653
  countdownRef.current = null;
@@ -1941,8 +1961,16 @@ function CameraCapture({
1941
1961
  invalidateMedia(item.key);
1942
1962
  } : void 0,
1943
1963
  onEdit: media.onReplacePhoto ? (item) => {
1944
- const url = getMediaUrl(item);
1945
- if (url) setEditTarget({ key: item.key, url });
1964
+ if (editPreparing) return;
1965
+ setEditPreparing(true);
1966
+ setEditPreparationError(null);
1967
+ void prepareMediaForEdit(item).then(
1968
+ (prepared) => setEditTarget({ key: item.key, ...prepared })
1969
+ ).catch(
1970
+ () => setEditPreparationError(
1971
+ "This photo could not be prepared for editing. Try again."
1972
+ )
1973
+ ).finally(() => setEditPreparing(false));
1946
1974
  } : void 0
1947
1975
  }
1948
1976
  ),
@@ -1963,6 +1991,14 @@ function CameraCapture({
1963
1991
  }
1964
1992
  }
1965
1993
  ),
1994
+ editPreparationError && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1995
+ "p",
1996
+ {
1997
+ role: "alert",
1998
+ className: "fixed inset-x-4 bottom-24 z-[70] rounded-xl bg-black/85 px-4 py-3 text-center text-sm text-[#FF6961]",
1999
+ children: editPreparationError
2000
+ }
2001
+ ),
1966
2002
  slots.overlays
1967
2003
  ] });
1968
2004
  }
@@ -2261,7 +2297,9 @@ function CameraCaptureV3({
2261
2297
  slots = {}
2262
2298
  }) {
2263
2299
  const [viewerKey, setViewerKey] = (0, import_react8.useState)(null);
2264
- const [editTarget, setEditTarget] = (0, import_react8.useState)(
2300
+ const [editTarget, setEditTarget] = (0, import_react8.useState)(null);
2301
+ const [editPreparing, setEditPreparing] = (0, import_react8.useState)(false);
2302
+ const [editPreparationError, setEditPreparationError] = (0, import_react8.useState)(
2265
2303
  null
2266
2304
  );
2267
2305
  const [gridOn, setGridOn] = (0, import_react8.useState)(false);
@@ -2275,6 +2313,12 @@ function CameraCaptureV3({
2275
2313
  (0, import_react8.useEffect)(() => {
2276
2314
  onReviewOpenChange?.(reviewOpen);
2277
2315
  }, [reviewOpen, onReviewOpenChange]);
2316
+ (0, import_react8.useEffect)(
2317
+ () => () => {
2318
+ if (editTarget?.revoke) URL.revokeObjectURL(editTarget.url);
2319
+ },
2320
+ [editTarget]
2321
+ );
2278
2322
  const clearCountdown = (0, import_react8.useCallback)(() => {
2279
2323
  if (countdownRef.current) clearInterval(countdownRef.current);
2280
2324
  countdownRef.current = null;
@@ -2493,8 +2537,16 @@ function CameraCaptureV3({
2493
2537
  invalidateMedia(item.key);
2494
2538
  } : void 0,
2495
2539
  onEdit: media.onReplacePhoto ? (item) => {
2496
- const url = getMediaUrl(item);
2497
- if (url) setEditTarget({ key: item.key, url });
2540
+ if (editPreparing) return;
2541
+ setEditPreparing(true);
2542
+ setEditPreparationError(null);
2543
+ void prepareMediaForEdit(item).then(
2544
+ (prepared) => setEditTarget({ key: item.key, ...prepared })
2545
+ ).catch(
2546
+ () => setEditPreparationError(
2547
+ "This photo could not be prepared for editing. Try again."
2548
+ )
2549
+ ).finally(() => setEditPreparing(false));
2498
2550
  } : void 0
2499
2551
  }
2500
2552
  ),
@@ -2515,6 +2567,14 @@ function CameraCaptureV3({
2515
2567
  }
2516
2568
  }
2517
2569
  ),
2570
+ editPreparationError && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2571
+ "p",
2572
+ {
2573
+ role: "alert",
2574
+ className: "fixed inset-x-4 bottom-24 z-[70] rounded-xl bg-black/85 px-4 py-3 text-center text-sm text-[#FF6961]",
2575
+ children: editPreparationError
2576
+ }
2577
+ ),
2518
2578
  slots.overlays
2519
2579
  ] });
2520
2580
  }