@devultur/convex 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +462 -9
- package/dist/bin/init.js +44 -23
- package/dist/chunk-EX3W77VY.js +245 -0
- package/dist/chunk-HOFTKJWT.js +58 -0
- package/dist/chunk-KAIVI7RB.js +21 -0
- package/dist/chunk-QBHIZBOQ.js +139 -0
- package/dist/{chunk-GKLFRSGI.js → chunk-YIPZZDGD.js} +8 -1
- package/dist/component/_generated/api.d.ts +6 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +27 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/library.d.ts +93 -0
- package/dist/component/library.d.ts.map +1 -0
- package/dist/component/library.js +74 -0
- package/dist/component/library.js.map +1 -0
- package/dist/component/mediaMeta.d.ts +46 -0
- package/dist/component/mediaMeta.d.ts.map +1 -0
- package/dist/component/mediaMeta.js +82 -0
- package/dist/component/mediaMeta.js.map +1 -0
- package/dist/component/metadata.d.ts +59 -0
- package/dist/component/metadata.d.ts.map +1 -0
- package/dist/component/metadata.js +32 -0
- package/dist/component/metadata.js.map +1 -0
- package/dist/component/public.d.ts +17 -1
- package/dist/component/public.d.ts.map +1 -1
- package/dist/component/public.js +20 -11
- package/dist/component/public.js.map +1 -1
- package/dist/component/schema.d.ts +17 -2
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +13 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/uploads.d.ts +13 -0
- package/dist/component/uploads.d.ts.map +1 -1
- package/dist/component/uploads.js +24 -5
- package/dist/component/uploads.js.map +1 -1
- package/dist/error-labels-BfqcFh2R.d.ts +19 -0
- package/dist/index.d.ts +204 -93
- package/dist/index.js +200 -128
- package/dist/locales/es.d.ts +11 -0
- package/dist/locales/es.js +162 -0
- package/dist/media-picker-labels-CiVcNUZo.d.ts +114 -0
- package/dist/{pdf-render-CiCK6V1X.d.ts → pdf-render-BwJGagvz.d.ts} +1 -1
- package/dist/pdfjs.d.ts +13 -2
- package/dist/pdfjs.js +9 -0
- package/dist/react.d.ts +462 -64
- package/dist/react.js +1625 -493
- package/dist/types-LrusGVDf.d.ts +62 -0
- package/dist/validators.d.ts +13 -1
- package/dist/validators.js +1 -1
- package/package.json +8 -4
package/dist/react.js
CHANGED
|
@@ -1,23 +1,62 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import {
|
|
3
|
+
mediaFromRecord
|
|
4
|
+
} from "./chunk-KAIVI7RB.js";
|
|
5
|
+
import {
|
|
6
|
+
countPages,
|
|
7
|
+
isRenderCancelled,
|
|
8
|
+
openPdf,
|
|
9
|
+
pageText,
|
|
10
|
+
renderPage
|
|
11
|
+
} from "./chunk-HOFTKJWT.js";
|
|
12
|
+
import {
|
|
13
|
+
attachmentDetails,
|
|
14
|
+
attachmentKindOf,
|
|
15
|
+
attachmentName,
|
|
16
|
+
defaultAttachmentLabels,
|
|
17
|
+
expectedContentType,
|
|
18
|
+
formatDuration,
|
|
19
|
+
formatFileSize,
|
|
20
|
+
mergeAttachmentLabels,
|
|
21
|
+
previewKindOf,
|
|
22
|
+
trustedContentType
|
|
23
|
+
} from "./chunk-QBHIZBOQ.js";
|
|
24
|
+
import {
|
|
25
|
+
describeDevulturError
|
|
26
|
+
} from "./chunk-EX3W77VY.js";
|
|
2
27
|
|
|
3
28
|
// src/react.tsx
|
|
4
|
-
import
|
|
29
|
+
import React22 from "react";
|
|
5
30
|
import {
|
|
6
|
-
buildMediaUrl,
|
|
31
|
+
buildMediaUrl as buildMediaUrl2,
|
|
7
32
|
CaptionLocaleLabel,
|
|
8
|
-
DEFAULT_TOKEN_EXPIRY_SECONDS,
|
|
9
33
|
MediaStatus,
|
|
10
|
-
mediaCaptionPath,
|
|
11
|
-
mediaPath as
|
|
12
|
-
mediaUrl as
|
|
13
|
-
SESSION_HEADER as SESSION_HEADER2,
|
|
34
|
+
mediaCaptionPath as mediaCaptionPath2,
|
|
35
|
+
mediaPath as mediaPath3,
|
|
36
|
+
mediaUrl as mediaUrl4,
|
|
14
37
|
visibilityFromKey
|
|
15
38
|
} from "@devultur/core";
|
|
16
39
|
import {
|
|
17
40
|
UploadHttpError,
|
|
18
41
|
uploadFileWithProgress
|
|
19
42
|
} from "@devultur/react";
|
|
20
|
-
import { useAction, useConvex, useMutation, useQuery } from "convex/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";
|
|
21
60
|
|
|
22
61
|
// src/image-recovery.ts
|
|
23
62
|
async function isSessionMismatch(response) {
|
|
@@ -29,89 +68,21 @@ async function isSessionMismatch(response) {
|
|
|
29
68
|
}
|
|
30
69
|
}
|
|
31
70
|
|
|
32
|
-
// src/media-fetch.ts
|
|
33
|
-
import { SESSION_HEADER } from "@devultur/core";
|
|
34
|
-
|
|
35
|
-
// src/preview-type.ts
|
|
36
|
-
var PREVIEW_KIND_BY_TYPE = /* @__PURE__ */ new Map([
|
|
37
|
-
["image/png", "image"],
|
|
38
|
-
["image/jpeg", "image"],
|
|
39
|
-
["image/webp", "image"],
|
|
40
|
-
["image/gif", "image"],
|
|
41
|
-
["image/avif", "image"],
|
|
42
|
-
["application/pdf", "pdf"],
|
|
43
|
-
["audio/mpeg", "audio"],
|
|
44
|
-
["audio/mp4", "audio"],
|
|
45
|
-
["audio/aac", "audio"],
|
|
46
|
-
["audio/ogg", "audio"],
|
|
47
|
-
["audio/wav", "audio"],
|
|
48
|
-
["audio/webm", "audio"],
|
|
49
|
-
["audio/flac", "audio"],
|
|
50
|
-
["text/plain", "text"],
|
|
51
|
-
["text/csv", "text"],
|
|
52
|
-
["application/json", "text"]
|
|
53
|
-
]);
|
|
54
|
-
var TYPE_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
55
|
-
["png", "image/png"],
|
|
56
|
-
["jpg", "image/jpeg"],
|
|
57
|
-
["jpeg", "image/jpeg"],
|
|
58
|
-
["webp", "image/webp"],
|
|
59
|
-
["gif", "image/gif"],
|
|
60
|
-
["avif", "image/avif"],
|
|
61
|
-
["pdf", "application/pdf"],
|
|
62
|
-
["mp3", "audio/mpeg"],
|
|
63
|
-
["m4a", "audio/mp4"],
|
|
64
|
-
["aac", "audio/aac"],
|
|
65
|
-
["ogg", "audio/ogg"],
|
|
66
|
-
["oga", "audio/ogg"],
|
|
67
|
-
["opus", "audio/ogg"],
|
|
68
|
-
["wav", "audio/wav"],
|
|
69
|
-
["weba", "audio/webm"],
|
|
70
|
-
["flac", "audio/flac"],
|
|
71
|
-
["txt", "text/plain"],
|
|
72
|
-
["csv", "text/csv"],
|
|
73
|
-
["json", "application/json"]
|
|
74
|
-
]);
|
|
75
|
-
var OPAQUE_CONTENT_TYPE = "application/octet-stream";
|
|
76
|
-
function normalizeContentType(value) {
|
|
77
|
-
const essence = value?.split(";")[0]?.trim().toLowerCase();
|
|
78
|
-
return essence || null;
|
|
79
|
-
}
|
|
80
|
-
function previewKindOf(contentType) {
|
|
81
|
-
const normalized = normalizeContentType(contentType);
|
|
82
|
-
if (!normalized) return null;
|
|
83
|
-
return PREVIEW_KIND_BY_TYPE.get(normalized) ?? null;
|
|
84
|
-
}
|
|
85
|
-
function expectedContentType(media, declaredType) {
|
|
86
|
-
if (declaredType) return normalizeContentType(declaredType);
|
|
87
|
-
const extension = media.filename?.split(".").pop()?.toLowerCase();
|
|
88
|
-
if (!extension || extension === media.filename?.toLowerCase()) return null;
|
|
89
|
-
return TYPE_BY_EXTENSION.get(extension) ?? null;
|
|
90
|
-
}
|
|
91
|
-
function trustedContentType(expectedType, responseType) {
|
|
92
|
-
const receivedKind = previewKindOf(responseType);
|
|
93
|
-
if (!expectedType) {
|
|
94
|
-
const contentType = receivedKind ? normalizeContentType(responseType) : null;
|
|
95
|
-
return { ok: true, contentType: contentType ?? OPAQUE_CONTENT_TYPE };
|
|
96
|
-
}
|
|
97
|
-
const expectedKind = previewKindOf(expectedType);
|
|
98
|
-
if (!expectedKind) return { ok: false, reason: "unsupported_type" };
|
|
99
|
-
if (receivedKind !== expectedKind) return { ok: false, reason: "type_mismatch" };
|
|
100
|
-
return { ok: true, contentType: normalizeContentType(expectedType) ?? OPAQUE_CONTENT_TYPE };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
71
|
// src/media-fetch.ts
|
|
104
72
|
var MediaFetchError = class extends Error {
|
|
105
73
|
code;
|
|
106
74
|
/** HTTP status, for `http_error`. */
|
|
107
75
|
status;
|
|
108
|
-
constructor(code, message, status) {
|
|
109
|
-
super(message);
|
|
76
|
+
constructor(code, message, status, options) {
|
|
77
|
+
super(message, options);
|
|
110
78
|
this.name = "MediaFetchError";
|
|
111
79
|
this.code = code;
|
|
112
80
|
this.status = status;
|
|
113
81
|
}
|
|
114
82
|
};
|
|
83
|
+
function tokenUnavailableError(cause) {
|
|
84
|
+
return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
|
|
85
|
+
}
|
|
115
86
|
async function fetchMedia(request) {
|
|
116
87
|
const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
|
|
117
88
|
const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
|
|
@@ -172,6 +143,159 @@ async function readCapped(body, maxBytes) {
|
|
|
172
143
|
return bytes;
|
|
173
144
|
}
|
|
174
145
|
|
|
146
|
+
// src/media-session.ts
|
|
147
|
+
import React2 from "react";
|
|
148
|
+
import {
|
|
149
|
+
buildMediaUrl,
|
|
150
|
+
mediaCaptionPath,
|
|
151
|
+
mediaUrl,
|
|
152
|
+
SESSION_HEADER as SESSION_HEADER2
|
|
153
|
+
} from "@devultur/core";
|
|
154
|
+
import { useAction } from "convex/react";
|
|
155
|
+
|
|
156
|
+
// src/viewer-token.ts
|
|
157
|
+
import React from "react";
|
|
158
|
+
import { DEFAULT_TOKEN_EXPIRY_SECONDS } from "@devultur/core";
|
|
159
|
+
import { convexToJson } from "convex/values";
|
|
160
|
+
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
161
|
+
var MIN_REFRESH_MS = 6e4;
|
|
162
|
+
var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
163
|
+
function extractTokenResult(result) {
|
|
164
|
+
const tokenValue = typeof result === "string" ? result : result?.token;
|
|
165
|
+
if (typeof tokenValue !== "string" || tokenValue === "") {
|
|
166
|
+
throw new Error("The viewer token action returned no token");
|
|
167
|
+
}
|
|
168
|
+
const expiresIn = typeof result === "object" ? result?.expiresIn : void 0;
|
|
169
|
+
const ttlMs = typeof expiresIn === "number" ? expiresIn * 1e3 : DEFAULT_TTL_MS;
|
|
170
|
+
return { tokenValue, ttlMs };
|
|
171
|
+
}
|
|
172
|
+
var asError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
173
|
+
function sortKeys(json) {
|
|
174
|
+
if (Array.isArray(json)) return json.map(sortKeys);
|
|
175
|
+
if (json === null || typeof json !== "object") return json;
|
|
176
|
+
return Object.fromEntries(
|
|
177
|
+
Object.keys(json).sort().map((name) => [name, sortKeys(json[name])])
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
function tokenArgsKey(tokenArgs) {
|
|
181
|
+
return JSON.stringify(sortKeys(convexToJson(tokenArgs ?? {})));
|
|
182
|
+
}
|
|
183
|
+
function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
184
|
+
const argsKey = tokenArgsKey(tokenArgs);
|
|
185
|
+
const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
|
|
186
|
+
const [issued, setIssued] = React.useState(null);
|
|
187
|
+
const [attempt, setAttempt] = React.useState(null);
|
|
188
|
+
const token = issued?.argsKey === argsKey ? issued.token : null;
|
|
189
|
+
const currentAttempt = attempt?.argsKey === argsKey ? attempt : null;
|
|
190
|
+
const currentArgsKeyRef = React.useRef(argsKey);
|
|
191
|
+
const refreshTimerRef = React.useRef(null);
|
|
192
|
+
const inFlightRef = React.useRef(null);
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
currentArgsKeyRef.current = argsKey;
|
|
195
|
+
}, [argsKey]);
|
|
196
|
+
React.useEffect(() => {
|
|
197
|
+
return () => {
|
|
198
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
199
|
+
};
|
|
200
|
+
}, []);
|
|
201
|
+
const request = React.useCallback(async () => {
|
|
202
|
+
const renew = () => {
|
|
203
|
+
request().catch((error) => console.warn("Devultur: could not renew the viewer token", error));
|
|
204
|
+
};
|
|
205
|
+
const result = await issueViewerToken({ ...stableArgs, sessionSecret });
|
|
206
|
+
if (currentArgsKeyRef.current !== argsKey) return null;
|
|
207
|
+
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
208
|
+
setIssued({ token: tokenValue, argsKey });
|
|
209
|
+
setAttempt(null);
|
|
210
|
+
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
211
|
+
refreshTimerRef.current = setTimeout(renew, Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS));
|
|
212
|
+
return tokenValue;
|
|
213
|
+
}, [issueViewerToken, stableArgs, argsKey, sessionSecret]);
|
|
214
|
+
const issueOnce = React.useCallback(() => {
|
|
215
|
+
const inFlight = inFlightRef.current;
|
|
216
|
+
if (inFlight?.argsKey === argsKey) return inFlight.promise;
|
|
217
|
+
setAttempt({ argsKey, status: "loading" });
|
|
218
|
+
const promise = request().catch((error) => {
|
|
219
|
+
if (currentArgsKeyRef.current === argsKey) setAttempt({ argsKey, status: "error", error: asError(error) });
|
|
220
|
+
throw error;
|
|
221
|
+
});
|
|
222
|
+
const settle = () => {
|
|
223
|
+
if (inFlightRef.current?.promise === promise) inFlightRef.current = null;
|
|
224
|
+
};
|
|
225
|
+
promise.then(settle, settle);
|
|
226
|
+
inFlightRef.current = { argsKey, promise };
|
|
227
|
+
return promise;
|
|
228
|
+
}, [request, argsKey]);
|
|
229
|
+
const hasAttempt = currentAttempt !== null;
|
|
230
|
+
React.useEffect(() => {
|
|
231
|
+
if (token || hasAttempt) return;
|
|
232
|
+
issueOnce().catch(() => {
|
|
233
|
+
});
|
|
234
|
+
}, [token, hasAttempt, issueOnce]);
|
|
235
|
+
const reissueToken = React.useCallback(
|
|
236
|
+
() => issueOnce().catch(() => {
|
|
237
|
+
setIssued((current) => current?.argsKey === argsKey ? null : current);
|
|
238
|
+
return null;
|
|
239
|
+
}),
|
|
240
|
+
[issueOnce, argsKey]
|
|
241
|
+
);
|
|
242
|
+
const retryToken = React.useCallback(() => issueOnce().catch(() => null), [issueOnce]);
|
|
243
|
+
const tokenStatus = token ? "ready" : currentAttempt?.status ?? "idle";
|
|
244
|
+
const tokenError = !token && currentAttempt?.status === "error" ? currentAttempt.error : null;
|
|
245
|
+
return { token, tokenStatus, tokenError, reissueToken, retryToken };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/media-session.ts
|
|
249
|
+
function useMediaSession({ issueViewerToken, tokenArgs, baseUrl }) {
|
|
250
|
+
const [sessionSecret] = React2.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
|
|
251
|
+
const issueViewerTokenAction = useAction(issueViewerToken);
|
|
252
|
+
const { token, tokenStatus, tokenError, reissueToken, retryToken } = useViewerToken({
|
|
253
|
+
issueViewerToken: issueViewerTokenAction,
|
|
254
|
+
sessionSecret,
|
|
255
|
+
tokenArgs
|
|
256
|
+
});
|
|
257
|
+
const url = React2.useCallback(
|
|
258
|
+
(media, options) => {
|
|
259
|
+
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
260
|
+
return mediaUrl(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
261
|
+
},
|
|
262
|
+
[baseUrl, token]
|
|
263
|
+
);
|
|
264
|
+
const getCaptionsText = React2.useCallback(
|
|
265
|
+
async (media, locale) => {
|
|
266
|
+
if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
|
|
267
|
+
const path = mediaCaptionPath(media, locale);
|
|
268
|
+
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
269
|
+
const isPrivate = media.visibility === "private";
|
|
270
|
+
const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
|
|
271
|
+
headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
|
|
272
|
+
});
|
|
273
|
+
let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
|
|
274
|
+
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
275
|
+
const fresh = await reissueToken();
|
|
276
|
+
if (fresh) response = await fetchWith(fresh);
|
|
277
|
+
}
|
|
278
|
+
if (!response.ok) throw new Error(`Captions fetch failed: ${response.status}`);
|
|
279
|
+
return response.text();
|
|
280
|
+
},
|
|
281
|
+
[baseUrl, token, reissueToken, sessionSecret]
|
|
282
|
+
);
|
|
283
|
+
return React2.useMemo(
|
|
284
|
+
() => ({
|
|
285
|
+
token,
|
|
286
|
+
tokenStatus,
|
|
287
|
+
tokenError,
|
|
288
|
+
retryToken,
|
|
289
|
+
baseUrl: baseUrl ?? null,
|
|
290
|
+
sessionSecret,
|
|
291
|
+
url,
|
|
292
|
+
getCaptionsText,
|
|
293
|
+
reissueToken
|
|
294
|
+
}),
|
|
295
|
+
[token, tokenStatus, tokenError, retryToken, baseUrl, sessionSecret, url, getCaptionsText, reissueToken]
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
175
299
|
// src/media-upload-derive.ts
|
|
176
300
|
function deriveUploadState(local, videoState, retry) {
|
|
177
301
|
const { phase, percent, file, failure, media, processRequested } = local;
|
|
@@ -215,7 +339,7 @@ function deriveUploadState(local, videoState, retry) {
|
|
|
215
339
|
}
|
|
216
340
|
|
|
217
341
|
// src/crop-upload.tsx
|
|
218
|
-
import
|
|
342
|
+
import React3 from "react";
|
|
219
343
|
import { ImageCropper } from "@devultur/react";
|
|
220
344
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
221
345
|
var DEFAULT_LABELS = {
|
|
@@ -239,18 +363,18 @@ function CropUpload({
|
|
|
239
363
|
children
|
|
240
364
|
}) {
|
|
241
365
|
const text = { ...DEFAULT_LABELS, ...labels };
|
|
242
|
-
const [pendingFile, setPendingFile] =
|
|
243
|
-
const inputRef =
|
|
366
|
+
const [pendingFile, setPendingFile] = React3.useState(null);
|
|
367
|
+
const inputRef = React3.useRef(null);
|
|
244
368
|
const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
|
|
245
|
-
const pickFile =
|
|
246
|
-
const handleCrop =
|
|
369
|
+
const pickFile = React3.useCallback(() => inputRef.current?.click(), []);
|
|
370
|
+
const handleCrop = React3.useCallback(
|
|
247
371
|
(baked) => {
|
|
248
372
|
setPendingFile(null);
|
|
249
373
|
upload(baked);
|
|
250
374
|
},
|
|
251
375
|
[upload]
|
|
252
376
|
);
|
|
253
|
-
const resetAll =
|
|
377
|
+
const resetAll = React3.useCallback(() => {
|
|
254
378
|
setPendingFile(null);
|
|
255
379
|
reset();
|
|
256
380
|
}, [reset]);
|
|
@@ -302,9 +426,268 @@ function CropUpload({
|
|
|
302
426
|
] });
|
|
303
427
|
}
|
|
304
428
|
|
|
429
|
+
// src/document-renderers.tsx
|
|
430
|
+
import React4 from "react";
|
|
431
|
+
|
|
432
|
+
// src/attachment-icons.tsx
|
|
433
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
434
|
+
var SHEET = "M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z M14 3v5h5";
|
|
435
|
+
var ICON_PATHS = {
|
|
436
|
+
file: SHEET,
|
|
437
|
+
text: `${SHEET} M9 13h6 M9 17h6`,
|
|
438
|
+
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`,
|
|
439
|
+
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",
|
|
440
|
+
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",
|
|
441
|
+
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"
|
|
442
|
+
};
|
|
443
|
+
function AttachmentIcon({ kind }) {
|
|
444
|
+
return /* @__PURE__ */ jsx2(
|
|
445
|
+
"svg",
|
|
446
|
+
{
|
|
447
|
+
"aria-hidden": "true",
|
|
448
|
+
focusable: "false",
|
|
449
|
+
width: "24",
|
|
450
|
+
height: "24",
|
|
451
|
+
viewBox: "0 0 24 24",
|
|
452
|
+
fill: "none",
|
|
453
|
+
stroke: "currentColor",
|
|
454
|
+
strokeWidth: "1.5",
|
|
455
|
+
strokeLinecap: "round",
|
|
456
|
+
strokeLinejoin: "round",
|
|
457
|
+
"data-part": "icon",
|
|
458
|
+
"data-kind": kind,
|
|
459
|
+
children: /* @__PURE__ */ jsx2("path", { d: ICON_PATHS[kind] })
|
|
460
|
+
}
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/document-renderers.tsx
|
|
465
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
466
|
+
function DocumentLink({ media, download = false, children, labels, ...anchorProps }) {
|
|
467
|
+
const { url } = useDevultur();
|
|
468
|
+
if (!media) return null;
|
|
469
|
+
const text = children ?? attachmentName(media, mergeAttachmentLabels(labels).untitled);
|
|
470
|
+
if (download || media.visibility === "private") {
|
|
471
|
+
return /* @__PURE__ */ jsx3(DownloadLink, { media, ...anchorProps, children: text });
|
|
472
|
+
}
|
|
473
|
+
return /* @__PURE__ */ jsx3("a", { href: url(media), ...anchorProps, children: text });
|
|
474
|
+
}
|
|
475
|
+
function AttachmentCard({
|
|
476
|
+
media,
|
|
477
|
+
contentType,
|
|
478
|
+
onOpen,
|
|
479
|
+
preview,
|
|
480
|
+
headingLevel = 3,
|
|
481
|
+
locale,
|
|
482
|
+
labels,
|
|
483
|
+
...articleProps
|
|
484
|
+
}) {
|
|
485
|
+
const headingId = React4.useId();
|
|
486
|
+
if (!media) return null;
|
|
487
|
+
const text = mergeAttachmentLabels(labels);
|
|
488
|
+
const kind = attachmentKindOf(media, contentType);
|
|
489
|
+
const details = attachmentDetails(media, kind, text, locale ?? text.locale);
|
|
490
|
+
const Heading = `h${headingLevel}`;
|
|
491
|
+
return /* @__PURE__ */ jsxs2("article", { "aria-labelledby": headingId, "data-kind": kind, ...articleProps, children: [
|
|
492
|
+
preview ?? /* @__PURE__ */ jsx3(AttachmentIcon, { kind }),
|
|
493
|
+
/* @__PURE__ */ jsx3(Heading, { id: headingId, "data-part": "name", children: attachmentName(media, text.untitled) }),
|
|
494
|
+
/* @__PURE__ */ jsx3("p", { "data-part": "details", children: details.join(" \xB7 ") }),
|
|
495
|
+
/* @__PURE__ */ jsxs2("div", { "data-part": "actions", children: [
|
|
496
|
+
/* @__PURE__ */ jsx3(DownloadLink, { media, "aria-describedby": headingId, "data-part": "download", children: text.download }),
|
|
497
|
+
onOpen && /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => onOpen(media), "aria-describedby": headingId, "data-part": "open", children: text.open })
|
|
498
|
+
] })
|
|
499
|
+
] });
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/image.tsx
|
|
503
|
+
import React5 from "react";
|
|
504
|
+
import { mediaUrl as mediaUrl2 } from "@devultur/core";
|
|
505
|
+
|
|
506
|
+
// src/responsive-image.ts
|
|
507
|
+
function normalizeWidths(widths) {
|
|
508
|
+
const validWidths = widths.filter((width) => Number.isInteger(width) && width > 0);
|
|
509
|
+
return [...new Set(validWidths)].sort((a, b) => a - b);
|
|
510
|
+
}
|
|
511
|
+
function buildSrcSet(widths, urlForWidth) {
|
|
512
|
+
return widths.map((width) => `${urlForWidth(width)} ${width}w`).join(", ");
|
|
513
|
+
}
|
|
514
|
+
function scaledHeight(targetWidth, width, height) {
|
|
515
|
+
if (!width || !height) return void 0;
|
|
516
|
+
return Math.round(targetWidth * height / width);
|
|
517
|
+
}
|
|
518
|
+
function pickWidth(widths, renderedWidth) {
|
|
519
|
+
const ascending = normalizeWidths(widths);
|
|
520
|
+
if (renderedWidth === null) return ascending.at(-1);
|
|
521
|
+
const fitting = ascending.filter((width) => width <= renderedWidth * 2);
|
|
522
|
+
return fitting.at(-1) ?? ascending[0];
|
|
523
|
+
}
|
|
524
|
+
var LENGTH = /^(\d+(?:\.\d+)?)(px|vw)$/;
|
|
525
|
+
function lengthInPixels(length, viewportWidth) {
|
|
526
|
+
const match = LENGTH.exec(length.trim());
|
|
527
|
+
if (!match) return null;
|
|
528
|
+
const amount = Number(match[1]);
|
|
529
|
+
return match[2] === "vw" ? amount * viewportWidth / 100 : amount;
|
|
530
|
+
}
|
|
531
|
+
function matchesMedia(condition) {
|
|
532
|
+
if (typeof window.matchMedia !== "function") return false;
|
|
533
|
+
return window.matchMedia(condition).matches;
|
|
534
|
+
}
|
|
535
|
+
function renderedWidthFromSizes(sizes) {
|
|
536
|
+
if (!sizes || typeof window === "undefined") return null;
|
|
537
|
+
const entries = sizes.split(",").map((entry) => entry.trim());
|
|
538
|
+
for (const entry of entries) {
|
|
539
|
+
const lengthStart = entry.lastIndexOf(" ") + 1;
|
|
540
|
+
const condition = entry.slice(0, lengthStart).trim();
|
|
541
|
+
if (condition && !matchesMedia(condition)) continue;
|
|
542
|
+
return lengthInPixels(entry.slice(lengthStart), window.innerWidth);
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/image.tsx
|
|
548
|
+
import { Fragment, jsx as jsx4 } from "react/jsx-runtime";
|
|
549
|
+
function Image({
|
|
550
|
+
media,
|
|
551
|
+
width,
|
|
552
|
+
height,
|
|
553
|
+
fit,
|
|
554
|
+
format,
|
|
555
|
+
quality,
|
|
556
|
+
widths,
|
|
557
|
+
sizes,
|
|
558
|
+
fallback = null,
|
|
559
|
+
externalFallback,
|
|
560
|
+
render,
|
|
561
|
+
alt,
|
|
562
|
+
onLoadError,
|
|
563
|
+
...imgProps
|
|
564
|
+
}) {
|
|
565
|
+
const { url } = useDevultur();
|
|
566
|
+
const isPrivate = media?.visibility === "private";
|
|
567
|
+
const responsiveWidths = widths?.length ? normalizeWidths(widths) : null;
|
|
568
|
+
const variantOf = (variantWidth) => ({
|
|
569
|
+
width: variantWidth,
|
|
570
|
+
height: scaledHeight(variantWidth, width, height),
|
|
571
|
+
fit,
|
|
572
|
+
format,
|
|
573
|
+
quality
|
|
574
|
+
});
|
|
575
|
+
let transforms = { width, height, fit, format, quality };
|
|
576
|
+
if (responsiveWidths && isPrivate) {
|
|
577
|
+
const pickedWidth = pickWidth(responsiveWidths, renderedWidthFromSizes(sizes) ?? width ?? null);
|
|
578
|
+
if (pickedWidth) transforms = variantOf(pickedWidth);
|
|
579
|
+
}
|
|
580
|
+
const { blobSrc, isFailed, handleError } = usePrivateImage(media, transforms, onLoadError);
|
|
581
|
+
let src;
|
|
582
|
+
let responsiveProps = {};
|
|
583
|
+
if (isFailed || !media) {
|
|
584
|
+
src = externalFallback ?? null;
|
|
585
|
+
} else if (isPrivate) {
|
|
586
|
+
src = blobSrc;
|
|
587
|
+
} else if (responsiveWidths) {
|
|
588
|
+
const largestWidth = responsiveWidths.at(-1);
|
|
589
|
+
src = url(media, variantOf(largestWidth));
|
|
590
|
+
const srcSet = buildSrcSet(responsiveWidths, (variantWidth) => url(media, variantOf(variantWidth)));
|
|
591
|
+
responsiveProps = { srcSet, sizes };
|
|
592
|
+
} else {
|
|
593
|
+
src = url(media, transforms);
|
|
594
|
+
}
|
|
595
|
+
if (!src) return /* @__PURE__ */ jsx4(Fragment, { children: fallback });
|
|
596
|
+
if (render) {
|
|
597
|
+
return React5.cloneElement(render, {
|
|
598
|
+
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const lazyProps = responsiveWidths ? { loading: "lazy", decoding: "async" } : {};
|
|
602
|
+
return /* @__PURE__ */ jsx4(
|
|
603
|
+
"img",
|
|
604
|
+
{
|
|
605
|
+
src,
|
|
606
|
+
alt: alt ?? media?.alt,
|
|
607
|
+
width: width ?? media?.width,
|
|
608
|
+
height: height ?? media?.height,
|
|
609
|
+
onError: handleError,
|
|
610
|
+
...lazyProps,
|
|
611
|
+
...responsiveProps,
|
|
612
|
+
...imgProps
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
function usePrivateImage(media, transforms, onLoadError) {
|
|
617
|
+
const { token, tokenStatus, tokenError, baseUrl, reissueToken, sessionSecret } = useDevultur();
|
|
618
|
+
const [isFailed, setIsFailed] = React5.useState(false);
|
|
619
|
+
const [blobSrc, setBlobSrc] = React5.useState(null);
|
|
620
|
+
const sessionRef = React5.useRef({ token, reissueToken, onLoadError });
|
|
621
|
+
React5.useEffect(() => {
|
|
622
|
+
sessionRef.current = { token, reissueToken, onLoadError };
|
|
623
|
+
});
|
|
624
|
+
const isPrivate = media?.visibility === "private";
|
|
625
|
+
const hasToken = token !== null;
|
|
626
|
+
const isTokenUnavailable = isPrivate && tokenStatus === "error";
|
|
627
|
+
const { width, height, fit, format, quality } = transforms;
|
|
628
|
+
React5.useEffect(() => {
|
|
629
|
+
setIsFailed(false);
|
|
630
|
+
setBlobSrc(null);
|
|
631
|
+
}, [media]);
|
|
632
|
+
React5.useEffect(() => {
|
|
633
|
+
if (!isTokenUnavailable || !media) return;
|
|
634
|
+
sessionRef.current.onLoadError?.(tokenUnavailableError(tokenError));
|
|
635
|
+
}, [isTokenUnavailable, media, tokenError]);
|
|
636
|
+
React5.useEffect(() => {
|
|
637
|
+
if (!isPrivate || isFailed || !media || !hasToken) return;
|
|
638
|
+
let cancelled = false;
|
|
639
|
+
let objectUrl = null;
|
|
640
|
+
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
641
|
+
const fetchTransforms = { width, height, fit, format, quality };
|
|
642
|
+
const fail = (error) => {
|
|
643
|
+
if (cancelled) return;
|
|
644
|
+
setIsFailed(true);
|
|
645
|
+
sessionRef.current.onLoadError?.(error);
|
|
646
|
+
};
|
|
647
|
+
(async () => {
|
|
648
|
+
try {
|
|
649
|
+
const response = await fetchMedia({
|
|
650
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...fetchTransforms, token: viewerToken }),
|
|
651
|
+
token: sessionRef.current.token,
|
|
652
|
+
sessionSecret,
|
|
653
|
+
reissueToken: sessionRef.current.reissueToken
|
|
654
|
+
});
|
|
655
|
+
if (!response.ok) {
|
|
656
|
+
const status = response.status;
|
|
657
|
+
fail(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
objectUrl = URL.createObjectURL(await response.blob());
|
|
661
|
+
if (cancelled) {
|
|
662
|
+
URL.revokeObjectURL(objectUrl);
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
setBlobSrc(objectUrl);
|
|
666
|
+
} catch (error) {
|
|
667
|
+
fail(error instanceof Error ? error : new Error(String(error)));
|
|
668
|
+
}
|
|
669
|
+
})();
|
|
670
|
+
return () => {
|
|
671
|
+
cancelled = true;
|
|
672
|
+
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
673
|
+
};
|
|
674
|
+
}, [isPrivate, isFailed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
675
|
+
const handleError = React5.useCallback(() => setIsFailed(true), []);
|
|
676
|
+
return { blobSrc, isFailed: isFailed || isTokenUnavailable, handleError };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/media-library.tsx
|
|
680
|
+
import React14 from "react";
|
|
681
|
+
|
|
682
|
+
// src/media-library-item.tsx
|
|
683
|
+
import React11 from "react";
|
|
684
|
+
|
|
685
|
+
// src/pdf-thumbnail.tsx
|
|
686
|
+
import React10 from "react";
|
|
687
|
+
|
|
305
688
|
// src/media-resource.ts
|
|
306
|
-
import
|
|
307
|
-
import { mediaPath, mediaUrl } from "@devultur/core";
|
|
689
|
+
import React6 from "react";
|
|
690
|
+
import { mediaPath, mediaUrl as mediaUrl3 } from "@devultur/core";
|
|
308
691
|
var IDLE = { value: null, contentType: null, isLoading: false, error: null };
|
|
309
692
|
var LOADING = { value: null, contentType: null, isLoading: true, error: null };
|
|
310
693
|
function useMediaResource(media, options) {
|
|
@@ -312,13 +695,18 @@ function useMediaResource(media, options) {
|
|
|
312
695
|
const { isEnabled, maxBytes, contentType } = options;
|
|
313
696
|
const isPrivate = media?.visibility === "private";
|
|
314
697
|
const isReady = !isPrivate || devultur.token !== null;
|
|
698
|
+
const isTokenUnavailable = isPrivate && devultur.tokenStatus === "error";
|
|
699
|
+
const tokenFailure = React6.useMemo(
|
|
700
|
+
() => isTokenUnavailable ? tokenUnavailableError(devultur.tokenError) : null,
|
|
701
|
+
[isTokenUnavailable, devultur.tokenError]
|
|
702
|
+
);
|
|
315
703
|
const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
|
|
316
|
-
const [settled, setSettled] =
|
|
317
|
-
const inputsRef =
|
|
318
|
-
|
|
704
|
+
const [settled, setSettled] = React6.useState(null);
|
|
705
|
+
const inputsRef = React6.useRef({ media, devultur, options });
|
|
706
|
+
React6.useEffect(() => {
|
|
319
707
|
inputsRef.current = { media, devultur, options };
|
|
320
708
|
});
|
|
321
|
-
|
|
709
|
+
React6.useEffect(() => {
|
|
322
710
|
if (!requestKey || !isReady) return;
|
|
323
711
|
const { media: target, devultur: context, options: current } = inputsRef.current;
|
|
324
712
|
if (!target || !context.baseUrl) return;
|
|
@@ -326,7 +714,7 @@ function useMediaResource(media, options) {
|
|
|
326
714
|
const controller = new AbortController();
|
|
327
715
|
let value = null;
|
|
328
716
|
loadMedia({
|
|
329
|
-
buildUrl: (viewerToken) =>
|
|
717
|
+
buildUrl: (viewerToken) => mediaUrl3(baseUrl, target, { token: viewerToken }),
|
|
330
718
|
token: context.token,
|
|
331
719
|
sessionSecret: context.sessionSecret,
|
|
332
720
|
reissueToken: context.reissueToken,
|
|
@@ -350,44 +738,22 @@ function useMediaResource(media, options) {
|
|
|
350
738
|
}, [requestKey, isReady]);
|
|
351
739
|
if (!requestKey) return IDLE;
|
|
352
740
|
if (!devultur.baseUrl) throw new Error("Devultur media previews require `baseUrl` on <DevulturProvider>");
|
|
741
|
+
if (tokenFailure) return { value: null, contentType: null, isLoading: false, error: tokenFailure };
|
|
353
742
|
if (settled?.requestKey !== requestKey) return LOADING;
|
|
354
743
|
return { value: settled.value, contentType: settled.contentType, isLoading: false, error: settled.error };
|
|
355
744
|
}
|
|
356
745
|
|
|
357
|
-
// src/media-object-url.ts
|
|
358
|
-
var createObjectUrl = (bytes, contentType) => URL.createObjectURL(new Blob([bytes], { type: contentType }));
|
|
359
|
-
var revokeObjectUrl = (objectUrl) => URL.revokeObjectURL(objectUrl);
|
|
360
|
-
function useMediaObjectUrl(media, options) {
|
|
361
|
-
const { url } = useDevultur();
|
|
362
|
-
const isEnabled = (options?.enabled ?? true) && Boolean(media);
|
|
363
|
-
const isUnverifiedPublic = media?.visibility === "public" && !options?.contentType;
|
|
364
|
-
const resource = useMediaResource(media, {
|
|
365
|
-
isEnabled: isEnabled && !isUnverifiedPublic,
|
|
366
|
-
maxBytes: options?.maxBytes,
|
|
367
|
-
contentType: options?.contentType,
|
|
368
|
-
decode: createObjectUrl,
|
|
369
|
-
release: revokeObjectUrl
|
|
370
|
-
});
|
|
371
|
-
if (media && isEnabled && isUnverifiedPublic) {
|
|
372
|
-
return { objectUrl: url(media), contentType: null, isLoading: false, error: null };
|
|
373
|
-
}
|
|
374
|
-
return {
|
|
375
|
-
objectUrl: resource.value,
|
|
376
|
-
contentType: resource.contentType,
|
|
377
|
-
isLoading: resource.isLoading,
|
|
378
|
-
error: resource.error
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
746
|
// src/pdf-context.tsx
|
|
383
|
-
import
|
|
747
|
+
import React7 from "react";
|
|
384
748
|
|
|
385
749
|
// src/preview-labels.ts
|
|
386
750
|
var ERROR_TEXT = {
|
|
387
751
|
http_error: "The file couldn't be loaded.",
|
|
388
752
|
too_large: "The file is too large to preview.",
|
|
389
753
|
unsupported_type: "This file type can't be previewed.",
|
|
390
|
-
type_mismatch: "The file's content doesn't match its type."
|
|
754
|
+
type_mismatch: "The file's content doesn't match its type.",
|
|
755
|
+
token_unavailable: "Access to this file couldn't be granted. Try again.",
|
|
756
|
+
session_mismatch: "Your session changed. Reload the page."
|
|
391
757
|
};
|
|
392
758
|
var defaultPreviewLabels = {
|
|
393
759
|
document: (title, pageCount) => `${title}, ${pageCount === 1 ? "1 page" : `${pageCount} pages`}`,
|
|
@@ -411,16 +777,16 @@ function mergePreviewLabels(...sources) {
|
|
|
411
777
|
}
|
|
412
778
|
|
|
413
779
|
// src/pdf-context.tsx
|
|
414
|
-
import { jsx as
|
|
415
|
-
var PdfPreviewContext =
|
|
780
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
781
|
+
var PdfPreviewContext = React7.createContext({});
|
|
416
782
|
function PdfPreviewProvider({
|
|
417
783
|
pdfjs,
|
|
418
784
|
workerSrc,
|
|
419
785
|
labels,
|
|
420
786
|
children
|
|
421
787
|
}) {
|
|
422
|
-
const config =
|
|
423
|
-
return /* @__PURE__ */
|
|
788
|
+
const config = React7.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
|
|
789
|
+
return /* @__PURE__ */ jsx5(PdfPreviewContext.Provider, { value: config, children });
|
|
424
790
|
}
|
|
425
791
|
var loadedModules = /* @__PURE__ */ new WeakMap();
|
|
426
792
|
function loadOnce(loader) {
|
|
@@ -431,9 +797,9 @@ function loadOnce(loader) {
|
|
|
431
797
|
return loading;
|
|
432
798
|
}
|
|
433
799
|
function usePdfjs(workerSrcOverride) {
|
|
434
|
-
const { pdfjs: loader, workerSrc } =
|
|
435
|
-
const [loaded, setLoaded] =
|
|
436
|
-
|
|
800
|
+
const { pdfjs: loader, workerSrc } = React7.useContext(PdfPreviewContext);
|
|
801
|
+
const [loaded, setLoaded] = React7.useState(null);
|
|
802
|
+
React7.useEffect(() => {
|
|
437
803
|
if (!loader) return;
|
|
438
804
|
let isCurrent = true;
|
|
439
805
|
loadOnce(loader).then((pdfjs) => {
|
|
@@ -448,61 +814,27 @@ function usePdfjs(workerSrcOverride) {
|
|
|
448
814
|
if (!loaded.pdfjs) return { status: "unavailable" };
|
|
449
815
|
return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
|
|
450
816
|
}
|
|
817
|
+
function usePdfPageCounter() {
|
|
818
|
+
const { pdfjs: loader, workerSrc } = React7.useContext(PdfPreviewContext);
|
|
819
|
+
return React7.useMemo(() => {
|
|
820
|
+
if (!loader) return null;
|
|
821
|
+
return async (file) => {
|
|
822
|
+
const pdfjs = await loadOnce(loader);
|
|
823
|
+
if (!pdfjs) return null;
|
|
824
|
+
return countPages(pdfjs, file, workerSrc);
|
|
825
|
+
};
|
|
826
|
+
}, [loader, workerSrc]);
|
|
827
|
+
}
|
|
451
828
|
function usePreviewLabels(labels) {
|
|
452
|
-
const providerLabels =
|
|
829
|
+
const providerLabels = React7.useContext(PdfPreviewContext).labels;
|
|
453
830
|
return mergePreviewLabels(providerLabels, labels);
|
|
454
831
|
}
|
|
455
832
|
|
|
456
|
-
// src/pdf-thumbnail.tsx
|
|
457
|
-
import React6 from "react";
|
|
458
|
-
|
|
459
|
-
// src/pdf-hooks.ts
|
|
460
|
-
import React4 from "react";
|
|
461
|
-
|
|
462
|
-
// src/pdf-render.ts
|
|
463
|
-
var MOBILE_MAX_CANVAS_PIXELS = 5242880;
|
|
464
|
-
var DESKTOP_MAX_CANVAS_PIXELS = 2 ** 25;
|
|
465
|
-
function openPdf(pdfjs, bytes, workerSrc) {
|
|
466
|
-
if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
467
|
-
const source = { data: bytes.slice(), enableScripting: false, isEvalSupported: false };
|
|
468
|
-
return pdfjs.getDocument(source);
|
|
469
|
-
}
|
|
470
|
-
function isMobileDevice() {
|
|
471
|
-
if (typeof navigator === "undefined") return false;
|
|
472
|
-
return /Android|iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;
|
|
473
|
-
}
|
|
474
|
-
function maxCanvasPixels() {
|
|
475
|
-
return isMobileDevice() ? MOBILE_MAX_CANVAS_PIXELS : DESKTOP_MAX_CANVAS_PIXELS;
|
|
476
|
-
}
|
|
477
|
-
function outputScale(cssWidth, cssHeight, maxPixels) {
|
|
478
|
-
const devicePixelRatio = typeof window !== "undefined" && window.devicePixelRatio || 1;
|
|
479
|
-
const cssPixels = cssWidth * cssHeight;
|
|
480
|
-
if (cssPixels <= 0) return devicePixelRatio;
|
|
481
|
-
if (cssPixels * devicePixelRatio ** 2 <= maxPixels) return devicePixelRatio;
|
|
482
|
-
return Math.sqrt(maxPixels / cssPixels);
|
|
483
|
-
}
|
|
484
|
-
function renderPage(page, canvas, cssWidth) {
|
|
485
|
-
const naturalViewport = page.getViewport({ scale: 1 });
|
|
486
|
-
const cssScale = cssWidth > 0 ? cssWidth / naturalViewport.width : 1;
|
|
487
|
-
const cssViewport = page.getViewport({ scale: cssScale });
|
|
488
|
-
const scale = outputScale(cssViewport.width, cssViewport.height, maxCanvasPixels());
|
|
489
|
-
const viewport = page.getViewport({ scale: cssScale * scale });
|
|
490
|
-
canvas.width = Math.floor(viewport.width);
|
|
491
|
-
canvas.height = Math.floor(viewport.height);
|
|
492
|
-
canvas.style.width = `${Math.floor(cssViewport.width)}px`;
|
|
493
|
-
canvas.style.height = `${Math.floor(cssViewport.height)}px`;
|
|
494
|
-
return page.render({ canvas, viewport });
|
|
495
|
-
}
|
|
496
|
-
async function pageText(page) {
|
|
497
|
-
const { items } = await page.getTextContent();
|
|
498
|
-
return items.map((item) => "str" in item && typeof item.str === "string" ? item.str : "").join(" ").replace(/\s+/g, " ").trim();
|
|
499
|
-
}
|
|
500
|
-
var isRenderCancelled = (error) => error instanceof Error && error.name === "RenderingCancelledException";
|
|
501
|
-
|
|
502
833
|
// src/pdf-hooks.ts
|
|
834
|
+
import React8 from "react";
|
|
503
835
|
function useIsVisible(ref, rootMargin = "200px") {
|
|
504
|
-
const [isVisible, setIsVisible] =
|
|
505
|
-
|
|
836
|
+
const [isVisible, setIsVisible] = React8.useState(false);
|
|
837
|
+
React8.useEffect(() => {
|
|
506
838
|
const element = ref.current;
|
|
507
839
|
if (isVisible || !element) return;
|
|
508
840
|
if (typeof IntersectionObserver === "undefined") {
|
|
@@ -523,12 +855,12 @@ function useIsVisible(ref, rootMargin = "200px") {
|
|
|
523
855
|
return isVisible;
|
|
524
856
|
}
|
|
525
857
|
function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
526
|
-
const [state, setState] =
|
|
858
|
+
const [state, setState] = React8.useState({
|
|
527
859
|
bytes: null,
|
|
528
860
|
document: null,
|
|
529
861
|
error: null
|
|
530
862
|
});
|
|
531
|
-
|
|
863
|
+
React8.useEffect(() => {
|
|
532
864
|
let isCurrent = true;
|
|
533
865
|
const loadingTask = openPdf(pdfjs, bytes, workerSrc);
|
|
534
866
|
loadingTask.promise.then((document2) => {
|
|
@@ -547,8 +879,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
|
547
879
|
}
|
|
548
880
|
function usePageRender(options) {
|
|
549
881
|
const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
|
|
550
|
-
const [state, setState] =
|
|
551
|
-
|
|
882
|
+
const [state, setState] = React8.useState({ isRendered: false, text: "" });
|
|
883
|
+
React8.useEffect(() => {
|
|
552
884
|
const canvas = canvasRef.current;
|
|
553
885
|
if (!document2 || !isEnabled || !canvas) return;
|
|
554
886
|
let isCurrent = true;
|
|
@@ -578,8 +910,8 @@ function usePageRender(options) {
|
|
|
578
910
|
}
|
|
579
911
|
|
|
580
912
|
// src/pdf-preview.tsx
|
|
581
|
-
import
|
|
582
|
-
import { Fragment, jsx as
|
|
913
|
+
import React9 from "react";
|
|
914
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
583
915
|
var VISUALLY_HIDDEN = {
|
|
584
916
|
position: "absolute",
|
|
585
917
|
width: 1,
|
|
@@ -602,11 +934,11 @@ function PdfPreview(props) {
|
|
|
602
934
|
contentType: expectedType,
|
|
603
935
|
decode: keepBytes
|
|
604
936
|
});
|
|
605
|
-
if (error) return /* @__PURE__ */
|
|
606
|
-
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */
|
|
937
|
+
if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(error) });
|
|
938
|
+
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx6(Fragment2, { children: loading });
|
|
607
939
|
const label = title ?? media.filename ?? labels.untitledPdf;
|
|
608
|
-
if (pdfjsState.status === "unavailable") return /* @__PURE__ */
|
|
609
|
-
return /* @__PURE__ */
|
|
940
|
+
if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx6(PdfFrame, { bytes, label, className });
|
|
941
|
+
return /* @__PURE__ */ jsx6(
|
|
610
942
|
PdfCanvasDocument,
|
|
611
943
|
{
|
|
612
944
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -624,17 +956,17 @@ function PdfPreview(props) {
|
|
|
624
956
|
function PdfCanvasDocument(props) {
|
|
625
957
|
const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
|
|
626
958
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
627
|
-
const containerRef =
|
|
628
|
-
const [width, setWidth] =
|
|
629
|
-
|
|
959
|
+
const containerRef = React9.useRef(null);
|
|
960
|
+
const [width, setWidth] = React9.useState(null);
|
|
961
|
+
React9.useLayoutEffect(() => {
|
|
630
962
|
if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
|
|
631
963
|
}, [document2]);
|
|
632
|
-
if (error) return /* @__PURE__ */
|
|
633
|
-
if (!document2) return /* @__PURE__ */
|
|
964
|
+
if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(error) });
|
|
965
|
+
if (!document2) return /* @__PURE__ */ jsx6(Fragment2, { children: loading });
|
|
634
966
|
const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
|
|
635
967
|
const isTruncated = shownPageCount < document2.numPages;
|
|
636
968
|
const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
|
|
637
|
-
return /* @__PURE__ */
|
|
969
|
+
return /* @__PURE__ */ jsxs3(
|
|
638
970
|
"div",
|
|
639
971
|
{
|
|
640
972
|
ref: containerRef,
|
|
@@ -642,7 +974,7 @@ function PdfCanvasDocument(props) {
|
|
|
642
974
|
"aria-label": labels.document(label, document2.numPages),
|
|
643
975
|
className,
|
|
644
976
|
children: [
|
|
645
|
-
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */
|
|
977
|
+
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx6(
|
|
646
978
|
PdfPageView,
|
|
647
979
|
{
|
|
648
980
|
document: document2,
|
|
@@ -652,7 +984,7 @@ function PdfCanvasDocument(props) {
|
|
|
652
984
|
},
|
|
653
985
|
pageNumber
|
|
654
986
|
)),
|
|
655
|
-
isTruncated && /* @__PURE__ */
|
|
987
|
+
isTruncated && /* @__PURE__ */ jsxs3("p", { children: [
|
|
656
988
|
labels.pageCap(shownPageCount, document2.numPages),
|
|
657
989
|
" ",
|
|
658
990
|
renderFallback(null)
|
|
@@ -662,8 +994,8 @@ function PdfCanvasDocument(props) {
|
|
|
662
994
|
);
|
|
663
995
|
}
|
|
664
996
|
function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
665
|
-
const pageRef =
|
|
666
|
-
const canvasRef =
|
|
997
|
+
const pageRef = React9.useRef(null);
|
|
998
|
+
const canvasRef = React9.useRef(null);
|
|
667
999
|
const isVisible = useIsVisible(pageRef);
|
|
668
1000
|
const { isRendered, text } = usePageRender({
|
|
669
1001
|
document: document2,
|
|
@@ -674,46 +1006,46 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
|
674
1006
|
shouldExtractText: true
|
|
675
1007
|
});
|
|
676
1008
|
const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
|
|
677
|
-
return /* @__PURE__ */
|
|
678
|
-
/* @__PURE__ */
|
|
679
|
-
text && /* @__PURE__ */
|
|
1009
|
+
return /* @__PURE__ */ jsxs3("section", { ref: pageRef, "aria-label": label, style: placeholderStyle, children: [
|
|
1010
|
+
/* @__PURE__ */ jsx6("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
|
|
1011
|
+
text && /* @__PURE__ */ jsx6("p", { style: VISUALLY_HIDDEN, children: text })
|
|
680
1012
|
] });
|
|
681
1013
|
}
|
|
682
1014
|
function PdfFrame({ bytes, label, className }) {
|
|
683
|
-
const [objectUrl, setObjectUrl] =
|
|
684
|
-
|
|
1015
|
+
const [objectUrl, setObjectUrl] = React9.useState(null);
|
|
1016
|
+
React9.useEffect(() => {
|
|
685
1017
|
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
686
1018
|
setObjectUrl(url);
|
|
687
1019
|
return () => URL.revokeObjectURL(url);
|
|
688
1020
|
}, [bytes]);
|
|
689
1021
|
if (!objectUrl) return null;
|
|
690
|
-
return /* @__PURE__ */
|
|
1022
|
+
return /* @__PURE__ */ jsx6("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
|
|
691
1023
|
}
|
|
692
1024
|
|
|
693
1025
|
// src/pdf-thumbnail.tsx
|
|
694
|
-
import { Fragment as
|
|
1026
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
695
1027
|
var DEFAULT_THUMBNAIL_WIDTH = 96;
|
|
696
1028
|
var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
|
|
697
1029
|
function PdfThumbnail(props) {
|
|
698
1030
|
const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
|
|
699
|
-
const containerRef =
|
|
1031
|
+
const containerRef = React10.useRef(null);
|
|
700
1032
|
const isVisible = useIsVisible(containerRef);
|
|
701
1033
|
const pdfjsState = usePdfjs(props.pdfWorkerSrc);
|
|
702
1034
|
const labels = usePreviewLabels(props.labels);
|
|
703
1035
|
const expectedType = media ? expectedContentType(media, contentType) : null;
|
|
704
|
-
const
|
|
1036
|
+
const isPdf2 = previewKindOf(expectedType) === "pdf";
|
|
705
1037
|
const { value: bytes, error } = useMediaResource(media, {
|
|
706
|
-
isEnabled: isVisible &&
|
|
1038
|
+
isEnabled: isVisible && isPdf2 && pdfjsState.status === "ready",
|
|
707
1039
|
maxBytes: props.maxBytes ?? DEFAULT_THUMBNAIL_MAX_BYTES,
|
|
708
1040
|
contentType: expectedType ?? void 0,
|
|
709
1041
|
decode: keepBytes
|
|
710
1042
|
});
|
|
711
1043
|
if (!media) return null;
|
|
712
|
-
const isUnavailable = !
|
|
1044
|
+
const isUnavailable = !isPdf2 || pdfjsState.status === "unavailable" || error !== null;
|
|
713
1045
|
const content = () => {
|
|
714
1046
|
if (isUnavailable) return fallback;
|
|
715
1047
|
if (!bytes || pdfjsState.status !== "ready") return loading;
|
|
716
|
-
return /* @__PURE__ */
|
|
1048
|
+
return /* @__PURE__ */ jsx7(
|
|
717
1049
|
ThumbnailCanvas,
|
|
718
1050
|
{
|
|
719
1051
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -726,54 +1058,612 @@ function PdfThumbnail(props) {
|
|
|
726
1058
|
}
|
|
727
1059
|
);
|
|
728
1060
|
};
|
|
729
|
-
return /* @__PURE__ */
|
|
1061
|
+
return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
|
|
730
1062
|
}
|
|
731
1063
|
function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
|
|
732
|
-
const canvasRef =
|
|
1064
|
+
const canvasRef = React10.useRef(null);
|
|
733
1065
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
734
1066
|
const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
|
|
735
|
-
if (error) return /* @__PURE__ */
|
|
736
|
-
return /* @__PURE__ */
|
|
1067
|
+
if (error) return /* @__PURE__ */ jsx7(Fragment3, { children: fallback });
|
|
1068
|
+
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
737
1069
|
!isRendered && loading,
|
|
738
|
-
/* @__PURE__ */
|
|
1070
|
+
/* @__PURE__ */ jsx7("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
|
|
739
1071
|
] });
|
|
740
1072
|
}
|
|
741
1073
|
|
|
742
|
-
// src/
|
|
743
|
-
import {
|
|
744
|
-
|
|
745
|
-
var
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
if (media.kind === "video") return /* @__PURE__ */ jsx5(VideoPreview, { media, className: viewProps.className });
|
|
751
|
-
const renderFallback = (error) => {
|
|
752
|
-
const fetchError = error instanceof MediaFetchError ? error : null;
|
|
753
|
-
if (typeof fallback === "function") return fallback(fetchError);
|
|
754
|
-
if (fallback !== void 0) return fallback;
|
|
755
|
-
return /* @__PURE__ */ jsx5(DefaultFallback, { media, error: fetchError, labels: text });
|
|
756
|
-
};
|
|
757
|
-
const expectedType = expectedContentType(media, contentType);
|
|
758
|
-
const kind = previewKindOf(expectedType);
|
|
759
|
-
if (!expectedType || !kind) return /* @__PURE__ */ jsx5(Fragment3, { children: renderFallback(unsupportedType(expectedType)) });
|
|
760
|
-
const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx5(LoadingStatus, { text: text.loading }) : loading;
|
|
761
|
-
const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
|
|
762
|
-
if (kind === "text") return /* @__PURE__ */ jsx5(TextPreview, { ...props });
|
|
763
|
-
if (kind === "pdf") return /* @__PURE__ */ jsx5(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
|
|
764
|
-
return /* @__PURE__ */ jsx5(BinaryPreview, { ...props, kind });
|
|
1074
|
+
// src/media-library-item.tsx
|
|
1075
|
+
import { Fragment as Fragment4, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1076
|
+
var THUMBNAIL_WIDTH = 160;
|
|
1077
|
+
var THUMBNAIL_WIDTHS = [THUMBNAIL_WIDTH, THUMBNAIL_WIDTH * 2];
|
|
1078
|
+
function partClass(part, classNames) {
|
|
1079
|
+
const own = part === "root" ? "devultur-media-library" : `devultur-media-library__${part}`;
|
|
1080
|
+
const extra = classNames[part];
|
|
1081
|
+
return extra ? `${own} ${extra}` : own;
|
|
765
1082
|
}
|
|
766
|
-
var
|
|
767
|
-
|
|
768
|
-
|
|
1083
|
+
var THUMBNAIL_STYLE = {
|
|
1084
|
+
display: "block",
|
|
1085
|
+
width: "100%",
|
|
1086
|
+
height: "var(--devultur-library-thumbnail-height, 120px)",
|
|
1087
|
+
objectFit: "cover"
|
|
1088
|
+
};
|
|
1089
|
+
function kindOf(record) {
|
|
1090
|
+
if (record.type !== "file") return record.type;
|
|
1091
|
+
return previewKindOf(record.contentType) ?? "file";
|
|
769
1092
|
}
|
|
770
|
-
function
|
|
771
|
-
|
|
1093
|
+
function Thumbnail({ record, media, pdfThumbnails }) {
|
|
1094
|
+
const kind = kindOf(record);
|
|
1095
|
+
const icon = /* @__PURE__ */ jsx8(AttachmentIcon, { kind });
|
|
1096
|
+
if (!media) return icon;
|
|
1097
|
+
if (record.type === "image") {
|
|
1098
|
+
return /* @__PURE__ */ jsx8(
|
|
1099
|
+
Image,
|
|
1100
|
+
{
|
|
1101
|
+
media,
|
|
1102
|
+
width: THUMBNAIL_WIDTH,
|
|
1103
|
+
widths: THUMBNAIL_WIDTHS,
|
|
1104
|
+
sizes: `${THUMBNAIL_WIDTH}px`,
|
|
1105
|
+
format: "auto",
|
|
1106
|
+
alt: "",
|
|
1107
|
+
style: THUMBNAIL_STYLE,
|
|
1108
|
+
fallback: icon
|
|
1109
|
+
}
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if (kind !== "pdf" || !pdfThumbnails) return icon;
|
|
1113
|
+
return /* @__PURE__ */ jsx8(PdfThumbnail, { media, contentType: record.contentType, width: THUMBNAIL_WIDTH, fallback: icon });
|
|
1114
|
+
}
|
|
1115
|
+
function DefaultContent({ record, media, pdfThumbnails, labels, classNames }) {
|
|
1116
|
+
const name = record.title ?? record.filename;
|
|
1117
|
+
return /* @__PURE__ */ jsxs5(Fragment4, { children: [
|
|
1118
|
+
/* @__PURE__ */ jsx8("span", { className: partClass("thumbnail", classNames), children: /* @__PURE__ */ jsx8(Thumbnail, { record, media, pdfThumbnails }) }),
|
|
1119
|
+
/* @__PURE__ */ jsx8("span", { className: partClass("name", classNames), children: name }),
|
|
1120
|
+
record.sizeBytes > 0 && /* @__PURE__ */ jsx8("span", { className: partClass("size", classNames), children: labels.size(record.sizeBytes) })
|
|
1121
|
+
] });
|
|
1122
|
+
}
|
|
1123
|
+
var ITEM_STYLE = {
|
|
1124
|
+
display: "flex",
|
|
1125
|
+
flexDirection: "column",
|
|
1126
|
+
gap: "var(--devultur-library-item-gap, 0.25rem)",
|
|
1127
|
+
cursor: "pointer",
|
|
1128
|
+
overflowWrap: "anywhere"
|
|
1129
|
+
};
|
|
1130
|
+
function MediaLibraryItem(props) {
|
|
1131
|
+
const { ref, record, isSelected, isActive, tabIndex, classNames, renderItem, onChoose, onFocus } = props;
|
|
1132
|
+
const media = React11.useMemo(() => mediaFromRecord(record), [record]);
|
|
1133
|
+
const handleKeyDown = (event) => {
|
|
1134
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1135
|
+
event.preventDefault();
|
|
1136
|
+
onChoose();
|
|
1137
|
+
};
|
|
1138
|
+
return /* @__PURE__ */ jsx8(
|
|
1139
|
+
"div",
|
|
1140
|
+
{
|
|
1141
|
+
ref,
|
|
1142
|
+
role: "option",
|
|
1143
|
+
"aria-selected": isSelected,
|
|
1144
|
+
tabIndex,
|
|
1145
|
+
"data-selected": isSelected || void 0,
|
|
1146
|
+
"data-type": record.type,
|
|
1147
|
+
className: partClass("item", classNames),
|
|
1148
|
+
style: {
|
|
1149
|
+
...ITEM_STYLE,
|
|
1150
|
+
boxShadow: isSelected ? "0 0 0 2px var(--devultur-library-accent, currentColor)" : void 0
|
|
1151
|
+
},
|
|
1152
|
+
onClick: onChoose,
|
|
1153
|
+
onKeyDown: handleKeyDown,
|
|
1154
|
+
onFocus,
|
|
1155
|
+
children: renderItem ? renderItem(record, { media, isSelected, isActive }) : /* @__PURE__ */ jsx8(
|
|
1156
|
+
DefaultContent,
|
|
1157
|
+
{
|
|
1158
|
+
record,
|
|
1159
|
+
media,
|
|
1160
|
+
pdfThumbnails: props.pdfThumbnails,
|
|
1161
|
+
labels: props.labels,
|
|
1162
|
+
classNames
|
|
1163
|
+
}
|
|
1164
|
+
)
|
|
1165
|
+
}
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/media-library-labels.ts
|
|
1170
|
+
var defaultMediaLibraryLabels = {
|
|
1171
|
+
search: "Search media",
|
|
1172
|
+
searchPlaceholder: "Search by title or filename",
|
|
1173
|
+
typeFilter: "Type",
|
|
1174
|
+
allTypes: "All types",
|
|
1175
|
+
types: { image: "Images", video: "Videos", file: "Files" },
|
|
1176
|
+
results: "Media",
|
|
1177
|
+
loading: "Loading media\u2026",
|
|
1178
|
+
empty: "No media yet.",
|
|
1179
|
+
noResults: (search) => `Nothing matches \u201C${search}\u201D.`,
|
|
1180
|
+
loadMore: "Load more",
|
|
1181
|
+
loadingMore: "Loading\u2026",
|
|
1182
|
+
error: (error) => describeDevulturError(error) ?? "The media couldn't be loaded.",
|
|
1183
|
+
retry: "Try again",
|
|
1184
|
+
size: (bytes) => formatFileSize(bytes, "en")
|
|
1185
|
+
};
|
|
1186
|
+
function mergeMediaLibraryLabels(labels) {
|
|
1187
|
+
const merged = { ...defaultMediaLibraryLabels };
|
|
1188
|
+
for (const [key, value] of Object.entries(labels ?? {})) {
|
|
1189
|
+
if (value !== void 0) merged[key] = value;
|
|
1190
|
+
}
|
|
1191
|
+
return merged;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/use-media-library.ts
|
|
1195
|
+
import React12 from "react";
|
|
1196
|
+
import { usePaginatedQuery } from "convex/react";
|
|
1197
|
+
var SEARCH_DEBOUNCE_MS = 250;
|
|
1198
|
+
var DEFAULT_PAGE_SIZE = 24;
|
|
1199
|
+
function useDebouncedSearch(value, delayMs) {
|
|
1200
|
+
const [debounced, setDebounced] = React12.useState(value);
|
|
1201
|
+
React12.useEffect(() => {
|
|
1202
|
+
if (value === "") {
|
|
1203
|
+
setDebounced("");
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const timer = setTimeout(() => setDebounced(value), delayMs);
|
|
1207
|
+
return () => clearTimeout(timer);
|
|
1208
|
+
}, [value, delayMs]);
|
|
1209
|
+
return value === "" ? "" : debounced;
|
|
1210
|
+
}
|
|
1211
|
+
function useMediaLibrary(options = {}) {
|
|
1212
|
+
const api = useDevulturApi();
|
|
1213
|
+
const pageSize = options.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
1214
|
+
const requestedSearch = (options.search ?? "").trim();
|
|
1215
|
+
const search = useDebouncedSearch(requestedSearch, SEARCH_DEBOUNCE_MS);
|
|
1216
|
+
const isSearching = search !== "";
|
|
1217
|
+
const typeArg = options.type ? { type: options.type } : {};
|
|
1218
|
+
const reference = isSearching ? api.searchMedia : api.listMediaPage;
|
|
1219
|
+
if (!reference) {
|
|
1220
|
+
const missing = isSearching ? "searchMedia" : "listMediaPage";
|
|
1221
|
+
throw new Error(`useMediaLibrary requires ${missing} in DevulturProvider api`);
|
|
1222
|
+
}
|
|
1223
|
+
const args = isSearching ? { search, ...typeArg } : typeArg;
|
|
1224
|
+
const result = usePaginatedQuery(reference, args, { initialNumItems: pageSize });
|
|
1225
|
+
const resultLoadMore = result.loadMore;
|
|
1226
|
+
const loadMore = React12.useCallback(
|
|
1227
|
+
(numItems) => resultLoadMore(numItems ?? pageSize),
|
|
1228
|
+
[resultLoadMore, pageSize]
|
|
1229
|
+
);
|
|
1230
|
+
return {
|
|
1231
|
+
items: result.results,
|
|
1232
|
+
status: result.status,
|
|
1233
|
+
loadMore,
|
|
1234
|
+
isLoading: result.isLoading,
|
|
1235
|
+
search,
|
|
1236
|
+
isSearchPending: requestedSearch !== search
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/use-roving-focus.ts
|
|
1241
|
+
import React13 from "react";
|
|
1242
|
+
function nextIndex(key, current, count, columns) {
|
|
1243
|
+
const last = count - 1;
|
|
1244
|
+
const clamp = (index) => Math.min(Math.max(index, 0), last);
|
|
1245
|
+
switch (key) {
|
|
1246
|
+
case "ArrowRight":
|
|
1247
|
+
return clamp(current + 1);
|
|
1248
|
+
case "ArrowLeft":
|
|
1249
|
+
return clamp(current - 1);
|
|
1250
|
+
case "ArrowDown":
|
|
1251
|
+
return current + columns > last ? current : current + columns;
|
|
1252
|
+
case "ArrowUp":
|
|
1253
|
+
return current - columns < 0 ? current : current - columns;
|
|
1254
|
+
case "Home":
|
|
1255
|
+
return 0;
|
|
1256
|
+
case "End":
|
|
1257
|
+
return last;
|
|
1258
|
+
default:
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
function columnCount(items) {
|
|
1263
|
+
const firstTop = items[0]?.getBoundingClientRect();
|
|
1264
|
+
if (!firstTop || firstTop.height === 0) return 1;
|
|
1265
|
+
const inFirstRow = items.filter((item) => item?.getBoundingClientRect().top === firstTop.top);
|
|
1266
|
+
return Math.max(inFirstRow.length, 1);
|
|
1267
|
+
}
|
|
1268
|
+
function useRovingFocus(count) {
|
|
1269
|
+
const [activeIndex, setActiveIndex] = React13.useState(0);
|
|
1270
|
+
const itemsRef = React13.useRef([]);
|
|
1271
|
+
const currentIndex = Math.min(activeIndex, Math.max(count - 1, 0));
|
|
1272
|
+
const itemRef = React13.useCallback(
|
|
1273
|
+
(index) => (element) => {
|
|
1274
|
+
itemsRef.current[index] = element;
|
|
1275
|
+
},
|
|
1276
|
+
[]
|
|
1277
|
+
);
|
|
1278
|
+
const handleKeyDown = React13.useCallback(
|
|
1279
|
+
(event) => {
|
|
1280
|
+
if (count === 0) return;
|
|
1281
|
+
const items = itemsRef.current.slice(0, count);
|
|
1282
|
+
const target = nextIndex(event.key, currentIndex, count, columnCount(items));
|
|
1283
|
+
if (target === null) return;
|
|
1284
|
+
event.preventDefault();
|
|
1285
|
+
setActiveIndex(target);
|
|
1286
|
+
items[target]?.focus();
|
|
1287
|
+
},
|
|
1288
|
+
[count, currentIndex]
|
|
1289
|
+
);
|
|
1290
|
+
const tabIndex = (index) => index === currentIndex ? 0 : -1;
|
|
1291
|
+
return { activeIndex: currentIndex, setActiveIndex, itemRef, handleKeyDown, tabIndex };
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
// src/media-library.tsx
|
|
1295
|
+
import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1296
|
+
var MEDIA_TYPES = ["image", "video", "file"];
|
|
1297
|
+
var ROOT_STYLE = {
|
|
1298
|
+
display: "flex",
|
|
1299
|
+
flexDirection: "column",
|
|
1300
|
+
gap: "var(--devultur-library-gap, 0.75rem)"
|
|
1301
|
+
};
|
|
1302
|
+
var LIST_STYLE = {
|
|
1303
|
+
display: "grid",
|
|
1304
|
+
gridTemplateColumns: "repeat(auto-fill, minmax(var(--devultur-library-item-width, 160px), 1fr))",
|
|
1305
|
+
gap: "var(--devultur-library-gap, 0.75rem)",
|
|
1306
|
+
margin: 0,
|
|
1307
|
+
padding: 0
|
|
1308
|
+
};
|
|
1309
|
+
function MediaLibrary(props) {
|
|
1310
|
+
const labels = React14.useMemo(() => mergeMediaLibraryLabels(props.labels), [props.labels]);
|
|
1311
|
+
const classNames = { ...props.classNames, root: props.className ?? props.classNames?.root };
|
|
1312
|
+
const [search, setSearch] = React14.useState("");
|
|
1313
|
+
const [chosenType, setChosenType] = React14.useState("");
|
|
1314
|
+
const [selectedKey, setSelectedKey] = React14.useState(null);
|
|
1315
|
+
const type = props.type ?? (chosenType === "" ? void 0 : chosenType);
|
|
1316
|
+
const { onSelect } = props;
|
|
1317
|
+
const isSelectable = props.selectable ?? props.selectedKeys !== void 0;
|
|
1318
|
+
const selectedKeys = props.selectedKeys ?? (selectedKey === null ? [] : [selectedKey]);
|
|
1319
|
+
const handleChoose = React14.useCallback(
|
|
1320
|
+
(record) => {
|
|
1321
|
+
if (isSelectable) setSelectedKey(record.key);
|
|
1322
|
+
onSelect(record, mediaFromRecord(record));
|
|
1323
|
+
},
|
|
1324
|
+
[isSelectable, onSelect]
|
|
1325
|
+
);
|
|
1326
|
+
return /* @__PURE__ */ jsxs6("div", { className: partClass("root", classNames), style: ROOT_STYLE, children: [
|
|
1327
|
+
/* @__PURE__ */ jsx9(
|
|
1328
|
+
"input",
|
|
1329
|
+
{
|
|
1330
|
+
type: "search",
|
|
1331
|
+
className: partClass("search", classNames),
|
|
1332
|
+
"aria-label": labels.search,
|
|
1333
|
+
placeholder: labels.searchPlaceholder,
|
|
1334
|
+
value: search,
|
|
1335
|
+
onChange: (event) => setSearch(event.target.value)
|
|
1336
|
+
}
|
|
1337
|
+
),
|
|
1338
|
+
!props.type && /* @__PURE__ */ jsxs6(
|
|
1339
|
+
"select",
|
|
1340
|
+
{
|
|
1341
|
+
className: partClass("filter", classNames),
|
|
1342
|
+
"aria-label": labels.typeFilter,
|
|
1343
|
+
value: chosenType,
|
|
1344
|
+
onChange: (event) => setChosenType(event.target.value),
|
|
1345
|
+
children: [
|
|
1346
|
+
/* @__PURE__ */ jsx9("option", { value: "", children: labels.allTypes }),
|
|
1347
|
+
MEDIA_TYPES.map((mediaType) => /* @__PURE__ */ jsx9("option", { value: mediaType, children: labels.types[mediaType] }, mediaType))
|
|
1348
|
+
]
|
|
1349
|
+
}
|
|
1350
|
+
),
|
|
1351
|
+
/* @__PURE__ */ jsx9(
|
|
1352
|
+
LibraryErrorBoundary,
|
|
1353
|
+
{
|
|
1354
|
+
resetKey: `${type ?? ""}\0${search}`,
|
|
1355
|
+
renderError: (error, retry) => /* @__PURE__ */ jsxs6("div", { role: "alert", className: partClass("status", classNames), children: [
|
|
1356
|
+
/* @__PURE__ */ jsx9("p", { children: labels.error(error) }),
|
|
1357
|
+
/* @__PURE__ */ jsx9("button", { type: "button", className: partClass("more", classNames), onClick: retry, children: labels.retry })
|
|
1358
|
+
] }),
|
|
1359
|
+
children: /* @__PURE__ */ jsx9(
|
|
1360
|
+
LibraryResults,
|
|
1361
|
+
{
|
|
1362
|
+
...props,
|
|
1363
|
+
type,
|
|
1364
|
+
search,
|
|
1365
|
+
labels,
|
|
1366
|
+
classNames,
|
|
1367
|
+
isSelectable,
|
|
1368
|
+
selectedKeys,
|
|
1369
|
+
onChoose: handleChoose
|
|
1370
|
+
}
|
|
1371
|
+
)
|
|
1372
|
+
}
|
|
1373
|
+
)
|
|
1374
|
+
] });
|
|
772
1375
|
}
|
|
773
|
-
function
|
|
1376
|
+
function LibraryResults(props) {
|
|
1377
|
+
const { labels, classNames } = props;
|
|
1378
|
+
const library = useMediaLibrary({ type: props.type, search: props.search, pageSize: props.pageSize });
|
|
1379
|
+
const items = props.filter ? library.items.filter(props.filter) : library.items;
|
|
1380
|
+
const roving = useRovingFocus(items.length);
|
|
1381
|
+
if (library.status === "LoadingFirstPage") {
|
|
1382
|
+
return /* @__PURE__ */ jsx9("p", { role: "status", className: partClass("status", classNames), children: labels.loading });
|
|
1383
|
+
}
|
|
1384
|
+
if (items.length === 0) {
|
|
1385
|
+
return /* @__PURE__ */ jsx9("p", { role: "status", className: partClass("status", classNames), children: library.search ? labels.noResults(library.search) : labels.empty });
|
|
1386
|
+
}
|
|
1387
|
+
const isLoadingMore = library.status === "LoadingMore";
|
|
1388
|
+
const canLoadMore = library.status === "CanLoadMore" || isLoadingMore;
|
|
1389
|
+
return /* @__PURE__ */ jsxs6(Fragment5, { children: [
|
|
1390
|
+
/* @__PURE__ */ jsx9(
|
|
1391
|
+
"div",
|
|
1392
|
+
{
|
|
1393
|
+
role: "listbox",
|
|
1394
|
+
"aria-label": labels.results,
|
|
1395
|
+
"aria-busy": isLoadingMore || void 0,
|
|
1396
|
+
"aria-multiselectable": props.multiple || void 0,
|
|
1397
|
+
className: partClass("list", classNames),
|
|
1398
|
+
style: LIST_STYLE,
|
|
1399
|
+
onKeyDown: roving.handleKeyDown,
|
|
1400
|
+
children: items.map((record, index) => /* @__PURE__ */ jsx9(
|
|
1401
|
+
MediaLibraryItem,
|
|
1402
|
+
{
|
|
1403
|
+
ref: roving.itemRef(index),
|
|
1404
|
+
record,
|
|
1405
|
+
isSelected: props.isSelectable && props.selectedKeys.includes(record.key),
|
|
1406
|
+
isActive: index === roving.activeIndex,
|
|
1407
|
+
tabIndex: roving.tabIndex(index),
|
|
1408
|
+
pdfThumbnails: props.pdfThumbnails ?? false,
|
|
1409
|
+
labels,
|
|
1410
|
+
classNames,
|
|
1411
|
+
renderItem: props.renderItem,
|
|
1412
|
+
onChoose: () => props.onChoose(record),
|
|
1413
|
+
onFocus: () => roving.setActiveIndex(index)
|
|
1414
|
+
},
|
|
1415
|
+
record.key
|
|
1416
|
+
))
|
|
1417
|
+
}
|
|
1418
|
+
),
|
|
1419
|
+
canLoadMore && /* @__PURE__ */ jsx9(
|
|
1420
|
+
"button",
|
|
1421
|
+
{
|
|
1422
|
+
type: "button",
|
|
1423
|
+
className: partClass("more", classNames),
|
|
1424
|
+
disabled: isLoadingMore,
|
|
1425
|
+
onClick: () => library.loadMore(),
|
|
1426
|
+
children: isLoadingMore ? labels.loadingMore : labels.loadMore
|
|
1427
|
+
}
|
|
1428
|
+
)
|
|
1429
|
+
] });
|
|
1430
|
+
}
|
|
1431
|
+
var LibraryErrorBoundary = class extends React14.Component {
|
|
1432
|
+
state = { hasError: false, error: null };
|
|
1433
|
+
static getDerivedStateFromError(error) {
|
|
1434
|
+
return { hasError: true, error };
|
|
1435
|
+
}
|
|
1436
|
+
componentDidUpdate(previous) {
|
|
1437
|
+
if (previous.resetKey === this.props.resetKey || !this.state.hasError) return;
|
|
1438
|
+
this.setState({ hasError: false, error: null });
|
|
1439
|
+
}
|
|
1440
|
+
retry = () => this.setState({ hasError: false, error: null });
|
|
1441
|
+
render() {
|
|
1442
|
+
if (this.state.hasError) return this.props.renderError(this.state.error, this.retry);
|
|
1443
|
+
return this.props.children;
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
// src/media-object-url.ts
|
|
1448
|
+
var createObjectUrl = (bytes, contentType) => URL.createObjectURL(new Blob([bytes], { type: contentType }));
|
|
1449
|
+
var revokeObjectUrl = (objectUrl) => URL.revokeObjectURL(objectUrl);
|
|
1450
|
+
function useMediaObjectUrl(media, options) {
|
|
1451
|
+
const { url } = useDevultur();
|
|
1452
|
+
const isEnabled = (options?.enabled ?? true) && Boolean(media);
|
|
1453
|
+
const isUnverifiedPublic = media?.visibility === "public" && !options?.contentType;
|
|
1454
|
+
const resource = useMediaResource(media, {
|
|
1455
|
+
isEnabled: isEnabled && !isUnverifiedPublic,
|
|
1456
|
+
maxBytes: options?.maxBytes,
|
|
1457
|
+
contentType: options?.contentType,
|
|
1458
|
+
decode: createObjectUrl,
|
|
1459
|
+
release: revokeObjectUrl
|
|
1460
|
+
});
|
|
1461
|
+
if (media && isEnabled && isUnverifiedPublic) {
|
|
1462
|
+
return { objectUrl: url(media), contentType: null, isLoading: false, error: null };
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
objectUrl: resource.value,
|
|
1466
|
+
contentType: resource.contentType,
|
|
1467
|
+
isLoading: resource.isLoading,
|
|
1468
|
+
error: resource.error
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// src/media-picker.tsx
|
|
1473
|
+
import React21 from "react";
|
|
1474
|
+
|
|
1475
|
+
// src/media-picker-accept.ts
|
|
1476
|
+
function matchesAccept(candidate, accept) {
|
|
1477
|
+
if (!accept || accept.length === 0) return true;
|
|
1478
|
+
const type = candidate.type.toLowerCase();
|
|
1479
|
+
const name = candidate.name.toLowerCase();
|
|
1480
|
+
return accept.some((rawPattern) => {
|
|
1481
|
+
const pattern = rawPattern.trim().toLowerCase();
|
|
1482
|
+
if (pattern.startsWith(".")) return name.endsWith(pattern);
|
|
1483
|
+
if (pattern.endsWith("/*")) return type.startsWith(pattern.slice(0, -1));
|
|
1484
|
+
return type === pattern;
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
function mediaTypeOfPattern(pattern) {
|
|
1488
|
+
const normalized = pattern.trim().toLowerCase();
|
|
1489
|
+
if (normalized.startsWith("image/")) return "image";
|
|
1490
|
+
if (normalized.startsWith("video/")) return "video";
|
|
1491
|
+
return "file";
|
|
1492
|
+
}
|
|
1493
|
+
function mediaTypeOfAccept(accept) {
|
|
1494
|
+
if (!accept || accept.length === 0) return void 0;
|
|
1495
|
+
const types = new Set(accept.map(mediaTypeOfPattern));
|
|
1496
|
+
if (types.size !== 1) return void 0;
|
|
1497
|
+
return [...types][0];
|
|
1498
|
+
}
|
|
1499
|
+
var ACCEPT_OF_TYPE = {
|
|
1500
|
+
image: ["image/*"],
|
|
1501
|
+
video: ["video/*"],
|
|
1502
|
+
file: void 0
|
|
1503
|
+
};
|
|
1504
|
+
function uploadAcceptOf(accept, type) {
|
|
1505
|
+
if (accept && accept.length > 0) return accept;
|
|
1506
|
+
return type ? ACCEPT_OF_TYPE[type] : void 0;
|
|
1507
|
+
}
|
|
1508
|
+
function recordMatchesAccept(record, accept) {
|
|
1509
|
+
return matchesAccept({ name: record.filename, type: record.contentType }, accept);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/media-picker-context.ts
|
|
1513
|
+
import React15 from "react";
|
|
1514
|
+
var MediaPickerContext = React15.createContext(null);
|
|
1515
|
+
function useMediaPicker() {
|
|
1516
|
+
const picker = React15.use(MediaPickerContext);
|
|
1517
|
+
if (!picker) throw new Error("useMediaPicker must be used within <MediaPicker>");
|
|
1518
|
+
return picker;
|
|
1519
|
+
}
|
|
1520
|
+
var mediaName = (media) => media.title ?? media.filename ?? media.id;
|
|
1521
|
+
|
|
1522
|
+
// src/media-picker-labels.ts
|
|
1523
|
+
var defaultMediaPickerLabels = {
|
|
1524
|
+
upload: (multiple) => multiple ? "Upload files" : "Upload a file",
|
|
1525
|
+
dropHint: (multiple) => multiple ? "or drop files here" : "or drop a file here",
|
|
1526
|
+
uploads: "Uploads",
|
|
1527
|
+
progress: (filename) => `Uploading ${filename}`,
|
|
1528
|
+
percent: (percent) => `${percent}%`,
|
|
1529
|
+
uploading: (filename) => `Uploading ${filename}.`,
|
|
1530
|
+
uploaded: (filename) => `${filename} uploaded and selected.`,
|
|
1531
|
+
cancel: "Cancel",
|
|
1532
|
+
cancelUpload: (filename) => `Cancel uploading ${filename}`,
|
|
1533
|
+
cancelled: (filename) => `Upload of ${filename} cancelled.`,
|
|
1534
|
+
notAccepted: (filename) => `${filename} isn't a file type you can pick here.`,
|
|
1535
|
+
uploadError: (error) => describeDevulturError(error) ?? "The upload failed. Try again.",
|
|
1536
|
+
retry: "Try again",
|
|
1537
|
+
dismiss: "Dismiss",
|
|
1538
|
+
selection: "Selected media",
|
|
1539
|
+
empty: "Nothing selected.",
|
|
1540
|
+
remove: "Remove",
|
|
1541
|
+
removeItem: (name) => `Remove ${name}`,
|
|
1542
|
+
clear: "Clear",
|
|
1543
|
+
replace: "Replace",
|
|
1544
|
+
selected: (name) => `${name} selected.`,
|
|
1545
|
+
removed: (name) => `${name} removed.`,
|
|
1546
|
+
cleared: "Selection cleared."
|
|
1547
|
+
};
|
|
1548
|
+
function mergeMediaPickerLabels(labels) {
|
|
1549
|
+
const merged = { ...defaultMediaPickerLabels };
|
|
1550
|
+
for (const [key, value] of Object.entries(labels ?? {})) {
|
|
1551
|
+
if (value !== void 0) merged[key] = value;
|
|
1552
|
+
}
|
|
1553
|
+
return merged;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// src/media-picker-library.tsx
|
|
1557
|
+
import React16 from "react";
|
|
1558
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
1559
|
+
function MediaPickerLibrary(props) {
|
|
1560
|
+
const picker = useMediaPicker();
|
|
1561
|
+
const { accept, toggle } = picker;
|
|
1562
|
+
const selectedKeys = picker.selected.map((media) => media.id);
|
|
1563
|
+
const filter = React16.useMemo(
|
|
1564
|
+
() => accept ? (record) => recordMatchesAccept(record, accept) : void 0,
|
|
1565
|
+
[accept]
|
|
1566
|
+
);
|
|
1567
|
+
return /* @__PURE__ */ jsx10(
|
|
1568
|
+
MediaLibrary,
|
|
1569
|
+
{
|
|
1570
|
+
...props,
|
|
1571
|
+
type: picker.libraryType,
|
|
1572
|
+
labels: picker.libraryLabels,
|
|
1573
|
+
selectedKeys,
|
|
1574
|
+
multiple: picker.multiple,
|
|
1575
|
+
filter,
|
|
1576
|
+
onSelect: (_record, media) => {
|
|
1577
|
+
if (media) toggle(media);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/media-picker-selection.tsx
|
|
1584
|
+
import React18 from "react";
|
|
1585
|
+
|
|
1586
|
+
// src/preview.tsx
|
|
1587
|
+
import { mediaPath as mediaPath2 } from "@devultur/core";
|
|
1588
|
+
|
|
1589
|
+
// src/video-preview.tsx
|
|
1590
|
+
import React17 from "react";
|
|
1591
|
+
import { VideoPlayer } from "@devultur/react";
|
|
1592
|
+
import { Fragment as Fragment6, jsx as jsx11 } from "react/jsx-runtime";
|
|
1593
|
+
var UNAUTHORIZED = 401;
|
|
1594
|
+
function fetchErrorOf(error) {
|
|
1595
|
+
if (error.status === UNAUTHORIZED && error.code === "session_mismatch") {
|
|
1596
|
+
return new MediaFetchError("session_mismatch", "The delivery refused this session's token", UNAUTHORIZED);
|
|
1597
|
+
}
|
|
1598
|
+
if (error.status === UNAUTHORIZED) return tokenUnavailableError();
|
|
1599
|
+
if (error.status) return new MediaFetchError("http_error", `Video fetch failed: ${error.status}`, error.status);
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
function VideoPreview({ media, renderFallback, className }) {
|
|
1603
|
+
const { tokenStatus, tokenError, reissueToken } = useDevultur();
|
|
774
1604
|
const videoProps = useVideoProps(media);
|
|
1605
|
+
const [failure, setFailure] = React17.useState(null);
|
|
1606
|
+
const hasReissuedRef = React17.useRef(false);
|
|
1607
|
+
const playerToken = videoProps?.token;
|
|
1608
|
+
const handleError = React17.useCallback(
|
|
1609
|
+
(error) => {
|
|
1610
|
+
const isFirstMismatch = error.code === "session_mismatch" && !hasReissuedRef.current;
|
|
1611
|
+
if (isFirstMismatch) {
|
|
1612
|
+
hasReissuedRef.current = true;
|
|
1613
|
+
void reissueToken();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
setFailure({ token: playerToken, error: fetchErrorOf(error) });
|
|
1617
|
+
},
|
|
1618
|
+
[reissueToken, playerToken]
|
|
1619
|
+
);
|
|
775
1620
|
if (!videoProps) return null;
|
|
776
|
-
|
|
1621
|
+
if (media.visibility === "private" && tokenStatus === "error") {
|
|
1622
|
+
return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(tokenUnavailableError(tokenError)) });
|
|
1623
|
+
}
|
|
1624
|
+
if (failure && failure.token === playerToken) return /* @__PURE__ */ jsx11(Fragment6, { children: renderFallback(failure.error) });
|
|
1625
|
+
return /* @__PURE__ */ jsx11(VideoPlayer, { ...videoProps, className, onError: handleError });
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// src/preview.tsx
|
|
1629
|
+
import { Fragment as Fragment7, jsx as jsx12 } from "react/jsx-runtime";
|
|
1630
|
+
var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
|
|
1631
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
1632
|
+
function Preview({ media, contentType, fallback, loading, labels, ...viewProps }) {
|
|
1633
|
+
const text = usePreviewLabels(labels);
|
|
1634
|
+
if (!media) return null;
|
|
1635
|
+
const renderFallback = (error) => {
|
|
1636
|
+
const fetchError = error instanceof MediaFetchError ? error : null;
|
|
1637
|
+
if (typeof fallback === "function") return fallback(fetchError);
|
|
1638
|
+
if (fallback !== void 0) return fallback;
|
|
1639
|
+
return /* @__PURE__ */ jsx12(DefaultFallback, { media, error: fetchError, labels: text });
|
|
1640
|
+
};
|
|
1641
|
+
if (media.kind === "video") {
|
|
1642
|
+
return /* @__PURE__ */ jsx12(
|
|
1643
|
+
VideoPreview,
|
|
1644
|
+
{
|
|
1645
|
+
media,
|
|
1646
|
+
renderFallback,
|
|
1647
|
+
className: viewProps.className
|
|
1648
|
+
},
|
|
1649
|
+
mediaPath2(media)
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
const expectedType = expectedContentType(media, contentType);
|
|
1653
|
+
const kind = previewKindOf(expectedType);
|
|
1654
|
+
if (!expectedType || !kind) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(unsupportedType(expectedType)) });
|
|
1655
|
+
const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx12(LoadingStatus, { text: text.loading }) : loading;
|
|
1656
|
+
const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
|
|
1657
|
+
if (kind === "text") return /* @__PURE__ */ jsx12(TextPreview, { ...props });
|
|
1658
|
+
if (kind === "pdf") return /* @__PURE__ */ jsx12(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
|
|
1659
|
+
return /* @__PURE__ */ jsx12(BinaryPreview, { ...props, kind });
|
|
1660
|
+
}
|
|
1661
|
+
var unsupportedType = (contentType) => new MediaFetchError("unsupported_type", `Can't preview ${contentType ?? "an unknown type"} inline`);
|
|
1662
|
+
function DefaultFallback({ media, error, labels }) {
|
|
1663
|
+
return /* @__PURE__ */ jsx12(DownloadLink, { media, title: error ? labels.error(error.code) : void 0, children: media.filename ?? labels.download });
|
|
1664
|
+
}
|
|
1665
|
+
function LoadingStatus({ text }) {
|
|
1666
|
+
return /* @__PURE__ */ jsx12("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
|
|
777
1667
|
}
|
|
778
1668
|
function BinaryPreview({
|
|
779
1669
|
media,
|
|
@@ -790,13 +1680,13 @@ function BinaryPreview({
|
|
|
790
1680
|
const isPublicImage = kind === "image" && media.visibility === "public";
|
|
791
1681
|
const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
|
|
792
1682
|
const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
|
|
793
|
-
if (verified.error) return /* @__PURE__ */
|
|
794
|
-
if (!objectUrl) return /* @__PURE__ */
|
|
1683
|
+
if (verified.error) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(verified.error) });
|
|
1684
|
+
if (!objectUrl) return /* @__PURE__ */ jsx12(Fragment7, { children: loading });
|
|
795
1685
|
const label = title ?? media.filename ?? labels.untitledMedia;
|
|
796
1686
|
if (kind === "image") {
|
|
797
|
-
return /* @__PURE__ */
|
|
1687
|
+
return /* @__PURE__ */ jsx12("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
|
|
798
1688
|
}
|
|
799
|
-
return /* @__PURE__ */
|
|
1689
|
+
return /* @__PURE__ */ jsx12("audio", { src: objectUrl, controls: true, "aria-label": label, className });
|
|
800
1690
|
}
|
|
801
1691
|
var decodeText = (bytes) => new TextDecoder().decode(bytes);
|
|
802
1692
|
function TextPreview({
|
|
@@ -814,93 +1704,411 @@ function TextPreview({
|
|
|
814
1704
|
contentType: expectedType,
|
|
815
1705
|
decode: decodeText
|
|
816
1706
|
});
|
|
817
|
-
if (error) return /* @__PURE__ */
|
|
818
|
-
if (text === null) return /* @__PURE__ */
|
|
819
|
-
return /* @__PURE__ */
|
|
1707
|
+
if (error) return /* @__PURE__ */ jsx12(Fragment7, { children: renderFallback(error) });
|
|
1708
|
+
if (text === null) return /* @__PURE__ */ jsx12(Fragment7, { children: loading });
|
|
1709
|
+
return /* @__PURE__ */ jsx12("pre", { title, className, children: text });
|
|
820
1710
|
}
|
|
821
1711
|
|
|
822
|
-
// src/
|
|
823
|
-
import {
|
|
824
|
-
var
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
|
|
1712
|
+
// src/media-picker-selection.tsx
|
|
1713
|
+
import { jsx as jsx13, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1714
|
+
var THUMBNAIL_WIDTH2 = 96;
|
|
1715
|
+
function MediaPickerSelection({ renderItem, headingLevel = 3, className }) {
|
|
1716
|
+
const picker = useMediaPicker();
|
|
1717
|
+
const { labels, selected } = picker;
|
|
1718
|
+
const headingId = React18.useId();
|
|
1719
|
+
const [previewedId, setPreviewedId] = React18.useState(null);
|
|
1720
|
+
const Heading = `h${headingLevel}`;
|
|
1721
|
+
const cardHeadingLevel = headingLevel + 1;
|
|
1722
|
+
const regionClass = className ? `devultur-media-picker__selection ${className}` : "devultur-media-picker__selection";
|
|
1723
|
+
const handleOpen = (media) => setPreviewedId((current) => current === media.id ? null : media.id);
|
|
1724
|
+
return /* @__PURE__ */ jsxs7(
|
|
1725
|
+
"section",
|
|
1726
|
+
{
|
|
1727
|
+
ref: picker.selectionRef,
|
|
1728
|
+
tabIndex: -1,
|
|
1729
|
+
"aria-labelledby": headingId,
|
|
1730
|
+
className: regionClass,
|
|
1731
|
+
children: [
|
|
1732
|
+
/* @__PURE__ */ jsx13(Heading, { id: headingId, children: labels.selection }),
|
|
1733
|
+
selected.length === 0 && /* @__PURE__ */ jsx13("p", { "data-part": "empty", children: labels.empty }),
|
|
1734
|
+
selected.length > 0 && /* @__PURE__ */ jsx13("ul", { "data-part": "items", children: selected.map((media) => /* @__PURE__ */ jsxs7("li", { "data-kind": media.kind, children: [
|
|
1735
|
+
renderItem ? renderItem(media) : /* @__PURE__ */ jsx13(
|
|
1736
|
+
AttachmentCard,
|
|
1737
|
+
{
|
|
1738
|
+
media,
|
|
1739
|
+
headingLevel: cardHeadingLevel,
|
|
1740
|
+
labels: picker.attachmentLabels,
|
|
1741
|
+
preview: media.kind === "image" ? /* @__PURE__ */ jsx13(Thumbnail2, { media }) : void 0,
|
|
1742
|
+
onOpen: handleOpen
|
|
1743
|
+
}
|
|
1744
|
+
),
|
|
1745
|
+
previewedId === media.id && /* @__PURE__ */ jsx13(Preview, { media, title: mediaName(media) }),
|
|
1746
|
+
/* @__PURE__ */ jsx13(
|
|
1747
|
+
"button",
|
|
1748
|
+
{
|
|
1749
|
+
type: "button",
|
|
1750
|
+
"aria-label": labels.removeItem(mediaName(media)),
|
|
1751
|
+
onClick: () => picker.remove(media),
|
|
1752
|
+
children: labels.remove
|
|
1753
|
+
}
|
|
1754
|
+
)
|
|
1755
|
+
] }, media.id)) }),
|
|
1756
|
+
/* @__PURE__ */ jsx13(SelectionActions, {})
|
|
1757
|
+
]
|
|
1758
|
+
}
|
|
1759
|
+
);
|
|
834
1760
|
}
|
|
835
|
-
function
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1761
|
+
function Thumbnail2({ media }) {
|
|
1762
|
+
return /* @__PURE__ */ jsx13(
|
|
1763
|
+
Image,
|
|
1764
|
+
{
|
|
1765
|
+
media,
|
|
1766
|
+
width: THUMBNAIL_WIDTH2,
|
|
1767
|
+
widths: [THUMBNAIL_WIDTH2, THUMBNAIL_WIDTH2 * 2],
|
|
1768
|
+
sizes: `${THUMBNAIL_WIDTH2}px`,
|
|
1769
|
+
format: "auto",
|
|
1770
|
+
alt: ""
|
|
1771
|
+
}
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
function SelectionActions() {
|
|
1775
|
+
const picker = useMediaPicker();
|
|
1776
|
+
const { labels } = picker;
|
|
1777
|
+
if (picker.selected.length === 0) return null;
|
|
1778
|
+
return /* @__PURE__ */ jsxs7("div", { "data-part": "actions", children: [
|
|
1779
|
+
!picker.multiple && /* @__PURE__ */ jsx13("button", { type: "button", "data-part": "replace", onClick: picker.openFileDialog, children: labels.replace }),
|
|
1780
|
+
/* @__PURE__ */ jsx13("button", { type: "button", "data-part": "clear", onClick: picker.clear, children: labels.clear })
|
|
1781
|
+
] });
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
// src/media-picker-upload.tsx
|
|
1785
|
+
import React19 from "react";
|
|
1786
|
+
import { jsx as jsx14, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1787
|
+
function MediaPickerUpload({ className }) {
|
|
1788
|
+
const picker = useMediaPicker();
|
|
1789
|
+
const { labels, multiple } = picker;
|
|
1790
|
+
const [isDragging, setIsDragging] = React19.useState(false);
|
|
1791
|
+
const handleDragOver = (event) => {
|
|
1792
|
+
event.preventDefault();
|
|
1793
|
+
setIsDragging(true);
|
|
1794
|
+
};
|
|
1795
|
+
const handleDragLeave = (event) => {
|
|
1796
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
1797
|
+
setIsDragging(false);
|
|
1798
|
+
};
|
|
1799
|
+
const handleDrop = (event) => {
|
|
1800
|
+
event.preventDefault();
|
|
1801
|
+
setIsDragging(false);
|
|
1802
|
+
const files = event.dataTransfer.files;
|
|
1803
|
+
if (files.length > 0) picker.start(files);
|
|
1804
|
+
};
|
|
1805
|
+
const zoneClass = className ? `devultur-media-picker__upload ${className}` : "devultur-media-picker__upload";
|
|
1806
|
+
return (
|
|
1807
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: dropping is a pointer shortcut; the button is the keyboard path.
|
|
1808
|
+
/* @__PURE__ */ jsxs8(
|
|
1809
|
+
"div",
|
|
1810
|
+
{
|
|
1811
|
+
className: zoneClass,
|
|
1812
|
+
"data-dragging": isDragging || void 0,
|
|
1813
|
+
onDragOver: handleDragOver,
|
|
1814
|
+
onDragLeave: handleDragLeave,
|
|
1815
|
+
onDrop: handleDrop,
|
|
1816
|
+
children: [
|
|
1817
|
+
/* @__PURE__ */ jsx14("button", { type: "button", "data-part": "upload", onClick: picker.openFileDialog, children: labels.upload(multiple) }),
|
|
1818
|
+
/* @__PURE__ */ jsx14("span", { "data-part": "drop-hint", children: labels.dropHint(multiple) }),
|
|
1819
|
+
picker.uploads.length > 0 && /* @__PURE__ */ jsx14("ul", { "aria-label": labels.uploads, "data-part": "uploads", children: picker.uploads.map((upload) => /* @__PURE__ */ jsx14(UploadRow, { upload }, upload.id)) })
|
|
1820
|
+
]
|
|
1821
|
+
}
|
|
1822
|
+
)
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
function UploadRow({ upload }) {
|
|
1826
|
+
const picker = useMediaPicker();
|
|
1827
|
+
const { labels } = picker;
|
|
1828
|
+
const filename = upload.file.name;
|
|
1829
|
+
if (upload.status === "failed") {
|
|
1830
|
+
return /* @__PURE__ */ jsxs8("li", { "data-status": "failed", children: [
|
|
1831
|
+
/* @__PURE__ */ jsx14("span", { "data-part": "filename", children: filename }),
|
|
1832
|
+
/* @__PURE__ */ jsx14("span", { role: "alert", "data-part": "error", children: upload.error }),
|
|
1833
|
+
upload.canRetry && /* @__PURE__ */ jsx14("button", { type: "button", onClick: () => picker.retry(upload.id), children: labels.retry }),
|
|
1834
|
+
/* @__PURE__ */ jsx14("button", { type: "button", onClick: () => picker.dismiss(upload.id), children: labels.dismiss })
|
|
1835
|
+
] });
|
|
1836
|
+
}
|
|
1837
|
+
const percent = Math.round(upload.fraction * 100);
|
|
1838
|
+
return /* @__PURE__ */ jsxs8("li", { "data-status": "uploading", children: [
|
|
1839
|
+
/* @__PURE__ */ jsx14("span", { "data-part": "filename", children: filename }),
|
|
1840
|
+
/* @__PURE__ */ jsx14("progress", { max: 100, value: percent, "aria-label": labels.progress(filename) }),
|
|
1841
|
+
/* @__PURE__ */ jsx14("span", { "data-part": "percent", children: labels.percent(percent) }),
|
|
1842
|
+
/* @__PURE__ */ jsx14("button", { type: "button", "aria-label": labels.cancelUpload(filename), onClick: () => picker.cancel(upload.id), children: labels.cancel })
|
|
1843
|
+
] });
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// src/use-media-picker-uploads.ts
|
|
1847
|
+
import React20 from "react";
|
|
1848
|
+
var isAbortError = (error) => typeof error === "object" && error !== null && error.name === "AbortError";
|
|
1849
|
+
var isPdf = (file) => file.type === "application/pdf" || file.name.toLowerCase().endsWith(".pdf");
|
|
1850
|
+
var nextUploadId = 0;
|
|
1851
|
+
async function pageCountOf(file, countPdfPages) {
|
|
1852
|
+
if (!countPdfPages || !isPdf(file)) return void 0;
|
|
1853
|
+
const pageCount = await countPdfPages(file).catch(() => null);
|
|
1854
|
+
return pageCount ?? void 0;
|
|
1855
|
+
}
|
|
1856
|
+
function useMediaPickerUploads(options) {
|
|
1857
|
+
const { upload } = useDevultur();
|
|
1858
|
+
const [uploads, setUploads] = React20.useState([]);
|
|
1859
|
+
const controllers = React20.useRef(/* @__PURE__ */ new Map());
|
|
1860
|
+
const optionsRef = React20.useRef(options);
|
|
1861
|
+
optionsRef.current = options;
|
|
1862
|
+
const patch = React20.useCallback((id, changes) => {
|
|
1863
|
+
setUploads((current) => current.map((entry) => entry.id === id ? { ...entry, ...changes } : entry));
|
|
1864
|
+
}, []);
|
|
1865
|
+
const drop = React20.useCallback((id) => {
|
|
1866
|
+
controllers.current.delete(id);
|
|
1867
|
+
setUploads((current) => current.filter((entry) => entry.id !== id));
|
|
1868
|
+
}, []);
|
|
1869
|
+
const run = React20.useCallback(
|
|
1870
|
+
async (id, file) => {
|
|
1871
|
+
const { labels, announce } = optionsRef.current;
|
|
1872
|
+
const controller = new AbortController();
|
|
1873
|
+
controllers.current.set(id, controller);
|
|
1874
|
+
patch(id, { status: "uploading", fraction: 0, error: null });
|
|
1875
|
+
announce(labels.uploading(file.name));
|
|
1876
|
+
try {
|
|
1877
|
+
const pageCount = await pageCountOf(file, optionsRef.current.countPdfPages);
|
|
1878
|
+
const uploaded = await upload(file, {
|
|
1879
|
+
pageCount,
|
|
1880
|
+
signal: controller.signal,
|
|
1881
|
+
onProgress: ({ fraction }) => patch(id, { fraction })
|
|
854
1882
|
});
|
|
855
|
-
|
|
1883
|
+
drop(id);
|
|
1884
|
+
const media = { ...uploaded, sizeBytes: file.size, ...pageCount ? { pageCount } : {} };
|
|
1885
|
+
optionsRef.current.onUploaded(media);
|
|
1886
|
+
announce(labels.uploaded(file.name));
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
if (isAbortError(error)) {
|
|
1889
|
+
drop(id);
|
|
1890
|
+
announce(labels.cancelled(file.name));
|
|
1891
|
+
return;
|
|
1892
|
+
}
|
|
1893
|
+
controllers.current.delete(id);
|
|
1894
|
+
patch(id, { status: "failed", error: labels.uploadError(error), canRetry: true });
|
|
1895
|
+
}
|
|
856
1896
|
},
|
|
857
|
-
[
|
|
1897
|
+
[upload, patch, drop]
|
|
858
1898
|
);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1899
|
+
const start = React20.useCallback(
|
|
1900
|
+
(files) => {
|
|
1901
|
+
const { accept, multiple, labels } = optionsRef.current;
|
|
1902
|
+
const chosen = multiple ? [...files] : [...files].slice(0, 1);
|
|
1903
|
+
const entries = chosen.map((file) => {
|
|
1904
|
+
const isAccepted = matchesAccept(file, accept);
|
|
1905
|
+
return {
|
|
1906
|
+
id: `upload-${nextUploadId++}`,
|
|
1907
|
+
file,
|
|
1908
|
+
fraction: 0,
|
|
1909
|
+
status: isAccepted ? "uploading" : "failed",
|
|
1910
|
+
error: isAccepted ? null : labels.notAccepted(file.name),
|
|
1911
|
+
canRetry: false
|
|
1912
|
+
};
|
|
1913
|
+
});
|
|
1914
|
+
setUploads((current) => [...current, ...entries]);
|
|
1915
|
+
for (const entry of entries) {
|
|
1916
|
+
if (entry.status === "uploading") run(entry.id, entry.file);
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
[run]
|
|
1920
|
+
);
|
|
1921
|
+
const cancel = React20.useCallback((id) => controllers.current.get(id)?.abort(), []);
|
|
1922
|
+
const retry = React20.useCallback(
|
|
1923
|
+
(id) => {
|
|
1924
|
+
const entry = uploads.find((candidate) => candidate.id === id);
|
|
1925
|
+
if (!entry?.canRetry) return;
|
|
1926
|
+
run(id, entry.file);
|
|
1927
|
+
},
|
|
1928
|
+
[uploads, run]
|
|
1929
|
+
);
|
|
1930
|
+
React20.useEffect(() => {
|
|
1931
|
+
const active = controllers.current;
|
|
883
1932
|
return () => {
|
|
884
|
-
|
|
1933
|
+
for (const controller of active.values()) controller.abort();
|
|
885
1934
|
};
|
|
886
1935
|
}, []);
|
|
887
|
-
|
|
1936
|
+
return { uploads, start, cancel, retry, dismiss: drop };
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/media-picker.tsx
|
|
1940
|
+
import { Fragment as Fragment8, jsx as jsx15, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1941
|
+
var EMPTY = [];
|
|
1942
|
+
function selectionOf(props) {
|
|
1943
|
+
if (props.value === void 0) return void 0;
|
|
1944
|
+
if (props.multiple) return props.value;
|
|
1945
|
+
return props.value ? [props.value] : EMPTY;
|
|
1946
|
+
}
|
|
1947
|
+
function emit(props, next) {
|
|
1948
|
+
if (props.multiple) {
|
|
1949
|
+
props.onChange(next);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
props.onChange(next[0] ?? null);
|
|
1953
|
+
}
|
|
1954
|
+
var isSame = (a, b) => a.id === b.id;
|
|
1955
|
+
function MediaPicker(props) {
|
|
1956
|
+
const isMultiple = props.multiple ?? false;
|
|
1957
|
+
const labels = React21.useMemo(() => mergeMediaPickerLabels(props.labels), [props.labels]);
|
|
1958
|
+
const accept = uploadAcceptOf(props.accept, props.type);
|
|
1959
|
+
const libraryType = props.type ?? mediaTypeOfAccept(props.accept);
|
|
1960
|
+
const countPdfPages = usePdfPageCounter();
|
|
1961
|
+
const [announcement, setAnnouncement] = React21.useState("");
|
|
1962
|
+
const inputRef = React21.useRef(null);
|
|
1963
|
+
const selectionRef = React21.useRef(null);
|
|
1964
|
+
const [ownSelection, setOwnSelection] = React21.useState(EMPTY);
|
|
1965
|
+
const controlledSelection = selectionOf(props);
|
|
1966
|
+
const selected = controlledSelection ?? ownSelection;
|
|
1967
|
+
const selectedRef = React21.useRef(selected);
|
|
1968
|
+
selectedRef.current = selected;
|
|
1969
|
+
const propsRef = React21.useRef(props);
|
|
1970
|
+
propsRef.current = props;
|
|
1971
|
+
const isControlled = controlledSelection !== void 0;
|
|
1972
|
+
const commit = React21.useCallback(
|
|
1973
|
+
(next) => {
|
|
1974
|
+
selectedRef.current = next;
|
|
1975
|
+
if (!isControlled) setOwnSelection(next);
|
|
1976
|
+
emit(propsRef.current, next);
|
|
1977
|
+
},
|
|
1978
|
+
[isControlled]
|
|
1979
|
+
);
|
|
1980
|
+
const announce = React21.useCallback((message) => setAnnouncement(message), []);
|
|
1981
|
+
const select = React21.useCallback(
|
|
1982
|
+
(media) => {
|
|
1983
|
+
const current = selectedRef.current.filter((item) => !isSame(item, media));
|
|
1984
|
+
commit(isMultiple ? [...current, media] : [media]);
|
|
1985
|
+
},
|
|
1986
|
+
[commit, isMultiple]
|
|
1987
|
+
);
|
|
1988
|
+
const remove = React21.useCallback(
|
|
1989
|
+
(media) => {
|
|
1990
|
+
commit(selectedRef.current.filter((item) => !isSame(item, media)));
|
|
1991
|
+
announce(labels.removed(mediaName(media)));
|
|
1992
|
+
selectionRef.current?.focus();
|
|
1993
|
+
},
|
|
1994
|
+
[commit, announce, labels]
|
|
1995
|
+
);
|
|
1996
|
+
const toggle = React21.useCallback(
|
|
1997
|
+
(media) => {
|
|
1998
|
+
const isSelected = selectedRef.current.some((item) => isSame(item, media));
|
|
1999
|
+
if (isMultiple && isSelected) {
|
|
2000
|
+
remove(media);
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
select(media);
|
|
2004
|
+
announce(labels.selected(mediaName(media)));
|
|
2005
|
+
},
|
|
2006
|
+
[isMultiple, remove, select, announce, labels]
|
|
2007
|
+
);
|
|
2008
|
+
const clear = React21.useCallback(() => {
|
|
2009
|
+
commit([]);
|
|
2010
|
+
announce(labels.cleared);
|
|
2011
|
+
selectionRef.current?.focus();
|
|
2012
|
+
}, [commit, announce, labels]);
|
|
2013
|
+
const handleUploaded = React21.useCallback(
|
|
2014
|
+
(media) => {
|
|
2015
|
+
select(media);
|
|
2016
|
+
selectionRef.current?.focus();
|
|
2017
|
+
},
|
|
2018
|
+
[select]
|
|
2019
|
+
);
|
|
2020
|
+
const uploads = useMediaPickerUploads({
|
|
2021
|
+
accept,
|
|
2022
|
+
multiple: isMultiple,
|
|
2023
|
+
labels,
|
|
2024
|
+
countPdfPages,
|
|
2025
|
+
onUploaded: handleUploaded,
|
|
2026
|
+
announce
|
|
2027
|
+
});
|
|
2028
|
+
const handleFileInput = (event) => {
|
|
2029
|
+
const files = event.target.files;
|
|
2030
|
+
if (files && files.length > 0) uploads.start(files);
|
|
2031
|
+
event.target.value = "";
|
|
2032
|
+
};
|
|
2033
|
+
const picker = {
|
|
2034
|
+
...uploads,
|
|
2035
|
+
selected,
|
|
2036
|
+
multiple: isMultiple,
|
|
2037
|
+
accept,
|
|
2038
|
+
libraryType,
|
|
2039
|
+
labels,
|
|
2040
|
+
libraryLabels: props.libraryLabels,
|
|
2041
|
+
attachmentLabels: props.attachmentLabels,
|
|
2042
|
+
select,
|
|
2043
|
+
toggle,
|
|
2044
|
+
remove,
|
|
2045
|
+
clear,
|
|
2046
|
+
openFileDialog: () => inputRef.current?.click(),
|
|
2047
|
+
announce,
|
|
2048
|
+
selectionRef
|
|
2049
|
+
};
|
|
2050
|
+
const content = typeof props.children === "function" ? props.children(picker) : props.children ?? /* @__PURE__ */ jsxs9(Fragment8, { children: [
|
|
2051
|
+
/* @__PURE__ */ jsx15(MediaPickerUpload, {}),
|
|
2052
|
+
/* @__PURE__ */ jsx15(MediaPickerLibrary, {}),
|
|
2053
|
+
/* @__PURE__ */ jsx15(MediaPickerSelection, {})
|
|
2054
|
+
] });
|
|
2055
|
+
const rootClass = props.className ? `devultur-media-picker ${props.className}` : "devultur-media-picker";
|
|
2056
|
+
return /* @__PURE__ */ jsx15(MediaPickerContext, { value: picker, children: /* @__PURE__ */ jsxs9("div", { className: rootClass, children: [
|
|
2057
|
+
/* @__PURE__ */ jsx15(
|
|
2058
|
+
"input",
|
|
2059
|
+
{
|
|
2060
|
+
ref: inputRef,
|
|
2061
|
+
type: "file",
|
|
2062
|
+
accept: accept?.join(","),
|
|
2063
|
+
multiple: isMultiple,
|
|
2064
|
+
tabIndex: -1,
|
|
2065
|
+
"aria-hidden": true,
|
|
2066
|
+
style: { display: "none" },
|
|
2067
|
+
onChange: handleFileInput,
|
|
2068
|
+
"data-part": "input"
|
|
2069
|
+
}
|
|
2070
|
+
),
|
|
2071
|
+
content,
|
|
2072
|
+
/* @__PURE__ */ jsx15("div", { role: "status", "aria-live": "polite", style: VISUALLY_HIDDEN, children: announcement })
|
|
2073
|
+
] }) });
|
|
2074
|
+
}
|
|
2075
|
+
MediaPicker.Upload = MediaPickerUpload;
|
|
2076
|
+
MediaPicker.Library = MediaPickerLibrary;
|
|
2077
|
+
MediaPicker.Selection = MediaPickerSelection;
|
|
2078
|
+
|
|
2079
|
+
// src/react.tsx
|
|
2080
|
+
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2081
|
+
var DevulturContext = React22.createContext(null);
|
|
2082
|
+
var DevulturApiContext = React22.createContext(null);
|
|
2083
|
+
var ProgressContext = React22.createContext(null);
|
|
2084
|
+
function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
|
|
2085
|
+
const session = useMediaSession({ issueViewerToken: issueViewerToken ?? api.issueViewerToken, tokenArgs, baseUrl });
|
|
2086
|
+
const createUploadUrlAction = useAction2(api.createUploadUrl);
|
|
2087
|
+
const deleteMediaAction = useAction2(api.deleteMedia);
|
|
2088
|
+
const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
|
|
2089
|
+
const deleteVideoAction = useAction2(api.deleteVideo ?? api.deleteMedia);
|
|
2090
|
+
const uploadUrl = React22.useCallback(
|
|
888
2091
|
async (file) => {
|
|
889
2092
|
const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
|
|
890
2093
|
return { url: result.url, key: result.key };
|
|
891
2094
|
},
|
|
892
2095
|
[createUploadUrlAction]
|
|
893
2096
|
);
|
|
894
|
-
const upload =
|
|
2097
|
+
const upload = React22.useCallback(
|
|
895
2098
|
async (file, options) => {
|
|
896
2099
|
const dimensions = await readImageDimensions(file);
|
|
2100
|
+
options?.signal?.throwIfAborted();
|
|
897
2101
|
const target = await createUploadUrlAction({
|
|
898
2102
|
filename: file.name,
|
|
899
2103
|
contentType: file.type,
|
|
900
2104
|
visibility: options?.visibility
|
|
901
2105
|
});
|
|
902
2106
|
await uploadFileWithProgress(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
|
|
903
|
-
if (api.confirmUpload)
|
|
2107
|
+
if (api.confirmUpload) {
|
|
2108
|
+
const pageCount = options?.pageCount;
|
|
2109
|
+
const hints = { ...dimensions, ...pageCount !== void 0 ? { pageCount } : {} };
|
|
2110
|
+
await confirmUploadBestEffort(confirmUploadAction, target.key, file, hints);
|
|
2111
|
+
}
|
|
904
2112
|
return {
|
|
905
2113
|
id: target.id,
|
|
906
2114
|
kind: target.kind,
|
|
@@ -912,59 +2120,23 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
912
2120
|
},
|
|
913
2121
|
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
914
2122
|
);
|
|
915
|
-
const
|
|
916
|
-
(media, options) => {
|
|
917
|
-
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
918
|
-
return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
919
|
-
},
|
|
920
|
-
[baseUrl, token]
|
|
921
|
-
);
|
|
922
|
-
const getCaptionsText = React7.useCallback(
|
|
923
|
-
async (media, locale) => {
|
|
924
|
-
if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
|
|
925
|
-
const path = mediaCaptionPath(media, locale);
|
|
926
|
-
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
927
|
-
const isPrivate = media.visibility === "private";
|
|
928
|
-
const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
|
|
929
|
-
headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
|
|
930
|
-
});
|
|
931
|
-
let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
|
|
932
|
-
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
933
|
-
const fresh = await reissueToken();
|
|
934
|
-
if (fresh) response = await fetchWith(fresh);
|
|
935
|
-
}
|
|
936
|
-
if (!response.ok) throw new Error(`Captions fetch failed: ${response.status}`);
|
|
937
|
-
return response.text();
|
|
938
|
-
},
|
|
939
|
-
[baseUrl, token, reissueToken, sessionSecret]
|
|
940
|
-
);
|
|
941
|
-
const deleteMedia = React7.useCallback(
|
|
2123
|
+
const deleteMedia = React22.useCallback(
|
|
942
2124
|
(media) => {
|
|
943
2125
|
if (media.kind === "video" && api.deleteVideo) {
|
|
944
2126
|
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
945
2127
|
return;
|
|
946
2128
|
}
|
|
947
|
-
deleteMediaAction({ key:
|
|
2129
|
+
deleteMediaAction({ key: mediaPath3(media) }).catch(console.error);
|
|
948
2130
|
},
|
|
949
2131
|
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
950
2132
|
);
|
|
951
|
-
const devulturValue =
|
|
952
|
-
() => ({
|
|
953
|
-
|
|
954
|
-
baseUrl: baseUrl ?? null,
|
|
955
|
-
sessionSecret,
|
|
956
|
-
uploadUrl,
|
|
957
|
-
upload,
|
|
958
|
-
url,
|
|
959
|
-
getCaptionsText,
|
|
960
|
-
deleteMedia,
|
|
961
|
-
reissueToken
|
|
962
|
-
}),
|
|
963
|
-
[token, baseUrl, sessionSecret, uploadUrl, upload, url, getCaptionsText, deleteMedia, reissueToken]
|
|
2133
|
+
const devulturValue = React22.useMemo(
|
|
2134
|
+
() => ({ ...session, uploadUrl, upload, deleteMedia }),
|
|
2135
|
+
[session, uploadUrl, upload, deleteMedia]
|
|
964
2136
|
);
|
|
965
|
-
let content = /* @__PURE__ */
|
|
2137
|
+
let content = /* @__PURE__ */ jsx16(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children }) });
|
|
966
2138
|
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
967
|
-
content = /* @__PURE__ */
|
|
2139
|
+
content = /* @__PURE__ */ jsx16(
|
|
968
2140
|
ProgressBridge,
|
|
969
2141
|
{
|
|
970
2142
|
save: api.saveProgress,
|
|
@@ -988,13 +2160,13 @@ function ProgressBridge({
|
|
|
988
2160
|
const saveMutation = useMutation(save);
|
|
989
2161
|
const markCompleteMutation = useMutation(markComplete);
|
|
990
2162
|
const resetMutation = useMutation(reset);
|
|
991
|
-
const onSave =
|
|
2163
|
+
const onSave = React22.useCallback(
|
|
992
2164
|
async (videoId, position) => {
|
|
993
2165
|
await saveMutation({ videoId, position });
|
|
994
2166
|
},
|
|
995
2167
|
[saveMutation]
|
|
996
2168
|
);
|
|
997
|
-
const onLoad =
|
|
2169
|
+
const onLoad = React22.useCallback(
|
|
998
2170
|
async (videoId) => {
|
|
999
2171
|
const result = await convex.query(load, { videoId });
|
|
1000
2172
|
if (!result) return null;
|
|
@@ -1005,46 +2177,69 @@ function ProgressBridge({
|
|
|
1005
2177
|
},
|
|
1006
2178
|
[convex, load]
|
|
1007
2179
|
);
|
|
1008
|
-
const onMarkComplete =
|
|
2180
|
+
const onMarkComplete = React22.useCallback(
|
|
1009
2181
|
async (videoId) => {
|
|
1010
2182
|
await markCompleteMutation({ videoId });
|
|
1011
2183
|
},
|
|
1012
2184
|
[markCompleteMutation]
|
|
1013
2185
|
);
|
|
1014
|
-
const onReset =
|
|
2186
|
+
const onReset = React22.useCallback(
|
|
1015
2187
|
async (videoId) => {
|
|
1016
2188
|
await resetMutation({ videoId });
|
|
1017
2189
|
},
|
|
1018
2190
|
[resetMutation]
|
|
1019
2191
|
);
|
|
1020
|
-
const value =
|
|
2192
|
+
const value = React22.useMemo(
|
|
1021
2193
|
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
1022
2194
|
[onSave, onLoad, onMarkComplete, onReset]
|
|
1023
2195
|
);
|
|
1024
|
-
return /* @__PURE__ */
|
|
2196
|
+
return /* @__PURE__ */ jsx16(ProgressContext, { value, children });
|
|
1025
2197
|
}
|
|
1026
2198
|
function useDevultur() {
|
|
1027
|
-
const context =
|
|
1028
|
-
if (!context) throw new Error("useDevultur must be used within <DevulturProvider>");
|
|
2199
|
+
const context = React22.use(DevulturContext);
|
|
2200
|
+
if (!context) throw new Error("useDevultur must be used within <DevulturProvider> or <DevulturViewerProvider>");
|
|
1029
2201
|
return context;
|
|
1030
2202
|
}
|
|
2203
|
+
function useDevulturApi() {
|
|
2204
|
+
const api = React22.use(DevulturApiContext);
|
|
2205
|
+
if (api) return api;
|
|
2206
|
+
if (React22.use(DevulturContext)) throw new DevulturCapabilityError("api");
|
|
2207
|
+
throw new Error("useDevulturApi must be used within <DevulturProvider>");
|
|
2208
|
+
}
|
|
2209
|
+
var VIEWER_ACTIONS = {
|
|
2210
|
+
uploadUrl: () => Promise.reject(new DevulturCapabilityError("uploadUrl")),
|
|
2211
|
+
upload: () => Promise.reject(new DevulturCapabilityError("upload")),
|
|
2212
|
+
deleteMedia: () => {
|
|
2213
|
+
throw new DevulturCapabilityError("deleteMedia");
|
|
2214
|
+
}
|
|
2215
|
+
};
|
|
2216
|
+
function DevulturViewerProvider({
|
|
2217
|
+
baseUrl,
|
|
2218
|
+
issueViewerToken,
|
|
2219
|
+
tokenArgs,
|
|
2220
|
+
children
|
|
2221
|
+
}) {
|
|
2222
|
+
const session = useMediaSession({ issueViewerToken, tokenArgs, baseUrl });
|
|
2223
|
+
const devulturValue = React22.useMemo(() => ({ ...session, ...VIEWER_ACTIONS }), [session]);
|
|
2224
|
+
return /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children });
|
|
2225
|
+
}
|
|
1031
2226
|
function useDevulturProgress() {
|
|
1032
|
-
const context =
|
|
2227
|
+
const context = React22.use(ProgressContext);
|
|
1033
2228
|
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
1034
2229
|
return context;
|
|
1035
2230
|
}
|
|
1036
2231
|
function useMediaUrl(key, options) {
|
|
1037
2232
|
const { token, baseUrl } = useDevultur();
|
|
1038
|
-
return
|
|
2233
|
+
return React22.useMemo(() => {
|
|
1039
2234
|
if (!key) return null;
|
|
1040
2235
|
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
1041
2236
|
const isPublic = visibilityFromKey(key) === "public";
|
|
1042
|
-
return
|
|
2237
|
+
return buildMediaUrl2(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
|
|
1043
2238
|
}, [key, baseUrl, token, options]);
|
|
1044
2239
|
}
|
|
1045
|
-
async function confirmUploadBestEffort(confirmUpload, key, file,
|
|
2240
|
+
async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
|
|
1046
2241
|
try {
|
|
1047
|
-
await confirmUpload({ key, filename: file.name, ...
|
|
2242
|
+
await confirmUpload({ key, filename: file.name, ...hints });
|
|
1048
2243
|
} catch (error) {
|
|
1049
2244
|
console.warn("Devultur: could not record the upload; it is recorded later", error);
|
|
1050
2245
|
}
|
|
@@ -1063,122 +2258,45 @@ async function readImageDimensions(file) {
|
|
|
1063
2258
|
function useVideoProps(media, options) {
|
|
1064
2259
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
1065
2260
|
const locales = options?.locales;
|
|
1066
|
-
return
|
|
2261
|
+
return React22.useMemo(() => {
|
|
1067
2262
|
if (media?.kind !== "video") return null;
|
|
1068
2263
|
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
1069
2264
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
1070
2265
|
const captions = (locales ?? []).flatMap((locale) => {
|
|
1071
|
-
const path =
|
|
2266
|
+
const path = mediaCaptionPath2(media, locale);
|
|
1072
2267
|
if (!path) return [];
|
|
1073
2268
|
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
1074
|
-
return [{ locale, label, src:
|
|
2269
|
+
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
1075
2270
|
});
|
|
1076
2271
|
return {
|
|
1077
|
-
src:
|
|
2272
|
+
src: buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken }),
|
|
1078
2273
|
token: viewerToken,
|
|
1079
2274
|
captions,
|
|
1080
2275
|
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
1081
2276
|
};
|
|
1082
2277
|
}, [media, baseUrl, token, locales, sessionSecret]);
|
|
1083
2278
|
}
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
height,
|
|
1088
|
-
fit,
|
|
1089
|
-
format,
|
|
1090
|
-
quality,
|
|
1091
|
-
fallback = null,
|
|
1092
|
-
externalFallback,
|
|
1093
|
-
render,
|
|
1094
|
-
alt,
|
|
1095
|
-
...imgProps
|
|
1096
|
-
}) {
|
|
1097
|
-
const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
|
|
1098
|
-
const [failed, setFailed] = React7.useState(false);
|
|
1099
|
-
const [blobSrc, setBlobSrc] = React7.useState(null);
|
|
1100
|
-
const sessionRef = React7.useRef({ token, reissueToken });
|
|
1101
|
-
React7.useEffect(() => {
|
|
1102
|
-
sessionRef.current = { token, reissueToken };
|
|
1103
|
-
});
|
|
1104
|
-
const isPrivate = media?.visibility === "private";
|
|
1105
|
-
const hasToken = token !== null;
|
|
1106
|
-
React7.useEffect(() => {
|
|
1107
|
-
setFailed(false);
|
|
1108
|
-
setBlobSrc(null);
|
|
1109
|
-
}, [media]);
|
|
1110
|
-
React7.useEffect(() => {
|
|
1111
|
-
if (!isPrivate || failed || !media || !hasToken) return;
|
|
1112
|
-
let cancelled = false;
|
|
1113
|
-
let objectUrl = null;
|
|
1114
|
-
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
1115
|
-
const transforms = { width, height, fit, format, quality };
|
|
1116
|
-
(async () => {
|
|
1117
|
-
try {
|
|
1118
|
-
const response = await fetchMedia({
|
|
1119
|
-
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...transforms, token: viewerToken }),
|
|
1120
|
-
token: sessionRef.current.token,
|
|
1121
|
-
sessionSecret,
|
|
1122
|
-
reissueToken: sessionRef.current.reissueToken
|
|
1123
|
-
});
|
|
1124
|
-
if (response.ok) {
|
|
1125
|
-
objectUrl = URL.createObjectURL(await response.blob());
|
|
1126
|
-
if (cancelled) {
|
|
1127
|
-
URL.revokeObjectURL(objectUrl);
|
|
1128
|
-
return;
|
|
1129
|
-
}
|
|
1130
|
-
setBlobSrc(objectUrl);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
} catch {
|
|
1134
|
-
}
|
|
1135
|
-
if (!cancelled) setFailed(true);
|
|
1136
|
-
})();
|
|
1137
|
-
return () => {
|
|
1138
|
-
cancelled = true;
|
|
1139
|
-
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1140
|
-
};
|
|
1141
|
-
}, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
1142
|
-
const handleError = React7.useCallback(() => setFailed(true), []);
|
|
1143
|
-
let src;
|
|
1144
|
-
if (failed || !media) {
|
|
1145
|
-
src = externalFallback ?? null;
|
|
1146
|
-
} else if (isPrivate) {
|
|
1147
|
-
src = blobSrc;
|
|
1148
|
-
} else {
|
|
1149
|
-
src = url(media, { width, height, fit, format, quality });
|
|
1150
|
-
}
|
|
1151
|
-
if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
|
|
1152
|
-
if (render) {
|
|
1153
|
-
return React7.cloneElement(render, {
|
|
1154
|
-
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
1155
|
-
});
|
|
1156
|
-
}
|
|
1157
|
-
return /* @__PURE__ */ jsx6(
|
|
1158
|
-
"img",
|
|
1159
|
-
{
|
|
1160
|
-
src,
|
|
1161
|
-
alt,
|
|
1162
|
-
width: width ?? media?.width,
|
|
1163
|
-
height: height ?? media?.height,
|
|
1164
|
-
onError: handleError,
|
|
1165
|
-
...imgProps
|
|
1166
|
-
}
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
1170
|
-
const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
1171
|
-
const handleClick = React7.useCallback(
|
|
2279
|
+
function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
|
|
2280
|
+
const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
2281
|
+
const handleClick = React22.useCallback(
|
|
1172
2282
|
async (event) => {
|
|
1173
2283
|
if (media?.visibility !== "private" || !baseUrl) return;
|
|
1174
2284
|
event.preventDefault();
|
|
2285
|
+
if (tokenStatus === "error") {
|
|
2286
|
+
onDownloadError?.(tokenUnavailableError(tokenError));
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
1175
2289
|
const response = await fetchMedia({
|
|
1176
|
-
buildUrl: (viewerToken) =>
|
|
2290
|
+
buildUrl: (viewerToken) => mediaUrl4(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
1177
2291
|
token,
|
|
1178
2292
|
sessionSecret,
|
|
1179
2293
|
reissueToken
|
|
1180
2294
|
});
|
|
1181
|
-
if (!response.ok)
|
|
2295
|
+
if (!response.ok) {
|
|
2296
|
+
const status = response.status;
|
|
2297
|
+
onDownloadError?.(new MediaFetchError("http_error", `Media fetch failed: ${status}`, status));
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
1182
2300
|
const objectUrl = URL.createObjectURL(await response.blob());
|
|
1183
2301
|
const anchor = document.createElement("a");
|
|
1184
2302
|
anchor.href = objectUrl;
|
|
@@ -1186,13 +2304,13 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
|
1186
2304
|
anchor.click();
|
|
1187
2305
|
URL.revokeObjectURL(objectUrl);
|
|
1188
2306
|
},
|
|
1189
|
-
[media, baseUrl, token, filename, sessionSecret, reissueToken]
|
|
2307
|
+
[media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
|
|
1190
2308
|
);
|
|
1191
2309
|
if (!media) return null;
|
|
1192
|
-
return /* @__PURE__ */
|
|
2310
|
+
return /* @__PURE__ */ jsx16("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
|
|
1193
2311
|
}
|
|
1194
2312
|
function useDevulturMedia(key) {
|
|
1195
|
-
const apiCtx =
|
|
2313
|
+
const apiCtx = React22.use(DevulturApiContext);
|
|
1196
2314
|
if (!apiCtx?.getMedia) {
|
|
1197
2315
|
throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
|
|
1198
2316
|
}
|
|
@@ -1249,7 +2367,7 @@ function useVideoState(media, options) {
|
|
|
1249
2367
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
1250
2368
|
const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
|
|
1251
2369
|
const locales = options?.locales;
|
|
1252
|
-
return
|
|
2370
|
+
return React22.useMemo(() => {
|
|
1253
2371
|
if (media?.kind !== "video") return { phase: "idle" };
|
|
1254
2372
|
if (status.isFailed) {
|
|
1255
2373
|
return {
|
|
@@ -1262,12 +2380,12 @@ function useVideoState(media, options) {
|
|
|
1262
2380
|
if (status.isReady) {
|
|
1263
2381
|
if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
|
|
1264
2382
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
1265
|
-
const playlist =
|
|
2383
|
+
const playlist = buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken });
|
|
1266
2384
|
const captions = (locales ?? status.captionLocales).flatMap((locale) => {
|
|
1267
|
-
const path =
|
|
2385
|
+
const path = mediaCaptionPath2(media, locale);
|
|
1268
2386
|
if (!path) return [];
|
|
1269
2387
|
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
1270
|
-
return [{ locale, label, src:
|
|
2388
|
+
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
1271
2389
|
});
|
|
1272
2390
|
return {
|
|
1273
2391
|
phase: "ready",
|
|
@@ -1293,23 +2411,22 @@ function useVideoState(media, options) {
|
|
|
1293
2411
|
}
|
|
1294
2412
|
function useMediaUpload(options) {
|
|
1295
2413
|
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
1296
|
-
const api =
|
|
1297
|
-
|
|
1298
|
-
const
|
|
1299
|
-
const
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1302
|
-
const [
|
|
1303
|
-
const [
|
|
1304
|
-
const [
|
|
1305
|
-
const [
|
|
1306
|
-
const [
|
|
1307
|
-
const
|
|
1308
|
-
const
|
|
1309
|
-
const
|
|
1310
|
-
const lastKeyRef = React7.useRef(null);
|
|
2414
|
+
const api = useDevulturApi();
|
|
2415
|
+
const createUploadUrlAction = useAction2(api.createUploadUrl);
|
|
2416
|
+
const deleteMediaAction = useAction2(api.deleteMedia);
|
|
2417
|
+
const confirmUploadAction = useAction2(api.confirmUpload ?? api.createUploadUrl);
|
|
2418
|
+
const processVideoAction = useAction2(api.processVideo ?? api.createUploadUrl);
|
|
2419
|
+
const [phase, setPhase] = React22.useState("idle");
|
|
2420
|
+
const [percent, setPercent] = React22.useState(0);
|
|
2421
|
+
const [currentFile, setCurrentFile] = React22.useState(null);
|
|
2422
|
+
const [media, setMedia] = React22.useState(null);
|
|
2423
|
+
const [uploadFailure, setUploadFailure] = React22.useState(null);
|
|
2424
|
+
const [processRequested, setProcessRequested] = React22.useState(false);
|
|
2425
|
+
const abortRef = React22.useRef(null);
|
|
2426
|
+
const lastFileRef = React22.useRef(null);
|
|
2427
|
+
const lastKeyRef = React22.useRef(null);
|
|
1311
2428
|
const videoState = useVideoState(media, { locales });
|
|
1312
|
-
const triggerProcess =
|
|
2429
|
+
const triggerProcess = React22.useCallback(
|
|
1313
2430
|
(key) => {
|
|
1314
2431
|
if (!api.processVideo) {
|
|
1315
2432
|
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
@@ -1322,7 +2439,7 @@ function useMediaUpload(options) {
|
|
|
1322
2439
|
},
|
|
1323
2440
|
[api.processVideo, processVideoAction]
|
|
1324
2441
|
);
|
|
1325
|
-
const runUpload =
|
|
2442
|
+
const runUpload = React22.useCallback(
|
|
1326
2443
|
async (file) => {
|
|
1327
2444
|
const controller = new AbortController();
|
|
1328
2445
|
abortRef.current = controller;
|
|
@@ -1374,7 +2491,7 @@ function useMediaUpload(options) {
|
|
|
1374
2491
|
};
|
|
1375
2492
|
setMedia(uploaded);
|
|
1376
2493
|
setPhase("uploaded");
|
|
1377
|
-
if (replaces) deleteMediaAction({ key:
|
|
2494
|
+
if (replaces) deleteMediaAction({ key: mediaPath3(replaces) }).catch(() => {
|
|
1378
2495
|
});
|
|
1379
2496
|
onComplete?.(uploaded);
|
|
1380
2497
|
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
@@ -1393,10 +2510,10 @@ function useMediaUpload(options) {
|
|
|
1393
2510
|
onError
|
|
1394
2511
|
]
|
|
1395
2512
|
);
|
|
1396
|
-
const cancel =
|
|
2513
|
+
const cancel = React22.useCallback(() => {
|
|
1397
2514
|
abortRef.current?.abort();
|
|
1398
2515
|
}, []);
|
|
1399
|
-
const reset =
|
|
2516
|
+
const reset = React22.useCallback(() => {
|
|
1400
2517
|
abortRef.current?.abort();
|
|
1401
2518
|
setPhase("idle");
|
|
1402
2519
|
setPercent(0);
|
|
@@ -1405,15 +2522,15 @@ function useMediaUpload(options) {
|
|
|
1405
2522
|
setUploadFailure(null);
|
|
1406
2523
|
setProcessRequested(false);
|
|
1407
2524
|
}, []);
|
|
1408
|
-
const retry =
|
|
2525
|
+
const retry = React22.useCallback(() => {
|
|
1409
2526
|
if (phase === "upload-failed" && lastFileRef.current) {
|
|
1410
2527
|
runUpload(lastFileRef.current);
|
|
1411
2528
|
return;
|
|
1412
2529
|
}
|
|
1413
2530
|
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
1414
2531
|
}, [phase, runUpload, triggerProcess]);
|
|
1415
|
-
|
|
1416
|
-
const state =
|
|
2532
|
+
React22.useEffect(() => () => abortRef.current?.abort(), []);
|
|
2533
|
+
const state = React22.useMemo(
|
|
1417
2534
|
() => deriveUploadState(
|
|
1418
2535
|
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
1419
2536
|
videoState,
|
|
@@ -1421,23 +2538,38 @@ function useMediaUpload(options) {
|
|
|
1421
2538
|
),
|
|
1422
2539
|
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
1423
2540
|
);
|
|
1424
|
-
const upload =
|
|
2541
|
+
const upload = React22.useCallback((file) => runUpload(file), [runUpload]);
|
|
1425
2542
|
return { upload, state, cancel, reset };
|
|
1426
2543
|
}
|
|
1427
2544
|
export {
|
|
2545
|
+
AttachmentCard,
|
|
1428
2546
|
CropUpload,
|
|
2547
|
+
DevulturCapabilityError,
|
|
1429
2548
|
DevulturProvider,
|
|
2549
|
+
DevulturViewerProvider,
|
|
2550
|
+
DocumentLink,
|
|
1430
2551
|
DownloadLink,
|
|
1431
2552
|
Image,
|
|
1432
2553
|
MediaFetchError,
|
|
2554
|
+
MediaLibrary,
|
|
2555
|
+
MediaPicker,
|
|
1433
2556
|
PdfPreviewProvider,
|
|
1434
2557
|
PdfThumbnail,
|
|
1435
2558
|
Preview,
|
|
2559
|
+
defaultAttachmentLabels,
|
|
2560
|
+
defaultMediaLibraryLabels,
|
|
2561
|
+
defaultMediaPickerLabels,
|
|
1436
2562
|
defaultPreviewLabels,
|
|
2563
|
+
formatDuration,
|
|
2564
|
+
formatFileSize,
|
|
2565
|
+
matchesAccept,
|
|
1437
2566
|
useDevultur,
|
|
2567
|
+
useDevulturApi,
|
|
1438
2568
|
useDevulturMedia,
|
|
1439
2569
|
useDevulturProgress,
|
|
2570
|
+
useMediaLibrary,
|
|
1440
2571
|
useMediaObjectUrl,
|
|
2572
|
+
useMediaPicker,
|
|
1441
2573
|
useMediaUpload,
|
|
1442
2574
|
useMediaUrl,
|
|
1443
2575
|
useVideoProps,
|