@devultur/convex 1.4.3 → 1.5.1
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 +123 -7
- package/dist/chunk-BDEO67OT.js +110 -0
- package/dist/{chunk-JZYKPLI4.js → chunk-RDVBE3G3.js} +20 -1
- package/dist/component/http.js +2 -2
- package/dist/component/library.d.ts +2 -2
- package/dist/component/library.js +3 -3
- package/dist/component/lock.js +3 -3
- package/dist/component/mediaClient.js +1 -1
- package/dist/component/metadata.js +2 -2
- package/dist/component/public.js +9 -9
- package/dist/component/uploads.js +6 -6
- package/dist/component/workerErrors.d.ts +1 -1
- package/dist/component/workerErrors.js +1 -1
- package/dist/hooks-uqpBd5UF.d.ts +171 -0
- package/dist/index.d.ts +74 -20
- package/dist/index.js +23 -4
- package/dist/locales/es-formal.d.ts +5 -3
- package/dist/locales/es-formal.js +6 -3
- package/dist/locales/es.d.ts +5 -3
- package/dist/locales/es.js +5 -3
- package/dist/{preview-labels-B7HNa7Va.d.ts → preview-labels-C2nUUY_f.d.ts} +1 -1
- package/dist/react/hooks.d.ts +6 -0
- package/dist/react/hooks.js +23 -0
- package/dist/react.d.ts +157 -275
- package/dist/react.js +958 -931
- package/dist/{types-DZxN6Eij.d.ts → types-C0ruHUcu.d.ts} +14 -8
- package/docs/guides/object-lock.md +517 -0
- package/docs/guides/plugins.md +205 -0
- package/docs/guides/self-host.md +523 -0
- package/docs/guides/video-player.md +105 -0
- package/package.json +10 -5
package/dist/react.js
CHANGED
|
@@ -24,39 +24,34 @@ import {
|
|
|
24
24
|
import {
|
|
25
25
|
describeDevulturError
|
|
26
26
|
} from "./chunk-YYOZ3PDH.js";
|
|
27
|
+
import {
|
|
28
|
+
DevulturApiContext,
|
|
29
|
+
DevulturCapabilityError,
|
|
30
|
+
DevulturContext,
|
|
31
|
+
DevulturProgressContext,
|
|
32
|
+
useDevultur,
|
|
33
|
+
useDevulturApi,
|
|
34
|
+
useDevulturOptional,
|
|
35
|
+
useDevulturProgress,
|
|
36
|
+
useMediaUrl,
|
|
37
|
+
useRequestTokenWhen,
|
|
38
|
+
useVideoProps
|
|
39
|
+
} from "./chunk-BDEO67OT.js";
|
|
27
40
|
|
|
28
41
|
// src/react.tsx
|
|
29
|
-
import
|
|
42
|
+
import React26 from "react";
|
|
43
|
+
import { mediaPath as mediaPath5 } from "@devultur/core";
|
|
44
|
+
import { uploadFileWithProgress as uploadFileWithProgress2 } from "@devultur/react";
|
|
45
|
+
import { useAction as useAction3, useConvex, useMutation } from "convex/react";
|
|
46
|
+
|
|
47
|
+
// src/media-session.ts
|
|
48
|
+
import React2 from "react";
|
|
30
49
|
import {
|
|
31
|
-
buildMediaUrl
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
mediaPath as mediaPath3,
|
|
36
|
-
mediaUrl as mediaUrl4,
|
|
37
|
-
visibilityFromKey
|
|
50
|
+
buildMediaUrl,
|
|
51
|
+
mediaCaptionPath,
|
|
52
|
+
mediaUrl,
|
|
53
|
+
SESSION_HEADER
|
|
38
54
|
} from "@devultur/core";
|
|
39
|
-
import {
|
|
40
|
-
UploadHttpError,
|
|
41
|
-
uploadFileWithProgress
|
|
42
|
-
} from "@devultur/react";
|
|
43
|
-
import { useAction as useAction2, useConvex, useMutation, useQuery } from "convex/react";
|
|
44
|
-
|
|
45
|
-
// src/capability-error.ts
|
|
46
|
-
var DevulturCapabilityError = class extends Error {
|
|
47
|
-
code = "capability_unavailable";
|
|
48
|
-
capability;
|
|
49
|
-
constructor(capability) {
|
|
50
|
-
super(
|
|
51
|
-
`Devultur: \`${capability}\` isn't available under <DevulturViewerProvider>. Use <DevulturProvider api> with the actions it needs.`
|
|
52
|
-
);
|
|
53
|
-
this.name = "DevulturCapabilityError";
|
|
54
|
-
this.capability = capability;
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// src/media-fetch.ts
|
|
59
|
-
import { SESSION_HEADER } from "@devultur/core";
|
|
60
55
|
|
|
61
56
|
// src/image-recovery.ts
|
|
62
57
|
async function isSessionMismatch(response) {
|
|
@@ -68,104 +63,6 @@ async function isSessionMismatch(response) {
|
|
|
68
63
|
}
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
// src/media-fetch.ts
|
|
72
|
-
var MediaFetchError = class extends Error {
|
|
73
|
-
code;
|
|
74
|
-
/** HTTP status, for `http_error` when the delivery answered. */
|
|
75
|
-
status;
|
|
76
|
-
/** The code's extra fields, e.g. `page_cap`'s `{ shown, total }`. */
|
|
77
|
-
details;
|
|
78
|
-
constructor(code, message, status, options) {
|
|
79
|
-
super(message, options);
|
|
80
|
-
this.name = "MediaFetchError";
|
|
81
|
-
this.code = code;
|
|
82
|
-
this.status = status;
|
|
83
|
-
this.details = options?.details;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
86
|
-
function networkError(cause) {
|
|
87
|
-
return new MediaFetchError("http_error", "Media fetch failed: no response", void 0, { cause });
|
|
88
|
-
}
|
|
89
|
-
function pageCapError(shown, total) {
|
|
90
|
-
const message = `Showing the first ${shown} of ${total} pages`;
|
|
91
|
-
return new MediaFetchError("page_cap", message, void 0, { details: { shown, total } });
|
|
92
|
-
}
|
|
93
|
-
function renderFailedError(cause) {
|
|
94
|
-
return new MediaFetchError("render_failed", "The media couldn't be rendered", void 0, { cause });
|
|
95
|
-
}
|
|
96
|
-
function tokenUnavailableError(cause) {
|
|
97
|
-
return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
|
|
98
|
-
}
|
|
99
|
-
async function fetchMedia(request) {
|
|
100
|
-
const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
|
|
101
|
-
const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
|
|
102
|
-
const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
|
|
103
|
-
if (!isPrivate || !canReissue || response.status !== 401) return response;
|
|
104
|
-
if (!await isSessionMismatch(response)) return response;
|
|
105
|
-
request.onReissue?.();
|
|
106
|
-
const freshToken = await reissueToken();
|
|
107
|
-
if (!freshToken) return response;
|
|
108
|
-
return fetch(buildUrl(freshToken), { headers, signal });
|
|
109
|
-
}
|
|
110
|
-
async function loadMedia(request) {
|
|
111
|
-
const response = await fetchMedia(request);
|
|
112
|
-
if (!response.ok) {
|
|
113
|
-
throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
|
|
114
|
-
}
|
|
115
|
-
const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
|
|
116
|
-
if (!trusted.ok) {
|
|
117
|
-
await response.body?.cancel();
|
|
118
|
-
throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
|
|
119
|
-
}
|
|
120
|
-
const bytes = await readMediaBody(response, request.maxBytes);
|
|
121
|
-
return { bytes, contentType: trusted.contentType };
|
|
122
|
-
}
|
|
123
|
-
function tooLarge(maxBytes) {
|
|
124
|
-
return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
|
|
125
|
-
}
|
|
126
|
-
async function readMediaBody(response, maxBytes) {
|
|
127
|
-
if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
|
|
128
|
-
const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
129
|
-
if (declaredBytes > maxBytes) {
|
|
130
|
-
await response.body?.cancel();
|
|
131
|
-
throw tooLarge(maxBytes);
|
|
132
|
-
}
|
|
133
|
-
if (!response.body) return new Uint8Array(0);
|
|
134
|
-
return readCapped(response.body, maxBytes);
|
|
135
|
-
}
|
|
136
|
-
async function readCapped(body, maxBytes) {
|
|
137
|
-
const reader = body.getReader();
|
|
138
|
-
const chunks = [];
|
|
139
|
-
let byteCount = 0;
|
|
140
|
-
for (; ; ) {
|
|
141
|
-
const { done, value } = await reader.read();
|
|
142
|
-
if (done) break;
|
|
143
|
-
byteCount += value.byteLength;
|
|
144
|
-
if (byteCount > maxBytes) {
|
|
145
|
-
await reader.cancel();
|
|
146
|
-
throw tooLarge(maxBytes);
|
|
147
|
-
}
|
|
148
|
-
chunks.push(value);
|
|
149
|
-
}
|
|
150
|
-
const bytes = new Uint8Array(byteCount);
|
|
151
|
-
let offset = 0;
|
|
152
|
-
for (const chunk of chunks) {
|
|
153
|
-
bytes.set(chunk, offset);
|
|
154
|
-
offset += chunk.byteLength;
|
|
155
|
-
}
|
|
156
|
-
return bytes;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// src/media-session.ts
|
|
160
|
-
import React2 from "react";
|
|
161
|
-
import {
|
|
162
|
-
buildMediaUrl,
|
|
163
|
-
mediaCaptionPath,
|
|
164
|
-
mediaUrl,
|
|
165
|
-
SESSION_HEADER as SESSION_HEADER2
|
|
166
|
-
} from "@devultur/core";
|
|
167
|
-
import { useAction } from "convex/react";
|
|
168
|
-
|
|
169
66
|
// src/viewer-token.ts
|
|
170
67
|
import React from "react";
|
|
171
68
|
import { DEFAULT_TOKEN_EXPIRY_SECONDS } from "@devultur/core";
|
|
@@ -193,21 +90,25 @@ function sortKeys(json) {
|
|
|
193
90
|
function tokenArgsKey(tokenArgs) {
|
|
194
91
|
return JSON.stringify(sortKeys(convexToJson(tokenArgs ?? {})));
|
|
195
92
|
}
|
|
196
|
-
function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
93
|
+
function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs, isLazy = false }) {
|
|
197
94
|
const argsKey = tokenArgsKey(tokenArgs);
|
|
198
95
|
const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
|
|
199
96
|
const [issued, setIssued] = React.useState(null);
|
|
200
97
|
const [attempt, setAttempt] = React.useState(null);
|
|
98
|
+
const [isRequested, setIsRequested] = React.useState(!isLazy);
|
|
201
99
|
const token = issued?.argsKey === argsKey ? issued.token : null;
|
|
202
100
|
const currentAttempt = attempt?.argsKey === argsKey ? attempt : null;
|
|
203
101
|
const currentArgsKeyRef = React.useRef(argsKey);
|
|
204
102
|
const refreshTimerRef = React.useRef(null);
|
|
205
103
|
const inFlightRef = React.useRef(null);
|
|
104
|
+
const isMountedRef = React.useRef(true);
|
|
206
105
|
React.useEffect(() => {
|
|
207
106
|
currentArgsKeyRef.current = argsKey;
|
|
208
107
|
}, [argsKey]);
|
|
209
108
|
React.useEffect(() => {
|
|
109
|
+
isMountedRef.current = true;
|
|
210
110
|
return () => {
|
|
111
|
+
isMountedRef.current = false;
|
|
211
112
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
212
113
|
};
|
|
213
114
|
}, []);
|
|
@@ -216,7 +117,7 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
216
117
|
request().catch((error) => console.warn("Devultur: could not renew the viewer token", error));
|
|
217
118
|
};
|
|
218
119
|
const result = await issueViewerToken({ ...stableArgs, sessionSecret });
|
|
219
|
-
if (currentArgsKeyRef.current !== argsKey) return null;
|
|
120
|
+
if (!isMountedRef.current || currentArgsKeyRef.current !== argsKey) return null;
|
|
220
121
|
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
221
122
|
setIssued({ token: tokenValue, argsKey });
|
|
222
123
|
setAttempt(null);
|
|
@@ -241,10 +142,11 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
241
142
|
}, [request, argsKey]);
|
|
242
143
|
const hasAttempt = currentAttempt !== null;
|
|
243
144
|
React.useEffect(() => {
|
|
244
|
-
if (token || hasAttempt) return;
|
|
145
|
+
if (!isRequested || token || hasAttempt) return;
|
|
245
146
|
issueOnce().catch(() => {
|
|
246
147
|
});
|
|
247
|
-
}, [token, hasAttempt, issueOnce]);
|
|
148
|
+
}, [isRequested, token, hasAttempt, issueOnce]);
|
|
149
|
+
const requestToken = React.useCallback(() => setIsRequested(true), []);
|
|
248
150
|
const reissueToken = React.useCallback(
|
|
249
151
|
() => issueOnce().catch(() => {
|
|
250
152
|
setIssued((current) => current?.argsKey === argsKey ? null : current);
|
|
@@ -252,27 +154,37 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
252
154
|
}),
|
|
253
155
|
[issueOnce, argsKey]
|
|
254
156
|
);
|
|
255
|
-
const retryToken = React.useCallback(() =>
|
|
157
|
+
const retryToken = React.useCallback(() => {
|
|
158
|
+
setIsRequested(true);
|
|
159
|
+
return issueOnce().catch(() => null);
|
|
160
|
+
}, [issueOnce]);
|
|
256
161
|
const tokenStatus = token ? "ready" : currentAttempt?.status ?? "idle";
|
|
257
162
|
const tokenError = !token && currentAttempt?.status === "error" ? currentAttempt.error : null;
|
|
258
|
-
return { token, tokenStatus, tokenError, reissueToken, retryToken };
|
|
163
|
+
return { token, tokenStatus, tokenError, reissueToken, retryToken, requestToken };
|
|
259
164
|
}
|
|
260
165
|
|
|
261
166
|
// src/media-session.ts
|
|
262
|
-
function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
|
|
167
|
+
function useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy }) {
|
|
263
168
|
const [sessionSecret] = React2.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
issueViewerToken: issueViewerTokenAction,
|
|
169
|
+
const { token, tokenStatus, tokenError, reissueToken, retryToken, requestToken } = useViewerToken({
|
|
170
|
+
issueViewerToken,
|
|
267
171
|
sessionSecret,
|
|
268
|
-
tokenArgs
|
|
172
|
+
tokenArgs,
|
|
173
|
+
isLazy
|
|
269
174
|
});
|
|
175
|
+
const hasRequestedRef = React2.useRef(!isLazy);
|
|
176
|
+
const requestTokenAfterRender = React2.useCallback(() => {
|
|
177
|
+
if (hasRequestedRef.current) return;
|
|
178
|
+
hasRequestedRef.current = true;
|
|
179
|
+
queueMicrotask(requestToken);
|
|
180
|
+
}, [requestToken]);
|
|
270
181
|
const url = React2.useCallback(
|
|
271
182
|
(media, options) => {
|
|
272
183
|
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
184
|
+
if (media.visibility === "private" && !token) requestTokenAfterRender();
|
|
273
185
|
return mediaUrl(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
274
186
|
},
|
|
275
|
-
[baseUrl, token]
|
|
187
|
+
[baseUrl, token, requestTokenAfterRender]
|
|
276
188
|
);
|
|
277
189
|
const getCaptionsText = React2.useCallback(
|
|
278
190
|
async (media, locale) => {
|
|
@@ -280,10 +192,11 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
|
|
|
280
192
|
const path = mediaCaptionPath(media, locale);
|
|
281
193
|
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
282
194
|
const isPrivate = media.visibility === "private";
|
|
283
|
-
const fetchWith = (
|
|
284
|
-
headers: sessionSecret ? { [
|
|
195
|
+
const fetchWith = (viewerToken2) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken2 }), {
|
|
196
|
+
headers: sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0
|
|
285
197
|
});
|
|
286
|
-
|
|
198
|
+
const viewerToken = isPrivate ? token ?? await retryToken() : null;
|
|
199
|
+
let response = await fetchWith(viewerToken ?? void 0);
|
|
287
200
|
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
288
201
|
const fresh = await reissueToken();
|
|
289
202
|
if (fresh) response = await fetchWith(fresh);
|
|
@@ -291,7 +204,7 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
|
|
|
291
204
|
if (!response.ok) throw new Error(`Captions fetch failed: ${response.status}`);
|
|
292
205
|
return response.text();
|
|
293
206
|
},
|
|
294
|
-
[baseUrl, token, reissueToken, sessionSecret]
|
|
207
|
+
[baseUrl, token, reissueToken, retryToken, sessionSecret]
|
|
295
208
|
);
|
|
296
209
|
return React2.useMemo(
|
|
297
210
|
() => ({
|
|
@@ -299,137 +212,424 @@ function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
|
|
|
299
212
|
tokenStatus,
|
|
300
213
|
tokenError,
|
|
301
214
|
retryToken,
|
|
215
|
+
requestToken,
|
|
302
216
|
baseUrl: baseUrl ?? null,
|
|
303
217
|
sessionSecret,
|
|
304
218
|
url,
|
|
305
219
|
getCaptionsText,
|
|
306
220
|
reissueToken
|
|
307
221
|
}),
|
|
308
|
-
[
|
|
222
|
+
[
|
|
223
|
+
token,
|
|
224
|
+
tokenStatus,
|
|
225
|
+
tokenError,
|
|
226
|
+
retryToken,
|
|
227
|
+
requestToken,
|
|
228
|
+
baseUrl,
|
|
229
|
+
sessionSecret,
|
|
230
|
+
url,
|
|
231
|
+
getCaptionsText,
|
|
232
|
+
reissueToken
|
|
233
|
+
]
|
|
309
234
|
);
|
|
310
235
|
}
|
|
311
236
|
|
|
312
|
-
// src/
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
phase: "failed",
|
|
319
|
-
stage: "upload",
|
|
320
|
-
reason: failure.reason,
|
|
321
|
-
retriable: failure.retriable,
|
|
322
|
-
message: failure.message,
|
|
323
|
-
retry
|
|
324
|
-
};
|
|
237
|
+
// src/upload-hints.ts
|
|
238
|
+
async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
|
|
239
|
+
try {
|
|
240
|
+
await confirmUpload({ key, filename: file.name, ...hints });
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.warn("Devultur: could not record the upload; it is recorded later", error);
|
|
325
243
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
duration: videoState.duration,
|
|
337
|
-
captions: videoState.captions,
|
|
338
|
-
playerProps: videoState.playerProps
|
|
339
|
-
};
|
|
340
|
-
case "failed":
|
|
341
|
-
return {
|
|
342
|
-
phase: "failed",
|
|
343
|
-
stage: "transcode",
|
|
344
|
-
reason: videoState.reason,
|
|
345
|
-
retriable: videoState.retriable,
|
|
346
|
-
message: videoState.message,
|
|
347
|
-
retry
|
|
348
|
-
};
|
|
349
|
-
default:
|
|
350
|
-
return { phase: "queued", media };
|
|
244
|
+
}
|
|
245
|
+
async function readImageDimensions(file) {
|
|
246
|
+
if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
|
|
247
|
+
try {
|
|
248
|
+
const bitmap = await createImageBitmap(file);
|
|
249
|
+
const dimensions = { width: bitmap.width, height: bitmap.height };
|
|
250
|
+
bitmap.close();
|
|
251
|
+
return dimensions;
|
|
252
|
+
} catch {
|
|
253
|
+
return {};
|
|
351
254
|
}
|
|
352
255
|
}
|
|
353
256
|
|
|
354
257
|
// src/crop-upload.tsx
|
|
355
|
-
import
|
|
258
|
+
import React5 from "react";
|
|
356
259
|
import { ImageCropper } from "@devultur/react";
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
260
|
+
|
|
261
|
+
// src/use-media-upload.ts
|
|
262
|
+
import React4 from "react";
|
|
263
|
+
import { mediaPath as mediaPath2 } from "@devultur/core";
|
|
264
|
+
import { UploadHttpError, uploadFileWithProgress } from "@devultur/react";
|
|
265
|
+
import { useAction } from "convex/react";
|
|
266
|
+
|
|
267
|
+
// src/media-hooks.ts
|
|
268
|
+
import React3 from "react";
|
|
269
|
+
import {
|
|
270
|
+
buildMediaUrl as buildMediaUrl2,
|
|
271
|
+
CaptionLocaleLabel,
|
|
272
|
+
MediaStatus,
|
|
273
|
+
mediaCaptionPath as mediaCaptionPath2,
|
|
274
|
+
mediaPath
|
|
275
|
+
} from "@devultur/core";
|
|
276
|
+
import { useQuery } from "convex/react";
|
|
277
|
+
function useDevulturMedia(key) {
|
|
278
|
+
const apiCtx = React3.use(DevulturApiContext);
|
|
279
|
+
if (!apiCtx?.getMedia) {
|
|
280
|
+
throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
|
|
281
|
+
}
|
|
282
|
+
const record = useQuery(apiCtx.getMedia, key ? { key } : "skip");
|
|
283
|
+
if (!record) {
|
|
284
|
+
return {
|
|
285
|
+
status: MediaStatus.IDLE,
|
|
286
|
+
progress: null,
|
|
287
|
+
error: null,
|
|
288
|
+
failureReason: null,
|
|
289
|
+
retriable: null,
|
|
290
|
+
isQueued: false,
|
|
291
|
+
isTranscoding: false,
|
|
292
|
+
isCaptioning: false,
|
|
293
|
+
isReady: false,
|
|
294
|
+
isFailed: false,
|
|
295
|
+
playlistUrl: null,
|
|
296
|
+
thumbnailKey: null,
|
|
297
|
+
captionLocales: [],
|
|
298
|
+
duration: null,
|
|
299
|
+
isLoading: record === void 0
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const status = record.transcodeStatus ?? MediaStatus.IDLE;
|
|
303
|
+
const isReady = status === MediaStatus.READY;
|
|
304
|
+
const isFailed = status === MediaStatus.FAILED;
|
|
305
|
+
const progress = record.transcodeProgress ? {
|
|
306
|
+
current: record.transcodeProgress.current,
|
|
307
|
+
total: record.transcodeProgress.total,
|
|
308
|
+
percent: record.transcodeProgress.total > 0 ? Math.round(
|
|
309
|
+
record.transcodeProgress.current / record.transcodeProgress.total * 100
|
|
310
|
+
) : 0,
|
|
311
|
+
phase: record.transcodeProgress.phase
|
|
312
|
+
} : null;
|
|
313
|
+
return {
|
|
314
|
+
status,
|
|
315
|
+
progress,
|
|
316
|
+
error: record.transcodeError ?? null,
|
|
317
|
+
failureReason: isFailed ? record.failureReason ?? "internal" : null,
|
|
318
|
+
retriable: isFailed ? record.retriable ?? true : null,
|
|
319
|
+
isQueued: status === MediaStatus.QUEUED,
|
|
320
|
+
isTranscoding: status === MediaStatus.TRANSCODING,
|
|
321
|
+
isCaptioning: status === MediaStatus.CAPTIONING,
|
|
322
|
+
isReady,
|
|
323
|
+
isFailed,
|
|
324
|
+
playlistUrl: record.playlistUrl ?? null,
|
|
325
|
+
thumbnailKey: record.thumbnailKey ?? null,
|
|
326
|
+
captionLocales: record.captionLocales ?? [],
|
|
327
|
+
duration: record.duration ?? null,
|
|
328
|
+
isLoading: false
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function useVideoState(media, options) {
|
|
332
|
+
const devultur = useDevultur();
|
|
333
|
+
const { token, baseUrl, sessionSecret } = devultur;
|
|
334
|
+
useRequestTokenWhen(devultur, media?.kind === "video" && media.visibility === "private");
|
|
335
|
+
const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
|
|
336
|
+
const locales = options?.locales;
|
|
337
|
+
return React3.useMemo(() => {
|
|
338
|
+
if (media?.kind !== "video") return { phase: "idle" };
|
|
339
|
+
if (status.isFailed) {
|
|
340
|
+
return {
|
|
341
|
+
phase: "failed",
|
|
342
|
+
reason: status.failureReason ?? "internal",
|
|
343
|
+
message: status.error ?? "Video processing failed",
|
|
344
|
+
retriable: status.retriable ?? true
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
if (status.isReady) {
|
|
348
|
+
if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
|
|
349
|
+
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
350
|
+
const playlist = buildMediaUrl2(baseUrl, mediaPath(media), { token: viewerToken });
|
|
351
|
+
const captions = (locales ?? status.captionLocales).flatMap((locale) => {
|
|
352
|
+
const path = mediaCaptionPath2(media, locale);
|
|
353
|
+
if (!path) return [];
|
|
354
|
+
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
355
|
+
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
phase: "ready",
|
|
359
|
+
media,
|
|
360
|
+
playlist,
|
|
361
|
+
duration: status.duration ?? 0,
|
|
362
|
+
captions,
|
|
363
|
+
playerProps: {
|
|
364
|
+
src: playlist,
|
|
365
|
+
token: viewerToken,
|
|
366
|
+
captions,
|
|
367
|
+
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (status.isQueued) return { phase: "queued" };
|
|
372
|
+
if (status.isTranscoding || status.isCaptioning) {
|
|
373
|
+
const step = status.progress?.phase ?? (status.isCaptioning ? "captioning" : "encoding");
|
|
374
|
+
return { phase: "processing", step, percent: status.progress?.percent ?? 0 };
|
|
375
|
+
}
|
|
376
|
+
return { phase: "idle" };
|
|
377
|
+
}, [media, status, baseUrl, token, locales, sessionSecret]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/media-upload-derive.ts
|
|
381
|
+
function deriveUploadState(local, videoState, retry) {
|
|
382
|
+
const { phase, percent, file, failure, media, processRequested } = local;
|
|
383
|
+
if (phase === "uploading") return { phase: "uploading", percent, file };
|
|
384
|
+
if (phase === "upload-failed" && failure) {
|
|
385
|
+
return {
|
|
386
|
+
phase: "failed",
|
|
387
|
+
stage: "upload",
|
|
388
|
+
reason: failure.reason,
|
|
389
|
+
retriable: failure.retriable,
|
|
390
|
+
message: failure.message,
|
|
391
|
+
retry
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
if (phase === "idle" || !media) return { phase: "idle" };
|
|
395
|
+
if (media.kind !== "video" || !processRequested) return { phase: "ready", media };
|
|
396
|
+
switch (videoState.phase) {
|
|
397
|
+
case "processing":
|
|
398
|
+
return { phase: "processing", media, step: videoState.step, percent: videoState.percent };
|
|
399
|
+
case "ready":
|
|
400
|
+
return {
|
|
401
|
+
phase: "ready",
|
|
402
|
+
media,
|
|
403
|
+
playlist: videoState.playlist,
|
|
404
|
+
duration: videoState.duration,
|
|
405
|
+
captions: videoState.captions,
|
|
406
|
+
playerProps: videoState.playerProps
|
|
407
|
+
};
|
|
408
|
+
case "failed":
|
|
409
|
+
return {
|
|
410
|
+
phase: "failed",
|
|
411
|
+
stage: "transcode",
|
|
412
|
+
reason: videoState.reason,
|
|
413
|
+
retriable: videoState.retriable,
|
|
414
|
+
message: videoState.message,
|
|
415
|
+
retry
|
|
416
|
+
};
|
|
417
|
+
default:
|
|
418
|
+
return { phase: "queued", media };
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/use-media-upload.ts
|
|
423
|
+
function useMediaUpload(options) {
|
|
424
|
+
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
425
|
+
const api = useDevulturApi();
|
|
426
|
+
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
427
|
+
const deleteMediaAction = useAction(api.deleteMedia);
|
|
428
|
+
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
429
|
+
const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
|
|
430
|
+
const [phase, setPhase] = React4.useState("idle");
|
|
431
|
+
const [percent, setPercent] = React4.useState(0);
|
|
432
|
+
const [currentFile, setCurrentFile] = React4.useState(null);
|
|
433
|
+
const [media, setMedia] = React4.useState(null);
|
|
434
|
+
const [uploadFailure, setUploadFailure] = React4.useState(null);
|
|
435
|
+
const [processRequested, setProcessRequested] = React4.useState(false);
|
|
436
|
+
const abortRef = React4.useRef(null);
|
|
437
|
+
const lastFileRef = React4.useRef(null);
|
|
438
|
+
const lastKeyRef = React4.useRef(null);
|
|
439
|
+
const videoState = useVideoState(media, { locales });
|
|
440
|
+
const triggerProcess = React4.useCallback(
|
|
441
|
+
(key) => {
|
|
442
|
+
if (!api.processVideo) {
|
|
443
|
+
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
444
|
+
}
|
|
445
|
+
lastKeyRef.current = key;
|
|
446
|
+
setProcessRequested(true);
|
|
447
|
+
processVideoAction({ key }).catch((err) => {
|
|
448
|
+
console.error("useMediaUpload: could not request the transcode", err);
|
|
449
|
+
});
|
|
450
|
+
},
|
|
451
|
+
[api.processVideo, processVideoAction]
|
|
452
|
+
);
|
|
453
|
+
const runUpload = React4.useCallback(
|
|
454
|
+
async (file) => {
|
|
455
|
+
const controller = new AbortController();
|
|
456
|
+
abortRef.current = controller;
|
|
457
|
+
lastFileRef.current = file;
|
|
458
|
+
setUploadFailure(null);
|
|
459
|
+
setMedia(null);
|
|
460
|
+
setProcessRequested(false);
|
|
461
|
+
setCurrentFile(file);
|
|
462
|
+
setPercent(0);
|
|
463
|
+
setPhase("uploading");
|
|
464
|
+
const fail = (reason, message) => {
|
|
465
|
+
const failure = { stage: "upload", reason, retriable: true, message };
|
|
466
|
+
setUploadFailure(failure);
|
|
467
|
+
setPhase("upload-failed");
|
|
468
|
+
onError?.(failure);
|
|
469
|
+
return null;
|
|
470
|
+
};
|
|
471
|
+
let result;
|
|
472
|
+
try {
|
|
473
|
+
result = await createUploadUrlAction({
|
|
474
|
+
filename: file.name,
|
|
475
|
+
contentType: file.type,
|
|
476
|
+
visibility
|
|
477
|
+
});
|
|
478
|
+
} catch (err) {
|
|
479
|
+
return fail("rejected", err instanceof Error ? err.message : "Could not start the upload");
|
|
480
|
+
}
|
|
481
|
+
const dimensions = await readImageDimensions(file);
|
|
482
|
+
try {
|
|
483
|
+
await uploadFileWithProgress(result.url, file, { onProgress: setPercent, signal: controller.signal });
|
|
484
|
+
} catch (err) {
|
|
485
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
486
|
+
setPhase("idle");
|
|
487
|
+
setCurrentFile(null);
|
|
488
|
+
setPercent(0);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
|
|
492
|
+
return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
|
|
493
|
+
}
|
|
494
|
+
if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
|
|
495
|
+
const uploaded = {
|
|
496
|
+
id: result.id,
|
|
497
|
+
kind: result.kind,
|
|
498
|
+
visibility: result.visibility,
|
|
499
|
+
filename: file.name,
|
|
500
|
+
key: result.key,
|
|
501
|
+
...dimensions
|
|
502
|
+
};
|
|
503
|
+
setMedia(uploaded);
|
|
504
|
+
setPhase("uploaded");
|
|
505
|
+
if (replaces) deleteMediaAction({ key: mediaPath2(replaces) }).catch(() => {
|
|
506
|
+
});
|
|
507
|
+
onComplete?.(uploaded);
|
|
508
|
+
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
509
|
+
return uploaded;
|
|
510
|
+
},
|
|
511
|
+
[
|
|
512
|
+
createUploadUrlAction,
|
|
513
|
+
confirmUploadAction,
|
|
514
|
+
api.confirmUpload,
|
|
515
|
+
deleteMediaAction,
|
|
516
|
+
triggerProcess,
|
|
517
|
+
visibility,
|
|
518
|
+
replaces,
|
|
519
|
+
process,
|
|
520
|
+
onComplete,
|
|
521
|
+
onError
|
|
522
|
+
]
|
|
523
|
+
);
|
|
524
|
+
const cancel = React4.useCallback(() => {
|
|
525
|
+
abortRef.current?.abort();
|
|
526
|
+
}, []);
|
|
527
|
+
const reset = React4.useCallback(() => {
|
|
528
|
+
abortRef.current?.abort();
|
|
529
|
+
setPhase("idle");
|
|
530
|
+
setPercent(0);
|
|
531
|
+
setCurrentFile(null);
|
|
532
|
+
setMedia(null);
|
|
533
|
+
setUploadFailure(null);
|
|
534
|
+
setProcessRequested(false);
|
|
535
|
+
}, []);
|
|
536
|
+
const retry = React4.useCallback(() => {
|
|
537
|
+
if (phase === "upload-failed" && lastFileRef.current) {
|
|
538
|
+
runUpload(lastFileRef.current);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
542
|
+
}, [phase, runUpload, triggerProcess]);
|
|
543
|
+
React4.useEffect(() => () => abortRef.current?.abort(), []);
|
|
544
|
+
const state = React4.useMemo(
|
|
545
|
+
() => deriveUploadState(
|
|
546
|
+
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
547
|
+
videoState,
|
|
548
|
+
retry
|
|
549
|
+
),
|
|
550
|
+
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
551
|
+
);
|
|
552
|
+
const upload = React4.useCallback((file) => runUpload(file), [runUpload]);
|
|
553
|
+
return { upload, state, cancel, reset };
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/crop-upload.tsx
|
|
557
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
558
|
+
var DEFAULT_LABELS = {
|
|
559
|
+
zoom: "Zoom",
|
|
560
|
+
confirm: "Crop and upload",
|
|
561
|
+
processing: "Processing\u2026",
|
|
562
|
+
cancel: "Cancel"
|
|
563
|
+
};
|
|
564
|
+
function CropUpload({
|
|
565
|
+
aspectRatio,
|
|
566
|
+
visibility,
|
|
567
|
+
outputFormat,
|
|
568
|
+
maxWidth,
|
|
569
|
+
minSourceWidth,
|
|
570
|
+
replaces,
|
|
571
|
+
onComplete,
|
|
572
|
+
onReject,
|
|
573
|
+
accept = ["image/*"],
|
|
574
|
+
cropperClassName,
|
|
575
|
+
labels,
|
|
576
|
+
children
|
|
577
|
+
}) {
|
|
578
|
+
const text = { ...DEFAULT_LABELS, ...labels };
|
|
579
|
+
const [pendingFile, setPendingFile] = React5.useState(null);
|
|
580
|
+
const inputRef = React5.useRef(null);
|
|
581
|
+
const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
|
|
582
|
+
const pickFile = React5.useCallback(() => inputRef.current?.click(), []);
|
|
583
|
+
const handleCrop = React5.useCallback(
|
|
584
|
+
(baked) => {
|
|
585
|
+
setPendingFile(null);
|
|
586
|
+
upload(baked);
|
|
587
|
+
},
|
|
588
|
+
[upload]
|
|
589
|
+
);
|
|
590
|
+
const resetAll = React5.useCallback(() => {
|
|
591
|
+
setPendingFile(null);
|
|
592
|
+
reset();
|
|
593
|
+
}, [reset]);
|
|
594
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
595
|
+
/* @__PURE__ */ jsx(
|
|
596
|
+
"input",
|
|
597
|
+
{
|
|
598
|
+
ref: inputRef,
|
|
599
|
+
type: "file",
|
|
600
|
+
accept: accept.join(","),
|
|
601
|
+
hidden: true,
|
|
602
|
+
onChange: (event) => {
|
|
603
|
+
const file = event.target.files?.[0];
|
|
604
|
+
if (file) setPendingFile(file);
|
|
605
|
+
event.target.value = "";
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
),
|
|
609
|
+
pendingFile && /* @__PURE__ */ jsx(
|
|
610
|
+
ImageCropper,
|
|
611
|
+
{
|
|
612
|
+
file: pendingFile,
|
|
613
|
+
aspectRatio,
|
|
614
|
+
outputFormat,
|
|
615
|
+
maxWidth,
|
|
616
|
+
minSourceWidth,
|
|
617
|
+
onCrop: handleCrop,
|
|
618
|
+
onReject,
|
|
619
|
+
className: cropperClassName,
|
|
620
|
+
children: ({ zoom, setZoom, confirm, isBaking }) => /* @__PURE__ */ jsxs("div", { className: "dv-crop-controls", children: [
|
|
621
|
+
/* @__PURE__ */ jsx(
|
|
622
|
+
"input",
|
|
623
|
+
{
|
|
624
|
+
type: "range",
|
|
625
|
+
min: 1,
|
|
626
|
+
max: 3,
|
|
627
|
+
step: 0.01,
|
|
628
|
+
value: zoom,
|
|
629
|
+
onChange: (event) => setZoom(Number(event.target.value)),
|
|
630
|
+
"aria-label": text.zoom
|
|
631
|
+
}
|
|
632
|
+
),
|
|
433
633
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: confirm, disabled: isBaking, children: isBaking ? text.processing : text.confirm }),
|
|
434
634
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setPendingFile(null), children: text.cancel })
|
|
435
635
|
] })
|
|
@@ -439,51 +639,182 @@ function CropUpload({
|
|
|
439
639
|
] });
|
|
440
640
|
}
|
|
441
641
|
|
|
442
|
-
// src/document-renderers.tsx
|
|
443
|
-
import
|
|
444
|
-
|
|
445
|
-
// src/attachment-icons.tsx
|
|
446
|
-
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
447
|
-
var SHEET = "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z M14 3v5h5";
|
|
448
|
-
var ICON_PATHS = {
|
|
449
|
-
file: SHEET,
|
|
450
|
-
text: `${SHEET} M9 13h6 M9 17h6`,
|
|
451
|
-
pdf: `${SHEET} M8.5 13.5h2a1 1 0 0 1 0 2h-2v-2 M8.5 15.5V17 M13.5 13.5V17h1a1.5 1.5 0 0 0 0-3.5z`,
|
|
452
|
-
image: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z M21 15l-5-5L5 21 M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z",
|
|
453
|
-
video: "M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z M10 9l5 3-5 3z",
|
|
454
|
-
audio: "M9 18V5l12-2v13 M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
|
|
455
|
-
};
|
|
456
|
-
function AttachmentIcon({ kind }) {
|
|
457
|
-
return /* @__PURE__ */ jsx2(
|
|
458
|
-
"svg",
|
|
459
|
-
{
|
|
460
|
-
"aria-hidden": "true",
|
|
461
|
-
focusable: "false",
|
|
462
|
-
width: "24",
|
|
463
|
-
height: "24",
|
|
464
|
-
viewBox: "0 0 24 24",
|
|
465
|
-
fill: "none",
|
|
466
|
-
stroke: "currentColor",
|
|
467
|
-
strokeWidth: "1.5",
|
|
468
|
-
strokeLinecap: "round",
|
|
469
|
-
strokeLinejoin: "round",
|
|
470
|
-
"data-part": "icon",
|
|
471
|
-
"data-kind": kind,
|
|
472
|
-
children: /* @__PURE__ */ jsx2("path", { d: ICON_PATHS[kind] })
|
|
473
|
-
}
|
|
642
|
+
// src/document-renderers.tsx
|
|
643
|
+
import React7 from "react";
|
|
644
|
+
|
|
645
|
+
// src/attachment-icons.tsx
|
|
646
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
647
|
+
var SHEET = "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z M14 3v5h5";
|
|
648
|
+
var ICON_PATHS = {
|
|
649
|
+
file: SHEET,
|
|
650
|
+
text: `${SHEET} M9 13h6 M9 17h6`,
|
|
651
|
+
pdf: `${SHEET} M8.5 13.5h2a1 1 0 0 1 0 2h-2v-2 M8.5 15.5V17 M13.5 13.5V17h1a1.5 1.5 0 0 0 0-3.5z`,
|
|
652
|
+
image: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z M21 15l-5-5L5 21 M8.5 10a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z",
|
|
653
|
+
video: "M4 5h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z M10 9l5 3-5 3z",
|
|
654
|
+
audio: "M9 18V5l12-2v13 M6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6z M18 19a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"
|
|
655
|
+
};
|
|
656
|
+
function AttachmentIcon({ kind }) {
|
|
657
|
+
return /* @__PURE__ */ jsx2(
|
|
658
|
+
"svg",
|
|
659
|
+
{
|
|
660
|
+
"aria-hidden": "true",
|
|
661
|
+
focusable: "false",
|
|
662
|
+
width: "24",
|
|
663
|
+
height: "24",
|
|
664
|
+
viewBox: "0 0 24 24",
|
|
665
|
+
fill: "none",
|
|
666
|
+
stroke: "currentColor",
|
|
667
|
+
strokeWidth: "1.5",
|
|
668
|
+
strokeLinecap: "round",
|
|
669
|
+
strokeLinejoin: "round",
|
|
670
|
+
"data-part": "icon",
|
|
671
|
+
"data-kind": kind,
|
|
672
|
+
children: /* @__PURE__ */ jsx2("path", { d: ICON_PATHS[kind] })
|
|
673
|
+
}
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/download-link.tsx
|
|
678
|
+
import React6 from "react";
|
|
679
|
+
import { mediaUrl as mediaUrl2 } from "@devultur/core";
|
|
680
|
+
|
|
681
|
+
// src/media-fetch.ts
|
|
682
|
+
import { SESSION_HEADER as SESSION_HEADER2 } from "@devultur/core";
|
|
683
|
+
var MediaFetchError = class extends Error {
|
|
684
|
+
code;
|
|
685
|
+
/** HTTP status, for `http_error` when the delivery answered. */
|
|
686
|
+
status;
|
|
687
|
+
/** The code's extra fields, e.g. `page_cap`'s `{ shown, total }`. */
|
|
688
|
+
details;
|
|
689
|
+
constructor(code, message, status, options) {
|
|
690
|
+
super(message, options);
|
|
691
|
+
this.name = "MediaFetchError";
|
|
692
|
+
this.code = code;
|
|
693
|
+
this.status = status;
|
|
694
|
+
this.details = options?.details;
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
function networkError(cause) {
|
|
698
|
+
return new MediaFetchError("http_error", "Media fetch failed: no response", void 0, { cause });
|
|
699
|
+
}
|
|
700
|
+
function pageCapError(shown, total) {
|
|
701
|
+
const message = `Showing the first ${shown} of ${total} pages`;
|
|
702
|
+
return new MediaFetchError("page_cap", message, void 0, { details: { shown, total } });
|
|
703
|
+
}
|
|
704
|
+
function renderFailedError(cause) {
|
|
705
|
+
return new MediaFetchError("render_failed", "The media couldn't be rendered", void 0, { cause });
|
|
706
|
+
}
|
|
707
|
+
function tokenUnavailableError(cause) {
|
|
708
|
+
return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
|
|
709
|
+
}
|
|
710
|
+
async function fetchMedia(request) {
|
|
711
|
+
const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
|
|
712
|
+
const headers = isPrivate && sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0;
|
|
713
|
+
const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
|
|
714
|
+
if (!isPrivate || !canReissue || response.status !== 401) return response;
|
|
715
|
+
if (!await isSessionMismatch(response)) return response;
|
|
716
|
+
request.onReissue?.();
|
|
717
|
+
const freshToken = await reissueToken();
|
|
718
|
+
if (!freshToken) return response;
|
|
719
|
+
return fetch(buildUrl(freshToken), { headers, signal });
|
|
720
|
+
}
|
|
721
|
+
async function loadMedia(request) {
|
|
722
|
+
const response = await fetchMedia(request);
|
|
723
|
+
if (!response.ok) {
|
|
724
|
+
throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
|
|
725
|
+
}
|
|
726
|
+
const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
|
|
727
|
+
if (!trusted.ok) {
|
|
728
|
+
await response.body?.cancel();
|
|
729
|
+
throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
|
|
730
|
+
}
|
|
731
|
+
const bytes = await readMediaBody(response, request.maxBytes);
|
|
732
|
+
return { bytes, contentType: trusted.contentType };
|
|
733
|
+
}
|
|
734
|
+
function tooLarge(maxBytes) {
|
|
735
|
+
return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
|
|
736
|
+
}
|
|
737
|
+
async function readMediaBody(response, maxBytes) {
|
|
738
|
+
if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
|
|
739
|
+
const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
740
|
+
if (declaredBytes > maxBytes) {
|
|
741
|
+
await response.body?.cancel();
|
|
742
|
+
throw tooLarge(maxBytes);
|
|
743
|
+
}
|
|
744
|
+
if (!response.body) return new Uint8Array(0);
|
|
745
|
+
return readCapped(response.body, maxBytes);
|
|
746
|
+
}
|
|
747
|
+
async function readCapped(body, maxBytes) {
|
|
748
|
+
const reader = body.getReader();
|
|
749
|
+
const chunks = [];
|
|
750
|
+
let byteCount = 0;
|
|
751
|
+
for (; ; ) {
|
|
752
|
+
const { done, value } = await reader.read();
|
|
753
|
+
if (done) break;
|
|
754
|
+
byteCount += value.byteLength;
|
|
755
|
+
if (byteCount > maxBytes) {
|
|
756
|
+
await reader.cancel();
|
|
757
|
+
throw tooLarge(maxBytes);
|
|
758
|
+
}
|
|
759
|
+
chunks.push(value);
|
|
760
|
+
}
|
|
761
|
+
const bytes = new Uint8Array(byteCount);
|
|
762
|
+
let offset = 0;
|
|
763
|
+
for (const chunk of chunks) {
|
|
764
|
+
bytes.set(chunk, offset);
|
|
765
|
+
offset += chunk.byteLength;
|
|
766
|
+
}
|
|
767
|
+
return bytes;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// src/download-link.tsx
|
|
771
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
772
|
+
function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
|
|
773
|
+
const devultur = useDevultur();
|
|
774
|
+
const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = devultur;
|
|
775
|
+
useRequestTokenWhen(devultur, media?.visibility === "private");
|
|
776
|
+
const handleClick = React6.useCallback(
|
|
777
|
+
async (event) => {
|
|
778
|
+
if (media?.visibility !== "private" || !baseUrl) return;
|
|
779
|
+
event.preventDefault();
|
|
780
|
+
if (tokenStatus === "error") {
|
|
781
|
+
onDownloadError?.(tokenUnavailableError(tokenError));
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const response = await fetchMedia({
|
|
785
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
786
|
+
token,
|
|
787
|
+
sessionSecret,
|
|
788
|
+
reissueToken
|
|
789
|
+
});
|
|
790
|
+
if (!response.ok) {
|
|
791
|
+
const status = response.status;
|
|
792
|
+
onDownloadError?.(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const objectUrl = URL.createObjectURL(await response.blob());
|
|
796
|
+
const anchor = document.createElement("a");
|
|
797
|
+
anchor.href = objectUrl;
|
|
798
|
+
anchor.download = filename ?? media.filename ?? "download";
|
|
799
|
+
anchor.click();
|
|
800
|
+
URL.revokeObjectURL(objectUrl);
|
|
801
|
+
},
|
|
802
|
+
[media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
|
|
474
803
|
);
|
|
804
|
+
if (!media) return null;
|
|
805
|
+
return /* @__PURE__ */ jsx3("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
|
|
475
806
|
}
|
|
476
807
|
|
|
477
808
|
// src/document-renderers.tsx
|
|
478
|
-
import { jsx as
|
|
809
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
479
810
|
function DocumentLink({ media, download = false, children, labels, ...anchorProps }) {
|
|
480
811
|
const { url } = useDevultur();
|
|
481
812
|
if (!media) return null;
|
|
482
813
|
const text = children ?? attachmentName(media, mergeAttachmentLabels(labels).untitled);
|
|
483
814
|
if (download || media.visibility === "private") {
|
|
484
|
-
return /* @__PURE__ */
|
|
815
|
+
return /* @__PURE__ */ jsx4(DownloadLink, { media, ...anchorProps, children: text });
|
|
485
816
|
}
|
|
486
|
-
return /* @__PURE__ */
|
|
817
|
+
return /* @__PURE__ */ jsx4("a", { href: url(media), ...anchorProps, children: text });
|
|
487
818
|
}
|
|
488
819
|
function AttachmentCard({
|
|
489
820
|
media,
|
|
@@ -495,26 +826,26 @@ function AttachmentCard({
|
|
|
495
826
|
labels,
|
|
496
827
|
...articleProps
|
|
497
828
|
}) {
|
|
498
|
-
const headingId =
|
|
829
|
+
const headingId = React7.useId();
|
|
499
830
|
if (!media) return null;
|
|
500
831
|
const text = mergeAttachmentLabels(labels);
|
|
501
832
|
const kind = attachmentKindOf(media, contentType);
|
|
502
833
|
const details = attachmentDetails(media, kind, text, locale ?? text.locale);
|
|
503
834
|
const Heading = `h${headingLevel}`;
|
|
504
835
|
return /* @__PURE__ */ jsxs2("article", { "aria-labelledby": headingId, "data-kind": kind, ...articleProps, children: [
|
|
505
|
-
preview ?? /* @__PURE__ */
|
|
506
|
-
/* @__PURE__ */
|
|
507
|
-
/* @__PURE__ */
|
|
836
|
+
preview ?? /* @__PURE__ */ jsx4(AttachmentIcon, { kind }),
|
|
837
|
+
/* @__PURE__ */ jsx4(Heading, { id: headingId, "data-part": "name", children: attachmentName(media, text.untitled) }),
|
|
838
|
+
/* @__PURE__ */ jsx4("p", { "data-part": "details", children: details.join(" \xB7 ") }),
|
|
508
839
|
/* @__PURE__ */ jsxs2("div", { "data-part": "actions", children: [
|
|
509
|
-
/* @__PURE__ */
|
|
510
|
-
onOpen && /* @__PURE__ */
|
|
840
|
+
/* @__PURE__ */ jsx4(DownloadLink, { media, "aria-describedby": headingId, "data-part": "download", children: text.download }),
|
|
841
|
+
onOpen && /* @__PURE__ */ jsx4("button", { type: "button", onClick: () => onOpen(media), "aria-describedby": headingId, "data-part": "open", children: text.open })
|
|
511
842
|
] })
|
|
512
843
|
] });
|
|
513
844
|
}
|
|
514
845
|
|
|
515
846
|
// src/image.tsx
|
|
516
|
-
import
|
|
517
|
-
import { mediaUrl as
|
|
847
|
+
import React8 from "react";
|
|
848
|
+
import { mediaUrl as mediaUrl3 } from "@devultur/core";
|
|
518
849
|
|
|
519
850
|
// src/responsive-image.ts
|
|
520
851
|
function normalizeWidths(widths) {
|
|
@@ -558,7 +889,7 @@ function renderedWidthFromSizes(sizes) {
|
|
|
558
889
|
}
|
|
559
890
|
|
|
560
891
|
// src/image.tsx
|
|
561
|
-
import { Fragment, jsx as
|
|
892
|
+
import { Fragment, jsx as jsx5 } from "react/jsx-runtime";
|
|
562
893
|
function Image({
|
|
563
894
|
media,
|
|
564
895
|
width,
|
|
@@ -605,14 +936,14 @@ function Image({
|
|
|
605
936
|
} else {
|
|
606
937
|
src = url(media, transforms);
|
|
607
938
|
}
|
|
608
|
-
if (!src) return /* @__PURE__ */
|
|
939
|
+
if (!src) return /* @__PURE__ */ jsx5(Fragment, { children: fallback });
|
|
609
940
|
if (render) {
|
|
610
|
-
return
|
|
941
|
+
return React8.cloneElement(render, {
|
|
611
942
|
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
612
943
|
});
|
|
613
944
|
}
|
|
614
945
|
const lazyProps = responsiveWidths ? { loading: "lazy", decoding: "async" } : {};
|
|
615
|
-
return /* @__PURE__ */
|
|
946
|
+
return /* @__PURE__ */ jsx5(
|
|
616
947
|
"img",
|
|
617
948
|
{
|
|
618
949
|
src,
|
|
@@ -627,26 +958,28 @@ function Image({
|
|
|
627
958
|
);
|
|
628
959
|
}
|
|
629
960
|
function usePrivateImage(media, transforms, onLoadError) {
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
const [
|
|
633
|
-
const
|
|
634
|
-
|
|
961
|
+
const devultur = useDevultur();
|
|
962
|
+
const { token, tokenStatus, tokenError, baseUrl, reissueToken, sessionSecret } = devultur;
|
|
963
|
+
const [isFailed, setIsFailed] = React8.useState(false);
|
|
964
|
+
const [blobSrc, setBlobSrc] = React8.useState(null);
|
|
965
|
+
const sessionRef = React8.useRef({ token, reissueToken, onLoadError });
|
|
966
|
+
React8.useEffect(() => {
|
|
635
967
|
sessionRef.current = { token, reissueToken, onLoadError };
|
|
636
968
|
});
|
|
637
969
|
const isPrivate = media?.visibility === "private";
|
|
970
|
+
useRequestTokenWhen(devultur, isPrivate);
|
|
638
971
|
const hasToken = token !== null;
|
|
639
972
|
const isTokenUnavailable = isPrivate && tokenStatus === "error";
|
|
640
973
|
const { width, height, fit, format, quality } = transforms;
|
|
641
|
-
|
|
974
|
+
React8.useEffect(() => {
|
|
642
975
|
setIsFailed(false);
|
|
643
976
|
setBlobSrc(null);
|
|
644
977
|
}, [media]);
|
|
645
|
-
|
|
978
|
+
React8.useEffect(() => {
|
|
646
979
|
if (!isTokenUnavailable || !media) return;
|
|
647
980
|
sessionRef.current.onLoadError?.(tokenUnavailableError(tokenError));
|
|
648
981
|
}, [isTokenUnavailable, media, tokenError]);
|
|
649
|
-
|
|
982
|
+
React8.useEffect(() => {
|
|
650
983
|
if (!isPrivate || isFailed || !media || !hasToken) return;
|
|
651
984
|
let cancelled = false;
|
|
652
985
|
let objectUrl = null;
|
|
@@ -660,7 +993,7 @@ function usePrivateImage(media, transforms, onLoadError) {
|
|
|
660
993
|
(async () => {
|
|
661
994
|
try {
|
|
662
995
|
const response = await fetchMedia({
|
|
663
|
-
buildUrl: (viewerToken) =>
|
|
996
|
+
buildUrl: (viewerToken) => mediaUrl3(baseUrl, media, { ...fetchTransforms, token: viewerToken }),
|
|
664
997
|
token: sessionRef.current.token,
|
|
665
998
|
sessionSecret,
|
|
666
999
|
reissueToken: sessionRef.current.reissueToken
|
|
@@ -685,41 +1018,42 @@ function usePrivateImage(media, transforms, onLoadError) {
|
|
|
685
1018
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
686
1019
|
};
|
|
687
1020
|
}, [isPrivate, isFailed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
688
|
-
const handleError =
|
|
1021
|
+
const handleError = React8.useCallback(() => setIsFailed(true), []);
|
|
689
1022
|
return { blobSrc, isFailed: isFailed || isTokenUnavailable, handleError };
|
|
690
1023
|
}
|
|
691
1024
|
|
|
692
1025
|
// src/media-library.tsx
|
|
693
|
-
import
|
|
1026
|
+
import React17 from "react";
|
|
694
1027
|
|
|
695
1028
|
// src/media-library-item.tsx
|
|
696
|
-
import
|
|
1029
|
+
import React14 from "react";
|
|
697
1030
|
|
|
698
1031
|
// src/pdf-thumbnail.tsx
|
|
699
|
-
import
|
|
1032
|
+
import React13 from "react";
|
|
700
1033
|
|
|
701
1034
|
// src/media-resource.ts
|
|
702
|
-
import
|
|
703
|
-
import { mediaPath, mediaUrl as
|
|
1035
|
+
import React9 from "react";
|
|
1036
|
+
import { mediaPath as mediaPath3, mediaUrl as mediaUrl4 } from "@devultur/core";
|
|
704
1037
|
var IDLE = { value: null, contentType: null, isLoading: false, error: null };
|
|
705
1038
|
var LOADING = { value: null, contentType: null, isLoading: true, error: null };
|
|
706
1039
|
function useMediaResource(media, options) {
|
|
707
1040
|
const devultur = useDevultur();
|
|
708
1041
|
const { isEnabled, maxBytes, contentType } = options;
|
|
709
1042
|
const isPrivate = media?.visibility === "private";
|
|
1043
|
+
useRequestTokenWhen(devultur, isPrivate && isEnabled);
|
|
710
1044
|
const isReady = !isPrivate || devultur.token !== null;
|
|
711
1045
|
const isTokenUnavailable = isPrivate && devultur.tokenStatus === "error";
|
|
712
|
-
const tokenFailure =
|
|
1046
|
+
const tokenFailure = React9.useMemo(
|
|
713
1047
|
() => isTokenUnavailable ? tokenUnavailableError(devultur.tokenError) : null,
|
|
714
1048
|
[isTokenUnavailable, devultur.tokenError]
|
|
715
1049
|
);
|
|
716
|
-
const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility,
|
|
717
|
-
const [settled, setSettled] =
|
|
718
|
-
const inputsRef =
|
|
719
|
-
|
|
1050
|
+
const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath3(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
|
|
1051
|
+
const [settled, setSettled] = React9.useState(null);
|
|
1052
|
+
const inputsRef = React9.useRef({ media, devultur, options });
|
|
1053
|
+
React9.useEffect(() => {
|
|
720
1054
|
inputsRef.current = { media, devultur, options };
|
|
721
1055
|
});
|
|
722
|
-
|
|
1056
|
+
React9.useEffect(() => {
|
|
723
1057
|
if (!requestKey || !isReady) return;
|
|
724
1058
|
const { media: target, devultur: context, options: current } = inputsRef.current;
|
|
725
1059
|
if (!target || !context.baseUrl) return;
|
|
@@ -727,7 +1061,7 @@ function useMediaResource(media, options) {
|
|
|
727
1061
|
const controller = new AbortController();
|
|
728
1062
|
let value = null;
|
|
729
1063
|
loadMedia({
|
|
730
|
-
buildUrl: (viewerToken) =>
|
|
1064
|
+
buildUrl: (viewerToken) => mediaUrl4(baseUrl, target, { token: viewerToken }),
|
|
731
1065
|
token: context.token,
|
|
732
1066
|
sessionSecret: context.sessionSecret,
|
|
733
1067
|
reissueToken: context.reissueToken,
|
|
@@ -757,7 +1091,7 @@ function useMediaResource(media, options) {
|
|
|
757
1091
|
}
|
|
758
1092
|
|
|
759
1093
|
// src/pdf-context.tsx
|
|
760
|
-
import
|
|
1094
|
+
import React10 from "react";
|
|
761
1095
|
|
|
762
1096
|
// src/preview-labels.ts
|
|
763
1097
|
var ERROR_TEXT = {
|
|
@@ -792,16 +1126,16 @@ function mergePreviewLabels(...sources) {
|
|
|
792
1126
|
}
|
|
793
1127
|
|
|
794
1128
|
// src/pdf-context.tsx
|
|
795
|
-
import { jsx as
|
|
796
|
-
var PdfPreviewContext = /* @__PURE__ */
|
|
1129
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1130
|
+
var PdfPreviewContext = /* @__PURE__ */ React10.createContext({});
|
|
797
1131
|
function PdfPreviewProvider({
|
|
798
1132
|
pdfjs,
|
|
799
1133
|
workerSrc,
|
|
800
1134
|
labels,
|
|
801
1135
|
children
|
|
802
1136
|
}) {
|
|
803
|
-
const config =
|
|
804
|
-
return /* @__PURE__ */
|
|
1137
|
+
const config = React10.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
|
|
1138
|
+
return /* @__PURE__ */ jsx6(PdfPreviewContext.Provider, { value: config, children });
|
|
805
1139
|
}
|
|
806
1140
|
var loadedModules = /* @__PURE__ */ new WeakMap();
|
|
807
1141
|
function loadOnce(loader) {
|
|
@@ -812,9 +1146,9 @@ function loadOnce(loader) {
|
|
|
812
1146
|
return loading;
|
|
813
1147
|
}
|
|
814
1148
|
function usePdfjs(workerSrcOverride) {
|
|
815
|
-
const { pdfjs: loader, workerSrc } =
|
|
816
|
-
const [loaded, setLoaded] =
|
|
817
|
-
|
|
1149
|
+
const { pdfjs: loader, workerSrc } = React10.useContext(PdfPreviewContext);
|
|
1150
|
+
const [loaded, setLoaded] = React10.useState(null);
|
|
1151
|
+
React10.useEffect(() => {
|
|
818
1152
|
if (!loader) return;
|
|
819
1153
|
let isCurrent = true;
|
|
820
1154
|
loadOnce(loader).then((pdfjs) => {
|
|
@@ -830,8 +1164,8 @@ function usePdfjs(workerSrcOverride) {
|
|
|
830
1164
|
return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
|
|
831
1165
|
}
|
|
832
1166
|
function usePdfPageCounter() {
|
|
833
|
-
const { pdfjs: loader, workerSrc } =
|
|
834
|
-
return
|
|
1167
|
+
const { pdfjs: loader, workerSrc } = React10.useContext(PdfPreviewContext);
|
|
1168
|
+
return React10.useMemo(() => {
|
|
835
1169
|
if (!loader) return null;
|
|
836
1170
|
return async (file) => {
|
|
837
1171
|
const pdfjs = await loadOnce(loader);
|
|
@@ -841,15 +1175,15 @@ function usePdfPageCounter() {
|
|
|
841
1175
|
}, [loader, workerSrc]);
|
|
842
1176
|
}
|
|
843
1177
|
function usePreviewLabels(labels) {
|
|
844
|
-
const providerLabels =
|
|
1178
|
+
const providerLabels = React10.useContext(PdfPreviewContext).labels;
|
|
845
1179
|
return mergePreviewLabels(providerLabels, labels);
|
|
846
1180
|
}
|
|
847
1181
|
|
|
848
1182
|
// src/pdf-hooks.ts
|
|
849
|
-
import
|
|
1183
|
+
import React11 from "react";
|
|
850
1184
|
function useIsVisible(ref, rootMargin = "200px") {
|
|
851
|
-
const [isVisible, setIsVisible] =
|
|
852
|
-
|
|
1185
|
+
const [isVisible, setIsVisible] = React11.useState(false);
|
|
1186
|
+
React11.useEffect(() => {
|
|
853
1187
|
const element = ref.current;
|
|
854
1188
|
if (isVisible || !element) return;
|
|
855
1189
|
if (typeof IntersectionObserver === "undefined") {
|
|
@@ -870,12 +1204,12 @@ function useIsVisible(ref, rootMargin = "200px") {
|
|
|
870
1204
|
return isVisible;
|
|
871
1205
|
}
|
|
872
1206
|
function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
873
|
-
const [state, setState] =
|
|
1207
|
+
const [state, setState] = React11.useState({
|
|
874
1208
|
bytes: null,
|
|
875
1209
|
document: null,
|
|
876
1210
|
error: null
|
|
877
1211
|
});
|
|
878
|
-
|
|
1212
|
+
React11.useEffect(() => {
|
|
879
1213
|
let isCurrent = true;
|
|
880
1214
|
const loadingTask = openPdf(pdfjs, bytes, workerSrc);
|
|
881
1215
|
loadingTask.promise.then((document2) => {
|
|
@@ -894,8 +1228,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
|
894
1228
|
}
|
|
895
1229
|
function usePageRender(options) {
|
|
896
1230
|
const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
|
|
897
|
-
const [state, setState] =
|
|
898
|
-
|
|
1231
|
+
const [state, setState] = React11.useState({ isRendered: false, text: "" });
|
|
1232
|
+
React11.useEffect(() => {
|
|
899
1233
|
const canvas = canvasRef.current;
|
|
900
1234
|
if (!document2 || !isEnabled || !canvas) return;
|
|
901
1235
|
let isCurrent = true;
|
|
@@ -925,8 +1259,8 @@ function usePageRender(options) {
|
|
|
925
1259
|
}
|
|
926
1260
|
|
|
927
1261
|
// src/pdf-preview.tsx
|
|
928
|
-
import
|
|
929
|
-
import { Fragment as Fragment2, jsx as
|
|
1262
|
+
import React12 from "react";
|
|
1263
|
+
import { Fragment as Fragment2, jsx as jsx7, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
930
1264
|
var VISUALLY_HIDDEN = {
|
|
931
1265
|
position: "absolute",
|
|
932
1266
|
width: 1,
|
|
@@ -949,11 +1283,11 @@ function PdfPreview(props) {
|
|
|
949
1283
|
contentType: expectedType,
|
|
950
1284
|
decode: keepBytes
|
|
951
1285
|
});
|
|
952
|
-
if (error) return /* @__PURE__ */
|
|
953
|
-
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */
|
|
1286
|
+
if (error) return /* @__PURE__ */ jsx7(Fragment2, { children: renderFallback(error) });
|
|
1287
|
+
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx7(Fragment2, { children: loading });
|
|
954
1288
|
const label = title ?? media.filename ?? labels.untitledPdf;
|
|
955
|
-
if (pdfjsState.status === "unavailable") return /* @__PURE__ */
|
|
956
|
-
return /* @__PURE__ */
|
|
1289
|
+
if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx7(PdfFrame, { bytes, label, className });
|
|
1290
|
+
return /* @__PURE__ */ jsx7(
|
|
957
1291
|
PdfCanvasDocument,
|
|
958
1292
|
{
|
|
959
1293
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -971,13 +1305,13 @@ function PdfPreview(props) {
|
|
|
971
1305
|
function PdfCanvasDocument(props) {
|
|
972
1306
|
const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
|
|
973
1307
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
974
|
-
const containerRef =
|
|
975
|
-
const [width, setWidth] =
|
|
976
|
-
|
|
1308
|
+
const containerRef = React12.useRef(null);
|
|
1309
|
+
const [width, setWidth] = React12.useState(null);
|
|
1310
|
+
React12.useLayoutEffect(() => {
|
|
977
1311
|
if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
|
|
978
1312
|
}, [document2]);
|
|
979
|
-
if (error) return /* @__PURE__ */
|
|
980
|
-
if (!document2) return /* @__PURE__ */
|
|
1313
|
+
if (error) return /* @__PURE__ */ jsx7(Fragment2, { children: renderFallback(renderFailedError(error)) });
|
|
1314
|
+
if (!document2) return /* @__PURE__ */ jsx7(Fragment2, { children: loading });
|
|
981
1315
|
const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
|
|
982
1316
|
const isTruncated = shownPageCount < document2.numPages;
|
|
983
1317
|
const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
|
|
@@ -989,7 +1323,7 @@ function PdfCanvasDocument(props) {
|
|
|
989
1323
|
"aria-label": labels.document(label, document2.numPages),
|
|
990
1324
|
className,
|
|
991
1325
|
children: [
|
|
992
|
-
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */
|
|
1326
|
+
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx7(
|
|
993
1327
|
PdfPageView,
|
|
994
1328
|
{
|
|
995
1329
|
document: document2,
|
|
@@ -1009,8 +1343,8 @@ function PdfCanvasDocument(props) {
|
|
|
1009
1343
|
);
|
|
1010
1344
|
}
|
|
1011
1345
|
function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
1012
|
-
const pageRef =
|
|
1013
|
-
const canvasRef =
|
|
1346
|
+
const pageRef = React12.useRef(null);
|
|
1347
|
+
const canvasRef = React12.useRef(null);
|
|
1014
1348
|
const isVisible = useIsVisible(pageRef);
|
|
1015
1349
|
const { isRendered, text } = usePageRender({
|
|
1016
1350
|
document: document2,
|
|
@@ -1022,28 +1356,28 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
|
1022
1356
|
});
|
|
1023
1357
|
const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
|
|
1024
1358
|
return /* @__PURE__ */ jsxs3("section", { ref: pageRef, "aria-label": label, style: placeholderStyle, children: [
|
|
1025
|
-
/* @__PURE__ */
|
|
1026
|
-
text && /* @__PURE__ */
|
|
1359
|
+
/* @__PURE__ */ jsx7("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
|
|
1360
|
+
text && /* @__PURE__ */ jsx7("p", { style: VISUALLY_HIDDEN, children: text })
|
|
1027
1361
|
] });
|
|
1028
1362
|
}
|
|
1029
1363
|
function PdfFrame({ bytes, label, className }) {
|
|
1030
|
-
const [objectUrl, setObjectUrl] =
|
|
1031
|
-
|
|
1364
|
+
const [objectUrl, setObjectUrl] = React12.useState(null);
|
|
1365
|
+
React12.useEffect(() => {
|
|
1032
1366
|
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
1033
1367
|
setObjectUrl(url);
|
|
1034
1368
|
return () => URL.revokeObjectURL(url);
|
|
1035
1369
|
}, [bytes]);
|
|
1036
1370
|
if (!objectUrl) return null;
|
|
1037
|
-
return /* @__PURE__ */
|
|
1371
|
+
return /* @__PURE__ */ jsx7("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
|
|
1038
1372
|
}
|
|
1039
1373
|
|
|
1040
1374
|
// src/pdf-thumbnail.tsx
|
|
1041
|
-
import { Fragment as Fragment3, jsx as
|
|
1375
|
+
import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
1042
1376
|
var DEFAULT_THUMBNAIL_WIDTH = 96;
|
|
1043
1377
|
var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
|
|
1044
1378
|
function PdfThumbnail(props) {
|
|
1045
1379
|
const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
|
|
1046
|
-
const containerRef =
|
|
1380
|
+
const containerRef = React13.useRef(null);
|
|
1047
1381
|
const isVisible = useIsVisible(containerRef);
|
|
1048
1382
|
const pdfjsState = usePdfjs(props.pdfWorkerSrc);
|
|
1049
1383
|
const labels = usePreviewLabels(props.labels);
|
|
@@ -1060,7 +1394,7 @@ function PdfThumbnail(props) {
|
|
|
1060
1394
|
const content = () => {
|
|
1061
1395
|
if (isUnavailable) return fallback;
|
|
1062
1396
|
if (!bytes || pdfjsState.status !== "ready") return loading;
|
|
1063
|
-
return /* @__PURE__ */
|
|
1397
|
+
return /* @__PURE__ */ jsx8(
|
|
1064
1398
|
ThumbnailCanvas,
|
|
1065
1399
|
{
|
|
1066
1400
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -1073,21 +1407,21 @@ function PdfThumbnail(props) {
|
|
|
1073
1407
|
}
|
|
1074
1408
|
);
|
|
1075
1409
|
};
|
|
1076
|
-
return /* @__PURE__ */
|
|
1410
|
+
return /* @__PURE__ */ jsx8("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
|
|
1077
1411
|
}
|
|
1078
1412
|
function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
|
|
1079
|
-
const canvasRef =
|
|
1413
|
+
const canvasRef = React13.useRef(null);
|
|
1080
1414
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
1081
1415
|
const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
|
|
1082
|
-
if (error) return /* @__PURE__ */
|
|
1416
|
+
if (error) return /* @__PURE__ */ jsx8(Fragment3, { children: fallback });
|
|
1083
1417
|
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
1084
1418
|
!isRendered && loading,
|
|
1085
|
-
/* @__PURE__ */
|
|
1419
|
+
/* @__PURE__ */ jsx8("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
|
|
1086
1420
|
] });
|
|
1087
1421
|
}
|
|
1088
1422
|
|
|
1089
1423
|
// src/media-library-item.tsx
|
|
1090
|
-
import { Fragment as Fragment4, jsx as
|
|
1424
|
+
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1091
1425
|
var THUMBNAIL_WIDTH = 160;
|
|
1092
1426
|
var THUMBNAIL_WIDTHS = [THUMBNAIL_WIDTH, THUMBNAIL_WIDTH * 2];
|
|
1093
1427
|
function partClass(part, classNames) {
|
|
@@ -1107,10 +1441,10 @@ function kindOf(record) {
|
|
|
1107
1441
|
}
|
|
1108
1442
|
function Thumbnail({ record, media, pdfThumbnails }) {
|
|
1109
1443
|
const kind = kindOf(record);
|
|
1110
|
-
const icon = /* @__PURE__ */
|
|
1444
|
+
const icon = /* @__PURE__ */ jsx9(AttachmentIcon, { kind });
|
|
1111
1445
|
if (!media) return icon;
|
|
1112
1446
|
if (record.type === "image") {
|
|
1113
|
-
return /* @__PURE__ */
|
|
1447
|
+
return /* @__PURE__ */ jsx9(
|
|
1114
1448
|
Image,
|
|
1115
1449
|
{
|
|
1116
1450
|
media,
|
|
@@ -1125,14 +1459,14 @@ function Thumbnail({ record, media, pdfThumbnails }) {
|
|
|
1125
1459
|
);
|
|
1126
1460
|
}
|
|
1127
1461
|
if (kind !== "pdf" || !pdfThumbnails) return icon;
|
|
1128
|
-
return /* @__PURE__ */
|
|
1462
|
+
return /* @__PURE__ */ jsx9(PdfThumbnail, { media, contentType: record.contentType, width: THUMBNAIL_WIDTH, fallback: icon });
|
|
1129
1463
|
}
|
|
1130
1464
|
function DefaultContent({ record, media, pdfThumbnails, labels, classNames }) {
|
|
1131
1465
|
const name = record.title ?? record.filename;
|
|
1132
1466
|
return /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1133
|
-
/* @__PURE__ */
|
|
1134
|
-
/* @__PURE__ */
|
|
1135
|
-
record.sizeBytes > 0 && /* @__PURE__ */
|
|
1467
|
+
/* @__PURE__ */ jsx9("span", { className: partClass("thumbnail", classNames), children: /* @__PURE__ */ jsx9(Thumbnail, { record, media, pdfThumbnails }) }),
|
|
1468
|
+
/* @__PURE__ */ jsx9("span", { className: partClass("name", classNames), children: name }),
|
|
1469
|
+
record.sizeBytes > 0 && /* @__PURE__ */ jsx9("span", { className: partClass("size", classNames), children: labels.size(record.sizeBytes) })
|
|
1136
1470
|
] });
|
|
1137
1471
|
}
|
|
1138
1472
|
var ITEM_STYLE = {
|
|
@@ -1144,13 +1478,13 @@ var ITEM_STYLE = {
|
|
|
1144
1478
|
};
|
|
1145
1479
|
function MediaLibraryItem(props) {
|
|
1146
1480
|
const { ref, record, isSelected, isActive, tabIndex, classNames, renderItem, onChoose, onFocus } = props;
|
|
1147
|
-
const media =
|
|
1481
|
+
const media = React14.useMemo(() => mediaFromRecord(record), [record]);
|
|
1148
1482
|
const handleKeyDown = (event) => {
|
|
1149
1483
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1150
1484
|
event.preventDefault();
|
|
1151
1485
|
onChoose();
|
|
1152
1486
|
};
|
|
1153
|
-
return /* @__PURE__ */
|
|
1487
|
+
return /* @__PURE__ */ jsx9(
|
|
1154
1488
|
"div",
|
|
1155
1489
|
{
|
|
1156
1490
|
ref,
|
|
@@ -1167,7 +1501,7 @@ function MediaLibraryItem(props) {
|
|
|
1167
1501
|
onClick: onChoose,
|
|
1168
1502
|
onKeyDown: handleKeyDown,
|
|
1169
1503
|
onFocus,
|
|
1170
|
-
children: renderItem ? renderItem(record, { media, isSelected, isActive }) : /* @__PURE__ */
|
|
1504
|
+
children: renderItem ? renderItem(record, { media, isSelected, isActive }) : /* @__PURE__ */ jsx9(
|
|
1171
1505
|
DefaultContent,
|
|
1172
1506
|
{
|
|
1173
1507
|
record,
|
|
@@ -1207,13 +1541,13 @@ function mergeMediaLibraryLabels(labels) {
|
|
|
1207
1541
|
}
|
|
1208
1542
|
|
|
1209
1543
|
// src/use-media-library.ts
|
|
1210
|
-
import
|
|
1544
|
+
import React15 from "react";
|
|
1211
1545
|
import { usePaginatedQuery } from "convex/react";
|
|
1212
1546
|
var SEARCH_DEBOUNCE_MS = 250;
|
|
1213
1547
|
var DEFAULT_PAGE_SIZE = 24;
|
|
1214
1548
|
function useDebouncedSearch(value, delayMs) {
|
|
1215
|
-
const [debounced, setDebounced] =
|
|
1216
|
-
|
|
1549
|
+
const [debounced, setDebounced] = React15.useState(value);
|
|
1550
|
+
React15.useEffect(() => {
|
|
1217
1551
|
if (value === "") {
|
|
1218
1552
|
setDebounced("");
|
|
1219
1553
|
return;
|
|
@@ -1238,7 +1572,7 @@ function useMediaLibrary(options = {}) {
|
|
|
1238
1572
|
const args = isSearching ? { search, ...typeArg } : typeArg;
|
|
1239
1573
|
const result = usePaginatedQuery(reference, args, { initialNumItems: pageSize });
|
|
1240
1574
|
const resultLoadMore = result.loadMore;
|
|
1241
|
-
const loadMore =
|
|
1575
|
+
const loadMore = React15.useCallback(
|
|
1242
1576
|
(numItems) => resultLoadMore(numItems ?? pageSize),
|
|
1243
1577
|
[resultLoadMore, pageSize]
|
|
1244
1578
|
);
|
|
@@ -1253,7 +1587,7 @@ function useMediaLibrary(options = {}) {
|
|
|
1253
1587
|
}
|
|
1254
1588
|
|
|
1255
1589
|
// src/use-roving-focus.ts
|
|
1256
|
-
import
|
|
1590
|
+
import React16 from "react";
|
|
1257
1591
|
function nextIndex(key, current, count, columns) {
|
|
1258
1592
|
const last = count - 1;
|
|
1259
1593
|
const clamp = (index) => Math.min(Math.max(index, 0), last);
|
|
@@ -1281,16 +1615,16 @@ function columnCount(items) {
|
|
|
1281
1615
|
return Math.max(inFirstRow.length, 1);
|
|
1282
1616
|
}
|
|
1283
1617
|
function useRovingFocus(count) {
|
|
1284
|
-
const [activeIndex, setActiveIndex] =
|
|
1285
|
-
const itemsRef =
|
|
1618
|
+
const [activeIndex, setActiveIndex] = React16.useState(0);
|
|
1619
|
+
const itemsRef = React16.useRef([]);
|
|
1286
1620
|
const currentIndex = Math.min(activeIndex, Math.max(count - 1, 0));
|
|
1287
|
-
const itemRef =
|
|
1621
|
+
const itemRef = React16.useCallback(
|
|
1288
1622
|
(index) => (element) => {
|
|
1289
1623
|
itemsRef.current[index] = element;
|
|
1290
1624
|
},
|
|
1291
1625
|
[]
|
|
1292
1626
|
);
|
|
1293
|
-
const handleKeyDown =
|
|
1627
|
+
const handleKeyDown = React16.useCallback(
|
|
1294
1628
|
(event) => {
|
|
1295
1629
|
if (count === 0) return;
|
|
1296
1630
|
const items = itemsRef.current.slice(0, count);
|
|
@@ -1307,7 +1641,7 @@ function useRovingFocus(count) {
|
|
|
1307
1641
|
}
|
|
1308
1642
|
|
|
1309
1643
|
// src/media-library.tsx
|
|
1310
|
-
import { Fragment as Fragment5, jsx as
|
|
1644
|
+
import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1311
1645
|
var MEDIA_TYPES = ["image", "video", "file"];
|
|
1312
1646
|
var ROOT_STYLE = {
|
|
1313
1647
|
display: "flex",
|
|
@@ -1322,16 +1656,16 @@ var LIST_STYLE = {
|
|
|
1322
1656
|
padding: 0
|
|
1323
1657
|
};
|
|
1324
1658
|
function MediaLibrary(props) {
|
|
1325
|
-
const labels =
|
|
1659
|
+
const labels = React17.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
|
|
1326
1660
|
const classNames = { ...props.classNames, root: props.className ?? props.classNames?.root };
|
|
1327
|
-
const [search, setSearch] =
|
|
1328
|
-
const [chosenType, setChosenType] =
|
|
1329
|
-
const [selectedKey, setSelectedKey] =
|
|
1661
|
+
const [search, setSearch] = React17.useState("");
|
|
1662
|
+
const [chosenType, setChosenType] = React17.useState("");
|
|
1663
|
+
const [selectedKey, setSelectedKey] = React17.useState(null);
|
|
1330
1664
|
const type = props.type ?? (chosenType === "" ? void 0 : chosenType);
|
|
1331
1665
|
const { onSelect } = props;
|
|
1332
1666
|
const isSelectable = props.selectable ?? props.selectedKeys !== void 0;
|
|
1333
1667
|
const selectedKeys = props.selectedKeys ?? (selectedKey === null ? [] : [selectedKey]);
|
|
1334
|
-
const handleChoose =
|
|
1668
|
+
const handleChoose = React17.useCallback(
|
|
1335
1669
|
(record) => {
|
|
1336
1670
|
if (isSelectable) setSelectedKey(record.key);
|
|
1337
1671
|
onSelect(record, mediaFromRecord(record));
|
|
@@ -1339,7 +1673,7 @@ function MediaLibrary(props) {
|
|
|
1339
1673
|
[isSelectable, onSelect]
|
|
1340
1674
|
);
|
|
1341
1675
|
return /* @__PURE__ */ jsxs6("div", { className: partClass("root", classNames), style: ROOT_STYLE, children: [
|
|
1342
|
-
/* @__PURE__ */
|
|
1676
|
+
/* @__PURE__ */ jsx10(
|
|
1343
1677
|
"input",
|
|
1344
1678
|
{
|
|
1345
1679
|
type: "search",
|
|
@@ -1358,20 +1692,20 @@ function MediaLibrary(props) {
|
|
|
1358
1692
|
value: chosenType,
|
|
1359
1693
|
onChange: (event) => setChosenType(event.target.value),
|
|
1360
1694
|
children: [
|
|
1361
|
-
/* @__PURE__ */
|
|
1362
|
-
MEDIA_TYPES.map((mediaType) => /* @__PURE__ */
|
|
1695
|
+
/* @__PURE__ */ jsx10("option", { value: "", children: labels.allTypes }),
|
|
1696
|
+
MEDIA_TYPES.map((mediaType) => /* @__PURE__ */ jsx10("option", { value: mediaType, children: labels.types[mediaType] }, mediaType))
|
|
1363
1697
|
]
|
|
1364
1698
|
}
|
|
1365
1699
|
),
|
|
1366
|
-
/* @__PURE__ */
|
|
1700
|
+
/* @__PURE__ */ jsx10(
|
|
1367
1701
|
LibraryErrorBoundary,
|
|
1368
1702
|
{
|
|
1369
1703
|
resetKey: `${type ?? ""}\0${search}`,
|
|
1370
1704
|
renderError: (error, retry) => /* @__PURE__ */ jsxs6("div", { role: "alert", className: partClass("status", classNames), children: [
|
|
1371
|
-
/* @__PURE__ */
|
|
1372
|
-
/* @__PURE__ */
|
|
1705
|
+
/* @__PURE__ */ jsx10("p", { children: labels.error(error) }),
|
|
1706
|
+
/* @__PURE__ */ jsx10("button", { type: "button", className: partClass("more", classNames), onClick: retry, children: labels.retry })
|
|
1373
1707
|
] }),
|
|
1374
|
-
children: /* @__PURE__ */
|
|
1708
|
+
children: /* @__PURE__ */ jsx10(
|
|
1375
1709
|
LibraryResults,
|
|
1376
1710
|
{
|
|
1377
1711
|
...props,
|
|
@@ -1394,15 +1728,15 @@ function LibraryResults(props) {
|
|
|
1394
1728
|
const items = props.filter ? library.items.filter(props.filter) : library.items;
|
|
1395
1729
|
const roving = useRovingFocus(items.length);
|
|
1396
1730
|
if (library.status === "LoadingFirstPage") {
|
|
1397
|
-
return /* @__PURE__ */
|
|
1731
|
+
return /* @__PURE__ */ jsx10("p", { role: "status", className: partClass("status", classNames), children: labels.loading });
|
|
1398
1732
|
}
|
|
1399
1733
|
if (items.length === 0) {
|
|
1400
|
-
return /* @__PURE__ */
|
|
1734
|
+
return /* @__PURE__ */ jsx10("p", { role: "status", className: partClass("status", classNames), children: library.search ? labels.noResults(library.search) : labels.empty });
|
|
1401
1735
|
}
|
|
1402
1736
|
const isLoadingMore = library.status === "LoadingMore";
|
|
1403
1737
|
const canLoadMore = library.status === "CanLoadMore" || isLoadingMore;
|
|
1404
1738
|
return /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1405
|
-
/* @__PURE__ */
|
|
1739
|
+
/* @__PURE__ */ jsx10(
|
|
1406
1740
|
"div",
|
|
1407
1741
|
{
|
|
1408
1742
|
role: "listbox",
|
|
@@ -1412,7 +1746,7 @@ function LibraryResults(props) {
|
|
|
1412
1746
|
className: partClass("list", classNames),
|
|
1413
1747
|
style: LIST_STYLE,
|
|
1414
1748
|
onKeyDown: roving.handleKeyDown,
|
|
1415
|
-
children: items.map((record, index) => /* @__PURE__ */
|
|
1749
|
+
children: items.map((record, index) => /* @__PURE__ */ jsx10(
|
|
1416
1750
|
MediaLibraryItem,
|
|
1417
1751
|
{
|
|
1418
1752
|
ref: roving.itemRef(index),
|
|
@@ -1431,7 +1765,7 @@ function LibraryResults(props) {
|
|
|
1431
1765
|
))
|
|
1432
1766
|
}
|
|
1433
1767
|
),
|
|
1434
|
-
canLoadMore && /* @__PURE__ */
|
|
1768
|
+
canLoadMore && /* @__PURE__ */ jsx10(
|
|
1435
1769
|
"button",
|
|
1436
1770
|
{
|
|
1437
1771
|
type: "button",
|
|
@@ -1443,7 +1777,7 @@ function LibraryResults(props) {
|
|
|
1443
1777
|
)
|
|
1444
1778
|
] });
|
|
1445
1779
|
}
|
|
1446
|
-
var LibraryErrorBoundary = class extends
|
|
1780
|
+
var LibraryErrorBoundary = class extends React17.Component {
|
|
1447
1781
|
state = { hasError: false, error: null };
|
|
1448
1782
|
static getDerivedStateFromError(error) {
|
|
1449
1783
|
return { hasError: true, error };
|
|
@@ -1485,7 +1819,7 @@ function useMediaObjectUrl(media, options) {
|
|
|
1485
1819
|
}
|
|
1486
1820
|
|
|
1487
1821
|
// src/media-picker.tsx
|
|
1488
|
-
import
|
|
1822
|
+
import React24 from "react";
|
|
1489
1823
|
|
|
1490
1824
|
// src/media-picker-accept.ts
|
|
1491
1825
|
function matchesAccept(candidate, accept) {
|
|
@@ -1525,10 +1859,10 @@ function recordMatchesAccept(record, accept) {
|
|
|
1525
1859
|
}
|
|
1526
1860
|
|
|
1527
1861
|
// src/media-picker-context.ts
|
|
1528
|
-
import
|
|
1529
|
-
var MediaPickerContext = /* @__PURE__ */
|
|
1862
|
+
import React18 from "react";
|
|
1863
|
+
var MediaPickerContext = /* @__PURE__ */ React18.createContext(null);
|
|
1530
1864
|
function useMediaPicker() {
|
|
1531
|
-
const picker =
|
|
1865
|
+
const picker = React18.use(MediaPickerContext);
|
|
1532
1866
|
if (!picker) throw new Error("useMediaPicker must be used within <MediaPicker>");
|
|
1533
1867
|
return picker;
|
|
1534
1868
|
}
|
|
@@ -1569,17 +1903,17 @@ function mergeMediaPickerLabels(labels) {
|
|
|
1569
1903
|
}
|
|
1570
1904
|
|
|
1571
1905
|
// src/media-picker-library.tsx
|
|
1572
|
-
import
|
|
1573
|
-
import { jsx as
|
|
1906
|
+
import React19 from "react";
|
|
1907
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
1574
1908
|
function MediaPickerLibrary(props) {
|
|
1575
1909
|
const picker = useMediaPicker();
|
|
1576
1910
|
const { accept, toggle } = picker;
|
|
1577
1911
|
const selectedKeys = picker.selected.map((media) => media.id);
|
|
1578
|
-
const filter =
|
|
1912
|
+
const filter = React19.useMemo(
|
|
1579
1913
|
() => accept ? (record) => recordMatchesAccept(record, accept) : void 0,
|
|
1580
1914
|
[accept]
|
|
1581
1915
|
);
|
|
1582
|
-
return /* @__PURE__ */
|
|
1916
|
+
return /* @__PURE__ */ jsx11(
|
|
1583
1917
|
MediaLibrary,
|
|
1584
1918
|
{
|
|
1585
1919
|
...props,
|
|
@@ -1596,15 +1930,15 @@ function MediaPickerLibrary(props) {
|
|
|
1596
1930
|
}
|
|
1597
1931
|
|
|
1598
1932
|
// src/media-picker-selection.tsx
|
|
1599
|
-
import
|
|
1933
|
+
import React21 from "react";
|
|
1600
1934
|
|
|
1601
1935
|
// src/preview.tsx
|
|
1602
|
-
import { mediaPath as
|
|
1936
|
+
import { mediaPath as mediaPath4 } from "@devultur/core";
|
|
1603
1937
|
|
|
1604
1938
|
// src/video-preview.tsx
|
|
1605
|
-
import
|
|
1939
|
+
import React20 from "react";
|
|
1606
1940
|
import { VideoPlayer } from "@devultur/react";
|
|
1607
|
-
import { Fragment as Fragment6, jsx as
|
|
1941
|
+
import { Fragment as Fragment6, jsx as jsx12 } from "react/jsx-runtime";
|
|
1608
1942
|
var UNAUTHORIZED = 401;
|
|
1609
1943
|
var HLS_NETWORK_FAILURE = /Load(Error|TimeOut)$/;
|
|
1610
1944
|
function fetchErrorOf(error) {
|
|
@@ -1616,13 +1950,13 @@ function fetchErrorOf(error) {
|
|
|
1616
1950
|
if (HLS_NETWORK_FAILURE.test(error.details)) return networkError(error);
|
|
1617
1951
|
return renderFailedError(error);
|
|
1618
1952
|
}
|
|
1619
|
-
function VideoPreview({ media, renderFallback, className }) {
|
|
1620
|
-
const { tokenStatus, tokenError, reissueToken } = useDevultur();
|
|
1953
|
+
function VideoPreview({ media, renderFallback, loading, metrics, className }) {
|
|
1954
|
+
const { token, tokenStatus, tokenError, reissueToken } = useDevultur();
|
|
1621
1955
|
const videoProps = useVideoProps(media);
|
|
1622
|
-
const [failure, setFailure] =
|
|
1623
|
-
const hasReissuedRef =
|
|
1956
|
+
const [failure, setFailure] = React20.useState(null);
|
|
1957
|
+
const hasReissuedRef = React20.useRef(false);
|
|
1624
1958
|
const playerToken = videoProps?.token;
|
|
1625
|
-
const handleError =
|
|
1959
|
+
const handleError = React20.useCallback(
|
|
1626
1960
|
(error) => {
|
|
1627
1961
|
const isFirstMismatch = error.code === "session_mismatch" && !hasReissuedRef.current;
|
|
1628
1962
|
if (isFirstMismatch) {
|
|
@@ -1635,52 +1969,54 @@ function VideoPreview({ media, renderFallback, className }) {
|
|
|
1635
1969
|
[reissueToken, playerToken]
|
|
1636
1970
|
);
|
|
1637
1971
|
if (!videoProps) return null;
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1641
|
-
if (failure && failure.token === playerToken) return /* @__PURE__ */
|
|
1642
|
-
return /* @__PURE__ */
|
|
1972
|
+
const isPrivate = media.visibility === "private";
|
|
1973
|
+
if (isPrivate && tokenStatus === "error") return /* @__PURE__ */ jsx12(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
|
|
1974
|
+
if (isPrivate && !token) return /* @__PURE__ */ jsx12(Fragment6, { children: loading });
|
|
1975
|
+
if (failure && failure.token === playerToken) return /* @__PURE__ */ jsx12(Fragment6, { children: renderFallback(failure.error) });
|
|
1976
|
+
return /* @__PURE__ */ jsx12(VideoPlayer, { ...videoProps, className, metrics, onError: handleError });
|
|
1643
1977
|
}
|
|
1644
1978
|
|
|
1645
1979
|
// src/preview.tsx
|
|
1646
|
-
import { Fragment as Fragment7, jsx as
|
|
1980
|
+
import { Fragment as Fragment7, jsx as jsx13 } from "react/jsx-runtime";
|
|
1647
1981
|
var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
|
|
1648
1982
|
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
1649
|
-
function Preview({ media, contentType, fallback, loading, labels, ...viewProps }) {
|
|
1983
|
+
function Preview({ media, contentType, fallback, loading, labels, metrics, ...viewProps }) {
|
|
1650
1984
|
const text = usePreviewLabels(labels);
|
|
1651
1985
|
if (!media) return null;
|
|
1652
1986
|
const renderFallback = (error) => {
|
|
1653
1987
|
const fetchError = error instanceof MediaFetchError ? error : networkError(error);
|
|
1654
1988
|
if (typeof fallback === "function") return fallback(fetchError);
|
|
1655
1989
|
if (fallback !== void 0) return fallback;
|
|
1656
|
-
return /* @__PURE__ */
|
|
1990
|
+
return /* @__PURE__ */ jsx13(DefaultFallback, { media, error: fetchError, labels: text });
|
|
1657
1991
|
};
|
|
1992
|
+
const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx13(LoadingStatus, { text: text.loading }) : loading;
|
|
1658
1993
|
if (media.kind === "video") {
|
|
1659
|
-
return /* @__PURE__ */
|
|
1994
|
+
return /* @__PURE__ */ jsx13(
|
|
1660
1995
|
VideoPreview,
|
|
1661
1996
|
{
|
|
1662
1997
|
media,
|
|
1663
1998
|
renderFallback,
|
|
1999
|
+
loading: loadingNode,
|
|
2000
|
+
metrics,
|
|
1664
2001
|
className: viewProps.className
|
|
1665
2002
|
},
|
|
1666
|
-
|
|
2003
|
+
mediaPath4(media)
|
|
1667
2004
|
);
|
|
1668
2005
|
}
|
|
1669
2006
|
const expectedType = expectedContentType(media, contentType);
|
|
1670
2007
|
const kind = previewKindOf(expectedType);
|
|
1671
|
-
if (!expectedType || !kind) return /* @__PURE__ */
|
|
1672
|
-
const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx12(LoadingStatus, { text: text.loading }) : loading;
|
|
2008
|
+
if (!expectedType || !kind) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(unsupportedType(expectedType)) });
|
|
1673
2009
|
const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
|
|
1674
|
-
if (kind === "text") return /* @__PURE__ */
|
|
1675
|
-
if (kind === "pdf") return /* @__PURE__ */
|
|
1676
|
-
return /* @__PURE__ */
|
|
2010
|
+
if (kind === "text") return /* @__PURE__ */ jsx13(TextPreview, { ...props });
|
|
2011
|
+
if (kind === "pdf") return /* @__PURE__ */ jsx13(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
|
|
2012
|
+
return /* @__PURE__ */ jsx13(BinaryPreview, { ...props, kind });
|
|
1677
2013
|
}
|
|
1678
2014
|
var unsupportedType = (contentType) => new MediaFetchError("unsupported_type", `Can't preview ${contentType ?? "an unknown type"} inline`);
|
|
1679
2015
|
function DefaultFallback({ media, error, labels }) {
|
|
1680
|
-
return /* @__PURE__ */
|
|
2016
|
+
return /* @__PURE__ */ jsx13(DownloadLink, { media, title: labels.error(error.code), children: media.filename ?? labels.download });
|
|
1681
2017
|
}
|
|
1682
2018
|
function LoadingStatus({ text }) {
|
|
1683
|
-
return /* @__PURE__ */
|
|
2019
|
+
return /* @__PURE__ */ jsx13("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
|
|
1684
2020
|
}
|
|
1685
2021
|
function BinaryPreview({
|
|
1686
2022
|
media,
|
|
@@ -1697,13 +2033,13 @@ function BinaryPreview({
|
|
|
1697
2033
|
const isPublicImage = kind === "image" && media.visibility === "public";
|
|
1698
2034
|
const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
|
|
1699
2035
|
const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
|
|
1700
|
-
if (verified.error) return /* @__PURE__ */
|
|
1701
|
-
if (!objectUrl) return /* @__PURE__ */
|
|
2036
|
+
if (verified.error) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(verified.error) });
|
|
2037
|
+
if (!objectUrl) return /* @__PURE__ */ jsx13(Fragment7, { children: loading });
|
|
1702
2038
|
const label = title ?? media.filename ?? labels.untitledMedia;
|
|
1703
2039
|
if (kind === "image") {
|
|
1704
|
-
return /* @__PURE__ */
|
|
2040
|
+
return /* @__PURE__ */ jsx13("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
|
|
1705
2041
|
}
|
|
1706
|
-
return /* @__PURE__ */
|
|
2042
|
+
return /* @__PURE__ */ jsx13("audio", { src: objectUrl, controls: true, "aria-label": label, className });
|
|
1707
2043
|
}
|
|
1708
2044
|
var decodeText = (bytes) => new TextDecoder().decode(bytes);
|
|
1709
2045
|
function TextPreview({
|
|
@@ -1721,19 +2057,19 @@ function TextPreview({
|
|
|
1721
2057
|
contentType: expectedType,
|
|
1722
2058
|
decode: decodeText
|
|
1723
2059
|
});
|
|
1724
|
-
if (error) return /* @__PURE__ */
|
|
1725
|
-
if (text === null) return /* @__PURE__ */
|
|
1726
|
-
return /* @__PURE__ */
|
|
2060
|
+
if (error) return /* @__PURE__ */ jsx13(Fragment7, { children: renderFallback(error) });
|
|
2061
|
+
if (text === null) return /* @__PURE__ */ jsx13(Fragment7, { children: loading });
|
|
2062
|
+
return /* @__PURE__ */ jsx13("pre", { title, className, children: text });
|
|
1727
2063
|
}
|
|
1728
2064
|
|
|
1729
2065
|
// src/media-picker-selection.tsx
|
|
1730
|
-
import { jsx as
|
|
2066
|
+
import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1731
2067
|
var THUMBNAIL_WIDTH2 = 96;
|
|
1732
2068
|
function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
|
|
1733
2069
|
const picker = useMediaPicker();
|
|
1734
2070
|
const { labels, selected } = picker;
|
|
1735
|
-
const headingId =
|
|
1736
|
-
const [previewedId, setPreviewedId] =
|
|
2071
|
+
const headingId = React21.useId();
|
|
2072
|
+
const [previewedId, setPreviewedId] = React21.useState(null);
|
|
1737
2073
|
const Heading = `h${headingLevel}`;
|
|
1738
2074
|
const cardHeadingLevel = headingLevel + 1;
|
|
1739
2075
|
const regionClass = className ? `devultur-media-picker__selection ${className}` : "devultur-media-picker__selection";
|
|
@@ -1746,21 +2082,21 @@ function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
|
|
|
1746
2082
|
"aria-labelledby": headingId,
|
|
1747
2083
|
className: regionClass,
|
|
1748
2084
|
children: [
|
|
1749
|
-
/* @__PURE__ */
|
|
1750
|
-
selected.length === 0 && /* @__PURE__ */
|
|
1751
|
-
selected.length > 0 && /* @__PURE__ */
|
|
1752
|
-
renderItem ? renderItem(media) : /* @__PURE__ */
|
|
2085
|
+
/* @__PURE__ */ jsx14(Heading, { id: headingId, children: labels.selection }),
|
|
2086
|
+
selected.length === 0 && /* @__PURE__ */ jsx14("p", { "data-part": "empty", children: labels.empty }),
|
|
2087
|
+
selected.length > 0 && /* @__PURE__ */ jsx14("ul", { "data-part": "items", children: selected.map((media) => /* @__PURE__ */ jsxs7("li", { "data-kind": media.kind, children: [
|
|
2088
|
+
renderItem ? renderItem(media) : /* @__PURE__ */ jsx14(
|
|
1753
2089
|
AttachmentCard,
|
|
1754
2090
|
{
|
|
1755
2091
|
media,
|
|
1756
2092
|
headingLevel: cardHeadingLevel,
|
|
1757
2093
|
labels: picker.attachmentLabels,
|
|
1758
|
-
preview: media.kind === "image" ? /* @__PURE__ */
|
|
2094
|
+
preview: media.kind === "image" ? /* @__PURE__ */ jsx14(Thumbnail2, { media }) : void 0,
|
|
1759
2095
|
onOpen: handleOpen
|
|
1760
2096
|
}
|
|
1761
2097
|
),
|
|
1762
|
-
previewedId === media.id && /* @__PURE__ */
|
|
1763
|
-
/* @__PURE__ */
|
|
2098
|
+
previewedId === media.id && /* @__PURE__ */ jsx14(Preview, { media, title: mediaName(media) }),
|
|
2099
|
+
/* @__PURE__ */ jsx14(
|
|
1764
2100
|
"button",
|
|
1765
2101
|
{
|
|
1766
2102
|
type: "button",
|
|
@@ -1770,13 +2106,13 @@ function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
|
|
|
1770
2106
|
}
|
|
1771
2107
|
)
|
|
1772
2108
|
] }, media.id)) }),
|
|
1773
|
-
/* @__PURE__ */
|
|
2109
|
+
/* @__PURE__ */ jsx14(SelectionActions, {})
|
|
1774
2110
|
]
|
|
1775
2111
|
}
|
|
1776
2112
|
);
|
|
1777
2113
|
}
|
|
1778
2114
|
function Thumbnail2({ media }) {
|
|
1779
|
-
return /* @__PURE__ */
|
|
2115
|
+
return /* @__PURE__ */ jsx14(
|
|
1780
2116
|
Image,
|
|
1781
2117
|
{
|
|
1782
2118
|
media,
|
|
@@ -1793,18 +2129,18 @@ function SelectionActions() {
|
|
|
1793
2129
|
const { labels } = picker;
|
|
1794
2130
|
if (picker.selected.length === 0) return null;
|
|
1795
2131
|
return /* @__PURE__ */ jsxs7("div", { "data-part": "actions", children: [
|
|
1796
|
-
!picker.multiple && /* @__PURE__ */
|
|
1797
|
-
/* @__PURE__ */
|
|
2132
|
+
!picker.multiple && /* @__PURE__ */ jsx14("button", { type: "button", "data-part": "replace", onClick: picker.openFileDialog, children: labels.replace }),
|
|
2133
|
+
/* @__PURE__ */ jsx14("button", { type: "button", "data-part": "clear", onClick: picker.clear, children: labels.clear })
|
|
1798
2134
|
] });
|
|
1799
2135
|
}
|
|
1800
2136
|
|
|
1801
2137
|
// src/media-picker-upload.tsx
|
|
1802
|
-
import
|
|
1803
|
-
import { jsx as
|
|
2138
|
+
import React22 from "react";
|
|
2139
|
+
import { jsx as jsx15, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1804
2140
|
function MediaPickerUpload({ className }) {
|
|
1805
2141
|
const picker = useMediaPicker();
|
|
1806
2142
|
const { labels, multiple } = picker;
|
|
1807
|
-
const [isDragging, setIsDragging] =
|
|
2143
|
+
const [isDragging, setIsDragging] = React22.useState(false);
|
|
1808
2144
|
const handleDragOver = (event) => {
|
|
1809
2145
|
event.preventDefault();
|
|
1810
2146
|
setIsDragging(true);
|
|
@@ -1831,9 +2167,9 @@ function MediaPickerUpload({ className }) {
|
|
|
1831
2167
|
onDragLeave: handleDragLeave,
|
|
1832
2168
|
onDrop: handleDrop,
|
|
1833
2169
|
children: [
|
|
1834
|
-
/* @__PURE__ */
|
|
1835
|
-
/* @__PURE__ */
|
|
1836
|
-
picker.uploads.length > 0 && /* @__PURE__ */
|
|
2170
|
+
/* @__PURE__ */ jsx15("button", { type: "button", "data-part": "upload", onClick: picker.openFileDialog, children: labels.upload(multiple) }),
|
|
2171
|
+
/* @__PURE__ */ jsx15("span", { "data-part": "drop-hint", children: labels.dropHint(multiple) }),
|
|
2172
|
+
picker.uploads.length > 0 && /* @__PURE__ */ jsx15("ul", { "aria-label": labels.uploads, "data-part": "uploads", children: picker.uploads.map((upload) => /* @__PURE__ */ jsx15(UploadRow, { upload }, upload.id)) })
|
|
1837
2173
|
]
|
|
1838
2174
|
}
|
|
1839
2175
|
)
|
|
@@ -1845,23 +2181,23 @@ function UploadRow({ upload }) {
|
|
|
1845
2181
|
const filename = upload.file.name;
|
|
1846
2182
|
if (upload.status === "failed") {
|
|
1847
2183
|
return /* @__PURE__ */ jsxs8("li", { "data-status": "failed", children: [
|
|
1848
|
-
/* @__PURE__ */
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
upload.canRetry && /* @__PURE__ */
|
|
1851
|
-
/* @__PURE__ */
|
|
2184
|
+
/* @__PURE__ */ jsx15("span", { "data-part": "filename", children: filename }),
|
|
2185
|
+
/* @__PURE__ */ jsx15("span", { role: "alert", "data-part": "error", children: upload.error }),
|
|
2186
|
+
upload.canRetry && /* @__PURE__ */ jsx15("button", { type: "button", onClick: () => picker.retry(upload.id), children: labels.retry }),
|
|
2187
|
+
/* @__PURE__ */ jsx15("button", { type: "button", onClick: () => picker.dismiss(upload.id), children: labels.dismiss })
|
|
1852
2188
|
] });
|
|
1853
2189
|
}
|
|
1854
2190
|
const percent = Math.round(upload.fraction * 100);
|
|
1855
2191
|
return /* @__PURE__ */ jsxs8("li", { "data-status": "uploading", children: [
|
|
1856
|
-
/* @__PURE__ */
|
|
1857
|
-
/* @__PURE__ */
|
|
1858
|
-
/* @__PURE__ */
|
|
1859
|
-
/* @__PURE__ */
|
|
2192
|
+
/* @__PURE__ */ jsx15("span", { "data-part": "filename", children: filename }),
|
|
2193
|
+
/* @__PURE__ */ jsx15("progress", { max: 100, value: percent, "aria-label": labels.progress(filename) }),
|
|
2194
|
+
/* @__PURE__ */ jsx15("span", { "data-part": "percent", children: labels.percent(percent) }),
|
|
2195
|
+
/* @__PURE__ */ jsx15("button", { type: "button", "aria-label": labels.cancelUpload(filename), onClick: () => picker.cancel(upload.id), children: labels.cancel })
|
|
1860
2196
|
] });
|
|
1861
2197
|
}
|
|
1862
2198
|
|
|
1863
2199
|
// src/use-media-picker-uploads.ts
|
|
1864
|
-
import
|
|
2200
|
+
import React23 from "react";
|
|
1865
2201
|
var isAbortError = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
|
|
1866
2202
|
var isPdf = (file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
|
1867
2203
|
var nextUploadId = 0;
|
|
@@ -1872,18 +2208,18 @@ async function pageCountOf(file, countPdfPages) {
|
|
|
1872
2208
|
}
|
|
1873
2209
|
function useMediaPickerUploads(options) {
|
|
1874
2210
|
const { upload } = useDevultur();
|
|
1875
|
-
const [uploads, setUploads] =
|
|
1876
|
-
const controllers =
|
|
1877
|
-
const optionsRef =
|
|
2211
|
+
const [uploads, setUploads] = React23.useState([]);
|
|
2212
|
+
const controllers = React23.useRef(/* @__PURE__ */ new Map());
|
|
2213
|
+
const optionsRef = React23.useRef(options);
|
|
1878
2214
|
optionsRef.current = options;
|
|
1879
|
-
const patch =
|
|
2215
|
+
const patch = React23.useCallback((id, changes) => {
|
|
1880
2216
|
setUploads((current) => current.map((entry) => entry.id === id ? { ...entry, ...changes } : entry));
|
|
1881
2217
|
}, []);
|
|
1882
|
-
const drop =
|
|
2218
|
+
const drop = React23.useCallback((id) => {
|
|
1883
2219
|
controllers.current.delete(id);
|
|
1884
2220
|
setUploads((current) => current.filter((entry) => entry.id !== id));
|
|
1885
2221
|
}, []);
|
|
1886
|
-
const run =
|
|
2222
|
+
const run = React23.useCallback(
|
|
1887
2223
|
async (id, file) => {
|
|
1888
2224
|
const { labels, announce } = optionsRef.current;
|
|
1889
2225
|
const controller = new AbortController();
|
|
@@ -1913,7 +2249,7 @@ function useMediaPickerUploads(options) {
|
|
|
1913
2249
|
},
|
|
1914
2250
|
[upload, patch, drop]
|
|
1915
2251
|
);
|
|
1916
|
-
const start =
|
|
2252
|
+
const start = React23.useCallback(
|
|
1917
2253
|
(files) => {
|
|
1918
2254
|
const { accept, multiple, labels } = optionsRef.current;
|
|
1919
2255
|
const chosen = multiple ? [...files] : [...files].slice(0, 1);
|
|
@@ -1935,8 +2271,8 @@ function useMediaPickerUploads(options) {
|
|
|
1935
2271
|
},
|
|
1936
2272
|
[run]
|
|
1937
2273
|
);
|
|
1938
|
-
const cancel =
|
|
1939
|
-
const retry =
|
|
2274
|
+
const cancel = React23.useCallback((id) => controllers.current.get(id)?.abort(), []);
|
|
2275
|
+
const retry = React23.useCallback(
|
|
1940
2276
|
(id) => {
|
|
1941
2277
|
const entry = uploads.find((candidate) => candidate.id === id);
|
|
1942
2278
|
if (!entry?.canRetry) return;
|
|
@@ -1944,7 +2280,7 @@ function useMediaPickerUploads(options) {
|
|
|
1944
2280
|
},
|
|
1945
2281
|
[uploads, run]
|
|
1946
2282
|
);
|
|
1947
|
-
|
|
2283
|
+
React23.useEffect(() => {
|
|
1948
2284
|
const active = controllers.current;
|
|
1949
2285
|
return () => {
|
|
1950
2286
|
for (const controller of active.values()) controller.abort();
|
|
@@ -1954,7 +2290,7 @@ function useMediaPickerUploads(options) {
|
|
|
1954
2290
|
}
|
|
1955
2291
|
|
|
1956
2292
|
// src/media-picker.tsx
|
|
1957
|
-
import { Fragment as Fragment8, jsx as
|
|
2293
|
+
import { Fragment as Fragment8, jsx as jsx16, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1958
2294
|
var EMPTY = [];
|
|
1959
2295
|
function selectionOf(props) {
|
|
1960
2296
|
if (props.value === void 0) return void 0;
|
|
@@ -1971,22 +2307,22 @@ function emit(props, next) {
|
|
|
1971
2307
|
var isSame = (a, b) => a.id === b.id;
|
|
1972
2308
|
function MediaPickerRoot(props) {
|
|
1973
2309
|
const isMultiple = props.multiple ?? false;
|
|
1974
|
-
const labels =
|
|
2310
|
+
const labels = React24.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
|
|
1975
2311
|
const accept = uploadAcceptOf(props.accept, props.type);
|
|
1976
2312
|
const libraryType = props.type ?? mediaTypeOfAccept(props.accept);
|
|
1977
2313
|
const countPdfPages = usePdfPageCounter();
|
|
1978
|
-
const [announcement, setAnnouncement] =
|
|
1979
|
-
const inputRef =
|
|
1980
|
-
const selectionRef =
|
|
1981
|
-
const [ownSelection, setOwnSelection] =
|
|
2314
|
+
const [announcement, setAnnouncement] = React24.useState("");
|
|
2315
|
+
const inputRef = React24.useRef(null);
|
|
2316
|
+
const selectionRef = React24.useRef(null);
|
|
2317
|
+
const [ownSelection, setOwnSelection] = React24.useState(EMPTY);
|
|
1982
2318
|
const controlledSelection = selectionOf(props);
|
|
1983
2319
|
const selected = controlledSelection ?? ownSelection;
|
|
1984
|
-
const selectedRef =
|
|
2320
|
+
const selectedRef = React24.useRef(selected);
|
|
1985
2321
|
selectedRef.current = selected;
|
|
1986
|
-
const propsRef =
|
|
2322
|
+
const propsRef = React24.useRef(props);
|
|
1987
2323
|
propsRef.current = props;
|
|
1988
2324
|
const isControlled = controlledSelection !== void 0;
|
|
1989
|
-
const commit =
|
|
2325
|
+
const commit = React24.useCallback(
|
|
1990
2326
|
(next) => {
|
|
1991
2327
|
selectedRef.current = next;
|
|
1992
2328
|
if (!isControlled) setOwnSelection(next);
|
|
@@ -1994,15 +2330,15 @@ function MediaPickerRoot(props) {
|
|
|
1994
2330
|
},
|
|
1995
2331
|
[isControlled]
|
|
1996
2332
|
);
|
|
1997
|
-
const announce =
|
|
1998
|
-
const select =
|
|
2333
|
+
const announce = React24.useCallback((message) => setAnnouncement(message), []);
|
|
2334
|
+
const select = React24.useCallback(
|
|
1999
2335
|
(media) => {
|
|
2000
2336
|
const current = selectedRef.current.filter((item) => !isSame(item, media));
|
|
2001
2337
|
commit(isMultiple ? [...current, media] : [media]);
|
|
2002
2338
|
},
|
|
2003
2339
|
[commit, isMultiple]
|
|
2004
2340
|
);
|
|
2005
|
-
const remove =
|
|
2341
|
+
const remove = React24.useCallback(
|
|
2006
2342
|
(media) => {
|
|
2007
2343
|
commit(selectedRef.current.filter((item) => !isSame(item, media)));
|
|
2008
2344
|
announce(labels.removed(mediaName(media)));
|
|
@@ -2010,7 +2346,7 @@ function MediaPickerRoot(props) {
|
|
|
2010
2346
|
},
|
|
2011
2347
|
[commit, announce, labels]
|
|
2012
2348
|
);
|
|
2013
|
-
const toggle =
|
|
2349
|
+
const toggle = React24.useCallback(
|
|
2014
2350
|
(media) => {
|
|
2015
2351
|
const isSelected = selectedRef.current.some((item) => isSame(item, media));
|
|
2016
2352
|
if (isMultiple && isSelected) {
|
|
@@ -2022,12 +2358,12 @@ function MediaPickerRoot(props) {
|
|
|
2022
2358
|
},
|
|
2023
2359
|
[isMultiple, remove, select, announce, labels]
|
|
2024
2360
|
);
|
|
2025
|
-
const clear =
|
|
2361
|
+
const clear = React24.useCallback(() => {
|
|
2026
2362
|
commit([]);
|
|
2027
2363
|
announce(labels.cleared);
|
|
2028
2364
|
selectionRef.current?.focus();
|
|
2029
2365
|
}, [commit, announce, labels]);
|
|
2030
|
-
const handleUploaded =
|
|
2366
|
+
const handleUploaded = React24.useCallback(
|
|
2031
2367
|
(media) => {
|
|
2032
2368
|
select(media);
|
|
2033
2369
|
selectionRef.current?.focus();
|
|
@@ -2065,13 +2401,13 @@ function MediaPickerRoot(props) {
|
|
|
2065
2401
|
selectionRef
|
|
2066
2402
|
};
|
|
2067
2403
|
const content = typeof props.children === "function" ? props.children(picker) : props.children ?? /* @__PURE__ */ jsxs9(Fragment8, { children: [
|
|
2068
|
-
/* @__PURE__ */
|
|
2069
|
-
/* @__PURE__ */
|
|
2070
|
-
/* @__PURE__ */
|
|
2404
|
+
/* @__PURE__ */ jsx16(MediaPickerUpload, {}),
|
|
2405
|
+
/* @__PURE__ */ jsx16(MediaPickerLibrary, {}),
|
|
2406
|
+
/* @__PURE__ */ jsx16(MediaPickerSelection, {})
|
|
2071
2407
|
] });
|
|
2072
2408
|
const rootClass = props.className ? `devultur-media-picker ${props.className}` : "devultur-media-picker";
|
|
2073
|
-
return /* @__PURE__ */
|
|
2074
|
-
/* @__PURE__ */
|
|
2409
|
+
return /* @__PURE__ */ jsx16(MediaPickerContext, { value: picker, children: /* @__PURE__ */ jsxs9("div", { className: rootClass, children: [
|
|
2410
|
+
/* @__PURE__ */ jsx16(
|
|
2075
2411
|
"input",
|
|
2076
2412
|
{
|
|
2077
2413
|
ref: inputRef,
|
|
@@ -2086,7 +2422,7 @@ function MediaPickerRoot(props) {
|
|
|
2086
2422
|
}
|
|
2087
2423
|
),
|
|
2088
2424
|
content,
|
|
2089
|
-
/* @__PURE__ */
|
|
2425
|
+
/* @__PURE__ */ jsx16("div", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, children: announcement })
|
|
2090
2426
|
] }) });
|
|
2091
2427
|
}
|
|
2092
2428
|
var MediaPicker = /* @__PURE__ */ Object.assign(MediaPickerRoot, {
|
|
@@ -2096,25 +2432,58 @@ var MediaPicker = /* @__PURE__ */ Object.assign(MediaPickerRoot, {
|
|
|
2096
2432
|
Selection: MediaPickerSelection
|
|
2097
2433
|
});
|
|
2098
2434
|
|
|
2435
|
+
// src/viewer-provider.tsx
|
|
2436
|
+
import React25 from "react";
|
|
2437
|
+
import { useAction as useAction2 } from "convex/react";
|
|
2438
|
+
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2439
|
+
var VIEWER_ACTIONS = {
|
|
2440
|
+
uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
|
|
2441
|
+
upload: () => Promise.reject(new DevulturCapabilityError("upload")),
|
|
2442
|
+
deleteMedia: () => {
|
|
2443
|
+
throw new DevulturCapabilityError("deleteMedia");
|
|
2444
|
+
}
|
|
2445
|
+
};
|
|
2446
|
+
var NO_TOKEN_ACTION = () => Promise.reject(new Error("<DevulturViewerProvider> has no `issueViewerToken`: private media can't be shown"));
|
|
2447
|
+
function DevulturViewerProvider({ issueViewerToken, ...props }) {
|
|
2448
|
+
if (!issueViewerToken) return /* @__PURE__ */ jsx17(ViewerSession, { ...props, issueViewerToken: NO_TOKEN_ACTION, lazyToken: true });
|
|
2449
|
+
return /* @__PURE__ */ jsx17(ConvexViewerSession, { ...props, issueViewerToken });
|
|
2450
|
+
}
|
|
2451
|
+
function ConvexViewerSession({
|
|
2452
|
+
issueViewerToken,
|
|
2453
|
+
...props
|
|
2454
|
+
}) {
|
|
2455
|
+
const issueViewerTokenAction = useAction2(issueViewerToken);
|
|
2456
|
+
return /* @__PURE__ */ jsx17(ViewerSession, { issueViewerToken: issueViewerTokenAction, ...props });
|
|
2457
|
+
}
|
|
2458
|
+
function ViewerSession({
|
|
2459
|
+
baseUrl,
|
|
2460
|
+
issueViewerToken,
|
|
2461
|
+
tokenArgs,
|
|
2462
|
+
lazyToken = false,
|
|
2463
|
+
children
|
|
2464
|
+
}) {
|
|
2465
|
+
const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl, isLazy: lazyToken });
|
|
2466
|
+
const devulturValue = React25.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
|
|
2467
|
+
return /* @__PURE__ */ jsx17(DevulturContext, { value: devulturValue, children });
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2099
2470
|
// src/react.tsx
|
|
2100
|
-
import { jsx as
|
|
2101
|
-
var DevulturContext = /* @__PURE__ */ React22.createContext(null);
|
|
2102
|
-
var DevulturApiContext = /* @__PURE__ */ React22.createContext(null);
|
|
2103
|
-
var ProgressContext = /* @__PURE__ */ React22.createContext(null);
|
|
2471
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
2104
2472
|
function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
|
|
2105
|
-
const
|
|
2106
|
-
const
|
|
2107
|
-
const
|
|
2108
|
-
const
|
|
2109
|
-
const
|
|
2110
|
-
const
|
|
2473
|
+
const issueViewerTokenAction = useAction3(issueViewerToken ?? api.issueViewerToken);
|
|
2474
|
+
const session = useMediaSession({ issueViewerToken: issueViewerTokenAction, tokenArgs, baseUrl });
|
|
2475
|
+
const createUploadUrlAction = useAction3(api.createUploadUrl);
|
|
2476
|
+
const deleteMediaAction = useAction3(api.deleteMedia);
|
|
2477
|
+
const confirmUploadAction = useAction3(api.confirmUpload ?? api.createUploadUrl);
|
|
2478
|
+
const deleteVideoAction = useAction3(api.deleteVideo ?? api.deleteMedia);
|
|
2479
|
+
const uploadUrl = React26.useCallback(
|
|
2111
2480
|
async (file) => {
|
|
2112
2481
|
const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
|
|
2113
2482
|
return { url: result.url, key: result.key };
|
|
2114
2483
|
},
|
|
2115
2484
|
[createUploadUrlAction]
|
|
2116
2485
|
);
|
|
2117
|
-
const upload =
|
|
2486
|
+
const upload = React26.useCallback(
|
|
2118
2487
|
async (file, options) => {
|
|
2119
2488
|
const dimensions = await readImageDimensions(file);
|
|
2120
2489
|
options?.signal?.throwIfAborted();
|
|
@@ -2123,7 +2492,7 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
|
|
|
2123
2492
|
contentType: file.type,
|
|
2124
2493
|
visibility: options?.visibility
|
|
2125
2494
|
});
|
|
2126
|
-
await
|
|
2495
|
+
await uploadFileWithProgress2(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
|
|
2127
2496
|
if (api.confirmUpload) {
|
|
2128
2497
|
const pageCount = options?.pageCount;
|
|
2129
2498
|
const hints = { ...dimensions, ...pageCount !== void 0 ? { pageCount } : {} };
|
|
@@ -2139,432 +2508,89 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
|
|
|
2139
2508
|
};
|
|
2140
2509
|
},
|
|
2141
2510
|
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
2142
|
-
);
|
|
2143
|
-
const deleteMedia =
|
|
2144
|
-
(media) => {
|
|
2145
|
-
if (media.kind === "video" && api.deleteVideo) {
|
|
2146
|
-
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
2147
|
-
return;
|
|
2148
|
-
}
|
|
2149
|
-
deleteMediaAction({ key: mediaPath3(media) }).catch(console.error);
|
|
2150
|
-
},
|
|
2151
|
-
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
2152
|
-
);
|
|
2153
|
-
const devulturValue = React22.useMemo(
|
|
2154
|
-
() => ({ ...session, uploadUrl, upload, deleteMedia }),
|
|
2155
|
-
[session, uploadUrl, upload, deleteMedia]
|
|
2156
|
-
);
|
|
2157
|
-
let content = /* @__PURE__ */ jsx16(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children }) });
|
|
2158
|
-
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
2159
|
-
content = /* @__PURE__ */ jsx16(
|
|
2160
|
-
ProgressBridge,
|
|
2161
|
-
{
|
|
2162
|
-
save: api.saveProgress,
|
|
2163
|
-
load: api.loadProgress,
|
|
2164
|
-
markComplete: api.markComplete,
|
|
2165
|
-
reset: api.resetProgress,
|
|
2166
|
-
children: content
|
|
2167
|
-
}
|
|
2168
|
-
);
|
|
2169
|
-
}
|
|
2170
|
-
return content;
|
|
2171
|
-
}
|
|
2172
|
-
function ProgressBridge({
|
|
2173
|
-
save,
|
|
2174
|
-
load,
|
|
2175
|
-
markComplete,
|
|
2176
|
-
reset,
|
|
2177
|
-
children
|
|
2178
|
-
}) {
|
|
2179
|
-
const convex = useConvex();
|
|
2180
|
-
const saveMutation = useMutation(save);
|
|
2181
|
-
const markCompleteMutation = useMutation(markComplete);
|
|
2182
|
-
const resetMutation = useMutation(reset);
|
|
2183
|
-
const onSave = React22.useCallback(
|
|
2184
|
-
async (videoId, position) => {
|
|
2185
|
-
await saveMutation({ videoId, position });
|
|
2186
|
-
},
|
|
2187
|
-
[saveMutation]
|
|
2188
|
-
);
|
|
2189
|
-
const onLoad = React22.useCallback(
|
|
2190
|
-
async (videoId) => {
|
|
2191
|
-
const result = await convex.query(load, { videoId });
|
|
2192
|
-
if (!result) return null;
|
|
2193
|
-
return {
|
|
2194
|
-
lastPosition: result.lastPosition,
|
|
2195
|
-
completedAt: result.completedAt ? new Date(result.completedAt) : null
|
|
2196
|
-
};
|
|
2197
|
-
},
|
|
2198
|
-
[convex, load]
|
|
2199
|
-
);
|
|
2200
|
-
const onMarkComplete = React22.useCallback(
|
|
2201
|
-
async (videoId) => {
|
|
2202
|
-
await markCompleteMutation({ videoId });
|
|
2203
|
-
},
|
|
2204
|
-
[markCompleteMutation]
|
|
2205
|
-
);
|
|
2206
|
-
const onReset = React22.useCallback(
|
|
2207
|
-
async (videoId) => {
|
|
2208
|
-
await resetMutation({ videoId });
|
|
2209
|
-
},
|
|
2210
|
-
[resetMutation]
|
|
2211
|
-
);
|
|
2212
|
-
const value = React22.useMemo(
|
|
2213
|
-
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
2214
|
-
[onSave, onLoad, onMarkComplete, onReset]
|
|
2215
|
-
);
|
|
2216
|
-
return /* @__PURE__ */ jsx16(ProgressContext, { value, children });
|
|
2217
|
-
}
|
|
2218
|
-
function useDevultur() {
|
|
2219
|
-
const context = React22.use(DevulturContext);
|
|
2220
|
-
if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
|
|
2221
|
-
return context;
|
|
2222
|
-
}
|
|
2223
|
-
function useDevulturApi() {
|
|
2224
|
-
const api = React22.use(DevulturApiContext);
|
|
2225
|
-
if (api) return api;
|
|
2226
|
-
if (React22.use(DevulturContext)) throw new DevulturCapabilityError("api");
|
|
2227
|
-
throw new Error("useDevulturApi must be used within <DevulturProvider>");
|
|
2228
|
-
}
|
|
2229
|
-
var VIEWER_ACTIONS = {
|
|
2230
|
-
uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
|
|
2231
|
-
upload: () => Promise.reject(new DevulturCapabilityError("upload")),
|
|
2232
|
-
deleteMedia: () => {
|
|
2233
|
-
throw new DevulturCapabilityError("deleteMedia");
|
|
2234
|
-
}
|
|
2235
|
-
};
|
|
2236
|
-
function DevulturViewerProvider({
|
|
2237
|
-
baseUrl,
|
|
2238
|
-
issueViewerToken,
|
|
2239
|
-
tokenArgs,
|
|
2240
|
-
children
|
|
2241
|
-
}) {
|
|
2242
|
-
const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl });
|
|
2243
|
-
const devulturValue = React22.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
|
|
2244
|
-
return /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children });
|
|
2245
|
-
}
|
|
2246
|
-
function useDevulturProgress() {
|
|
2247
|
-
const context = React22.use(ProgressContext);
|
|
2248
|
-
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
2249
|
-
return context;
|
|
2250
|
-
}
|
|
2251
|
-
function useMediaUrl(key, options) {
|
|
2252
|
-
const { token, baseUrl } = useDevultur();
|
|
2253
|
-
return React22.useMemo(() => {
|
|
2254
|
-
if (!key) return null;
|
|
2255
|
-
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
2256
|
-
const isPublic = visibilityFromKey(key) === "public";
|
|
2257
|
-
return buildMediaUrl2(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
|
|
2258
|
-
}, [key, baseUrl, token, options]);
|
|
2259
|
-
}
|
|
2260
|
-
async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
|
|
2261
|
-
try {
|
|
2262
|
-
await confirmUpload({ key, filename: file.name, ...hints });
|
|
2263
|
-
} catch (error) {
|
|
2264
|
-
console.warn("Devultur: could not record the upload; it is recorded later", error);
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
async function readImageDimensions(file) {
|
|
2268
|
-
if (typeof createImageBitmap !== "function" || !file.type.startsWith("image/")) return {};
|
|
2269
|
-
try {
|
|
2270
|
-
const bitmap = await createImageBitmap(file);
|
|
2271
|
-
const dimensions = { width: bitmap.width, height: bitmap.height };
|
|
2272
|
-
bitmap.close();
|
|
2273
|
-
return dimensions;
|
|
2274
|
-
} catch {
|
|
2275
|
-
return {};
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
function useVideoProps(media, options) {
|
|
2279
|
-
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
2280
|
-
const locales = options?.locales;
|
|
2281
|
-
return React22.useMemo(() => {
|
|
2282
|
-
if (media?.kind !== "video") return null;
|
|
2283
|
-
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
2284
|
-
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
2285
|
-
const captions = (locales ?? []).flatMap((locale) => {
|
|
2286
|
-
const path = mediaCaptionPath2(media, locale);
|
|
2287
|
-
if (!path) return [];
|
|
2288
|
-
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
2289
|
-
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
2290
|
-
});
|
|
2291
|
-
return {
|
|
2292
|
-
src: buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken }),
|
|
2293
|
-
token: viewerToken,
|
|
2294
|
-
captions,
|
|
2295
|
-
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
2296
|
-
};
|
|
2297
|
-
}, [media, baseUrl, token, locales, sessionSecret]);
|
|
2298
|
-
}
|
|
2299
|
-
function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
|
|
2300
|
-
const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
2301
|
-
const handleClick = React22.useCallback(
|
|
2302
|
-
async (event) => {
|
|
2303
|
-
if (media?.visibility !== "private" || !baseUrl) return;
|
|
2304
|
-
event.preventDefault();
|
|
2305
|
-
if (tokenStatus === "error") {
|
|
2306
|
-
onDownloadError?.(tokenUnavailableError(tokenError));
|
|
2307
|
-
return;
|
|
2308
|
-
}
|
|
2309
|
-
const response = await fetchMedia({
|
|
2310
|
-
buildUrl: (viewerToken) => mediaUrl4(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
2311
|
-
token,
|
|
2312
|
-
sessionSecret,
|
|
2313
|
-
reissueToken
|
|
2314
|
-
});
|
|
2315
|
-
if (!response.ok) {
|
|
2316
|
-
const status = response.status;
|
|
2317
|
-
onDownloadError?.(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
|
|
2511
|
+
);
|
|
2512
|
+
const deleteMedia = React26.useCallback(
|
|
2513
|
+
(media) => {
|
|
2514
|
+
if (media.kind === "video" && api.deleteVideo) {
|
|
2515
|
+
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
2318
2516
|
return;
|
|
2319
2517
|
}
|
|
2320
|
-
|
|
2321
|
-
const anchor = document.createElement("a");
|
|
2322
|
-
anchor.href = objectUrl;
|
|
2323
|
-
anchor.download = filename ?? media.filename ?? "download";
|
|
2324
|
-
anchor.click();
|
|
2325
|
-
URL.revokeObjectURL(objectUrl);
|
|
2518
|
+
deleteMediaAction({ key: mediaPath5(media) }).catch(console.error);
|
|
2326
2519
|
},
|
|
2327
|
-
[
|
|
2520
|
+
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
2328
2521
|
);
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
if (
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
isQueued: false,
|
|
2346
|
-
isTranscoding: false,
|
|
2347
|
-
isCaptioning: false,
|
|
2348
|
-
isReady: false,
|
|
2349
|
-
isFailed: false,
|
|
2350
|
-
playlistUrl: null,
|
|
2351
|
-
thumbnailKey: null,
|
|
2352
|
-
captionLocales: [],
|
|
2353
|
-
duration: null,
|
|
2354
|
-
isLoading: record === void 0
|
|
2355
|
-
};
|
|
2522
|
+
const devulturValue = React26.useMemo(
|
|
2523
|
+
() => ({ ...session, uploadUrl, upload, deleteMedia }),
|
|
2524
|
+
[session, uploadUrl, upload, deleteMedia]
|
|
2525
|
+
);
|
|
2526
|
+
let content = /* @__PURE__ */ jsx18(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx18(DevulturContext, { value: devulturValue, children }) });
|
|
2527
|
+
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
2528
|
+
content = /* @__PURE__ */ jsx18(
|
|
2529
|
+
ProgressBridge,
|
|
2530
|
+
{
|
|
2531
|
+
save: api.saveProgress,
|
|
2532
|
+
load: api.loadProgress,
|
|
2533
|
+
markComplete: api.markComplete,
|
|
2534
|
+
reset: api.resetProgress,
|
|
2535
|
+
children: content
|
|
2536
|
+
}
|
|
2537
|
+
);
|
|
2356
2538
|
}
|
|
2357
|
-
|
|
2358
|
-
const isReady = status === MediaStatus.READY;
|
|
2359
|
-
const isFailed = status === MediaStatus.FAILED;
|
|
2360
|
-
const progress = record.transcodeProgress ? {
|
|
2361
|
-
current: record.transcodeProgress.current,
|
|
2362
|
-
total: record.transcodeProgress.total,
|
|
2363
|
-
percent: record.transcodeProgress.total > 0 ? Math.round(
|
|
2364
|
-
record.transcodeProgress.current / record.transcodeProgress.total * 100
|
|
2365
|
-
) : 0,
|
|
2366
|
-
phase: record.transcodeProgress.phase
|
|
2367
|
-
} : null;
|
|
2368
|
-
return {
|
|
2369
|
-
status,
|
|
2370
|
-
progress,
|
|
2371
|
-
error: record.transcodeError ?? null,
|
|
2372
|
-
failureReason: isFailed ? record.failureReason ?? "internal" : null,
|
|
2373
|
-
retriable: isFailed ? record.retriable ?? true : null,
|
|
2374
|
-
isQueued: status === MediaStatus.QUEUED,
|
|
2375
|
-
isTranscoding: status === MediaStatus.TRANSCODING,
|
|
2376
|
-
isCaptioning: status === MediaStatus.CAPTIONING,
|
|
2377
|
-
isReady,
|
|
2378
|
-
isFailed,
|
|
2379
|
-
playlistUrl: record.playlistUrl ?? null,
|
|
2380
|
-
thumbnailKey: record.thumbnailKey ?? null,
|
|
2381
|
-
captionLocales: record.captionLocales ?? [],
|
|
2382
|
-
duration: record.duration ?? null,
|
|
2383
|
-
isLoading: false
|
|
2384
|
-
};
|
|
2539
|
+
return content;
|
|
2385
2540
|
}
|
|
2386
|
-
function
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
2408
|
-
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
2409
|
-
});
|
|
2541
|
+
function ProgressBridge({
|
|
2542
|
+
save,
|
|
2543
|
+
load,
|
|
2544
|
+
markComplete,
|
|
2545
|
+
reset,
|
|
2546
|
+
children
|
|
2547
|
+
}) {
|
|
2548
|
+
const convex = useConvex();
|
|
2549
|
+
const saveMutation = useMutation(save);
|
|
2550
|
+
const markCompleteMutation = useMutation(markComplete);
|
|
2551
|
+
const resetMutation = useMutation(reset);
|
|
2552
|
+
const onSave = React26.useCallback(
|
|
2553
|
+
async (videoId, position) => {
|
|
2554
|
+
await saveMutation({ videoId, position });
|
|
2555
|
+
},
|
|
2556
|
+
[saveMutation]
|
|
2557
|
+
);
|
|
2558
|
+
const onLoad = React26.useCallback(
|
|
2559
|
+
async (videoId) => {
|
|
2560
|
+
const result = await convex.query(load, { videoId });
|
|
2561
|
+
if (!result) return null;
|
|
2410
2562
|
return {
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
playlist,
|
|
2414
|
-
duration: status.duration ?? 0,
|
|
2415
|
-
captions,
|
|
2416
|
-
playerProps: {
|
|
2417
|
-
src: playlist,
|
|
2418
|
-
token: viewerToken,
|
|
2419
|
-
captions,
|
|
2420
|
-
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
2421
|
-
}
|
|
2563
|
+
lastPosition: result.lastPosition,
|
|
2564
|
+
completedAt: result.completedAt ? new Date(result.completedAt) : null
|
|
2422
2565
|
};
|
|
2423
|
-
}
|
|
2424
|
-
if (status.isQueued) return { phase: "queued" };
|
|
2425
|
-
if (status.isTranscoding || status.isCaptioning) {
|
|
2426
|
-
const step = status.progress?.phase ?? (status.isCaptioning ? "captioning" : "encoding");
|
|
2427
|
-
return { phase: "processing", step, percent: status.progress?.percent ?? 0 };
|
|
2428
|
-
}
|
|
2429
|
-
return { phase: "idle" };
|
|
2430
|
-
}, [media, status, baseUrl, token, locales, sessionSecret]);
|
|
2431
|
-
}
|
|
2432
|
-
function useMediaUpload(options) {
|
|
2433
|
-
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
2434
|
-
const api = useDevulturApi();
|
|
2435
|
-
const createUploadUrlAction = useAction2(api.createUploadUrl);
|
|
2436
|
-
const deleteMediaAction = useAction2(api.deleteMedia);
|
|
2437
|
-
const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
|
|
2438
|
-
const processVideoAction = useAction2(api.processVideo ?? api.createUploadUrl);
|
|
2439
|
-
const [phase, setPhase] = React22.useState("idle");
|
|
2440
|
-
const [percent, setPercent] = React22.useState(0);
|
|
2441
|
-
const [currentFile, setCurrentFile] = React22.useState(null);
|
|
2442
|
-
const [media, setMedia] = React22.useState(null);
|
|
2443
|
-
const [uploadFailure, setUploadFailure] = React22.useState(null);
|
|
2444
|
-
const [processRequested, setProcessRequested] = React22.useState(false);
|
|
2445
|
-
const abortRef = React22.useRef(null);
|
|
2446
|
-
const lastFileRef = React22.useRef(null);
|
|
2447
|
-
const lastKeyRef = React22.useRef(null);
|
|
2448
|
-
const videoState = useVideoState(media, { locales });
|
|
2449
|
-
const triggerProcess = React22.useCallback(
|
|
2450
|
-
(key) => {
|
|
2451
|
-
if (!api.processVideo) {
|
|
2452
|
-
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
2453
|
-
}
|
|
2454
|
-
lastKeyRef.current = key;
|
|
2455
|
-
setProcessRequested(true);
|
|
2456
|
-
processVideoAction({ key }).catch((err) => {
|
|
2457
|
-
console.error("useMediaUpload: could not request the transcode", err);
|
|
2458
|
-
});
|
|
2459
2566
|
},
|
|
2460
|
-
[
|
|
2567
|
+
[convex, load]
|
|
2461
2568
|
);
|
|
2462
|
-
const
|
|
2463
|
-
async (
|
|
2464
|
-
|
|
2465
|
-
abortRef.current = controller;
|
|
2466
|
-
lastFileRef.current = file;
|
|
2467
|
-
setUploadFailure(null);
|
|
2468
|
-
setMedia(null);
|
|
2469
|
-
setProcessRequested(false);
|
|
2470
|
-
setCurrentFile(file);
|
|
2471
|
-
setPercent(0);
|
|
2472
|
-
setPhase("uploading");
|
|
2473
|
-
const fail = (reason, message) => {
|
|
2474
|
-
const failure = { stage: "upload", reason, retriable: true, message };
|
|
2475
|
-
setUploadFailure(failure);
|
|
2476
|
-
setPhase("upload-failed");
|
|
2477
|
-
onError?.(failure);
|
|
2478
|
-
return null;
|
|
2479
|
-
};
|
|
2480
|
-
let result;
|
|
2481
|
-
try {
|
|
2482
|
-
result = await createUploadUrlAction({
|
|
2483
|
-
filename: file.name,
|
|
2484
|
-
contentType: file.type,
|
|
2485
|
-
visibility
|
|
2486
|
-
});
|
|
2487
|
-
} catch (err) {
|
|
2488
|
-
return fail("rejected", err instanceof Error ? err.message : "Could not start the upload");
|
|
2489
|
-
}
|
|
2490
|
-
const dimensions = await readImageDimensions(file);
|
|
2491
|
-
try {
|
|
2492
|
-
await uploadFileWithProgress(result.url, file, { onProgress: setPercent, signal: controller.signal });
|
|
2493
|
-
} catch (err) {
|
|
2494
|
-
if (err instanceof DOMException && err.name === "AbortError") {
|
|
2495
|
-
setPhase("idle");
|
|
2496
|
-
setCurrentFile(null);
|
|
2497
|
-
setPercent(0);
|
|
2498
|
-
return null;
|
|
2499
|
-
}
|
|
2500
|
-
const rejected = err instanceof UploadHttpError && (err.status === 413 || err.status === 415);
|
|
2501
|
-
return fail(rejected ? "rejected" : "network", err instanceof Error ? err.message : "The upload failed");
|
|
2502
|
-
}
|
|
2503
|
-
if (api.confirmUpload) await confirmUploadBestEffort(confirmUploadAction, result.key, file, dimensions);
|
|
2504
|
-
const uploaded = {
|
|
2505
|
-
id: result.id,
|
|
2506
|
-
kind: result.kind,
|
|
2507
|
-
visibility: result.visibility,
|
|
2508
|
-
filename: file.name,
|
|
2509
|
-
key: result.key,
|
|
2510
|
-
...dimensions
|
|
2511
|
-
};
|
|
2512
|
-
setMedia(uploaded);
|
|
2513
|
-
setPhase("uploaded");
|
|
2514
|
-
if (replaces) deleteMediaAction({ key: mediaPath3(replaces) }).catch(() => {
|
|
2515
|
-
});
|
|
2516
|
-
onComplete?.(uploaded);
|
|
2517
|
-
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
2518
|
-
return uploaded;
|
|
2569
|
+
const onMarkComplete = React26.useCallback(
|
|
2570
|
+
async (videoId) => {
|
|
2571
|
+
await markCompleteMutation({ videoId });
|
|
2519
2572
|
},
|
|
2520
|
-
[
|
|
2521
|
-
createUploadUrlAction,
|
|
2522
|
-
confirmUploadAction,
|
|
2523
|
-
api.confirmUpload,
|
|
2524
|
-
deleteMediaAction,
|
|
2525
|
-
triggerProcess,
|
|
2526
|
-
visibility,
|
|
2527
|
-
replaces,
|
|
2528
|
-
process,
|
|
2529
|
-
onComplete,
|
|
2530
|
-
onError
|
|
2531
|
-
]
|
|
2573
|
+
[markCompleteMutation]
|
|
2532
2574
|
);
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
setPhase("idle");
|
|
2539
|
-
setPercent(0);
|
|
2540
|
-
setCurrentFile(null);
|
|
2541
|
-
setMedia(null);
|
|
2542
|
-
setUploadFailure(null);
|
|
2543
|
-
setProcessRequested(false);
|
|
2544
|
-
}, []);
|
|
2545
|
-
const retry = React22.useCallback(() => {
|
|
2546
|
-
if (phase === "upload-failed" && lastFileRef.current) {
|
|
2547
|
-
runUpload(lastFileRef.current);
|
|
2548
|
-
return;
|
|
2549
|
-
}
|
|
2550
|
-
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
2551
|
-
}, [phase, runUpload, triggerProcess]);
|
|
2552
|
-
React22.useEffect(() => () => abortRef.current?.abort(), []);
|
|
2553
|
-
const state = React22.useMemo(
|
|
2554
|
-
() => deriveUploadState(
|
|
2555
|
-
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
2556
|
-
videoState,
|
|
2557
|
-
retry
|
|
2558
|
-
),
|
|
2559
|
-
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
2575
|
+
const onReset = React26.useCallback(
|
|
2576
|
+
async (videoId) => {
|
|
2577
|
+
await resetMutation({ videoId });
|
|
2578
|
+
},
|
|
2579
|
+
[resetMutation]
|
|
2560
2580
|
);
|
|
2561
|
-
const
|
|
2562
|
-
|
|
2581
|
+
const value = React26.useMemo(
|
|
2582
|
+
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
2583
|
+
[onSave, onLoad, onMarkComplete, onReset]
|
|
2584
|
+
);
|
|
2585
|
+
return /* @__PURE__ */ jsx18(DevulturProgressContext, { value, children });
|
|
2563
2586
|
}
|
|
2564
2587
|
export {
|
|
2565
2588
|
AttachmentCard,
|
|
2566
2589
|
CropUpload,
|
|
2590
|
+
DevulturApiContext,
|
|
2567
2591
|
DevulturCapabilityError,
|
|
2592
|
+
DevulturContext,
|
|
2593
|
+
DevulturProgressContext,
|
|
2568
2594
|
DevulturProvider,
|
|
2569
2595
|
DevulturViewerProvider,
|
|
2570
2596
|
DocumentLink,
|
|
@@ -2586,6 +2612,7 @@ export {
|
|
|
2586
2612
|
useDevultur,
|
|
2587
2613
|
useDevulturApi,
|
|
2588
2614
|
useDevulturMedia,
|
|
2615
|
+
useDevulturOptional,
|
|
2589
2616
|
useDevulturProgress,
|
|
2590
2617
|
useMediaLibrary,
|
|
2591
2618
|
useMediaObjectUrl,
|