@ai-matrx/capture 0.5.2 → 0.5.3
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 +5 -0
- package/dist/react.cjs +66 -11
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +66 -11
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/capture
|
|
2
2
|
|
|
3
|
+
## 0.5.3 — 2026-09-01
|
|
4
|
+
|
|
5
|
+
- `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.
|
|
6
|
+
- Rotate and flip now bake the active crop selection into their output instead of discarding it. Exact fail-first coverage proves Square crop → Rotate → newly loaded bitmap → Save emits one edited blob.
|
|
7
|
+
|
|
3
8
|
## 0.5.2 — 2026-09-01
|
|
4
9
|
|
|
5
10
|
- `CameraCapture` no longer wraps the injected recents thumbnail in its Open-library button. Persisted-media error fallbacks may own remedy buttons (for example, Copy URL); those controls now remain valid siblings, act without also opening the library, and the non-interactive thumbnail area still opens the library.
|
package/dist/react.cjs
CHANGED
|
@@ -1230,6 +1230,8 @@ function ImageEditSheet({
|
|
|
1230
1230
|
const [workingSrc, setWorkingSrc] = (0, import_react4.useState)(null);
|
|
1231
1231
|
const [aspect, setAspect] = (0, import_react4.useState)("free");
|
|
1232
1232
|
const [crop, setCrop] = (0, import_react4.useState)(FULL_CROP);
|
|
1233
|
+
const [imageReady, setImageReady] = (0, import_react4.useState)(false);
|
|
1234
|
+
const [transforming, setTransforming] = (0, import_react4.useState)(false);
|
|
1233
1235
|
const [saving, setSaving] = (0, import_react4.useState)(false);
|
|
1234
1236
|
const [editError, setEditError] = (0, import_react4.useState)(null);
|
|
1235
1237
|
const stageRef = (0, import_react4.useRef)(null);
|
|
@@ -1245,6 +1247,8 @@ function ImageEditSheet({
|
|
|
1245
1247
|
setWorkingSrc(null);
|
|
1246
1248
|
setAspect("free");
|
|
1247
1249
|
setCrop(FULL_CROP);
|
|
1250
|
+
setImageReady(false);
|
|
1251
|
+
setTransforming(false);
|
|
1248
1252
|
setSaving(false);
|
|
1249
1253
|
setEditError(null);
|
|
1250
1254
|
}
|
|
@@ -1253,21 +1257,53 @@ function ImageEditSheet({
|
|
|
1253
1257
|
const bake = (0, import_react4.useCallback)(
|
|
1254
1258
|
(op) => {
|
|
1255
1259
|
const img = imgRef.current;
|
|
1256
|
-
if (!img || img.naturalWidth === 0 || saving)
|
|
1260
|
+
if (!img || !imageReady || img.naturalWidth === 0 || saving || transforming) {
|
|
1261
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
setTransforming(true);
|
|
1265
|
+
setEditError(null);
|
|
1257
1266
|
try {
|
|
1267
|
+
const sourceX = Math.round(crop.x * img.naturalWidth);
|
|
1268
|
+
const sourceY = Math.round(crop.y * img.naturalHeight);
|
|
1269
|
+
const sourceWidth = Math.max(
|
|
1270
|
+
1,
|
|
1271
|
+
Math.min(
|
|
1272
|
+
img.naturalWidth - sourceX,
|
|
1273
|
+
Math.round(crop.w * img.naturalWidth)
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
const sourceHeight = Math.max(
|
|
1277
|
+
1,
|
|
1278
|
+
Math.min(
|
|
1279
|
+
img.naturalHeight - sourceY,
|
|
1280
|
+
Math.round(crop.h * img.naturalHeight)
|
|
1281
|
+
)
|
|
1282
|
+
);
|
|
1258
1283
|
const swap = op === "rotate-left";
|
|
1259
1284
|
const canvas = document.createElement("canvas");
|
|
1260
|
-
canvas.width = swap ?
|
|
1261
|
-
canvas.height = swap ?
|
|
1285
|
+
canvas.width = swap ? sourceHeight : sourceWidth;
|
|
1286
|
+
canvas.height = swap ? sourceWidth : sourceHeight;
|
|
1262
1287
|
const ctx = canvas.getContext("2d");
|
|
1263
1288
|
if (!ctx) throw new Error("no 2d context");
|
|
1264
1289
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
1265
1290
|
if (swap) ctx.rotate(-Math.PI / 2);
|
|
1266
1291
|
else ctx.scale(-1, 1);
|
|
1267
|
-
ctx.drawImage(
|
|
1292
|
+
ctx.drawImage(
|
|
1293
|
+
img,
|
|
1294
|
+
sourceX,
|
|
1295
|
+
sourceY,
|
|
1296
|
+
sourceWidth,
|
|
1297
|
+
sourceHeight,
|
|
1298
|
+
-sourceWidth / 2,
|
|
1299
|
+
-sourceHeight / 2,
|
|
1300
|
+
sourceWidth,
|
|
1301
|
+
sourceHeight
|
|
1302
|
+
);
|
|
1268
1303
|
canvas.toBlob(
|
|
1269
1304
|
(blob) => {
|
|
1270
1305
|
if (!blob) {
|
|
1306
|
+
setTransforming(false);
|
|
1271
1307
|
setEditError(
|
|
1272
1308
|
"This image is too large to process on this device."
|
|
1273
1309
|
);
|
|
@@ -1276,26 +1312,32 @@ function ImageEditSheet({
|
|
|
1276
1312
|
revokeOwned();
|
|
1277
1313
|
const url = URL.createObjectURL(blob);
|
|
1278
1314
|
ownedUrlsRef.current.push(url);
|
|
1315
|
+
setImageReady(false);
|
|
1279
1316
|
setWorkingSrc(url);
|
|
1280
1317
|
setCrop(FULL_CROP);
|
|
1281
1318
|
setAspect("free");
|
|
1319
|
+
setTransforming(false);
|
|
1282
1320
|
setEditError(null);
|
|
1283
1321
|
},
|
|
1284
1322
|
"image/jpeg",
|
|
1285
1323
|
0.95
|
|
1286
1324
|
);
|
|
1287
1325
|
} catch {
|
|
1326
|
+
setTransforming(false);
|
|
1288
1327
|
setEditError("This image could not be edited on this device.");
|
|
1289
1328
|
}
|
|
1290
1329
|
},
|
|
1291
|
-
[saving, revokeOwned]
|
|
1330
|
+
[crop, imageReady, saving, transforming, revokeOwned]
|
|
1292
1331
|
);
|
|
1293
1332
|
const applyAspect = (0, import_react4.useCallback)((preset) => {
|
|
1333
|
+
const img = imgRef.current;
|
|
1334
|
+
if (!img || img.naturalWidth === 0) {
|
|
1335
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1294
1338
|
setAspect(preset);
|
|
1295
1339
|
const ratio = ASPECTS.find((a) => a.id === preset)?.ratio ?? null;
|
|
1296
1340
|
if (ratio === null) return;
|
|
1297
|
-
const img = imgRef.current;
|
|
1298
|
-
if (!img || img.naturalWidth === 0) return;
|
|
1299
1341
|
const imageRatio = img.naturalWidth / img.naturalHeight;
|
|
1300
1342
|
let w = 1;
|
|
1301
1343
|
let h = 1;
|
|
@@ -1360,7 +1402,10 @@ function ImageEditSheet({
|
|
|
1360
1402
|
}, []);
|
|
1361
1403
|
const save = (0, import_react4.useCallback)(() => {
|
|
1362
1404
|
const img = imgRef.current;
|
|
1363
|
-
if (!img || img.naturalWidth === 0)
|
|
1405
|
+
if (!img || !imageReady || img.naturalWidth === 0) {
|
|
1406
|
+
setEditError("The image is still loading \u2014 wait a moment and try again.");
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1364
1409
|
setSaving(true);
|
|
1365
1410
|
setEditError(null);
|
|
1366
1411
|
try {
|
|
@@ -1390,7 +1435,7 @@ function ImageEditSheet({
|
|
|
1390
1435
|
setSaving(false);
|
|
1391
1436
|
setEditError("Saving failed on this device \u2014 try again.");
|
|
1392
1437
|
}
|
|
1393
|
-
}, [crop, onSave, onClose]);
|
|
1438
|
+
}, [crop, imageReady, onSave, onClose]);
|
|
1394
1439
|
if (!open || !src) return null;
|
|
1395
1440
|
return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
1396
1441
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
|
|
@@ -1418,12 +1463,13 @@ function ImageEditSheet({
|
|
|
1418
1463
|
{
|
|
1419
1464
|
type: "button",
|
|
1420
1465
|
onClick: save,
|
|
1421
|
-
disabled: saving,
|
|
1466
|
+
disabled: saving || transforming || !imageReady,
|
|
1422
1467
|
"aria-label": "Save edited image",
|
|
1468
|
+
"aria-busy": saving || transforming || !imageReady,
|
|
1423
1469
|
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-semibold text-[#FFCC00] disabled:opacity-50",
|
|
1424
1470
|
children: [
|
|
1425
1471
|
/* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CheckIcon, { className: "h-5 w-5" }),
|
|
1426
|
-
"Save"
|
|
1472
|
+
saving ? "Saving\u2026" : transforming || !imageReady ? "Loading\u2026" : "Save"
|
|
1427
1473
|
]
|
|
1428
1474
|
}
|
|
1429
1475
|
)
|
|
@@ -1446,6 +1492,12 @@ function ImageEditSheet({
|
|
|
1446
1492
|
src: workingSrc ?? src,
|
|
1447
1493
|
alt: "Image being edited",
|
|
1448
1494
|
draggable: false,
|
|
1495
|
+
onLoad: () => setImageReady(true),
|
|
1496
|
+
onError: () => {
|
|
1497
|
+
setImageReady(false);
|
|
1498
|
+
setTransforming(false);
|
|
1499
|
+
setEditError("This image could not be loaded on this device.");
|
|
1500
|
+
},
|
|
1449
1501
|
className: "max-h-[62dvh] max-w-full select-none object-contain"
|
|
1450
1502
|
}
|
|
1451
1503
|
),
|
|
@@ -1489,6 +1541,7 @@ function ImageEditSheet({
|
|
|
1489
1541
|
{
|
|
1490
1542
|
type: "button",
|
|
1491
1543
|
onClick: () => applyAspect(a.id),
|
|
1544
|
+
disabled: saving || transforming || !imageReady,
|
|
1492
1545
|
"aria-pressed": aspect === a.id,
|
|
1493
1546
|
className: cn(
|
|
1494
1547
|
"touch-manipulation rounded-full px-3.5 py-1.5 text-[12px] font-semibold uppercase tracking-wide transition-colors",
|
|
@@ -1504,6 +1557,7 @@ function ImageEditSheet({
|
|
|
1504
1557
|
{
|
|
1505
1558
|
type: "button",
|
|
1506
1559
|
onClick: () => bake("rotate-left"),
|
|
1560
|
+
disabled: saving || transforming || !imageReady,
|
|
1507
1561
|
"aria-label": "Rotate left",
|
|
1508
1562
|
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
1509
1563
|
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(RotateCcwIcon, { className: "h-5 w-5" })
|
|
@@ -1514,6 +1568,7 @@ function ImageEditSheet({
|
|
|
1514
1568
|
{
|
|
1515
1569
|
type: "button",
|
|
1516
1570
|
onClick: () => bake("flip"),
|
|
1571
|
+
disabled: saving || transforming || !imageReady,
|
|
1517
1572
|
"aria-label": "Flip horizontally",
|
|
1518
1573
|
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
1519
1574
|
children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(FlipHorizontal2Icon, { className: "h-5 w-5" })
|