@ai-matrx/capture 0.5.2 → 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 +10 -0
- package/dist/react.cjs +131 -16
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +3 -0
- package/dist/react.d.ts +3 -0
- package/dist/react.js +131 -16
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.d.cts
CHANGED
|
@@ -231,6 +231,9 @@ interface CaptureMediaItem {
|
|
|
231
231
|
/** Async URL resolution for persisted items. Called at most once per key
|
|
232
232
|
* while cached; concurrent callers share the in-flight promise. */
|
|
233
233
|
resolve?: () => Promise<ResolvedMedia>;
|
|
234
|
+
/** Fresh bytes for pixel editing. Persisted hosts should provide this
|
|
235
|
+
* instead of asking the editor to borrow a cache-owned viewer URL. */
|
|
236
|
+
resolveEditBlob?: () => Promise<Blob>;
|
|
234
237
|
/** Upload/processing state — the filmstrip renders it honestly. */
|
|
235
238
|
status?: "ready" | "uploading" | "error";
|
|
236
239
|
/** Accent ring on the filmstrip tile (e.g. a delineator frame). */
|
package/dist/react.d.ts
CHANGED
|
@@ -231,6 +231,9 @@ interface CaptureMediaItem {
|
|
|
231
231
|
/** Async URL resolution for persisted items. Called at most once per key
|
|
232
232
|
* while cached; concurrent callers share the in-flight promise. */
|
|
233
233
|
resolve?: () => Promise<ResolvedMedia>;
|
|
234
|
+
/** Fresh bytes for pixel editing. Persisted hosts should provide this
|
|
235
|
+
* instead of asking the editor to borrow a cache-owned viewer URL. */
|
|
236
|
+
resolveEditBlob?: () => Promise<Blob>;
|
|
234
237
|
/** Upload/processing state — the filmstrip renders it honestly. */
|
|
235
238
|
status?: "ready" | "uploading" | "error";
|
|
236
239
|
/** Accent ring on the filmstrip tile (e.g. a delineator frame). */
|
package/dist/react.js
CHANGED
|
@@ -661,6 +661,16 @@ function invalidateMedia(key) {
|
|
|
661
661
|
if (entry?.revoke && entry.url) URL.revokeObjectURL(entry.url);
|
|
662
662
|
notify(key);
|
|
663
663
|
}
|
|
664
|
+
async function prepareMediaForEdit(item) {
|
|
665
|
+
if (item.resolveEditBlob) {
|
|
666
|
+
const blob = await item.resolveEditBlob();
|
|
667
|
+
if (blob.size === 0) throw new Error("media snapshot was empty");
|
|
668
|
+
return { url: URL.createObjectURL(blob), revoke: true };
|
|
669
|
+
}
|
|
670
|
+
const url = item.src ?? getMediaUrl(item);
|
|
671
|
+
if (!url) throw new Error("media item has no editable source");
|
|
672
|
+
return { url, revoke: false };
|
|
673
|
+
}
|
|
664
674
|
function useMediaUrl(item) {
|
|
665
675
|
const [, bump] = useState2(0);
|
|
666
676
|
const key = item?.key ?? null;
|
|
@@ -1175,6 +1185,8 @@ function ImageEditSheet({
|
|
|
1175
1185
|
const [workingSrc, setWorkingSrc] = useState4(null);
|
|
1176
1186
|
const [aspect, setAspect] = useState4("free");
|
|
1177
1187
|
const [crop, setCrop] = useState4(FULL_CROP);
|
|
1188
|
+
const [imageReady, setImageReady] = useState4(false);
|
|
1189
|
+
const [transforming, setTransforming] = useState4(false);
|
|
1178
1190
|
const [saving, setSaving] = useState4(false);
|
|
1179
1191
|
const [editError, setEditError] = useState4(null);
|
|
1180
1192
|
const stageRef = useRef2(null);
|
|
@@ -1190,6 +1202,8 @@ function ImageEditSheet({
|
|
|
1190
1202
|
setWorkingSrc(null);
|
|
1191
1203
|
setAspect("free");
|
|
1192
1204
|
setCrop(FULL_CROP);
|
|
1205
|
+
setImageReady(false);
|
|
1206
|
+
setTransforming(false);
|
|
1193
1207
|
setSaving(false);
|
|
1194
1208
|
setEditError(null);
|
|
1195
1209
|
}
|
|
@@ -1198,21 +1212,53 @@ function ImageEditSheet({
|
|
|
1198
1212
|
const bake = useCallback3(
|
|
1199
1213
|
(op) => {
|
|
1200
1214
|
const img = imgRef.current;
|
|
1201
|
-
if (!img || img.naturalWidth === 0 || saving)
|
|
1215
|
+
if (!img || !imageReady || img.naturalWidth === 0 || saving || transforming) {
|
|
1216
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
setTransforming(true);
|
|
1220
|
+
setEditError(null);
|
|
1202
1221
|
try {
|
|
1222
|
+
const sourceX = Math.round(crop.x * img.naturalWidth);
|
|
1223
|
+
const sourceY = Math.round(crop.y * img.naturalHeight);
|
|
1224
|
+
const sourceWidth = Math.max(
|
|
1225
|
+
1,
|
|
1226
|
+
Math.min(
|
|
1227
|
+
img.naturalWidth - sourceX,
|
|
1228
|
+
Math.round(crop.w * img.naturalWidth)
|
|
1229
|
+
)
|
|
1230
|
+
);
|
|
1231
|
+
const sourceHeight = Math.max(
|
|
1232
|
+
1,
|
|
1233
|
+
Math.min(
|
|
1234
|
+
img.naturalHeight - sourceY,
|
|
1235
|
+
Math.round(crop.h * img.naturalHeight)
|
|
1236
|
+
)
|
|
1237
|
+
);
|
|
1203
1238
|
const swap = op === "rotate-left";
|
|
1204
1239
|
const canvas = document.createElement("canvas");
|
|
1205
|
-
canvas.width = swap ?
|
|
1206
|
-
canvas.height = swap ?
|
|
1240
|
+
canvas.width = swap ? sourceHeight : sourceWidth;
|
|
1241
|
+
canvas.height = swap ? sourceWidth : sourceHeight;
|
|
1207
1242
|
const ctx = canvas.getContext("2d");
|
|
1208
1243
|
if (!ctx) throw new Error("no 2d context");
|
|
1209
1244
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
1210
1245
|
if (swap) ctx.rotate(-Math.PI / 2);
|
|
1211
1246
|
else ctx.scale(-1, 1);
|
|
1212
|
-
ctx.drawImage(
|
|
1247
|
+
ctx.drawImage(
|
|
1248
|
+
img,
|
|
1249
|
+
sourceX,
|
|
1250
|
+
sourceY,
|
|
1251
|
+
sourceWidth,
|
|
1252
|
+
sourceHeight,
|
|
1253
|
+
-sourceWidth / 2,
|
|
1254
|
+
-sourceHeight / 2,
|
|
1255
|
+
sourceWidth,
|
|
1256
|
+
sourceHeight
|
|
1257
|
+
);
|
|
1213
1258
|
canvas.toBlob(
|
|
1214
1259
|
(blob) => {
|
|
1215
1260
|
if (!blob) {
|
|
1261
|
+
setTransforming(false);
|
|
1216
1262
|
setEditError(
|
|
1217
1263
|
"This image is too large to process on this device."
|
|
1218
1264
|
);
|
|
@@ -1221,26 +1267,32 @@ function ImageEditSheet({
|
|
|
1221
1267
|
revokeOwned();
|
|
1222
1268
|
const url = URL.createObjectURL(blob);
|
|
1223
1269
|
ownedUrlsRef.current.push(url);
|
|
1270
|
+
setImageReady(false);
|
|
1224
1271
|
setWorkingSrc(url);
|
|
1225
1272
|
setCrop(FULL_CROP);
|
|
1226
1273
|
setAspect("free");
|
|
1274
|
+
setTransforming(false);
|
|
1227
1275
|
setEditError(null);
|
|
1228
1276
|
},
|
|
1229
1277
|
"image/jpeg",
|
|
1230
1278
|
0.95
|
|
1231
1279
|
);
|
|
1232
1280
|
} catch {
|
|
1281
|
+
setTransforming(false);
|
|
1233
1282
|
setEditError("This image could not be edited on this device.");
|
|
1234
1283
|
}
|
|
1235
1284
|
},
|
|
1236
|
-
[saving, revokeOwned]
|
|
1285
|
+
[crop, imageReady, saving, transforming, revokeOwned]
|
|
1237
1286
|
);
|
|
1238
1287
|
const applyAspect = useCallback3((preset) => {
|
|
1288
|
+
const img = imgRef.current;
|
|
1289
|
+
if (!img || img.naturalWidth === 0) {
|
|
1290
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1239
1293
|
setAspect(preset);
|
|
1240
1294
|
const ratio = ASPECTS.find((a) => a.id === preset)?.ratio ?? null;
|
|
1241
1295
|
if (ratio === null) return;
|
|
1242
|
-
const img = imgRef.current;
|
|
1243
|
-
if (!img || img.naturalWidth === 0) return;
|
|
1244
1296
|
const imageRatio = img.naturalWidth / img.naturalHeight;
|
|
1245
1297
|
let w = 1;
|
|
1246
1298
|
let h = 1;
|
|
@@ -1305,7 +1357,10 @@ function ImageEditSheet({
|
|
|
1305
1357
|
}, []);
|
|
1306
1358
|
const save = useCallback3(() => {
|
|
1307
1359
|
const img = imgRef.current;
|
|
1308
|
-
if (!img || img.naturalWidth === 0)
|
|
1360
|
+
if (!img || !imageReady || img.naturalWidth === 0) {
|
|
1361
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1309
1364
|
setSaving(true);
|
|
1310
1365
|
setEditError(null);
|
|
1311
1366
|
try {
|
|
@@ -1335,7 +1390,7 @@ function ImageEditSheet({
|
|
|
1335
1390
|
setSaving(false);
|
|
1336
1391
|
setEditError("Saving failed on this device \u2014 try again.");
|
|
1337
1392
|
}
|
|
1338
|
-
}, [crop, onSave, onClose]);
|
|
1393
|
+
}, [crop, imageReady, onSave, onClose]);
|
|
1339
1394
|
if (!open || !src) return null;
|
|
1340
1395
|
return /* @__PURE__ */ jsxs9("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
1341
1396
|
/* @__PURE__ */ jsxs9(
|
|
@@ -1363,12 +1418,13 @@ function ImageEditSheet({
|
|
|
1363
1418
|
{
|
|
1364
1419
|
type: "button",
|
|
1365
1420
|
onClick: save,
|
|
1366
|
-
disabled: saving,
|
|
1421
|
+
disabled: saving || transforming || !imageReady,
|
|
1367
1422
|
"aria-label": "Save edited image",
|
|
1423
|
+
"aria-busy": saving || transforming || !imageReady,
|
|
1368
1424
|
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-semibold text-[#FFCC00] disabled:opacity-50",
|
|
1369
1425
|
children: [
|
|
1370
1426
|
/* @__PURE__ */ jsx11(CheckIcon, { className: "h-5 w-5" }),
|
|
1371
|
-
"Save"
|
|
1427
|
+
saving ? "Saving\u2026" : transforming || !imageReady ? "Loading\u2026" : "Save"
|
|
1372
1428
|
]
|
|
1373
1429
|
}
|
|
1374
1430
|
)
|
|
@@ -1391,6 +1447,12 @@ function ImageEditSheet({
|
|
|
1391
1447
|
src: workingSrc ?? src,
|
|
1392
1448
|
alt: "Image being edited",
|
|
1393
1449
|
draggable: false,
|
|
1450
|
+
onLoad: () => setImageReady(true),
|
|
1451
|
+
onError: () => {
|
|
1452
|
+
setImageReady(false);
|
|
1453
|
+
setTransforming(false);
|
|
1454
|
+
setEditError("This image could not be loaded on this device.");
|
|
1455
|
+
},
|
|
1394
1456
|
className: "max-h-[62dvh] max-w-full select-none object-contain"
|
|
1395
1457
|
}
|
|
1396
1458
|
),
|
|
@@ -1434,6 +1496,7 @@ function ImageEditSheet({
|
|
|
1434
1496
|
{
|
|
1435
1497
|
type: "button",
|
|
1436
1498
|
onClick: () => applyAspect(a.id),
|
|
1499
|
+
disabled: saving || transforming || !imageReady,
|
|
1437
1500
|
"aria-pressed": aspect === a.id,
|
|
1438
1501
|
className: cn(
|
|
1439
1502
|
"touch-manipulation rounded-full px-3.5 py-1.5 text-[12px] font-semibold uppercase tracking-wide transition-colors",
|
|
@@ -1449,6 +1512,7 @@ function ImageEditSheet({
|
|
|
1449
1512
|
{
|
|
1450
1513
|
type: "button",
|
|
1451
1514
|
onClick: () => bake("rotate-left"),
|
|
1515
|
+
disabled: saving || transforming || !imageReady,
|
|
1452
1516
|
"aria-label": "Rotate left",
|
|
1453
1517
|
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
1454
1518
|
children: /* @__PURE__ */ jsx11(RotateCcwIcon, { className: "h-5 w-5" })
|
|
@@ -1459,6 +1523,7 @@ function ImageEditSheet({
|
|
|
1459
1523
|
{
|
|
1460
1524
|
type: "button",
|
|
1461
1525
|
onClick: () => bake("flip"),
|
|
1526
|
+
disabled: saving || transforming || !imageReady,
|
|
1462
1527
|
"aria-label": "Flip horizontally",
|
|
1463
1528
|
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
1464
1529
|
children: /* @__PURE__ */ jsx11(FlipHorizontal2Icon, { className: "h-5 w-5" })
|
|
@@ -1494,6 +1559,10 @@ function CameraCapture({
|
|
|
1494
1559
|
const [optionsOpen, setOptionsOpen] = useState5(false);
|
|
1495
1560
|
const [viewerKey, setViewerKey] = useState5(null);
|
|
1496
1561
|
const [editTarget, setEditTarget] = useState5(null);
|
|
1562
|
+
const [editPreparing, setEditPreparing] = useState5(false);
|
|
1563
|
+
const [editPreparationError, setEditPreparationError] = useState5(
|
|
1564
|
+
null
|
|
1565
|
+
);
|
|
1497
1566
|
const [gridOn, setGridOn] = useState5(false);
|
|
1498
1567
|
const [timerSetting, setTimerSetting] = useState5(0);
|
|
1499
1568
|
const [aspect, setAspect] = useState5("full");
|
|
@@ -1518,6 +1587,12 @@ function CameraCapture({
|
|
|
1518
1587
|
useEffect5(() => {
|
|
1519
1588
|
onReviewOpenChange?.(reviewOpen);
|
|
1520
1589
|
}, [reviewOpen, onReviewOpenChange]);
|
|
1590
|
+
useEffect5(
|
|
1591
|
+
() => () => {
|
|
1592
|
+
if (editTarget?.revoke) URL.revokeObjectURL(editTarget.url);
|
|
1593
|
+
},
|
|
1594
|
+
[editTarget]
|
|
1595
|
+
);
|
|
1521
1596
|
const clearCountdown = useCallback4(() => {
|
|
1522
1597
|
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
1523
1598
|
countdownRef.current = null;
|
|
@@ -1831,8 +1906,16 @@ function CameraCapture({
|
|
|
1831
1906
|
invalidateMedia(item.key);
|
|
1832
1907
|
} : void 0,
|
|
1833
1908
|
onEdit: media.onReplacePhoto ? (item) => {
|
|
1834
|
-
|
|
1835
|
-
|
|
1909
|
+
if (editPreparing) return;
|
|
1910
|
+
setEditPreparing(true);
|
|
1911
|
+
setEditPreparationError(null);
|
|
1912
|
+
void prepareMediaForEdit(item).then(
|
|
1913
|
+
(prepared) => setEditTarget({ key: item.key, ...prepared })
|
|
1914
|
+
).catch(
|
|
1915
|
+
() => setEditPreparationError(
|
|
1916
|
+
"This photo could not be prepared for editing. Try again."
|
|
1917
|
+
)
|
|
1918
|
+
).finally(() => setEditPreparing(false));
|
|
1836
1919
|
} : void 0
|
|
1837
1920
|
}
|
|
1838
1921
|
),
|
|
@@ -1853,6 +1936,14 @@ function CameraCapture({
|
|
|
1853
1936
|
}
|
|
1854
1937
|
}
|
|
1855
1938
|
),
|
|
1939
|
+
editPreparationError && /* @__PURE__ */ jsx12(
|
|
1940
|
+
"p",
|
|
1941
|
+
{
|
|
1942
|
+
role: "alert",
|
|
1943
|
+
className: "fixed inset-x-4 bottom-24 z-[70] rounded-xl bg-black/85 px-4 py-3 text-center text-sm text-[#FF6961]",
|
|
1944
|
+
children: editPreparationError
|
|
1945
|
+
}
|
|
1946
|
+
),
|
|
1856
1947
|
slots.overlays
|
|
1857
1948
|
] });
|
|
1858
1949
|
}
|
|
@@ -2151,7 +2242,9 @@ function CameraCaptureV3({
|
|
|
2151
2242
|
slots = {}
|
|
2152
2243
|
}) {
|
|
2153
2244
|
const [viewerKey, setViewerKey] = useState8(null);
|
|
2154
|
-
const [editTarget, setEditTarget] = useState8(
|
|
2245
|
+
const [editTarget, setEditTarget] = useState8(null);
|
|
2246
|
+
const [editPreparing, setEditPreparing] = useState8(false);
|
|
2247
|
+
const [editPreparationError, setEditPreparationError] = useState8(
|
|
2155
2248
|
null
|
|
2156
2249
|
);
|
|
2157
2250
|
const [gridOn, setGridOn] = useState8(false);
|
|
@@ -2165,6 +2258,12 @@ function CameraCaptureV3({
|
|
|
2165
2258
|
useEffect7(() => {
|
|
2166
2259
|
onReviewOpenChange?.(reviewOpen);
|
|
2167
2260
|
}, [reviewOpen, onReviewOpenChange]);
|
|
2261
|
+
useEffect7(
|
|
2262
|
+
() => () => {
|
|
2263
|
+
if (editTarget?.revoke) URL.revokeObjectURL(editTarget.url);
|
|
2264
|
+
},
|
|
2265
|
+
[editTarget]
|
|
2266
|
+
);
|
|
2168
2267
|
const clearCountdown = useCallback6(() => {
|
|
2169
2268
|
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
2170
2269
|
countdownRef.current = null;
|
|
@@ -2383,8 +2482,16 @@ function CameraCaptureV3({
|
|
|
2383
2482
|
invalidateMedia(item.key);
|
|
2384
2483
|
} : void 0,
|
|
2385
2484
|
onEdit: media.onReplacePhoto ? (item) => {
|
|
2386
|
-
|
|
2387
|
-
|
|
2485
|
+
if (editPreparing) return;
|
|
2486
|
+
setEditPreparing(true);
|
|
2487
|
+
setEditPreparationError(null);
|
|
2488
|
+
void prepareMediaForEdit(item).then(
|
|
2489
|
+
(prepared) => setEditTarget({ key: item.key, ...prepared })
|
|
2490
|
+
).catch(
|
|
2491
|
+
() => setEditPreparationError(
|
|
2492
|
+
"This photo could not be prepared for editing. Try again."
|
|
2493
|
+
)
|
|
2494
|
+
).finally(() => setEditPreparing(false));
|
|
2388
2495
|
} : void 0
|
|
2389
2496
|
}
|
|
2390
2497
|
),
|
|
@@ -2405,6 +2512,14 @@ function CameraCaptureV3({
|
|
|
2405
2512
|
}
|
|
2406
2513
|
}
|
|
2407
2514
|
),
|
|
2515
|
+
editPreparationError && /* @__PURE__ */ jsx15(
|
|
2516
|
+
"p",
|
|
2517
|
+
{
|
|
2518
|
+
role: "alert",
|
|
2519
|
+
className: "fixed inset-x-4 bottom-24 z-[70] rounded-xl bg-black/85 px-4 py-3 text-center text-sm text-[#FF6961]",
|
|
2520
|
+
children: editPreparationError
|
|
2521
|
+
}
|
|
2522
|
+
),
|
|
2408
2523
|
slots.overlays
|
|
2409
2524
|
] });
|
|
2410
2525
|
}
|