@calmlens/sdk 0.16.3 → 0.18.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 +425 -181
- package/bin/calmlens.js +16016 -0
- package/bin/package.json +1 -0
- package/cjs/BrowserUpload-BT091eOB.d.ts +11 -0
- package/cjs/BrowserUpload-CQwXEGFv.js +10 -0
- package/cjs/CalmLensAppClient-CHQGu-Do.js +89 -0
- package/cjs/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/cjs/CalmLensConnection-B8Bu2j3y.js +659 -0
- package/cjs/CalmLensServerClient-C5o2A8MW.js +645 -0
- package/cjs/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/cjs/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/cjs/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/cjs/CryptoUtils-B_x8lMIx-YBW1-bvL.js +42 -0
- package/cjs/FileIcons-Ob003MdS.js +175 -0
- package/cjs/Jurisdiction-FusEXb4U-zVs21tDv.js +91 -0
- package/cjs/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/cjs/ModerationContextLimits-DelWfmiJ.js +1311 -0
- package/cjs/ReactUpload-CAyCsyG6.d.ts +17 -0
- package/cjs/ReactUpload-Cfg5JYUq.js +54 -0
- package/cjs/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/cjs/TestTriggers-DTc_iMJF-BjMPq4_3.js +1327 -0
- package/cjs/UploadController-BGO1eyzb.js +618 -0
- package/{esm/index-C9Xw1aid.d.ts → cjs/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/cjs/app.d.ts +7 -44
- package/cjs/app.js +11 -89
- package/cjs/elements.d.ts +88 -0
- package/cjs/elements.js +555 -0
- package/cjs/file-icons.d.ts +67 -0
- package/cjs/file-icons.js +10 -0
- package/cjs/index-CI4BzN4W.d.ts +252 -0
- package/cjs/index.d.ts +6 -4
- package/cjs/index.js +9 -4
- package/cjs/react-native.d.ts +56 -0
- package/cjs/react-native.js +495 -0
- package/cjs/react.d.ts +30 -0
- package/cjs/react.js +370 -0
- package/cjs/server.d.ts +12 -191
- package/cjs/server.js +18 -378
- package/cjs/test-inputs.d.ts +2 -2
- package/cjs/test-inputs.js +9 -264
- package/cjs/testing.d.ts +285 -190
- package/cjs/testing.js +2393 -1203
- package/cjs/ui-CC1GOoMg.js +583 -0
- package/cjs/ui-wIiMfqMB.d.ts +526 -0
- package/cjs/ui.d.ts +2 -0
- package/cjs/ui.js +27 -0
- package/cjs/vue.d.ts +490 -0
- package/cjs/vue.js +419 -0
- package/esm/BrowserUpload-BT091eOB.d.ts +11 -0
- package/esm/BrowserUpload-D2xBWqhm.js +5 -0
- package/esm/CalmLensAppClient-CV_K26Ce.d.ts +40 -0
- package/esm/CalmLensAppClient-DuFas_dW.js +84 -0
- package/esm/CalmLensConnection-MxwwLXdl.js +582 -0
- package/esm/CalmLensServerClient-D80_mKpW.js +586 -0
- package/esm/CalmLensServerClient-JtP687a3.d.ts +235 -0
- package/esm/CalmLensTypes-9Vt16pc7.d.ts +40400 -0
- package/esm/Classification-94kSYWZw-CMnQKW13.d.ts +5873 -0
- package/esm/CryptoUtils-B_x8lMIx-DhuWGzqn.js +25 -0
- package/esm/FileIcons-M6dzjFxd.js +128 -0
- package/esm/Jurisdiction-FusEXb4U-1ov6m5yE.js +46 -0
- package/esm/ModerationContext-9lGm-Mna-CcmbRnOy.d.ts +67 -0
- package/esm/ModerationContextLimits-BaX2QnU8.js +1114 -0
- package/esm/ReactUpload-CAyCsyG6.d.ts +17 -0
- package/esm/ReactUpload-CL2XC8WS.js +37 -0
- package/esm/TestTriggers-By07LR31-CmK0yTyR.d.ts +271 -0
- package/esm/TestTriggers-DTc_iMJF-HfB533Gx.js +1267 -0
- package/esm/UploadController-qjoYGCwx.js +589 -0
- package/{cjs/index-C9Xw1aid.d.ts → esm/WebhookPayload-2aL31v0J.d.ts} +131 -155
- package/esm/app.d.ts +7 -44
- package/esm/app.js +3 -86
- package/esm/elements.d.ts +88 -0
- package/esm/elements.js +528 -0
- package/esm/file-icons.d.ts +67 -0
- package/esm/file-icons.js +1 -0
- package/esm/index-CI4BzN4W.d.ts +252 -0
- package/esm/index.d.ts +6 -4
- package/esm/index.js +1 -1
- package/esm/react-native.d.ts +56 -0
- package/esm/react-native.js +467 -0
- package/esm/react.d.ts +30 -0
- package/esm/react.js +343 -0
- package/esm/server.d.ts +12 -191
- package/esm/server.js +3 -369
- package/esm/styles.css +147 -0
- package/esm/svelte/CalmLensMedia.svelte +134 -0
- package/esm/svelte/CalmLensUploader.svelte +175 -0
- package/esm/svelte/Icon.svelte +7 -0
- package/esm/svelte/index.d.ts +37 -0
- package/esm/svelte/index.js +6 -0
- package/esm/test-inputs.d.ts +2 -2
- package/esm/test-inputs.js +1 -256
- package/esm/testing.d.ts +285 -190
- package/esm/testing.js +2329 -1122
- package/esm/ui-CCY3yXFn.js +458 -0
- package/esm/ui-wIiMfqMB.d.ts +526 -0
- package/esm/ui.d.ts +2 -0
- package/esm/ui.js +3 -0
- package/esm/vue.d.ts +490 -0
- package/esm/vue.js +392 -0
- package/package.json +118 -13
- package/cjs/CalmLensConnection-BNUHp_AQ.js +0 -234
- package/cjs/CalmLensUtils-o171ownK.js +0 -699
- package/cjs/CalmLensVersion-DwhgxQK5.js +0 -480
- package/cjs/CalmLensWebhooks-BKrq30Yp.js +0 -201
- package/cjs/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/cjs/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/cjs/WebhookPayload-CJoIkGBD.d.ts +0 -11753
- package/esm/CalmLensConnection-XTqmlwzG.js +0 -193
- package/esm/CalmLensUtils-DsF8Aa_x.js +0 -574
- package/esm/CalmLensVersion-DcifRKsx.js +0 -415
- package/esm/CalmLensWebhooks-BvGyk5d3.js +0 -148
- package/esm/Classification-BNhgwGEm-DmA9VEuE.d.ts +0 -4806
- package/esm/TestTriggers-CRKtulyp-vrzEq2wm.d.ts +0 -44
- package/esm/WebhookPayload-CJoIkGBD.d.ts +0 -11753
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { O as CalmLensError, k as SDK_ERROR_CODES } from "./ModerationContextLimits-BaX2QnU8.js";
|
|
2
|
+
import { t as CalmLensAppClient } from "./CalmLensAppClient-DuFas_dW.js";
|
|
3
|
+
// src/contract/PlaceholderHashDecode.js
|
|
4
|
+
/**
|
|
5
|
+
* Decodes a ThumbHash to an RGBA image. RGB is not be premultiplied by A.
|
|
6
|
+
*
|
|
7
|
+
* @param hash The bytes of the ThumbHash.
|
|
8
|
+
* @returns The width, height, and pixels of the rendered placeholder image.
|
|
9
|
+
*/
|
|
10
|
+
function thumbHashToRGBA(hash) {
|
|
11
|
+
const { PI, min, max, cos, round } = Math;
|
|
12
|
+
const header24 = hash[0] | hash[1] << 8 | hash[2] << 16;
|
|
13
|
+
const header16 = hash[3] | hash[4] << 8;
|
|
14
|
+
const l_dc = (header24 & 63) / 63;
|
|
15
|
+
const p_dc = (header24 >> 6 & 63) / 31.5 - 1;
|
|
16
|
+
const q_dc = (header24 >> 12 & 63) / 31.5 - 1;
|
|
17
|
+
const l_scale = (header24 >> 18 & 31) / 31;
|
|
18
|
+
const hasAlpha = header24 >> 23;
|
|
19
|
+
const p_scale = (header16 >> 3 & 63) / 63;
|
|
20
|
+
const q_scale = (header16 >> 9 & 63) / 63;
|
|
21
|
+
const isLandscape = header16 >> 15;
|
|
22
|
+
const lx = max(3, isLandscape ? hasAlpha ? 5 : 7 : header16 & 7);
|
|
23
|
+
const ly = max(3, isLandscape ? header16 & 7 : hasAlpha ? 5 : 7);
|
|
24
|
+
const a_dc = hasAlpha ? (hash[5] & 15) / 15 : 1;
|
|
25
|
+
const a_scale = (hash[5] >> 4) / 15;
|
|
26
|
+
const ac_start = hasAlpha ? 6 : 5;
|
|
27
|
+
let ac_index = 0;
|
|
28
|
+
const decodeChannel = (nx, ny, scale) => {
|
|
29
|
+
const ac = [];
|
|
30
|
+
for (let cy = 0; cy < ny; cy++) for (let cx = cy ? 0 : 1; cx * ny < nx * (ny - cy); cx++) ac.push(((hash[ac_start + (ac_index >> 1)] >> ((ac_index++ & 1) << 2) & 15) / 7.5 - 1) * scale);
|
|
31
|
+
return ac;
|
|
32
|
+
};
|
|
33
|
+
const l_ac = decodeChannel(lx, ly, l_scale);
|
|
34
|
+
const p_ac = decodeChannel(3, 3, p_scale * 1.25);
|
|
35
|
+
const q_ac = decodeChannel(3, 3, q_scale * 1.25);
|
|
36
|
+
const a_ac = hasAlpha && decodeChannel(5, 5, a_scale);
|
|
37
|
+
const ratio = thumbHashToApproximateAspectRatio(hash);
|
|
38
|
+
const w = round(ratio > 1 ? 32 : 32 * ratio);
|
|
39
|
+
const h = round(ratio > 1 ? 32 / ratio : 32);
|
|
40
|
+
const rgba = new Uint8Array(w * h * 4);
|
|
41
|
+
const fx = [];
|
|
42
|
+
const fy = [];
|
|
43
|
+
for (let y = 0, i = 0; y < h; y++) for (let x = 0; x < w; x++, i += 4) {
|
|
44
|
+
let l = l_dc;
|
|
45
|
+
let p = p_dc;
|
|
46
|
+
let q = q_dc;
|
|
47
|
+
let a = a_dc;
|
|
48
|
+
for (let cx = 0, n = max(lx, hasAlpha ? 5 : 3); cx < n; cx++) fx[cx] = cos(PI / w * (x + .5) * cx);
|
|
49
|
+
for (let cy = 0, n = max(ly, hasAlpha ? 5 : 3); cy < n; cy++) fy[cy] = cos(PI / h * (y + .5) * cy);
|
|
50
|
+
for (let cy = 0, j = 0; cy < ly; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx * ly < lx * (ly - cy); cx++, j++) l += l_ac[j] * fx[cx] * fy2;
|
|
51
|
+
for (let cy = 0, j = 0; cy < 3; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 3 - cy; cx++, j++) {
|
|
52
|
+
const f = fx[cx] * fy2;
|
|
53
|
+
p += p_ac[j] * f;
|
|
54
|
+
q += q_ac[j] * f;
|
|
55
|
+
}
|
|
56
|
+
if (hasAlpha) for (let cy = 0, j = 0; cy < 5; cy++) for (let cx = cy ? 0 : 1, fy2 = fy[cy] * 2; cx < 5 - cy; cx++, j++) a += a_ac[j] * fx[cx] * fy2;
|
|
57
|
+
const b = l - 2 / 3 * p;
|
|
58
|
+
const r = (3 * l - b + q) / 2;
|
|
59
|
+
const g = r - q;
|
|
60
|
+
rgba[i] = max(0, 255 * min(1, r));
|
|
61
|
+
rgba[i + 1] = max(0, 255 * min(1, g));
|
|
62
|
+
rgba[i + 2] = max(0, 255 * min(1, b));
|
|
63
|
+
rgba[i + 3] = max(0, 255 * min(1, a));
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
w,
|
|
67
|
+
h,
|
|
68
|
+
rgba
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Extracts the approximate aspect ratio of the original image.
|
|
73
|
+
*
|
|
74
|
+
* @param hash The bytes of the ThumbHash.
|
|
75
|
+
* @returns The approximate aspect ratio (i.e. width / height).
|
|
76
|
+
*/
|
|
77
|
+
function thumbHashToApproximateAspectRatio(hash) {
|
|
78
|
+
const header = hash[3];
|
|
79
|
+
const hasAlpha = hash[2] & 128;
|
|
80
|
+
const isLandscape = hash[4] & 128;
|
|
81
|
+
return (isLandscape ? hasAlpha ? 5 : 7 : header & 7) / (isLandscape ? header & 7 : hasAlpha ? 5 : 7);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Encodes an RGBA image to PNG bytes. RGB should not be premultiplied by
|
|
85
|
+
* A. This is optimized for speed and simplicity and does not optimize for size
|
|
86
|
+
* at all. This doesn't do any compression (all values are stored uncompressed).
|
|
87
|
+
*
|
|
88
|
+
* @param w The width of the input image. Must be between 1 and 16383px.
|
|
89
|
+
* @param h The height of the input image. Must be between 1 and 65535px.
|
|
90
|
+
* @param rgba The pixels in the input image, row-by-row. Must have w*h*4 elements.
|
|
91
|
+
* @returns The bytes of a PNG for the input image.
|
|
92
|
+
*/
|
|
93
|
+
function rgbaToPng(w, h, rgba) {
|
|
94
|
+
if (!Number.isInteger(w) || !Number.isInteger(h) || w < 1 || w > 16383 || h < 1 || h > 65535 || rgba.length !== w * h * 4) throw new RangeError("Invalid PNG dimensions or pixels");
|
|
95
|
+
const row = w * 4 + 1;
|
|
96
|
+
const idat = 6 + h * (5 + row);
|
|
97
|
+
const bytes = [
|
|
98
|
+
137,
|
|
99
|
+
80,
|
|
100
|
+
78,
|
|
101
|
+
71,
|
|
102
|
+
13,
|
|
103
|
+
10,
|
|
104
|
+
26,
|
|
105
|
+
10,
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
0,
|
|
109
|
+
13,
|
|
110
|
+
73,
|
|
111
|
+
72,
|
|
112
|
+
68,
|
|
113
|
+
82,
|
|
114
|
+
0,
|
|
115
|
+
0,
|
|
116
|
+
w >> 8,
|
|
117
|
+
w & 255,
|
|
118
|
+
0,
|
|
119
|
+
0,
|
|
120
|
+
h >> 8,
|
|
121
|
+
h & 255,
|
|
122
|
+
8,
|
|
123
|
+
6,
|
|
124
|
+
0,
|
|
125
|
+
0,
|
|
126
|
+
0,
|
|
127
|
+
0,
|
|
128
|
+
0,
|
|
129
|
+
0,
|
|
130
|
+
0,
|
|
131
|
+
idat >>> 24,
|
|
132
|
+
idat >> 16 & 255,
|
|
133
|
+
idat >> 8 & 255,
|
|
134
|
+
idat & 255,
|
|
135
|
+
73,
|
|
136
|
+
68,
|
|
137
|
+
65,
|
|
138
|
+
84,
|
|
139
|
+
120,
|
|
140
|
+
1
|
|
141
|
+
];
|
|
142
|
+
const table = [
|
|
143
|
+
0,
|
|
144
|
+
498536548,
|
|
145
|
+
997073096,
|
|
146
|
+
651767980,
|
|
147
|
+
1994146192,
|
|
148
|
+
1802195444,
|
|
149
|
+
1303535960,
|
|
150
|
+
1342533948,
|
|
151
|
+
-306674912,
|
|
152
|
+
-267414716,
|
|
153
|
+
-690576408,
|
|
154
|
+
-882789492,
|
|
155
|
+
-1687895376,
|
|
156
|
+
-2032938284,
|
|
157
|
+
-1609899400,
|
|
158
|
+
-1111625188
|
|
159
|
+
];
|
|
160
|
+
let a = 1;
|
|
161
|
+
let b = 0;
|
|
162
|
+
for (let y = 0, i = 0, end = row - 1; y < h; y++, end += row - 1) {
|
|
163
|
+
bytes.push(y + 1 < h ? 0 : 1, row & 255, row >> 8, ~row & 255, row >> 8 ^ 255, 0);
|
|
164
|
+
for (b = (b + a) % 65521; i < end; i++) {
|
|
165
|
+
const u = rgba[i] & 255;
|
|
166
|
+
bytes.push(u);
|
|
167
|
+
a = (a + u) % 65521;
|
|
168
|
+
b = (b + a) % 65521;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
bytes.push(b >> 8, b & 255, a >> 8, a & 255, 0, 0, 0, 0, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130);
|
|
172
|
+
for (let [start, end] of [[12, 29], [37, 41 + idat]]) {
|
|
173
|
+
let c = -1;
|
|
174
|
+
for (let i = start; i < end; i++) {
|
|
175
|
+
c ^= bytes[i];
|
|
176
|
+
c = c >>> 4 ^ table[c & 15];
|
|
177
|
+
c = c >>> 4 ^ table[c & 15];
|
|
178
|
+
}
|
|
179
|
+
c = ~c;
|
|
180
|
+
bytes[end++] = c >>> 24;
|
|
181
|
+
bytes[end++] = c >> 16 & 255;
|
|
182
|
+
bytes[end++] = c >> 8 & 255;
|
|
183
|
+
bytes[end++] = c & 255;
|
|
184
|
+
}
|
|
185
|
+
return Uint8Array.from(bytes);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Decodes a ThumbHash to a PNG data URL. This is a convenience function that
|
|
189
|
+
* just calls "thumbHashToRGBA" followed by "rgbaToDataURL".
|
|
190
|
+
*
|
|
191
|
+
* @param hash The bytes of the ThumbHash.
|
|
192
|
+
* @returns A data URL containing a PNG for the rendered ThumbHash.
|
|
193
|
+
*/
|
|
194
|
+
const placeholderHashToBytes = (hash) => Uint8Array.from(atob(hash), (c) => c.charCodeAt(0));
|
|
195
|
+
/** Renders a base64 ThumbHash as a small PNG, which the browser scales to the element's size. */
|
|
196
|
+
function placeholderHashToPng(hash) {
|
|
197
|
+
const image = thumbHashToRGBA(placeholderHashToBytes(hash));
|
|
198
|
+
return rgbaToPng(image.w, image.h, image.rgba);
|
|
199
|
+
}
|
|
200
|
+
function placeholderHashToDataURL(hash) {
|
|
201
|
+
return `data:image/png;base64,${btoa(String.fromCharCode(...placeholderHashToPng(hash)))}`;
|
|
202
|
+
}
|
|
203
|
+
// src/ui/ProgressFetch.ts
|
|
204
|
+
/**
|
|
205
|
+
* A `fetch` that reports how many bytes of the request body have been sent, which `fetch` itself
|
|
206
|
+
* can't do. It sends through `XMLHttpRequest`, so the upload components pass it to
|
|
207
|
+
* `CalmLensAppClient` as `uploadFetch`. Without `XMLHttpRequest` it falls back to `fetch` and
|
|
208
|
+
* reports nothing.
|
|
209
|
+
*/
|
|
210
|
+
const createProgressFetch = (onProgress) => {
|
|
211
|
+
if (typeof XMLHttpRequest === "undefined") return (input, init) => fetch(input, init);
|
|
212
|
+
return (input, init = {}) => new Promise((resolve, reject) => {
|
|
213
|
+
const signal = init.signal;
|
|
214
|
+
const abortError = () => signal?.reason ?? new DOMException("The upload was cancelled", "AbortError");
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
reject(abortError());
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
220
|
+
const xhr = new XMLHttpRequest();
|
|
221
|
+
xhr.open(init.method ?? "GET", url);
|
|
222
|
+
xhr.responseType = "arraybuffer";
|
|
223
|
+
for (const [name, value] of new Headers(init.headers)) xhr.setRequestHeader(name, value);
|
|
224
|
+
xhr.upload.onprogress = (event) => {
|
|
225
|
+
if (event.lengthComputable) onProgress(event.loaded, event.total);
|
|
226
|
+
};
|
|
227
|
+
const onAbort = () => xhr.abort();
|
|
228
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
229
|
+
const done = () => signal?.removeEventListener("abort", onAbort);
|
|
230
|
+
xhr.onload = () => {
|
|
231
|
+
done();
|
|
232
|
+
const headers = new Headers();
|
|
233
|
+
for (const line of xhr.getAllResponseHeaders().trim().split(/[\r\n]+/)) {
|
|
234
|
+
const separator = line.indexOf(":");
|
|
235
|
+
if (separator > 0) headers.append(line.slice(0, separator).trim(), line.slice(separator + 1).trim());
|
|
236
|
+
}
|
|
237
|
+
const empty = [
|
|
238
|
+
101,
|
|
239
|
+
204,
|
|
240
|
+
205,
|
|
241
|
+
304
|
|
242
|
+
].includes(xhr.status);
|
|
243
|
+
resolve(new Response(empty ? null : xhr.response, {
|
|
244
|
+
status: xhr.status,
|
|
245
|
+
statusText: xhr.statusText,
|
|
246
|
+
headers
|
|
247
|
+
}));
|
|
248
|
+
};
|
|
249
|
+
xhr.onerror = () => {
|
|
250
|
+
done();
|
|
251
|
+
reject(/* @__PURE__ */ new TypeError("Network request failed"));
|
|
252
|
+
};
|
|
253
|
+
xhr.ontimeout = xhr.onerror;
|
|
254
|
+
xhr.onabort = () => {
|
|
255
|
+
done();
|
|
256
|
+
reject(abortError());
|
|
257
|
+
};
|
|
258
|
+
xhr.send(init.body ?? null);
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
// src/ui/UploadController.ts
|
|
262
|
+
const DEFAULT_MAX_FILES = 10;
|
|
263
|
+
const DEFAULT_CONCURRENCY = 3;
|
|
264
|
+
const DEFAULT_DECISION_TIMEOUT_MS = 15e3;
|
|
265
|
+
/** Whether a file matches an `accept` list such as `image/*,.pdf`. */
|
|
266
|
+
const isFileAccepted = (file, accept) => {
|
|
267
|
+
const patterns = (accept ?? "").split(",").map((pattern) => pattern.trim().toLowerCase()).filter(Boolean);
|
|
268
|
+
if (patterns.length === 0) return true;
|
|
269
|
+
const type = file.type.toLowerCase();
|
|
270
|
+
const name = file.name.toLowerCase();
|
|
271
|
+
return patterns.some((pattern) => {
|
|
272
|
+
if (pattern === "*" || pattern === "*/*") return true;
|
|
273
|
+
if (pattern.endsWith("/*")) return type.startsWith(pattern.slice(0, -1));
|
|
274
|
+
if (pattern.startsWith(".")) return name.endsWith(pattern);
|
|
275
|
+
return type === pattern;
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
/** A browser file or blob, which uploads as it is. A file with a `uri` is read for its bytes. */
|
|
279
|
+
const isBlob = (file) => !file.uri && typeof Blob !== "undefined" && file instanceof Blob;
|
|
280
|
+
/** An image preview for a picked file: its own URI, or an object URL for a browser file. */
|
|
281
|
+
const createPreviewUrl = (file) => {
|
|
282
|
+
if (!file.type.startsWith("image/")) return void 0;
|
|
283
|
+
if (file.uri) return file.uri;
|
|
284
|
+
return isBlob(file) && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
|
|
285
|
+
};
|
|
286
|
+
/** Releases an object URL `createPreviewUrl` made; a file's own URI is left alone. */
|
|
287
|
+
const revokePreviewUrl = (url) => {
|
|
288
|
+
if (url?.startsWith("blob:")) URL.revokeObjectURL(url);
|
|
289
|
+
};
|
|
290
|
+
const isRejected = (status) => status === "blocked" || status === "purged";
|
|
291
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
292
|
+
const timer = setTimeout(resolve, ms);
|
|
293
|
+
signal.addEventListener("abort", () => {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
resolve();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
let nextKey = 0;
|
|
299
|
+
/**
|
|
300
|
+
* Uploads files through an upload preset and tracks each one until it has a moderation
|
|
301
|
+
* decision. It holds no UI: the React, Vue, Svelte and web components render its snapshot, and
|
|
302
|
+
* you can render your own with `subscribe`, which also makes it a Svelte store.
|
|
303
|
+
*/
|
|
304
|
+
var UploadController = class {
|
|
305
|
+
#options;
|
|
306
|
+
#snapshot;
|
|
307
|
+
#listeners;
|
|
308
|
+
#aborts;
|
|
309
|
+
/** How to check each upload again, with the client and token it was uploaded with. */
|
|
310
|
+
#checks;
|
|
311
|
+
constructor(options) {
|
|
312
|
+
this.#snapshot = {
|
|
313
|
+
items: [],
|
|
314
|
+
busy: false
|
|
315
|
+
};
|
|
316
|
+
this.#listeners = /* @__PURE__ */ new Set();
|
|
317
|
+
this.#aborts = /* @__PURE__ */ new Map();
|
|
318
|
+
this.#checks = /* @__PURE__ */ new Map();
|
|
319
|
+
this.getSnapshot = () => this.#snapshot;
|
|
320
|
+
this.subscribe = (listener) => {
|
|
321
|
+
this.#listeners.add(listener);
|
|
322
|
+
listener(this.#snapshot);
|
|
323
|
+
return () => {
|
|
324
|
+
this.#listeners.delete(listener);
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
this.#options = options;
|
|
328
|
+
}
|
|
329
|
+
/** The current options. */
|
|
330
|
+
get options() {
|
|
331
|
+
return this.#options;
|
|
332
|
+
}
|
|
333
|
+
/** Replaces the options, for example when a component's props change. */
|
|
334
|
+
setOptions(options) {
|
|
335
|
+
this.#options = options;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Adds files from a file input, a drop, a paste or a phone's picker, and starts uploading them,
|
|
339
|
+
* or keeps them `selected` with `autoUpload: false`.
|
|
340
|
+
*/
|
|
341
|
+
addFiles(files) {
|
|
342
|
+
const { multiple = false, maxFiles = DEFAULT_MAX_FILES, accept, maxFileSize, autoUpload = true } = this.#options;
|
|
343
|
+
let picked = Array.from(files);
|
|
344
|
+
if (picked.length === 0) return;
|
|
345
|
+
if (!multiple) {
|
|
346
|
+
picked = picked.slice(0, 1);
|
|
347
|
+
for (const item of this.#snapshot.items) this.remove(item.key);
|
|
348
|
+
}
|
|
349
|
+
const room = (multiple ? maxFiles : 1) - this.#snapshot.items.length;
|
|
350
|
+
const added = picked.slice(0, Math.max(0, room)).map((file) => {
|
|
351
|
+
const key = `upload-${++nextKey}`;
|
|
352
|
+
const previewUrl = createPreviewUrl(file);
|
|
353
|
+
const errorCode = !isFileAccepted(file, accept) ? "file/not-accepted" : maxFileSize !== void 0 && file.size > maxFileSize ? "file/too-large" : void 0;
|
|
354
|
+
return errorCode ? {
|
|
355
|
+
key,
|
|
356
|
+
file,
|
|
357
|
+
previewUrl,
|
|
358
|
+
stage: "failed",
|
|
359
|
+
progress: null,
|
|
360
|
+
errorCode
|
|
361
|
+
} : {
|
|
362
|
+
key,
|
|
363
|
+
file,
|
|
364
|
+
previewUrl,
|
|
365
|
+
stage: autoUpload ? "queued" : "selected",
|
|
366
|
+
progress: null
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
this.#set([...this.#snapshot.items, ...added]);
|
|
370
|
+
for (const item of added) if (item.stage === "failed") this.#options.onFailed?.(item);
|
|
371
|
+
this.#pump();
|
|
372
|
+
}
|
|
373
|
+
/** Uploads every `selected` file, for an uploader with `autoUpload: false`. */
|
|
374
|
+
start() {
|
|
375
|
+
for (const item of this.#snapshot.items) if (item.stage === "selected") this.#update(item.key, { stage: "queued" });
|
|
376
|
+
this.#pump();
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Tries a failed file again: an upload that finished but couldn't be checked is checked again,
|
|
380
|
+
* anything else is uploaded again. A finished upload still `pending` is checked again too.
|
|
381
|
+
*/
|
|
382
|
+
retry(key) {
|
|
383
|
+
const item = this.#find(key);
|
|
384
|
+
if (!item || item.errorCode?.startsWith("file/")) return;
|
|
385
|
+
const pending = item.stage === "complete" && item.status === "pending";
|
|
386
|
+
if (item.stage !== "failed" && !(pending && this.#checks.has(key))) return;
|
|
387
|
+
this.#update(key, {
|
|
388
|
+
stage: "queued",
|
|
389
|
+
progress: null,
|
|
390
|
+
errorCode: void 0,
|
|
391
|
+
assetId: this.#checks.has(key) ? item.assetId : void 0
|
|
392
|
+
});
|
|
393
|
+
this.#pump();
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Stops a queued or running file and keeps it in the list as failed with `sdk/cancelled`, so
|
|
397
|
+
* `retry` can upload it again. Finished files are left as they are.
|
|
398
|
+
*/
|
|
399
|
+
cancel(key) {
|
|
400
|
+
const item = this.#find(key);
|
|
401
|
+
if (!item || item.stage !== "queued" && item.stage !== "uploading" && item.stage !== "checking") return;
|
|
402
|
+
this.#aborts.get(key)?.abort();
|
|
403
|
+
this.#aborts.delete(key);
|
|
404
|
+
this.#checks.delete(key);
|
|
405
|
+
this.#update(key, {
|
|
406
|
+
stage: "failed",
|
|
407
|
+
progress: null,
|
|
408
|
+
errorCode: SDK_ERROR_CODES.CANCELLED
|
|
409
|
+
});
|
|
410
|
+
this.#pump();
|
|
411
|
+
}
|
|
412
|
+
/** Cancels a file's upload if it's running and removes it from the list. */
|
|
413
|
+
remove(key) {
|
|
414
|
+
const item = this.#find(key);
|
|
415
|
+
if (!item) return;
|
|
416
|
+
this.#aborts.get(key)?.abort();
|
|
417
|
+
this.#aborts.delete(key);
|
|
418
|
+
this.#checks.delete(key);
|
|
419
|
+
revokePreviewUrl(item.previewUrl);
|
|
420
|
+
this.#set(this.#snapshot.items.filter((other) => other.key !== key));
|
|
421
|
+
this.#options.onRemoved?.(item);
|
|
422
|
+
this.#pump();
|
|
423
|
+
}
|
|
424
|
+
/** Cancels every upload and empties the list, without calling `onRemoved`. */
|
|
425
|
+
clear() {
|
|
426
|
+
for (const abort of this.#aborts.values()) abort.abort();
|
|
427
|
+
this.#aborts.clear();
|
|
428
|
+
this.#checks.clear();
|
|
429
|
+
for (const item of this.#snapshot.items) revokePreviewUrl(item.previewUrl);
|
|
430
|
+
this.#set([]);
|
|
431
|
+
}
|
|
432
|
+
#find(key) {
|
|
433
|
+
return this.#snapshot.items.find((item) => item.key === key);
|
|
434
|
+
}
|
|
435
|
+
#set(items) {
|
|
436
|
+
const busy = items.some((item) => item.stage === "queued" || item.stage === "uploading" || item.stage === "checking");
|
|
437
|
+
this.#snapshot = {
|
|
438
|
+
items,
|
|
439
|
+
busy
|
|
440
|
+
};
|
|
441
|
+
for (const listener of this.#listeners) listener(this.#snapshot);
|
|
442
|
+
}
|
|
443
|
+
#update(key, changes) {
|
|
444
|
+
if (!this.#find(key)) return;
|
|
445
|
+
this.#set(this.#snapshot.items.map((item) => item.key === key ? {
|
|
446
|
+
...item,
|
|
447
|
+
...changes
|
|
448
|
+
} : item));
|
|
449
|
+
}
|
|
450
|
+
#pump() {
|
|
451
|
+
const limit = this.#options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
452
|
+
for (const item of this.#snapshot.items) {
|
|
453
|
+
if (this.#aborts.size >= limit) return;
|
|
454
|
+
if (item.stage === "queued" && !this.#aborts.has(item.key)) this.#upload(item);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async #upload({ key, file, assetId }) {
|
|
458
|
+
const abort = new AbortController();
|
|
459
|
+
this.#aborts.set(key, abort);
|
|
460
|
+
this.#update(key, {
|
|
461
|
+
stage: "uploading",
|
|
462
|
+
progress: 0
|
|
463
|
+
});
|
|
464
|
+
const options = this.#options;
|
|
465
|
+
try {
|
|
466
|
+
const onProgress = (sent, total) => {
|
|
467
|
+
if (!abort.signal.aborted) this.#update(key, { progress: total ? sent / total : null });
|
|
468
|
+
};
|
|
469
|
+
let check = assetId ? this.#checks.get(key) : void 0;
|
|
470
|
+
let asset;
|
|
471
|
+
if (assetId && check) {
|
|
472
|
+
this.#update(key, {
|
|
473
|
+
stage: "checking",
|
|
474
|
+
progress: null
|
|
475
|
+
});
|
|
476
|
+
asset = await check(assetId);
|
|
477
|
+
} else {
|
|
478
|
+
const client = await this.#clientFor(file, abort.signal, onProgress);
|
|
479
|
+
asset = await client.create();
|
|
480
|
+
check = client.retrieve;
|
|
481
|
+
if (check) this.#checks.set(key, check);
|
|
482
|
+
this.#update(key, {
|
|
483
|
+
stage: "checking",
|
|
484
|
+
progress: null,
|
|
485
|
+
assetId: asset.id
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const deadline = Date.now() + (options.decisionTimeoutMs ?? DEFAULT_DECISION_TIMEOUT_MS);
|
|
489
|
+
for (let delay = 1e3; asset.status === "pending" && check && Date.now() < deadline; delay *= 2) {
|
|
490
|
+
await sleep(Math.min(delay, Math.max(0, deadline - Date.now())), abort.signal);
|
|
491
|
+
if (abort.signal.aborted) return;
|
|
492
|
+
asset = await check(asset.id);
|
|
493
|
+
}
|
|
494
|
+
if (abort.signal.aborted) return;
|
|
495
|
+
this.#update(key, {
|
|
496
|
+
stage: "complete",
|
|
497
|
+
status: asset.status
|
|
498
|
+
});
|
|
499
|
+
const item = this.#find(key);
|
|
500
|
+
if (!item) return;
|
|
501
|
+
if (isRejected(asset.status)) options.onRejected?.(item);
|
|
502
|
+
else options.onUploaded?.({
|
|
503
|
+
assetId: asset.id,
|
|
504
|
+
status: asset.status,
|
|
505
|
+
file
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (abort.signal.aborted) return;
|
|
509
|
+
const errorCode = CalmLensError.isCalmLensError(error) ? error.code : SDK_ERROR_CODES.TRANSPORT_ERROR;
|
|
510
|
+
this.#update(key, {
|
|
511
|
+
stage: "failed",
|
|
512
|
+
progress: null,
|
|
513
|
+
errorCode
|
|
514
|
+
});
|
|
515
|
+
const item = this.#find(key);
|
|
516
|
+
if (item) options.onFailed?.(item);
|
|
517
|
+
} finally {
|
|
518
|
+
if (this.#aborts.get(key) === abort) this.#aborts.delete(key);
|
|
519
|
+
this.#pump();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/** Creates and checks one file's upload, through `upload` or the upload preset. */
|
|
523
|
+
async #clientFor(file, signal, onProgress) {
|
|
524
|
+
const { upload, checkStatus, uploadPresetId, createClient, baseUrl } = this.#options;
|
|
525
|
+
if (upload) return {
|
|
526
|
+
create: async () => {
|
|
527
|
+
const { assetId, status } = await upload(file, {
|
|
528
|
+
signal,
|
|
529
|
+
onProgress
|
|
530
|
+
});
|
|
531
|
+
return {
|
|
532
|
+
id: assetId,
|
|
533
|
+
status
|
|
534
|
+
};
|
|
535
|
+
},
|
|
536
|
+
retrieve: checkStatus ? async (assetId) => ({
|
|
537
|
+
id: assetId,
|
|
538
|
+
status: await checkStatus(assetId, { signal })
|
|
539
|
+
}) : void 0
|
|
540
|
+
};
|
|
541
|
+
if (!uploadPresetId) throw new CalmLensError(SDK_ERROR_CODES.INVALID_CONFIG, { message: "The uploader needs an uploadPresetId or an upload function" });
|
|
542
|
+
const options = {
|
|
543
|
+
uploadPresetId,
|
|
544
|
+
token: await this.#tokenFor(file, signal),
|
|
545
|
+
baseUrl,
|
|
546
|
+
signal,
|
|
547
|
+
uploadFetch: createProgressFetch(onProgress)
|
|
548
|
+
};
|
|
549
|
+
const client = createClient ? createClient(options) : new CalmLensAppClient(options);
|
|
550
|
+
return {
|
|
551
|
+
create: async () => {
|
|
552
|
+
const type = file.type || "application/octet-stream";
|
|
553
|
+
return client.assets.create({
|
|
554
|
+
name: file.name,
|
|
555
|
+
content: isBlob(file) ? {
|
|
556
|
+
data: file,
|
|
557
|
+
type
|
|
558
|
+
} : {
|
|
559
|
+
uint8Array: new Uint8Array(await file.arrayBuffer()),
|
|
560
|
+
type
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
retrieve: (assetId) => client.assets.retrieve({ assetId })
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
async #tokenFor(file, signal) {
|
|
568
|
+
const { token, tokenUrl } = this.#options;
|
|
569
|
+
if (typeof token === "function") return await token(file);
|
|
570
|
+
if (token !== void 0) return token;
|
|
571
|
+
if (!tokenUrl) return void 0;
|
|
572
|
+
const response = await fetch(tokenUrl, {
|
|
573
|
+
method: "POST",
|
|
574
|
+
headers: { "Content-Type": "application/json" },
|
|
575
|
+
body: JSON.stringify({
|
|
576
|
+
name: file.name,
|
|
577
|
+
type: file.type,
|
|
578
|
+
size: file.size
|
|
579
|
+
}),
|
|
580
|
+
credentials: "same-origin",
|
|
581
|
+
signal
|
|
582
|
+
});
|
|
583
|
+
const body = await response.json().catch(() => ({}));
|
|
584
|
+
if (!response.ok || typeof body.token !== "string") throw new CalmLensError(typeof body.code === "string" ? body.code : "upload-token/refused", { message: `Your token endpoint answered ${response.status} without a token` });
|
|
585
|
+
return body.token;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const createUploadController = (options) => new UploadController(options);
|
|
589
|
+
export { placeholderHashToDataURL as a, createProgressFetch as i, createUploadController as n, isFileAccepted as r, UploadController as t };
|