@ibanzajoe/uploader 1.5.0 → 1.7.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 +16 -5
- package/dist/{chunk-7LTEX76J.js → chunk-MAPCHOFK.js} +77 -7
- package/dist/chunk-MAPCHOFK.js.map +1 -0
- package/dist/{client-DW6DcS6o.d.cts → client-DxYeqjzt.d.cts} +122 -4
- package/dist/{client-DW6DcS6o.d.ts → client-DxYeqjzt.d.ts} +122 -4
- package/dist/core.cjs +76 -6
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/index.cjs +169 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -7
- package/dist/index.d.ts +40 -7
- package/dist/index.js +92 -6
- package/dist/index.js.map +1 -1
- package/dist/styles.css +42 -0
- package/package.json +1 -1
- package/dist/chunk-7LTEX76J.js.map +0 -1
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,
|
|
@@ -180,6 +182,7 @@ function xhrPut(url, body, opts) {
|
|
|
180
182
|
});
|
|
181
183
|
}
|
|
182
184
|
var UploaderClient = class {
|
|
185
|
+
/** The publishable key (`pk_…`) — a project identifier, not a credential. */
|
|
183
186
|
apikey;
|
|
184
187
|
apiUrl;
|
|
185
188
|
security;
|
|
@@ -194,7 +197,7 @@ var UploaderClient = class {
|
|
|
194
197
|
#capsPromise = null;
|
|
195
198
|
constructor(options) {
|
|
196
199
|
this.apikey = options.apikey;
|
|
197
|
-
this.apiUrl = (options.apiUrl ?? "https://api.
|
|
200
|
+
this.apiUrl = (options.apiUrl ?? "https://api.postila.app").replace(/\/$/, "");
|
|
198
201
|
this.security = options.security;
|
|
199
202
|
this.#directUploadOption = options.directUpload;
|
|
200
203
|
this.#deliveryProtection = options.deliveryProtection;
|
|
@@ -306,15 +309,18 @@ var UploaderClient = class {
|
|
|
306
309
|
// ─── Auth headers ──────────────────────────────────────────────────────────
|
|
307
310
|
/**
|
|
308
311
|
* Build the auth headers shared by all requests.
|
|
309
|
-
* Attaches the API key and, when present, the signed policy pair.
|
|
312
|
+
* Attaches the API key and, when present, the signed policy pair. A per-call
|
|
313
|
+
* `security` overrides the client-level one (the file-management methods
|
|
314
|
+
* need policies with different `call` grants than uploads).
|
|
310
315
|
*/
|
|
311
|
-
#authHeaders() {
|
|
316
|
+
#authHeaders(security) {
|
|
312
317
|
const headers = {
|
|
313
318
|
"X-Uploader-Key": this.apikey
|
|
314
319
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
headers["X-Uploader-
|
|
320
|
+
const pair = security ?? this.security;
|
|
321
|
+
if (pair) {
|
|
322
|
+
headers["X-Uploader-Policy"] = pair.policy;
|
|
323
|
+
headers["X-Uploader-Signature"] = pair.signature;
|
|
318
324
|
}
|
|
319
325
|
return headers;
|
|
320
326
|
}
|
|
@@ -434,6 +440,72 @@ var UploaderClient = class {
|
|
|
434
440
|
}
|
|
435
441
|
return this.#uploadMultipart(file, plan.parts, opts);
|
|
436
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* List the account's files (paginated, 50 per page).
|
|
445
|
+
*
|
|
446
|
+
* Accounts with required signed policies must supply a policy whose `call`
|
|
447
|
+
* includes `'list'` — a handle-bound `'read'` policy from a delivery URL is
|
|
448
|
+
* deliberately not enough to enumerate the account.
|
|
449
|
+
*
|
|
450
|
+
* @throws {UploaderError} with code NETWORK_ERROR | SERVER_ERROR |
|
|
451
|
+
* CLIENT_ERROR | INVALID_RESPONSE
|
|
452
|
+
*/
|
|
453
|
+
async listFiles(query = {}, opts = {}) {
|
|
454
|
+
const params = new URLSearchParams();
|
|
455
|
+
for (const [k, v] of Object.entries(query)) {
|
|
456
|
+
if (v !== void 0) params.set(k, String(v));
|
|
457
|
+
}
|
|
458
|
+
const qs = params.toString();
|
|
459
|
+
const res = await fetchWithRetry(
|
|
460
|
+
`${this.apiUrl}/api/files${qs ? `?${qs}` : ""}`,
|
|
461
|
+
{ headers: this.#authHeaders(opts.security) },
|
|
462
|
+
opts.signal
|
|
463
|
+
);
|
|
464
|
+
const body = await res.json().catch(() => null);
|
|
465
|
+
if (!body || !Array.isArray(body.files)) {
|
|
466
|
+
throw new UploaderError("INVALID_RESPONSE", "Unexpected response shape from file list API");
|
|
467
|
+
}
|
|
468
|
+
return body;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Fetch one file's record by its public handle.
|
|
472
|
+
*
|
|
473
|
+
* Accounts with required signed policies must supply a policy whose `call`
|
|
474
|
+
* includes `'read'` (handle-bound policies must match this handle).
|
|
475
|
+
*
|
|
476
|
+
* @throws {UploaderError} — CLIENT_ERROR with statusCode 404 when the handle
|
|
477
|
+
* does not exist (or belongs to another account).
|
|
478
|
+
*/
|
|
479
|
+
async getFile(handle, opts = {}) {
|
|
480
|
+
const res = await fetchWithRetry(
|
|
481
|
+
`${this.apiUrl}/api/files/${encodeURIComponent(handle)}`,
|
|
482
|
+
{ headers: this.#authHeaders(opts.security) },
|
|
483
|
+
opts.signal
|
|
484
|
+
);
|
|
485
|
+
const body = await res.json().catch(() => null);
|
|
486
|
+
if (!body || typeof body.handle !== "string") {
|
|
487
|
+
throw new UploaderError("INVALID_RESPONSE", "Unexpected response shape from file API");
|
|
488
|
+
}
|
|
489
|
+
return body;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Delete a file by its public handle.
|
|
493
|
+
*
|
|
494
|
+
* The file stops being served immediately (soft-delete + edge cache purge);
|
|
495
|
+
* the stored bytes and derivatives are removed by a background cleanup.
|
|
496
|
+
* Accounts with required signed policies must supply a policy whose `call`
|
|
497
|
+
* includes `'remove'` (handle-bound policies must match this handle).
|
|
498
|
+
*
|
|
499
|
+
* @throws {UploaderError} — CLIENT_ERROR with statusCode 404 when the handle
|
|
500
|
+
* does not exist (or belongs to another account).
|
|
501
|
+
*/
|
|
502
|
+
async deleteFile(handle, opts = {}) {
|
|
503
|
+
await fetchWithRetry(
|
|
504
|
+
`${this.apiUrl}/api/files/${encodeURIComponent(handle)}`,
|
|
505
|
+
{ method: "DELETE", headers: this.#authHeaders(opts.security) },
|
|
506
|
+
opts.signal
|
|
507
|
+
);
|
|
508
|
+
}
|
|
437
509
|
/**
|
|
438
510
|
* Upload multiple files with concurrency limiting.
|
|
439
511
|
*
|
|
@@ -1275,6 +1347,19 @@ var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
|
1275
1347
|
function isCameraSupported() {
|
|
1276
1348
|
return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === "function" && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined";
|
|
1277
1349
|
}
|
|
1350
|
+
function isMobileDevice() {
|
|
1351
|
+
if (typeof navigator === "undefined") return false;
|
|
1352
|
+
const uaData = navigator.userAgentData;
|
|
1353
|
+
if (typeof uaData?.mobile === "boolean") return uaData.mobile;
|
|
1354
|
+
const ua = navigator.userAgent ?? "";
|
|
1355
|
+
if (/Android|iPhone|iPad|iPod|Windows Phone|webOS|BlackBerry|Opera Mini|Mobile/i.test(ua)) {
|
|
1356
|
+
return true;
|
|
1357
|
+
}
|
|
1358
|
+
return /Macintosh/.test(ua) && (navigator.maxTouchPoints ?? 0) > 1;
|
|
1359
|
+
}
|
|
1360
|
+
function defaultCameraFacingMode() {
|
|
1361
|
+
return isMobileDevice() ? "environment" : "user";
|
|
1362
|
+
}
|
|
1278
1363
|
var IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic|heif)$/i;
|
|
1279
1364
|
function acceptsImages(accept) {
|
|
1280
1365
|
if (!accept || accept.length === 0) return true;
|
|
@@ -1315,7 +1400,7 @@ function describeCameraError(err) {
|
|
|
1315
1400
|
function CameraCapture({
|
|
1316
1401
|
onCapture,
|
|
1317
1402
|
onCancel,
|
|
1318
|
-
facingMode
|
|
1403
|
+
facingMode,
|
|
1319
1404
|
outputType = "image/jpeg",
|
|
1320
1405
|
quality: quality2 = 0.92,
|
|
1321
1406
|
fileNamePrefix = "camera-photo",
|
|
@@ -1328,12 +1413,32 @@ function CameraCapture({
|
|
|
1328
1413
|
const [status, setStatus] = (0, import_react4.useState)("initializing");
|
|
1329
1414
|
const [error, setError] = (0, import_react4.useState)(null);
|
|
1330
1415
|
const [stillUrl, setStillUrl] = (0, import_react4.useState)("");
|
|
1331
|
-
const
|
|
1416
|
+
const [facing, setFacing] = (0, import_react4.useState)(
|
|
1417
|
+
() => facingMode ?? defaultCameraFacingMode()
|
|
1418
|
+
);
|
|
1419
|
+
const [canFlip, setCanFlip] = (0, import_react4.useState)(false);
|
|
1420
|
+
const mirror = facing === "user";
|
|
1421
|
+
(0, import_react4.useEffect)(() => {
|
|
1422
|
+
if (facingMode) setFacing(facingMode);
|
|
1423
|
+
}, [facingMode]);
|
|
1332
1424
|
const stopStream = (0, import_react4.useCallback)(() => {
|
|
1333
1425
|
streamRef.current?.getTracks().forEach((t) => t.stop());
|
|
1334
1426
|
streamRef.current = null;
|
|
1335
1427
|
if (videoRef.current) videoRef.current.srcObject = null;
|
|
1336
1428
|
}, []);
|
|
1429
|
+
const probeCameraCount = (0, import_react4.useCallback)(async () => {
|
|
1430
|
+
const media = typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
|
|
1431
|
+
if (!media || typeof media.enumerateDevices !== "function") {
|
|
1432
|
+
setCanFlip(isMobileDevice());
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
const devices = await media.enumerateDevices();
|
|
1437
|
+
setCanFlip(devices.filter((d) => d.kind === "videoinput").length > 1);
|
|
1438
|
+
} catch {
|
|
1439
|
+
setCanFlip(isMobileDevice());
|
|
1440
|
+
}
|
|
1441
|
+
}, []);
|
|
1337
1442
|
const startStream = (0, import_react4.useCallback)(async () => {
|
|
1338
1443
|
setError(null);
|
|
1339
1444
|
setStatus("initializing");
|
|
@@ -1342,9 +1447,12 @@ function CameraCapture({
|
|
|
1342
1447
|
setStatus("error");
|
|
1343
1448
|
return;
|
|
1344
1449
|
}
|
|
1450
|
+
stopStream();
|
|
1345
1451
|
try {
|
|
1346
1452
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
1347
|
-
|
|
1453
|
+
// Non-exact: a device with no camera on the requested side falls back to
|
|
1454
|
+
// the one it has rather than throwing OverconstrainedError.
|
|
1455
|
+
video: { facingMode: facing },
|
|
1348
1456
|
audio: false
|
|
1349
1457
|
});
|
|
1350
1458
|
if (videoRef.current == null) {
|
|
@@ -1354,11 +1462,12 @@ function CameraCapture({
|
|
|
1354
1462
|
streamRef.current = stream;
|
|
1355
1463
|
videoRef.current.srcObject = stream;
|
|
1356
1464
|
setStatus("live");
|
|
1465
|
+
void probeCameraCount();
|
|
1357
1466
|
} catch (err) {
|
|
1358
1467
|
setError(describeCameraError(err));
|
|
1359
1468
|
setStatus("error");
|
|
1360
1469
|
}
|
|
1361
|
-
}, [
|
|
1470
|
+
}, [facing, probeCameraCount, stopStream]);
|
|
1362
1471
|
(0, import_react4.useEffect)(() => {
|
|
1363
1472
|
void startStream();
|
|
1364
1473
|
return () => {
|
|
@@ -1406,6 +1515,9 @@ function CameraCapture({
|
|
|
1406
1515
|
quality2
|
|
1407
1516
|
);
|
|
1408
1517
|
}, [mirror, outputType, quality2, stopStream]);
|
|
1518
|
+
const flipCamera = (0, import_react4.useCallback)(() => {
|
|
1519
|
+
setFacing((current) => current === "user" ? "environment" : "user");
|
|
1520
|
+
}, []);
|
|
1409
1521
|
const retake = (0, import_react4.useCallback)(() => {
|
|
1410
1522
|
blobRef.current = null;
|
|
1411
1523
|
setStillUrl("");
|
|
@@ -1424,6 +1536,7 @@ function CameraCapture({
|
|
|
1424
1536
|
className: ["uploader-camera", className ?? ""].filter(Boolean).join(" "),
|
|
1425
1537
|
style,
|
|
1426
1538
|
"data-testid": "camera-capture",
|
|
1539
|
+
"data-facing": facing,
|
|
1427
1540
|
children: [
|
|
1428
1541
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "uploader-camera-stage", children: [
|
|
1429
1542
|
status === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-error", role: "alert", children: error }) : status === "captured" ? (
|
|
@@ -1440,7 +1553,50 @@ function CameraCapture({
|
|
|
1440
1553
|
"data-mirror": mirror ? "true" : void 0
|
|
1441
1554
|
}
|
|
1442
1555
|
),
|
|
1443
|
-
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" })
|
|
1556
|
+
status === "initializing" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "uploader-camera-status", "aria-live": "polite", children: "Starting camera\u2026" }),
|
|
1557
|
+
canFlip && (status === "live" || status === "initializing") && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1558
|
+
"button",
|
|
1559
|
+
{
|
|
1560
|
+
type: "button",
|
|
1561
|
+
className: "uploader-camera-flip",
|
|
1562
|
+
"aria-label": facing === "user" ? "Switch to rear camera" : "Switch to front camera",
|
|
1563
|
+
disabled: status !== "live",
|
|
1564
|
+
onClick: flipCamera,
|
|
1565
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox: "0 0 24 24", width: "20", height: "20", "aria-hidden": "true", focusable: "false", children: [
|
|
1566
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1567
|
+
"path",
|
|
1568
|
+
{
|
|
1569
|
+
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",
|
|
1570
|
+
fill: "none",
|
|
1571
|
+
stroke: "currentColor",
|
|
1572
|
+
strokeWidth: "1.6",
|
|
1573
|
+
strokeLinejoin: "round"
|
|
1574
|
+
}
|
|
1575
|
+
),
|
|
1576
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1577
|
+
"path",
|
|
1578
|
+
{
|
|
1579
|
+
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",
|
|
1580
|
+
fill: "none",
|
|
1581
|
+
stroke: "currentColor",
|
|
1582
|
+
strokeWidth: "1.6",
|
|
1583
|
+
strokeLinecap: "round"
|
|
1584
|
+
}
|
|
1585
|
+
),
|
|
1586
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1587
|
+
"path",
|
|
1588
|
+
{
|
|
1589
|
+
d: "m9.4 10.2-.1 2.1 2.1-.6M14.6 13.8l.1-2.1-2.1.6",
|
|
1590
|
+
fill: "none",
|
|
1591
|
+
stroke: "currentColor",
|
|
1592
|
+
strokeWidth: "1.6",
|
|
1593
|
+
strokeLinecap: "round",
|
|
1594
|
+
strokeLinejoin: "round"
|
|
1595
|
+
}
|
|
1596
|
+
)
|
|
1597
|
+
] })
|
|
1598
|
+
}
|
|
1599
|
+
)
|
|
1444
1600
|
] }),
|
|
1445
1601
|
status === "live" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "uploader-camera-hint", children: "Frame your shot, then tap the shutter to capture." }),
|
|
1446
1602
|
/* @__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 +2288,12 @@ function DropPane({
|
|
|
2132
2288
|
PickerOverlay,
|
|
2133
2289
|
UploaderClient,
|
|
2134
2290
|
crop,
|
|
2291
|
+
defaultCameraFacingMode,
|
|
2135
2292
|
editImage,
|
|
2136
2293
|
flip,
|
|
2137
2294
|
flop,
|
|
2138
2295
|
isCameraSupported,
|
|
2296
|
+
isMobileDevice,
|
|
2139
2297
|
output,
|
|
2140
2298
|
quality,
|
|
2141
2299
|
resize,
|