@ibanzajoe/uploader 1.4.0 → 1.6.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.
- package/README.md +25 -3
- package/dist/index.cjs +93 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -5
- package/dist/index.d.ts +38 -5
- package/dist/index.js +91 -5
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +3 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +35 -6
- package/dist/server.d.ts +35 -6
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/dist/styles.css +42 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,12 +201,23 @@ const url = getSignedDeliveryUrl({
|
|
|
201
201
|
handle: file.handle,
|
|
202
202
|
secret: process.env.UPLOADER_API_SECRET, // the API key's secret
|
|
203
203
|
baseUrl: 'https://edge.your-domain.com', // origin of FileResult.url
|
|
204
|
-
expiresIn:
|
|
204
|
+
expiresIn: 3600, // minimum seconds valid (default 300)
|
|
205
|
+
// stableWindow: 3600, // URL is byte-identical within this window
|
|
205
206
|
// ops: [resize({ w: 400 }), output({ format: 'webp' })], // optional transform
|
|
206
207
|
})
|
|
207
208
|
// → https://edge.…/file/<handle>?policy=…&signature=… (hand to <img src>)
|
|
208
209
|
```
|
|
209
210
|
|
|
211
|
+
Calling this per view is fine and expected. The expiry is **quantized** to
|
|
212
|
+
`stableWindow` (defaults to `expiresIn`), so repeated calls inside one window
|
|
213
|
+
return the exact same URL string — which is what lets the browser cache the
|
|
214
|
+
image instead of re-fetching it on every render. Without that, each render
|
|
215
|
+
produces a different URL, and to a browser a different URL is a different
|
|
216
|
+
image.
|
|
217
|
+
|
|
218
|
+
Longer windows cache better; shorter windows revoke sooner. Pass
|
|
219
|
+
`stableWindow: 0` to opt out and get an exact per-call expiry.
|
|
220
|
+
|
|
210
221
|
Prefer to build it yourself? `withSignedPolicy(url, { policy, signature })` from
|
|
211
222
|
`@ibanzajoe/uploader/core` appends a `policy`/`signature` pair you produced.
|
|
212
223
|
|
|
@@ -270,6 +281,11 @@ flows through the exact same pipeline as a dropped or browsed file — it lands
|
|
|
270
281
|
the queue with a preview, can be cropped/rotated in the in-picker editor, and is
|
|
271
282
|
then uploaded normally.
|
|
272
283
|
|
|
284
|
+
**Front or rear camera.** On phones and tablets the picker opens the **rear**
|
|
285
|
+
camera; on desktops it opens the front one. Where the device exposes more than
|
|
286
|
+
one camera, a flip control in the corner of the viewfinder switches between them,
|
|
287
|
+
so the default is only the starting side. Set `cameraFacingMode` to pin it.
|
|
288
|
+
|
|
273
289
|
```tsx
|
|
274
290
|
<PickerOverlay
|
|
275
291
|
apikey="pk_…"
|
|
@@ -279,7 +295,8 @@ then uploaded normally.
|
|
|
279
295
|
accept: ['image/*'],
|
|
280
296
|
// Sources offered to the user. Omit to offer everything supported.
|
|
281
297
|
fromSources: ['local_file_system', 'camera'],
|
|
282
|
-
//
|
|
298
|
+
// Omit for the per-device default (rear on mobile, front on desktop).
|
|
299
|
+
// 'user' = front/selfie (mirrored) · 'environment' = rear camera
|
|
283
300
|
cameraFacingMode: 'environment',
|
|
284
301
|
}}
|
|
285
302
|
/>
|
|
@@ -290,8 +307,13 @@ Notes:
|
|
|
290
307
|
permission. Permission/no-camera errors are surfaced inline with a retry.
|
|
291
308
|
- Pass `fromSources: ['local_file_system']` to hide the camera even where it is
|
|
292
309
|
supported; omit `fromSources` to offer it by default.
|
|
310
|
+
- The flip control appears only when `enumerateDevices()` reports two or more
|
|
311
|
+
video inputs (checked after permission is granted, when the list is accurate).
|
|
312
|
+
- The front camera is mirrored in both the preview and the captured still; the
|
|
313
|
+
rear camera is not.
|
|
293
314
|
- The `<CameraCapture>` component and the `isCameraSupported()` /
|
|
294
|
-
`shouldOfferCamera()`
|
|
315
|
+
`shouldOfferCamera()` / `isMobileDevice()` / `defaultCameraFacingMode()`
|
|
316
|
+
helpers are exported for fully custom pickers.
|
|
295
317
|
|
|
296
318
|
## Signed policies
|
|
297
319
|
|
package/dist/index.cjs
CHANGED
|
@@ -26,10 +26,12 @@ __export(src_exports, {
|
|
|
26
26
|
PickerOverlay: () => PickerOverlay,
|
|
27
27
|
UploaderClient: () => UploaderClient,
|
|
28
28
|
crop: () => crop,
|
|
29
|
+
defaultCameraFacingMode: () => defaultCameraFacingMode,
|
|
29
30
|
editImage: () => editImage,
|
|
30
31
|
flip: () => flip,
|
|
31
32
|
flop: () => flop,
|
|
32
33
|
isCameraSupported: () => isCameraSupported,
|
|
34
|
+
isMobileDevice: () => isMobileDevice,
|
|
33
35
|
output: () => output,
|
|
34
36
|
quality: () => quality,
|
|
35
37
|
resize: () => resize,
|
|
@@ -1275,6 +1277,19 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
|
1275
1277
|
function isCameraSupported() {
|
|
1276
1278
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined";
|
|
1277
1279
|
}
|
|
1280
|
+
function isMobileDevice() {
|
|
1281
|
+
if (typeof navigator === "undefined") return false;
|
|
1282
|
+
const uaData = navigator.userAgentData;
|
|
1283
|
+
if (typeof uaData?.mobile === "boolean") return uaData.mobile;
|
|
1284
|
+
const ua = navigator.userAgent ?? "";
|
|
1285
|
+
if (/Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|Mobile/i.test(ua)) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
return /Macintosh/.test(ua) && (navigator.maxTouchPoints ?? 0) > 1;
|
|
1289
|
+
}
|
|
1290
|
+
function defaultCameraFacingMode() {
|
|
1291
|
+
return isMobileDevice() ? "environment" : "user";
|
|
1292
|
+
}
|
|
1278
1293
|
var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
|
|
1279
1294
|
function acceptsImages(accept) {
|
|
1280
1295
|
if (!accept || accept.length === 0) return true;
|
|
@@ -1315,7 +1330,7 @@ function describeCameraError(err) {
|
|
|
1315
1330
|
function CameraCapture({
|
|
1316
1331
|
onCapture,
|
|
1317
1332
|
onCancel,
|
|
1318
|
-
facingMode
|
|
1333
|
+
facingMode,
|
|
1319
1334
|
outputType = "image/jpeg",
|
|
1320
1335
|
quality: quality2 = 0.92,
|
|
1321
1336
|
fileNamePrefix = "camera-photo",
|
|
@@ -1328,12 +1343,32 @@ function CameraCapture({
|
|
|
1328
1343
|
const [status, setStatus] = (0, import_react4.useState)("initializing");
|
|
1329
1344
|
const [error, setError] = (0, import_react4.useState)(null);
|
|
1330
1345
|
const [stillUrl, setStillUrl] = (0, import_react4.useState)("");
|
|
1331
|
-
const
|
|
1346
|
+
const [facing, setFacing] = (0, import_react4.useState)(
|
|
1347
|
+
() => facingMode ?? defaultCameraFacingMode()
|
|
1348
|
+
);
|
|
1349
|
+
const [canFlip, setCanFlip] = (0, import_react4.useState)(false);
|
|
1350
|
+
const mirror = facing === "user";
|
|
1351
|
+
(0, import_react4.useEffect)(() => {
|
|
1352
|
+
if (facingMode) setFacing(facingMode);
|
|
1353
|
+
}, [facingMode]);
|
|
1332
1354
|
const stopStream = (0, import_react4.useCallback)(() => {
|
|
1333
1355
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1334
1356
|
streamRef.current = null;
|
|
1335
1357
|
if (videoRef.current) videoRef.current.srcObject = null;
|
|
1336
1358
|
}, []);
|
|
1359
|
+
const probeCameraCount = (0, import_react4.useCallback)(async () => {
|
|
1360
|
+
const media = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
|
|
1361
|
+
if (!media || typeof media.enumerateDevices !== "function") {
|
|
1362
|
+
setCanFlip(isMobileDevice());
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
try {
|
|
1366
|
+
const devices = await media.enumerateDevices();
|
|
1367
|
+
setCanFlip(devices.filter((d) => d.kind === "videoinput").length > 1);
|
|
1368
|
+
} catch {
|
|
1369
|
+
setCanFlip(isMobileDevice());
|
|
1370
|
+
}
|
|
1371
|
+
}, []);
|
|
1337
1372
|
const startStream = (0, import_react4.useCallback)(async () => {
|
|
1338
1373
|
setError(null);
|
|
1339
1374
|
setStatus("initializing");
|
|
@@ -1342,9 +1377,12 @@ function CameraCapture({
|
|
|
1342
1377
|
setStatus("error");
|
|
1343
1378
|
return;
|
|
1344
1379
|
}
|
|
1380
|
+
stopStream();
|
|
1345
1381
|
try {
|
|
1346
1382
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1347
|
-
|
|
1383
|
+
// Non-exact: a device with no camera on the requested side falls back to
|
|
1384
|
+
// the one it has rather than throwing OverconstrainedError.
|
|
1385
|
+
video: { facingMode: facing },
|
|
1348
1386
|
audio: false
|
|
1349
1387
|
});
|
|
1350
1388
|
if (videoRef.current == null) {
|
|
@@ -1354,11 +1392,12 @@ function CameraCapture({
|
|
|
1354
1392
|
streamRef.current = stream;
|
|
1355
1393
|
videoRef.current.srcObject = stream;
|
|
1356
1394
|
setStatus("live");
|
|
1395
|
+
void probeCameraCount();
|
|
1357
1396
|
} catch (err) {
|
|
1358
1397
|
setError(describeCameraError(err));
|
|
1359
1398
|
setStatus("error");
|
|
1360
1399
|
}
|
|
1361
|
-
}, [
|
|
1400
|
+
}, [facing, probeCameraCount, stopStream]);
|
|
1362
1401
|
(0, import_react4.useEffect)(() => {
|
|
1363
1402
|
void startStream();
|
|
1364
1403
|
return () => {
|
|
@@ -1406,6 +1445,9 @@ function CameraCapture({
|
|
|
1406
1445
|
quality2
|
|
1407
1446
|
);
|
|
1408
1447
|
}, [mirror, outputType, quality2, stopStream]);
|
|
1448
|
+
const flipCamera = (0, import_react4.useCallback)(() => {
|
|
1449
|
+
setFacing((current) => current === "user" ? "environment" : "user");
|
|
1450
|
+
}, []);
|
|
1409
1451
|
const retake = (0, import_react4.useCallback)(() => {
|
|
1410
1452
|
blobRef.current = null;
|
|
1411
1453
|
setStillUrl("");
|
|
@@ -1424,6 +1466,7 @@ function CameraCapture({
|
|
|
1424
1466
|
className: ["uploader-camera", className ?? ""].filter(Boolean).join(" "),
|
|
1425
1467
|
style,
|
|
1426
1468
|
"data-testid": "camera-capture",
|
|
1469
|
+
"data-facing": facing,
|
|
1427
1470
|
children: [
|
|
1428
1471
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-camera-stage", children: [
|
|
1429
1472
|
status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-error", role: "alert", children: error }) : status === "captured" ? (
|
|
@@ -1440,7 +1483,50 @@ function CameraCapture({
|
|
|
1440
1483
|
"data-mirror": mirror ? "true" : void 0
|
|
1441
1484
|
}
|
|
1442
1485
|
),
|
|
1443
|
-
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" })
|
|
1486
|
+
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" }),
|
|
1487
|
+
canFlip && (status === "live" || status === "initializing") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1488
|
+
"button",
|
|
1489
|
+
{
|
|
1490
|
+
type: "button",
|
|
1491
|
+
className: "uploader-camera-flip",
|
|
1492
|
+
"aria-label": facing === "user" ? "Switch to rear camera" : "Switch to front camera",
|
|
1493
|
+
disabled: status !== "live",
|
|
1494
|
+
onClick: flipCamera,
|
|
1495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true", focusable: "false", children: [
|
|
1496
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1497
|
+
"path",
|
|
1498
|
+
{
|
|
1499
|
+
d: "M4 8.5A2.5 2.5 0 0 1 6.5 6h1.2l1-1.7a1 1 0 0 1 .87-.5h4.86a1 1 0 0 1 .86.5l1 1.7h1.21A2.5 2.5 0 0 1 20 8.5v8A2.5 2.5 0 0 1 17.5 19h-11A2.5 2.5 0 0 1 4 16.5v-8Z",
|
|
1500
|
+
fill: "none",
|
|
1501
|
+
stroke: "currentColor",
|
|
1502
|
+
strokeWidth: "1.6",
|
|
1503
|
+
strokeLinejoin: "round"
|
|
1504
|
+
}
|
|
1505
|
+
),
|
|
1506
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1507
|
+
"path",
|
|
1508
|
+
{
|
|
1509
|
+
d: "M9.4 12.6a2.6 2.6 0 0 0 4.45 1.6M14.6 11.4a2.6 2.6 0 0 0-4.45-1.6",
|
|
1510
|
+
fill: "none",
|
|
1511
|
+
stroke: "currentColor",
|
|
1512
|
+
strokeWidth: "1.6",
|
|
1513
|
+
strokeLinecap: "round"
|
|
1514
|
+
}
|
|
1515
|
+
),
|
|
1516
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1517
|
+
"path",
|
|
1518
|
+
{
|
|
1519
|
+
d: "m9.4 10.2-.1 2.1 2.1-.6M14.6 13.8l.1-2.1-2.1.6",
|
|
1520
|
+
fill: "none",
|
|
1521
|
+
stroke: "currentColor",
|
|
1522
|
+
strokeWidth: "1.6",
|
|
1523
|
+
strokeLinecap: "round",
|
|
1524
|
+
strokeLinejoin: "round"
|
|
1525
|
+
}
|
|
1526
|
+
)
|
|
1527
|
+
] })
|
|
1528
|
+
}
|
|
1529
|
+
)
|
|
1444
1530
|
] }),
|
|
1445
1531
|
status === "live" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "uploader-camera-hint", children: "Frame your shot, then tap the shutter to capture." }),
|
|
1446
1532
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-actions", children: status === "captured" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
@@ -2132,10 +2218,12 @@ function DropPane({
|
|
|
2132
2218
|
PickerOverlay,
|
|
2133
2219
|
UploaderClient,
|
|
2134
2220
|
crop,
|
|
2221
|
+
defaultCameraFacingMode,
|
|
2135
2222
|
editImage,
|
|
2136
2223
|
flip,
|
|
2137
2224
|
flop,
|
|
2138
2225
|
isCameraSupported,
|
|
2226
|
+
isMobileDevice,
|
|
2139
2227
|
output,
|
|
2140
2228
|
quality,
|
|
2141
2229
|
resize,
|