@devultur/convex 1.3.2 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +426 -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 +441 -66
- package/dist/react.js +1663 -550
- package/dist/types-LrusGVDf.d.ts +62 -0
- package/dist/validators.d.ts +17 -5
- package/dist/validators.js +1 -1
- package/package.json +8 -4
package/dist/react.js
CHANGED
|
@@ -1,22 +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
33
|
MediaStatus,
|
|
9
|
-
mediaCaptionPath,
|
|
10
|
-
mediaPath as
|
|
11
|
-
mediaUrl as
|
|
12
|
-
SESSION_HEADER as SESSION_HEADER2,
|
|
34
|
+
mediaCaptionPath as mediaCaptionPath2,
|
|
35
|
+
mediaPath as mediaPath3,
|
|
36
|
+
mediaUrl as mediaUrl4,
|
|
13
37
|
visibilityFromKey
|
|
14
38
|
} from "@devultur/core";
|
|
15
39
|
import {
|
|
16
40
|
UploadHttpError,
|
|
17
41
|
uploadFileWithProgress
|
|
18
42
|
} from "@devultur/react";
|
|
19
|
-
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";
|
|
20
60
|
|
|
21
61
|
// src/image-recovery.ts
|
|
22
62
|
async function isSessionMismatch(response) {
|
|
@@ -28,89 +68,21 @@ async function isSessionMismatch(response) {
|
|
|
28
68
|
}
|
|
29
69
|
}
|
|
30
70
|
|
|
31
|
-
// src/media-fetch.ts
|
|
32
|
-
import { SESSION_HEADER } from "@devultur/core";
|
|
33
|
-
|
|
34
|
-
// src/preview-type.ts
|
|
35
|
-
var PREVIEW_KIND_BY_TYPE = /* @__PURE__ */ new Map([
|
|
36
|
-
["image/png", "image"],
|
|
37
|
-
["image/jpeg", "image"],
|
|
38
|
-
["image/webp", "image"],
|
|
39
|
-
["image/gif", "image"],
|
|
40
|
-
["image/avif", "image"],
|
|
41
|
-
["application/pdf", "pdf"],
|
|
42
|
-
["audio/mpeg", "audio"],
|
|
43
|
-
["audio/mp4", "audio"],
|
|
44
|
-
["audio/aac", "audio"],
|
|
45
|
-
["audio/ogg", "audio"],
|
|
46
|
-
["audio/wav", "audio"],
|
|
47
|
-
["audio/webm", "audio"],
|
|
48
|
-
["audio/flac", "audio"],
|
|
49
|
-
["text/plain", "text"],
|
|
50
|
-
["text/csv", "text"],
|
|
51
|
-
["application/json", "text"]
|
|
52
|
-
]);
|
|
53
|
-
var TYPE_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
54
|
-
["png", "image/png"],
|
|
55
|
-
["jpg", "image/jpeg"],
|
|
56
|
-
["jpeg", "image/jpeg"],
|
|
57
|
-
["webp", "image/webp"],
|
|
58
|
-
["gif", "image/gif"],
|
|
59
|
-
["avif", "image/avif"],
|
|
60
|
-
["pdf", "application/pdf"],
|
|
61
|
-
["mp3", "audio/mpeg"],
|
|
62
|
-
["m4a", "audio/mp4"],
|
|
63
|
-
["aac", "audio/aac"],
|
|
64
|
-
["ogg", "audio/ogg"],
|
|
65
|
-
["oga", "audio/ogg"],
|
|
66
|
-
["opus", "audio/ogg"],
|
|
67
|
-
["wav", "audio/wav"],
|
|
68
|
-
["weba", "audio/webm"],
|
|
69
|
-
["flac", "audio/flac"],
|
|
70
|
-
["txt", "text/plain"],
|
|
71
|
-
["csv", "text/csv"],
|
|
72
|
-
["json", "application/json"]
|
|
73
|
-
]);
|
|
74
|
-
var OPAQUE_CONTENT_TYPE = "application/octet-stream";
|
|
75
|
-
function normalizeContentType(value) {
|
|
76
|
-
const essence = value?.split(";")[0]?.trim().toLowerCase();
|
|
77
|
-
return essence || null;
|
|
78
|
-
}
|
|
79
|
-
function previewKindOf(contentType) {
|
|
80
|
-
const normalized = normalizeContentType(contentType);
|
|
81
|
-
if (!normalized) return null;
|
|
82
|
-
return PREVIEW_KIND_BY_TYPE.get(normalized) ?? null;
|
|
83
|
-
}
|
|
84
|
-
function expectedContentType(media, declaredType) {
|
|
85
|
-
if (declaredType) return normalizeContentType(declaredType);
|
|
86
|
-
const extension = media.filename?.split(".").pop()?.toLowerCase();
|
|
87
|
-
if (!extension || extension === media.filename?.toLowerCase()) return null;
|
|
88
|
-
return TYPE_BY_EXTENSION.get(extension) ?? null;
|
|
89
|
-
}
|
|
90
|
-
function trustedContentType(expectedType, responseType) {
|
|
91
|
-
const receivedKind = previewKindOf(responseType);
|
|
92
|
-
if (!expectedType) {
|
|
93
|
-
const contentType = receivedKind ? normalizeContentType(responseType) : null;
|
|
94
|
-
return { ok: true, contentType: contentType ?? OPAQUE_CONTENT_TYPE };
|
|
95
|
-
}
|
|
96
|
-
const expectedKind = previewKindOf(expectedType);
|
|
97
|
-
if (!expectedKind) return { ok: false, reason: "unsupported_type" };
|
|
98
|
-
if (receivedKind !== expectedKind) return { ok: false, reason: "type_mismatch" };
|
|
99
|
-
return { ok: true, contentType: normalizeContentType(expectedType) ?? OPAQUE_CONTENT_TYPE };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
71
|
// src/media-fetch.ts
|
|
103
72
|
var MediaFetchError = class extends Error {
|
|
104
73
|
code;
|
|
105
74
|
/** HTTP status, for `http_error`. */
|
|
106
75
|
status;
|
|
107
|
-
constructor(code, message, status) {
|
|
108
|
-
super(message);
|
|
76
|
+
constructor(code, message, status, options) {
|
|
77
|
+
super(message, options);
|
|
109
78
|
this.name = "MediaFetchError";
|
|
110
79
|
this.code = code;
|
|
111
80
|
this.status = status;
|
|
112
81
|
}
|
|
113
82
|
};
|
|
83
|
+
function tokenUnavailableError(cause) {
|
|
84
|
+
return new MediaFetchError("token_unavailable", "No viewer token: its issuance failed", void 0, { cause });
|
|
85
|
+
}
|
|
114
86
|
async function fetchMedia(request) {
|
|
115
87
|
const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
|
|
116
88
|
const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
|
|
@@ -171,47 +143,15 @@ async function readCapped(body, maxBytes) {
|
|
|
171
143
|
return bytes;
|
|
172
144
|
}
|
|
173
145
|
|
|
174
|
-
// src/media-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
retriable: failure.retriable,
|
|
184
|
-
message: failure.message,
|
|
185
|
-
retry
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
if (phase === "idle" || !media) return { phase: "idle" };
|
|
189
|
-
if (media.kind !== "video" || !processRequested) return { phase: "ready", media };
|
|
190
|
-
switch (videoState.phase) {
|
|
191
|
-
case "processing":
|
|
192
|
-
return { phase: "processing", media, step: videoState.step, percent: videoState.percent };
|
|
193
|
-
case "ready":
|
|
194
|
-
return {
|
|
195
|
-
phase: "ready",
|
|
196
|
-
media,
|
|
197
|
-
playlist: videoState.playlist,
|
|
198
|
-
duration: videoState.duration,
|
|
199
|
-
captions: videoState.captions,
|
|
200
|
-
playerProps: videoState.playerProps
|
|
201
|
-
};
|
|
202
|
-
case "failed":
|
|
203
|
-
return {
|
|
204
|
-
phase: "failed",
|
|
205
|
-
stage: "transcode",
|
|
206
|
-
reason: videoState.reason,
|
|
207
|
-
retriable: videoState.retriable,
|
|
208
|
-
message: videoState.message,
|
|
209
|
-
retry
|
|
210
|
-
};
|
|
211
|
-
default:
|
|
212
|
-
return { phase: "queued", media };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
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";
|
|
215
155
|
|
|
216
156
|
// src/viewer-token.ts
|
|
217
157
|
import React from "react";
|
|
@@ -221,10 +161,15 @@ var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
|
221
161
|
var MIN_REFRESH_MS = 6e4;
|
|
222
162
|
var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
223
163
|
function extractTokenResult(result) {
|
|
224
|
-
const tokenValue = typeof result === "string" ? result : result
|
|
225
|
-
|
|
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;
|
|
226
170
|
return { tokenValue, ttlMs };
|
|
227
171
|
}
|
|
172
|
+
var asError = (error) => error instanceof Error ? error : new Error(String(error));
|
|
228
173
|
function sortKeys(json) {
|
|
229
174
|
if (Array.isArray(json)) return json.map(sortKeys);
|
|
230
175
|
if (json === null || typeof json !== "object") return json;
|
|
@@ -239,7 +184,9 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
239
184
|
const argsKey = tokenArgsKey(tokenArgs);
|
|
240
185
|
const stableArgs = React.useMemo(() => tokenArgs ?? {}, [argsKey]);
|
|
241
186
|
const [issued, setIssued] = React.useState(null);
|
|
187
|
+
const [attempt, setAttempt] = React.useState(null);
|
|
242
188
|
const token = issued?.argsKey === argsKey ? issued.token : null;
|
|
189
|
+
const currentAttempt = attempt?.argsKey === argsKey ? attempt : null;
|
|
243
190
|
const currentArgsKeyRef = React.useRef(argsKey);
|
|
244
191
|
const refreshTimerRef = React.useRef(null);
|
|
245
192
|
const inFlightRef = React.useRef(null);
|
|
@@ -251,21 +198,27 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
251
198
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
252
199
|
};
|
|
253
200
|
}, []);
|
|
254
|
-
const request = React.useCallback(() => {
|
|
255
|
-
const renew = () =>
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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;
|
|
264
213
|
}, [issueViewerToken, stableArgs, argsKey, sessionSecret]);
|
|
265
214
|
const issueOnce = React.useCallback(() => {
|
|
266
215
|
const inFlight = inFlightRef.current;
|
|
267
216
|
if (inFlight?.argsKey === argsKey) return inFlight.promise;
|
|
268
|
-
|
|
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
|
+
});
|
|
269
222
|
const settle = () => {
|
|
270
223
|
if (inFlightRef.current?.promise === promise) inFlightRef.current = null;
|
|
271
224
|
};
|
|
@@ -273,16 +226,120 @@ function useViewerToken({ issueViewerToken, sessionSecret, tokenArgs }) {
|
|
|
273
226
|
inFlightRef.current = { argsKey, promise };
|
|
274
227
|
return promise;
|
|
275
228
|
}, [request, argsKey]);
|
|
229
|
+
const hasAttempt = currentAttempt !== null;
|
|
276
230
|
React.useEffect(() => {
|
|
277
|
-
if (token) return;
|
|
278
|
-
issueOnce()
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
+
|
|
299
|
+
// src/media-upload-derive.ts
|
|
300
|
+
function deriveUploadState(local, videoState, retry) {
|
|
301
|
+
const { phase, percent, file, failure, media, processRequested } = local;
|
|
302
|
+
if (phase === "uploading") return { phase: "uploading", percent, file };
|
|
303
|
+
if (phase === "upload-failed" && failure) {
|
|
304
|
+
return {
|
|
305
|
+
phase: "failed",
|
|
306
|
+
stage: "upload",
|
|
307
|
+
reason: failure.reason,
|
|
308
|
+
retriable: failure.retriable,
|
|
309
|
+
message: failure.message,
|
|
310
|
+
retry
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (phase === "idle" || !media) return { phase: "idle" };
|
|
314
|
+
if (media.kind !== "video" || !processRequested) return { phase: "ready", media };
|
|
315
|
+
switch (videoState.phase) {
|
|
316
|
+
case "processing":
|
|
317
|
+
return { phase: "processing", media, step: videoState.step, percent: videoState.percent };
|
|
318
|
+
case "ready":
|
|
319
|
+
return {
|
|
320
|
+
phase: "ready",
|
|
321
|
+
media,
|
|
322
|
+
playlist: videoState.playlist,
|
|
323
|
+
duration: videoState.duration,
|
|
324
|
+
captions: videoState.captions,
|
|
325
|
+
playerProps: videoState.playerProps
|
|
326
|
+
};
|
|
327
|
+
case "failed":
|
|
328
|
+
return {
|
|
329
|
+
phase: "failed",
|
|
330
|
+
stage: "transcode",
|
|
331
|
+
reason: videoState.reason,
|
|
332
|
+
retriable: videoState.retriable,
|
|
333
|
+
message: videoState.message,
|
|
334
|
+
retry
|
|
335
|
+
};
|
|
336
|
+
default:
|
|
337
|
+
return { phase: "queued", media };
|
|
338
|
+
}
|
|
282
339
|
}
|
|
283
340
|
|
|
284
341
|
// src/crop-upload.tsx
|
|
285
|
-
import
|
|
342
|
+
import React3 from "react";
|
|
286
343
|
import { ImageCropper } from "@devultur/react";
|
|
287
344
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
288
345
|
var DEFAULT_LABELS = {
|
|
@@ -306,18 +363,18 @@ function CropUpload({
|
|
|
306
363
|
children
|
|
307
364
|
}) {
|
|
308
365
|
const text = { ...DEFAULT_LABELS, ...labels };
|
|
309
|
-
const [pendingFile, setPendingFile] =
|
|
310
|
-
const inputRef =
|
|
366
|
+
const [pendingFile, setPendingFile] = React3.useState(null);
|
|
367
|
+
const inputRef = React3.useRef(null);
|
|
311
368
|
const { upload, state, cancel, reset } = useMediaUpload({ visibility, replaces, onComplete });
|
|
312
|
-
const pickFile =
|
|
313
|
-
const handleCrop =
|
|
369
|
+
const pickFile = React3.useCallback(() => inputRef.current?.click(), []);
|
|
370
|
+
const handleCrop = React3.useCallback(
|
|
314
371
|
(baked) => {
|
|
315
372
|
setPendingFile(null);
|
|
316
373
|
upload(baked);
|
|
317
374
|
},
|
|
318
375
|
[upload]
|
|
319
376
|
);
|
|
320
|
-
const resetAll =
|
|
377
|
+
const resetAll = React3.useCallback(() => {
|
|
321
378
|
setPendingFile(null);
|
|
322
379
|
reset();
|
|
323
380
|
}, [reset]);
|
|
@@ -369,104 +426,346 @@ function CropUpload({
|
|
|
369
426
|
] });
|
|
370
427
|
}
|
|
371
428
|
|
|
372
|
-
// src/
|
|
373
|
-
import
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (controller.signal.aborted) return;
|
|
406
|
-
value = current.decode(loaded.bytes, loaded.contentType);
|
|
407
|
-
setSettled({ requestKey, value, contentType: loaded.contentType, isLoading: false, error: null });
|
|
408
|
-
}).catch((error) => {
|
|
409
|
-
if (controller.signal.aborted) return;
|
|
410
|
-
const failure = error instanceof Error ? error : new Error(String(error));
|
|
411
|
-
setSettled({ requestKey, value: null, contentType: null, isLoading: false, error: failure });
|
|
412
|
-
});
|
|
413
|
-
return () => {
|
|
414
|
-
controller.abort();
|
|
415
|
-
if (value !== null) current.release?.(value);
|
|
416
|
-
};
|
|
417
|
-
}, [requestKey, isReady]);
|
|
418
|
-
if (!requestKey) return IDLE;
|
|
419
|
-
if (!devultur.baseUrl) throw new Error("Devultur media previews require `baseUrl` on <DevulturProvider>");
|
|
420
|
-
if (settled?.requestKey !== requestKey) return LOADING;
|
|
421
|
-
return { value: settled.value, contentType: settled.contentType, isLoading: false, error: settled.error };
|
|
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
|
+
);
|
|
422
462
|
}
|
|
423
463
|
|
|
424
|
-
// src/
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
function useMediaObjectUrl(media, options) {
|
|
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 }) {
|
|
428
467
|
const { url } = useDevultur();
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
maxBytes: options?.maxBytes,
|
|
434
|
-
contentType: options?.contentType,
|
|
435
|
-
decode: createObjectUrl,
|
|
436
|
-
release: revokeObjectUrl
|
|
437
|
-
});
|
|
438
|
-
if (media && isEnabled && isUnverifiedPublic) {
|
|
439
|
-
return { objectUrl: url(media), contentType: null, isLoading: false, error: null };
|
|
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 });
|
|
440
472
|
}
|
|
441
|
-
return {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
+
] });
|
|
447
500
|
}
|
|
448
501
|
|
|
449
|
-
// src/
|
|
450
|
-
import
|
|
502
|
+
// src/image.tsx
|
|
503
|
+
import React5 from "react";
|
|
504
|
+
import { mediaUrl as mediaUrl2 } from "@devultur/core";
|
|
451
505
|
|
|
452
|
-
// src/
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
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
|
+
|
|
688
|
+
// src/media-resource.ts
|
|
689
|
+
import React6 from "react";
|
|
690
|
+
import { mediaPath, mediaUrl as mediaUrl3 } from "@devultur/core";
|
|
691
|
+
var IDLE = { value: null, contentType: null, isLoading: false, error: null };
|
|
692
|
+
var LOADING = { value: null, contentType: null, isLoading: true, error: null };
|
|
693
|
+
function useMediaResource(media, options) {
|
|
694
|
+
const devultur = useDevultur();
|
|
695
|
+
const { isEnabled, maxBytes, contentType } = options;
|
|
696
|
+
const isPrivate = media?.visibility === "private";
|
|
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
|
+
);
|
|
703
|
+
const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
|
|
704
|
+
const [settled, setSettled] = React6.useState(null);
|
|
705
|
+
const inputsRef = React6.useRef({ media, devultur, options });
|
|
706
|
+
React6.useEffect(() => {
|
|
707
|
+
inputsRef.current = { media, devultur, options };
|
|
708
|
+
});
|
|
709
|
+
React6.useEffect(() => {
|
|
710
|
+
if (!requestKey || !isReady) return;
|
|
711
|
+
const { media: target, devultur: context, options: current } = inputsRef.current;
|
|
712
|
+
if (!target || !context.baseUrl) return;
|
|
713
|
+
const baseUrl = context.baseUrl;
|
|
714
|
+
const controller = new AbortController();
|
|
715
|
+
let value = null;
|
|
716
|
+
loadMedia({
|
|
717
|
+
buildUrl: (viewerToken) => mediaUrl3(baseUrl, target, { token: viewerToken }),
|
|
718
|
+
token: context.token,
|
|
719
|
+
sessionSecret: context.sessionSecret,
|
|
720
|
+
reissueToken: context.reissueToken,
|
|
721
|
+
isPrivate: target.visibility === "private",
|
|
722
|
+
signal: controller.signal,
|
|
723
|
+
maxBytes: current.maxBytes,
|
|
724
|
+
contentType: current.contentType
|
|
725
|
+
}).then((loaded) => {
|
|
726
|
+
if (controller.signal.aborted) return;
|
|
727
|
+
value = current.decode(loaded.bytes, loaded.contentType);
|
|
728
|
+
setSettled({ requestKey, value, contentType: loaded.contentType, isLoading: false, error: null });
|
|
729
|
+
}).catch((error) => {
|
|
730
|
+
if (controller.signal.aborted) return;
|
|
731
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
732
|
+
setSettled({ requestKey, value: null, contentType: null, isLoading: false, error: failure });
|
|
733
|
+
});
|
|
734
|
+
return () => {
|
|
735
|
+
controller.abort();
|
|
736
|
+
if (value !== null) current.release?.(value);
|
|
737
|
+
};
|
|
738
|
+
}, [requestKey, isReady]);
|
|
739
|
+
if (!requestKey) return IDLE;
|
|
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 };
|
|
742
|
+
if (settled?.requestKey !== requestKey) return LOADING;
|
|
743
|
+
return { value: settled.value, contentType: settled.contentType, isLoading: false, error: settled.error };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/pdf-context.tsx
|
|
747
|
+
import React7 from "react";
|
|
748
|
+
|
|
749
|
+
// src/preview-labels.ts
|
|
750
|
+
var ERROR_TEXT = {
|
|
751
|
+
http_error: "The file couldn't be loaded.",
|
|
752
|
+
too_large: "The file is too large to preview.",
|
|
753
|
+
unsupported_type: "This file type can't be previewed.",
|
|
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."
|
|
757
|
+
};
|
|
758
|
+
var defaultPreviewLabels = {
|
|
759
|
+
document: (title, pageCount) => `${title}, ${pageCount === 1 ? "1 page" : `${pageCount} pages`}`,
|
|
760
|
+
page: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
761
|
+
loading: "Loading\u2026",
|
|
762
|
+
pageCap: (shown, total) => `Showing the first ${shown} of ${total} pages.`,
|
|
763
|
+
error: (code) => ERROR_TEXT[code],
|
|
764
|
+
thumbnail: (title) => title,
|
|
765
|
+
untitledPdf: "PDF",
|
|
766
|
+
untitledMedia: "Preview",
|
|
767
|
+
download: "Download"
|
|
768
|
+
};
|
|
470
769
|
function mergePreviewLabels(...sources) {
|
|
471
770
|
const merged = { ...defaultPreviewLabels };
|
|
472
771
|
for (const source of sources) {
|
|
@@ -478,16 +777,16 @@ function mergePreviewLabels(...sources) {
|
|
|
478
777
|
}
|
|
479
778
|
|
|
480
779
|
// src/pdf-context.tsx
|
|
481
|
-
import { jsx as
|
|
482
|
-
var PdfPreviewContext =
|
|
780
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
781
|
+
var PdfPreviewContext = React7.createContext({});
|
|
483
782
|
function PdfPreviewProvider({
|
|
484
783
|
pdfjs,
|
|
485
784
|
workerSrc,
|
|
486
785
|
labels,
|
|
487
786
|
children
|
|
488
787
|
}) {
|
|
489
|
-
const config =
|
|
490
|
-
return /* @__PURE__ */
|
|
788
|
+
const config = React7.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
|
|
789
|
+
return /* @__PURE__ */ jsx5(PdfPreviewContext.Provider, { value: config, children });
|
|
491
790
|
}
|
|
492
791
|
var loadedModules = /* @__PURE__ */ new WeakMap();
|
|
493
792
|
function loadOnce(loader) {
|
|
@@ -498,9 +797,9 @@ function loadOnce(loader) {
|
|
|
498
797
|
return loading;
|
|
499
798
|
}
|
|
500
799
|
function usePdfjs(workerSrcOverride) {
|
|
501
|
-
const { pdfjs: loader, workerSrc } =
|
|
502
|
-
const [loaded, setLoaded] =
|
|
503
|
-
|
|
800
|
+
const { pdfjs: loader, workerSrc } = React7.useContext(PdfPreviewContext);
|
|
801
|
+
const [loaded, setLoaded] = React7.useState(null);
|
|
802
|
+
React7.useEffect(() => {
|
|
504
803
|
if (!loader) return;
|
|
505
804
|
let isCurrent = true;
|
|
506
805
|
loadOnce(loader).then((pdfjs) => {
|
|
@@ -515,61 +814,27 @@ function usePdfjs(workerSrcOverride) {
|
|
|
515
814
|
if (!loaded.pdfjs) return { status: "unavailable" };
|
|
516
815
|
return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
|
|
517
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
|
+
}
|
|
518
828
|
function usePreviewLabels(labels) {
|
|
519
|
-
const providerLabels =
|
|
829
|
+
const providerLabels = React7.useContext(PdfPreviewContext).labels;
|
|
520
830
|
return mergePreviewLabels(providerLabels, labels);
|
|
521
831
|
}
|
|
522
832
|
|
|
523
|
-
// src/pdf-thumbnail.tsx
|
|
524
|
-
import React7 from "react";
|
|
525
|
-
|
|
526
|
-
// src/pdf-hooks.ts
|
|
527
|
-
import React5 from "react";
|
|
528
|
-
|
|
529
|
-
// src/pdf-render.ts
|
|
530
|
-
var MOBILE_MAX_CANVAS_PIXELS = 5242880;
|
|
531
|
-
var DESKTOP_MAX_CANVAS_PIXELS = 2 ** 25;
|
|
532
|
-
function openPdf(pdfjs, bytes, workerSrc) {
|
|
533
|
-
if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
534
|
-
const source = { data: bytes.slice(), enableScripting: false, isEvalSupported: false };
|
|
535
|
-
return pdfjs.getDocument(source);
|
|
536
|
-
}
|
|
537
|
-
function isMobileDevice() {
|
|
538
|
-
if (typeof navigator === "undefined") return false;
|
|
539
|
-
return /Android|iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;
|
|
540
|
-
}
|
|
541
|
-
function maxCanvasPixels() {
|
|
542
|
-
return isMobileDevice() ? MOBILE_MAX_CANVAS_PIXELS : DESKTOP_MAX_CANVAS_PIXELS;
|
|
543
|
-
}
|
|
544
|
-
function outputScale(cssWidth, cssHeight, maxPixels) {
|
|
545
|
-
const devicePixelRatio = typeof window !== "undefined" && window.devicePixelRatio || 1;
|
|
546
|
-
const cssPixels = cssWidth * cssHeight;
|
|
547
|
-
if (cssPixels <= 0) return devicePixelRatio;
|
|
548
|
-
if (cssPixels * devicePixelRatio ** 2 <= maxPixels) return devicePixelRatio;
|
|
549
|
-
return Math.sqrt(maxPixels / cssPixels);
|
|
550
|
-
}
|
|
551
|
-
function renderPage(page, canvas, cssWidth) {
|
|
552
|
-
const naturalViewport = page.getViewport({ scale: 1 });
|
|
553
|
-
const cssScale = cssWidth > 0 ? cssWidth / naturalViewport.width : 1;
|
|
554
|
-
const cssViewport = page.getViewport({ scale: cssScale });
|
|
555
|
-
const scale = outputScale(cssViewport.width, cssViewport.height, maxCanvasPixels());
|
|
556
|
-
const viewport = page.getViewport({ scale: cssScale * scale });
|
|
557
|
-
canvas.width = Math.floor(viewport.width);
|
|
558
|
-
canvas.height = Math.floor(viewport.height);
|
|
559
|
-
canvas.style.width = `${Math.floor(cssViewport.width)}px`;
|
|
560
|
-
canvas.style.height = `${Math.floor(cssViewport.height)}px`;
|
|
561
|
-
return page.render({ canvas, viewport });
|
|
562
|
-
}
|
|
563
|
-
async function pageText(page) {
|
|
564
|
-
const { items } = await page.getTextContent();
|
|
565
|
-
return items.map((item) => "str" in item && typeof item.str === "string" ? item.str : "").join(" ").replace(/\s+/g, " ").trim();
|
|
566
|
-
}
|
|
567
|
-
var isRenderCancelled = (error) => error instanceof Error && error.name === "RenderingCancelledException";
|
|
568
|
-
|
|
569
833
|
// src/pdf-hooks.ts
|
|
834
|
+
import React8 from "react";
|
|
570
835
|
function useIsVisible(ref, rootMargin = "200px") {
|
|
571
|
-
const [isVisible, setIsVisible] =
|
|
572
|
-
|
|
836
|
+
const [isVisible, setIsVisible] = React8.useState(false);
|
|
837
|
+
React8.useEffect(() => {
|
|
573
838
|
const element = ref.current;
|
|
574
839
|
if (isVisible || !element) return;
|
|
575
840
|
if (typeof IntersectionObserver === "undefined") {
|
|
@@ -590,12 +855,12 @@ function useIsVisible(ref, rootMargin = "200px") {
|
|
|
590
855
|
return isVisible;
|
|
591
856
|
}
|
|
592
857
|
function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
593
|
-
const [state, setState] =
|
|
858
|
+
const [state, setState] = React8.useState({
|
|
594
859
|
bytes: null,
|
|
595
860
|
document: null,
|
|
596
861
|
error: null
|
|
597
862
|
});
|
|
598
|
-
|
|
863
|
+
React8.useEffect(() => {
|
|
599
864
|
let isCurrent = true;
|
|
600
865
|
const loadingTask = openPdf(pdfjs, bytes, workerSrc);
|
|
601
866
|
loadingTask.promise.then((document2) => {
|
|
@@ -614,8 +879,8 @@ function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
|
614
879
|
}
|
|
615
880
|
function usePageRender(options) {
|
|
616
881
|
const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
|
|
617
|
-
const [state, setState] =
|
|
618
|
-
|
|
882
|
+
const [state, setState] = React8.useState({ isRendered: false, text: "" });
|
|
883
|
+
React8.useEffect(() => {
|
|
619
884
|
const canvas = canvasRef.current;
|
|
620
885
|
if (!document2 || !isEnabled || !canvas) return;
|
|
621
886
|
let isCurrent = true;
|
|
@@ -645,8 +910,8 @@ function usePageRender(options) {
|
|
|
645
910
|
}
|
|
646
911
|
|
|
647
912
|
// src/pdf-preview.tsx
|
|
648
|
-
import
|
|
649
|
-
import { Fragment, jsx as
|
|
913
|
+
import React9 from "react";
|
|
914
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
650
915
|
var VISUALLY_HIDDEN = {
|
|
651
916
|
position: "absolute",
|
|
652
917
|
width: 1,
|
|
@@ -669,11 +934,11 @@ function PdfPreview(props) {
|
|
|
669
934
|
contentType: expectedType,
|
|
670
935
|
decode: keepBytes
|
|
671
936
|
});
|
|
672
|
-
if (error) return /* @__PURE__ */
|
|
673
|
-
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 });
|
|
674
939
|
const label = title ?? media.filename ?? labels.untitledPdf;
|
|
675
|
-
if (pdfjsState.status === "unavailable") return /* @__PURE__ */
|
|
676
|
-
return /* @__PURE__ */
|
|
940
|
+
if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx6(PdfFrame, { bytes, label, className });
|
|
941
|
+
return /* @__PURE__ */ jsx6(
|
|
677
942
|
PdfCanvasDocument,
|
|
678
943
|
{
|
|
679
944
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -691,17 +956,17 @@ function PdfPreview(props) {
|
|
|
691
956
|
function PdfCanvasDocument(props) {
|
|
692
957
|
const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
|
|
693
958
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
694
|
-
const containerRef =
|
|
695
|
-
const [width, setWidth] =
|
|
696
|
-
|
|
959
|
+
const containerRef = React9.useRef(null);
|
|
960
|
+
const [width, setWidth] = React9.useState(null);
|
|
961
|
+
React9.useLayoutEffect(() => {
|
|
697
962
|
if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
|
|
698
963
|
}, [document2]);
|
|
699
|
-
if (error) return /* @__PURE__ */
|
|
700
|
-
if (!document2) return /* @__PURE__ */
|
|
964
|
+
if (error) return /* @__PURE__ */ jsx6(Fragment2, { children: renderFallback(error) });
|
|
965
|
+
if (!document2) return /* @__PURE__ */ jsx6(Fragment2, { children: loading });
|
|
701
966
|
const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
|
|
702
967
|
const isTruncated = shownPageCount < document2.numPages;
|
|
703
968
|
const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
|
|
704
|
-
return /* @__PURE__ */
|
|
969
|
+
return /* @__PURE__ */ jsxs3(
|
|
705
970
|
"div",
|
|
706
971
|
{
|
|
707
972
|
ref: containerRef,
|
|
@@ -709,7 +974,7 @@ function PdfCanvasDocument(props) {
|
|
|
709
974
|
"aria-label": labels.document(label, document2.numPages),
|
|
710
975
|
className,
|
|
711
976
|
children: [
|
|
712
|
-
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */
|
|
977
|
+
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx6(
|
|
713
978
|
PdfPageView,
|
|
714
979
|
{
|
|
715
980
|
document: document2,
|
|
@@ -719,7 +984,7 @@ function PdfCanvasDocument(props) {
|
|
|
719
984
|
},
|
|
720
985
|
pageNumber
|
|
721
986
|
)),
|
|
722
|
-
isTruncated && /* @__PURE__ */
|
|
987
|
+
isTruncated && /* @__PURE__ */ jsxs3("p", { children: [
|
|
723
988
|
labels.pageCap(shownPageCount, document2.numPages),
|
|
724
989
|
" ",
|
|
725
990
|
renderFallback(null)
|
|
@@ -729,8 +994,8 @@ function PdfCanvasDocument(props) {
|
|
|
729
994
|
);
|
|
730
995
|
}
|
|
731
996
|
function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
732
|
-
const pageRef =
|
|
733
|
-
const canvasRef =
|
|
997
|
+
const pageRef = React9.useRef(null);
|
|
998
|
+
const canvasRef = React9.useRef(null);
|
|
734
999
|
const isVisible = useIsVisible(pageRef);
|
|
735
1000
|
const { isRendered, text } = usePageRender({
|
|
736
1001
|
document: document2,
|
|
@@ -741,46 +1006,46 @@ function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
|
741
1006
|
shouldExtractText: true
|
|
742
1007
|
});
|
|
743
1008
|
const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
|
|
744
|
-
return /* @__PURE__ */
|
|
745
|
-
/* @__PURE__ */
|
|
746
|
-
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 })
|
|
747
1012
|
] });
|
|
748
1013
|
}
|
|
749
1014
|
function PdfFrame({ bytes, label, className }) {
|
|
750
|
-
const [objectUrl, setObjectUrl] =
|
|
751
|
-
|
|
1015
|
+
const [objectUrl, setObjectUrl] = React9.useState(null);
|
|
1016
|
+
React9.useEffect(() => {
|
|
752
1017
|
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
753
1018
|
setObjectUrl(url);
|
|
754
1019
|
return () => URL.revokeObjectURL(url);
|
|
755
1020
|
}, [bytes]);
|
|
756
1021
|
if (!objectUrl) return null;
|
|
757
|
-
return /* @__PURE__ */
|
|
1022
|
+
return /* @__PURE__ */ jsx6("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
|
|
758
1023
|
}
|
|
759
1024
|
|
|
760
1025
|
// src/pdf-thumbnail.tsx
|
|
761
|
-
import { Fragment as
|
|
1026
|
+
import { Fragment as Fragment3, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
762
1027
|
var DEFAULT_THUMBNAIL_WIDTH = 96;
|
|
763
1028
|
var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
|
|
764
1029
|
function PdfThumbnail(props) {
|
|
765
1030
|
const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
|
|
766
|
-
const containerRef =
|
|
1031
|
+
const containerRef = React10.useRef(null);
|
|
767
1032
|
const isVisible = useIsVisible(containerRef);
|
|
768
1033
|
const pdfjsState = usePdfjs(props.pdfWorkerSrc);
|
|
769
1034
|
const labels = usePreviewLabels(props.labels);
|
|
770
1035
|
const expectedType = media ? expectedContentType(media, contentType) : null;
|
|
771
|
-
const
|
|
1036
|
+
const isPdf2 = previewKindOf(expectedType) === "pdf";
|
|
772
1037
|
const { value: bytes, error } = useMediaResource(media, {
|
|
773
|
-
isEnabled: isVisible &&
|
|
1038
|
+
isEnabled: isVisible && isPdf2 && pdfjsState.status === "ready",
|
|
774
1039
|
maxBytes: props.maxBytes ?? DEFAULT_THUMBNAIL_MAX_BYTES,
|
|
775
1040
|
contentType: expectedType ?? void 0,
|
|
776
1041
|
decode: keepBytes
|
|
777
1042
|
});
|
|
778
1043
|
if (!media) return null;
|
|
779
|
-
const isUnavailable = !
|
|
1044
|
+
const isUnavailable = !isPdf2 || pdfjsState.status === "unavailable" || error !== null;
|
|
780
1045
|
const content = () => {
|
|
781
1046
|
if (isUnavailable) return fallback;
|
|
782
1047
|
if (!bytes || pdfjsState.status !== "ready") return loading;
|
|
783
|
-
return /* @__PURE__ */
|
|
1048
|
+
return /* @__PURE__ */ jsx7(
|
|
784
1049
|
ThumbnailCanvas,
|
|
785
1050
|
{
|
|
786
1051
|
pdfjs: pdfjsState.pdfjs,
|
|
@@ -793,54 +1058,612 @@ function PdfThumbnail(props) {
|
|
|
793
1058
|
}
|
|
794
1059
|
);
|
|
795
1060
|
};
|
|
796
|
-
return /* @__PURE__ */
|
|
1061
|
+
return /* @__PURE__ */ jsx7("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
|
|
797
1062
|
}
|
|
798
1063
|
function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
|
|
799
|
-
const canvasRef =
|
|
1064
|
+
const canvasRef = React10.useRef(null);
|
|
800
1065
|
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
801
1066
|
const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
|
|
802
|
-
if (error) return /* @__PURE__ */
|
|
803
|
-
return /* @__PURE__ */
|
|
1067
|
+
if (error) return /* @__PURE__ */ jsx7(Fragment3, { children: fallback });
|
|
1068
|
+
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
804
1069
|
!isRendered && loading,
|
|
805
|
-
/* @__PURE__ */
|
|
1070
|
+
/* @__PURE__ */ jsx7("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
|
|
1071
|
+
] });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
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;
|
|
1082
|
+
}
|
|
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";
|
|
1092
|
+
}
|
|
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) })
|
|
806
1121
|
] });
|
|
807
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
|
+
] });
|
|
1375
|
+
}
|
|
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";
|
|
808
1585
|
|
|
809
1586
|
// src/preview.tsx
|
|
1587
|
+
import { mediaPath as mediaPath2 } from "@devultur/core";
|
|
1588
|
+
|
|
1589
|
+
// src/video-preview.tsx
|
|
1590
|
+
import React17 from "react";
|
|
810
1591
|
import { VideoPlayer } from "@devultur/react";
|
|
811
|
-
import { Fragment as
|
|
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();
|
|
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
|
+
);
|
|
1620
|
+
if (!videoProps) return null;
|
|
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";
|
|
812
1630
|
var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
|
|
813
1631
|
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
814
1632
|
function Preview({ media, contentType, fallback, loading, labels, ...viewProps }) {
|
|
815
1633
|
const text = usePreviewLabels(labels);
|
|
816
1634
|
if (!media) return null;
|
|
817
|
-
if (media.kind === "video") return /* @__PURE__ */ jsx5(VideoPreview, { media, className: viewProps.className });
|
|
818
1635
|
const renderFallback = (error) => {
|
|
819
1636
|
const fetchError = error instanceof MediaFetchError ? error : null;
|
|
820
1637
|
if (typeof fallback === "function") return fallback(fetchError);
|
|
821
1638
|
if (fallback !== void 0) return fallback;
|
|
822
|
-
return /* @__PURE__ */
|
|
1639
|
+
return /* @__PURE__ */ jsx12(DefaultFallback, { media, error: fetchError, labels: text });
|
|
823
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
|
+
}
|
|
824
1652
|
const expectedType = expectedContentType(media, contentType);
|
|
825
1653
|
const kind = previewKindOf(expectedType);
|
|
826
|
-
if (!expectedType || !kind) return /* @__PURE__ */
|
|
827
|
-
const loadingNode = loading === void 0 ? /* @__PURE__ */
|
|
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;
|
|
828
1656
|
const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
|
|
829
|
-
if (kind === "text") return /* @__PURE__ */
|
|
830
|
-
if (kind === "pdf") return /* @__PURE__ */
|
|
831
|
-
return /* @__PURE__ */
|
|
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 });
|
|
832
1660
|
}
|
|
833
1661
|
var unsupportedType = (contentType) => new MediaFetchError("unsupported_type", `Can't preview ${contentType ?? "an unknown type"} inline`);
|
|
834
1662
|
function DefaultFallback({ media, error, labels }) {
|
|
835
|
-
return /* @__PURE__ */
|
|
1663
|
+
return /* @__PURE__ */ jsx12(DownloadLink, { media, title: error ? labels.error(error.code) : void 0, children: media.filename ?? labels.download });
|
|
836
1664
|
}
|
|
837
1665
|
function LoadingStatus({ text }) {
|
|
838
|
-
return /* @__PURE__ */
|
|
839
|
-
}
|
|
840
|
-
function VideoPreview({ media, className }) {
|
|
841
|
-
const videoProps = useVideoProps(media);
|
|
842
|
-
if (!videoProps) return null;
|
|
843
|
-
return /* @__PURE__ */ jsx5(VideoPlayer, { ...videoProps, className });
|
|
1666
|
+
return /* @__PURE__ */ jsx12("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
|
|
844
1667
|
}
|
|
845
1668
|
function BinaryPreview({
|
|
846
1669
|
media,
|
|
@@ -857,13 +1680,13 @@ function BinaryPreview({
|
|
|
857
1680
|
const isPublicImage = kind === "image" && media.visibility === "public";
|
|
858
1681
|
const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
|
|
859
1682
|
const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
|
|
860
|
-
if (verified.error) return /* @__PURE__ */
|
|
861
|
-
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 });
|
|
862
1685
|
const label = title ?? media.filename ?? labels.untitledMedia;
|
|
863
1686
|
if (kind === "image") {
|
|
864
|
-
return /* @__PURE__ */
|
|
1687
|
+
return /* @__PURE__ */ jsx12("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
|
|
865
1688
|
}
|
|
866
|
-
return /* @__PURE__ */
|
|
1689
|
+
return /* @__PURE__ */ jsx12("audio", { src: objectUrl, controls: true, "aria-label": label, className });
|
|
867
1690
|
}
|
|
868
1691
|
var decodeText = (bytes) => new TextDecoder().decode(bytes);
|
|
869
1692
|
function TextPreview({
|
|
@@ -881,45 +1704,411 @@ function TextPreview({
|
|
|
881
1704
|
contentType: expectedType,
|
|
882
1705
|
decode: decodeText
|
|
883
1706
|
});
|
|
884
|
-
if (error) return /* @__PURE__ */
|
|
885
|
-
if (text === null) return /* @__PURE__ */
|
|
886
|
-
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 });
|
|
1710
|
+
}
|
|
1711
|
+
|
|
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
|
+
);
|
|
1760
|
+
}
|
|
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 })
|
|
1882
|
+
});
|
|
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
|
+
}
|
|
1896
|
+
},
|
|
1897
|
+
[upload, patch, drop]
|
|
1898
|
+
);
|
|
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;
|
|
1932
|
+
return () => {
|
|
1933
|
+
for (const controller of active.values()) controller.abort();
|
|
1934
|
+
};
|
|
1935
|
+
}, []);
|
|
1936
|
+
return { uploads, start, cancel, retry, dismiss: drop };
|
|
887
1937
|
}
|
|
888
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
|
+
|
|
889
2079
|
// src/react.tsx
|
|
890
|
-
import {
|
|
891
|
-
var DevulturContext =
|
|
892
|
-
var DevulturApiContext =
|
|
893
|
-
var ProgressContext =
|
|
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);
|
|
894
2084
|
function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children }) {
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
});
|
|
902
|
-
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
903
|
-
const deleteMediaAction = useAction(api.deleteMedia);
|
|
904
|
-
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
905
|
-
const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
|
|
906
|
-
const uploadUrl = React8.useCallback(
|
|
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(
|
|
907
2091
|
async (file) => {
|
|
908
2092
|
const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
|
|
909
2093
|
return { url: result.url, key: result.key };
|
|
910
2094
|
},
|
|
911
2095
|
[createUploadUrlAction]
|
|
912
2096
|
);
|
|
913
|
-
const upload =
|
|
2097
|
+
const upload = React22.useCallback(
|
|
914
2098
|
async (file, options) => {
|
|
915
2099
|
const dimensions = await readImageDimensions(file);
|
|
2100
|
+
options?.signal?.throwIfAborted();
|
|
916
2101
|
const target = await createUploadUrlAction({
|
|
917
2102
|
filename: file.name,
|
|
918
2103
|
contentType: file.type,
|
|
919
2104
|
visibility: options?.visibility
|
|
920
2105
|
});
|
|
921
2106
|
await uploadFileWithProgress(target.url, file, { onBytes: options?.onProgress, signal: options?.signal });
|
|
922
|
-
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
|
+
}
|
|
923
2112
|
return {
|
|
924
2113
|
id: target.id,
|
|
925
2114
|
kind: target.kind,
|
|
@@ -931,59 +2120,23 @@ function DevulturProvider({ api, baseUrl, tokenArgs, issueViewerToken, children
|
|
|
931
2120
|
},
|
|
932
2121
|
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
933
2122
|
);
|
|
934
|
-
const
|
|
935
|
-
(media, options) => {
|
|
936
|
-
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
937
|
-
return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
938
|
-
},
|
|
939
|
-
[baseUrl, token]
|
|
940
|
-
);
|
|
941
|
-
const getCaptionsText = React8.useCallback(
|
|
942
|
-
async (media, locale) => {
|
|
943
|
-
if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
|
|
944
|
-
const path = mediaCaptionPath(media, locale);
|
|
945
|
-
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
946
|
-
const isPrivate = media.visibility === "private";
|
|
947
|
-
const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
|
|
948
|
-
headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
|
|
949
|
-
});
|
|
950
|
-
let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
|
|
951
|
-
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
952
|
-
const fresh = await reissueToken();
|
|
953
|
-
if (fresh) response = await fetchWith(fresh);
|
|
954
|
-
}
|
|
955
|
-
if (!response.ok) throw new Error(`Captions fetch failed: ${response.status}`);
|
|
956
|
-
return response.text();
|
|
957
|
-
},
|
|
958
|
-
[baseUrl, token, reissueToken, sessionSecret]
|
|
959
|
-
);
|
|
960
|
-
const deleteMedia = React8.useCallback(
|
|
2123
|
+
const deleteMedia = React22.useCallback(
|
|
961
2124
|
(media) => {
|
|
962
2125
|
if (media.kind === "video" && api.deleteVideo) {
|
|
963
2126
|
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
964
2127
|
return;
|
|
965
2128
|
}
|
|
966
|
-
deleteMediaAction({ key:
|
|
2129
|
+
deleteMediaAction({ key: mediaPath3(media) }).catch(console.error);
|
|
967
2130
|
},
|
|
968
2131
|
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
969
2132
|
);
|
|
970
|
-
const devulturValue =
|
|
971
|
-
() => ({
|
|
972
|
-
|
|
973
|
-
baseUrl: baseUrl ?? null,
|
|
974
|
-
sessionSecret,
|
|
975
|
-
uploadUrl,
|
|
976
|
-
upload,
|
|
977
|
-
url,
|
|
978
|
-
getCaptionsText,
|
|
979
|
-
deleteMedia,
|
|
980
|
-
reissueToken
|
|
981
|
-
}),
|
|
982
|
-
[token, baseUrl, sessionSecret, uploadUrl, upload, url, getCaptionsText, deleteMedia, reissueToken]
|
|
2133
|
+
const devulturValue = React22.useMemo(
|
|
2134
|
+
() => ({ ...session, uploadUrl, upload, deleteMedia }),
|
|
2135
|
+
[session, uploadUrl, upload, deleteMedia]
|
|
983
2136
|
);
|
|
984
|
-
let content = /* @__PURE__ */
|
|
2137
|
+
let content = /* @__PURE__ */ jsx16(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx16(DevulturContext, { value: devulturValue, children }) });
|
|
985
2138
|
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
986
|
-
content = /* @__PURE__ */
|
|
2139
|
+
content = /* @__PURE__ */ jsx16(
|
|
987
2140
|
ProgressBridge,
|
|
988
2141
|
{
|
|
989
2142
|
save: api.saveProgress,
|
|
@@ -1007,13 +2160,13 @@ function ProgressBridge({
|
|
|
1007
2160
|
const saveMutation = useMutation(save);
|
|
1008
2161
|
const markCompleteMutation = useMutation(markComplete);
|
|
1009
2162
|
const resetMutation = useMutation(reset);
|
|
1010
|
-
const onSave =
|
|
2163
|
+
const onSave = React22.useCallback(
|
|
1011
2164
|
async (videoId, position) => {
|
|
1012
2165
|
await saveMutation({ videoId, position });
|
|
1013
2166
|
},
|
|
1014
2167
|
[saveMutation]
|
|
1015
2168
|
);
|
|
1016
|
-
const onLoad =
|
|
2169
|
+
const onLoad = React22.useCallback(
|
|
1017
2170
|
async (videoId) => {
|
|
1018
2171
|
const result = await convex.query(load, { videoId });
|
|
1019
2172
|
if (!result) return null;
|
|
@@ -1024,46 +2177,69 @@ function ProgressBridge({
|
|
|
1024
2177
|
},
|
|
1025
2178
|
[convex, load]
|
|
1026
2179
|
);
|
|
1027
|
-
const onMarkComplete =
|
|
2180
|
+
const onMarkComplete = React22.useCallback(
|
|
1028
2181
|
async (videoId) => {
|
|
1029
2182
|
await markCompleteMutation({ videoId });
|
|
1030
2183
|
},
|
|
1031
2184
|
[markCompleteMutation]
|
|
1032
2185
|
);
|
|
1033
|
-
const onReset =
|
|
2186
|
+
const onReset = React22.useCallback(
|
|
1034
2187
|
async (videoId) => {
|
|
1035
2188
|
await resetMutation({ videoId });
|
|
1036
2189
|
},
|
|
1037
2190
|
[resetMutation]
|
|
1038
2191
|
);
|
|
1039
|
-
const value =
|
|
2192
|
+
const value = React22.useMemo(
|
|
1040
2193
|
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
1041
2194
|
[onSave, onLoad, onMarkComplete, onReset]
|
|
1042
2195
|
);
|
|
1043
|
-
return /* @__PURE__ */
|
|
2196
|
+
return /* @__PURE__ */ jsx16(ProgressContext, { value, children });
|
|
1044
2197
|
}
|
|
1045
2198
|
function useDevultur() {
|
|
1046
|
-
const context =
|
|
1047
|
-
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>");
|
|
1048
2201
|
return context;
|
|
1049
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
|
+
}
|
|
1050
2226
|
function useDevulturProgress() {
|
|
1051
|
-
const context =
|
|
2227
|
+
const context = React22.use(ProgressContext);
|
|
1052
2228
|
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
1053
2229
|
return context;
|
|
1054
2230
|
}
|
|
1055
2231
|
function useMediaUrl(key, options) {
|
|
1056
2232
|
const { token, baseUrl } = useDevultur();
|
|
1057
|
-
return
|
|
2233
|
+
return React22.useMemo(() => {
|
|
1058
2234
|
if (!key) return null;
|
|
1059
2235
|
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
1060
2236
|
const isPublic = visibilityFromKey(key) === "public";
|
|
1061
|
-
return
|
|
2237
|
+
return buildMediaUrl2(baseUrl, key, { ...options, token: isPublic ? void 0 : token ?? void 0 });
|
|
1062
2238
|
}, [key, baseUrl, token, options]);
|
|
1063
2239
|
}
|
|
1064
|
-
async function confirmUploadBestEffort(confirmUpload, key, file,
|
|
2240
|
+
async function confirmUploadBestEffort(confirmUpload, key, file, hints) {
|
|
1065
2241
|
try {
|
|
1066
|
-
await confirmUpload({ key, filename: file.name, ...
|
|
2242
|
+
await confirmUpload({ key, filename: file.name, ...hints });
|
|
1067
2243
|
} catch (error) {
|
|
1068
2244
|
console.warn("Devultur: could not record the upload; it is recorded later", error);
|
|
1069
2245
|
}
|
|
@@ -1082,122 +2258,45 @@ async function readImageDimensions(file) {
|
|
|
1082
2258
|
function useVideoProps(media, options) {
|
|
1083
2259
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
1084
2260
|
const locales = options?.locales;
|
|
1085
|
-
return
|
|
2261
|
+
return React22.useMemo(() => {
|
|
1086
2262
|
if (media?.kind !== "video") return null;
|
|
1087
2263
|
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
1088
2264
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
1089
2265
|
const captions = (locales ?? []).flatMap((locale) => {
|
|
1090
|
-
const path =
|
|
2266
|
+
const path = mediaCaptionPath2(media, locale);
|
|
1091
2267
|
if (!path) return [];
|
|
1092
2268
|
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
1093
|
-
return [{ locale, label, src:
|
|
2269
|
+
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
1094
2270
|
});
|
|
1095
2271
|
return {
|
|
1096
|
-
src:
|
|
2272
|
+
src: buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken }),
|
|
1097
2273
|
token: viewerToken,
|
|
1098
2274
|
captions,
|
|
1099
2275
|
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
1100
2276
|
};
|
|
1101
2277
|
}, [media, baseUrl, token, locales, sessionSecret]);
|
|
1102
2278
|
}
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
height,
|
|
1107
|
-
fit,
|
|
1108
|
-
format,
|
|
1109
|
-
quality,
|
|
1110
|
-
fallback = null,
|
|
1111
|
-
externalFallback,
|
|
1112
|
-
render,
|
|
1113
|
-
alt,
|
|
1114
|
-
...imgProps
|
|
1115
|
-
}) {
|
|
1116
|
-
const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
|
|
1117
|
-
const [failed, setFailed] = React8.useState(false);
|
|
1118
|
-
const [blobSrc, setBlobSrc] = React8.useState(null);
|
|
1119
|
-
const sessionRef = React8.useRef({ token, reissueToken });
|
|
1120
|
-
React8.useEffect(() => {
|
|
1121
|
-
sessionRef.current = { token, reissueToken };
|
|
1122
|
-
});
|
|
1123
|
-
const isPrivate = media?.visibility === "private";
|
|
1124
|
-
const hasToken = token !== null;
|
|
1125
|
-
React8.useEffect(() => {
|
|
1126
|
-
setFailed(false);
|
|
1127
|
-
setBlobSrc(null);
|
|
1128
|
-
}, [media]);
|
|
1129
|
-
React8.useEffect(() => {
|
|
1130
|
-
if (!isPrivate || failed || !media || !hasToken) return;
|
|
1131
|
-
let cancelled = false;
|
|
1132
|
-
let objectUrl = null;
|
|
1133
|
-
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
1134
|
-
const transforms = { width, height, fit, format, quality };
|
|
1135
|
-
(async () => {
|
|
1136
|
-
try {
|
|
1137
|
-
const response = await fetchMedia({
|
|
1138
|
-
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...transforms, token: viewerToken }),
|
|
1139
|
-
token: sessionRef.current.token,
|
|
1140
|
-
sessionSecret,
|
|
1141
|
-
reissueToken: sessionRef.current.reissueToken
|
|
1142
|
-
});
|
|
1143
|
-
if (response.ok) {
|
|
1144
|
-
objectUrl = URL.createObjectURL(await response.blob());
|
|
1145
|
-
if (cancelled) {
|
|
1146
|
-
URL.revokeObjectURL(objectUrl);
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
setBlobSrc(objectUrl);
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
} catch {
|
|
1153
|
-
}
|
|
1154
|
-
if (!cancelled) setFailed(true);
|
|
1155
|
-
})();
|
|
1156
|
-
return () => {
|
|
1157
|
-
cancelled = true;
|
|
1158
|
-
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
1159
|
-
};
|
|
1160
|
-
}, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
1161
|
-
const handleError = React8.useCallback(() => setFailed(true), []);
|
|
1162
|
-
let src;
|
|
1163
|
-
if (failed || !media) {
|
|
1164
|
-
src = externalFallback ?? null;
|
|
1165
|
-
} else if (isPrivate) {
|
|
1166
|
-
src = blobSrc;
|
|
1167
|
-
} else {
|
|
1168
|
-
src = url(media, { width, height, fit, format, quality });
|
|
1169
|
-
}
|
|
1170
|
-
if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
|
|
1171
|
-
if (render) {
|
|
1172
|
-
return React8.cloneElement(render, {
|
|
1173
|
-
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
1174
|
-
});
|
|
1175
|
-
}
|
|
1176
|
-
return /* @__PURE__ */ jsx6(
|
|
1177
|
-
"img",
|
|
1178
|
-
{
|
|
1179
|
-
src,
|
|
1180
|
-
alt,
|
|
1181
|
-
width: width ?? media?.width,
|
|
1182
|
-
height: height ?? media?.height,
|
|
1183
|
-
onError: handleError,
|
|
1184
|
-
...imgProps
|
|
1185
|
-
}
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
1189
|
-
const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
1190
|
-
const handleClick = React8.useCallback(
|
|
2279
|
+
function DownloadLink({ media, filename, onDownloadError, children, ...anchorProps }) {
|
|
2280
|
+
const { url, token, tokenStatus, tokenError, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
2281
|
+
const handleClick = React22.useCallback(
|
|
1191
2282
|
async (event) => {
|
|
1192
2283
|
if (media?.visibility !== "private" || !baseUrl) return;
|
|
1193
2284
|
event.preventDefault();
|
|
2285
|
+
if (tokenStatus === "error") {
|
|
2286
|
+
onDownloadError?.(tokenUnavailableError(tokenError));
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
1194
2289
|
const response = await fetchMedia({
|
|
1195
|
-
buildUrl: (viewerToken) =>
|
|
2290
|
+
buildUrl: (viewerToken) => mediaUrl4(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
1196
2291
|
token,
|
|
1197
2292
|
sessionSecret,
|
|
1198
2293
|
reissueToken
|
|
1199
2294
|
});
|
|
1200
|
-
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
|
+
}
|
|
1201
2300
|
const objectUrl = URL.createObjectURL(await response.blob());
|
|
1202
2301
|
const anchor = document.createElement("a");
|
|
1203
2302
|
anchor.href = objectUrl;
|
|
@@ -1205,13 +2304,13 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
|
1205
2304
|
anchor.click();
|
|
1206
2305
|
URL.revokeObjectURL(objectUrl);
|
|
1207
2306
|
},
|
|
1208
|
-
[media, baseUrl, token, filename, sessionSecret, reissueToken]
|
|
2307
|
+
[media, baseUrl, token, tokenStatus, tokenError, filename, sessionSecret, reissueToken, onDownloadError]
|
|
1209
2308
|
);
|
|
1210
2309
|
if (!media) return null;
|
|
1211
|
-
return /* @__PURE__ */
|
|
2310
|
+
return /* @__PURE__ */ jsx16("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
|
|
1212
2311
|
}
|
|
1213
2312
|
function useDevulturMedia(key) {
|
|
1214
|
-
const apiCtx =
|
|
2313
|
+
const apiCtx = React22.use(DevulturApiContext);
|
|
1215
2314
|
if (!apiCtx?.getMedia) {
|
|
1216
2315
|
throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
|
|
1217
2316
|
}
|
|
@@ -1268,7 +2367,7 @@ function useVideoState(media, options) {
|
|
|
1268
2367
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
1269
2368
|
const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
|
|
1270
2369
|
const locales = options?.locales;
|
|
1271
|
-
return
|
|
2370
|
+
return React22.useMemo(() => {
|
|
1272
2371
|
if (media?.kind !== "video") return { phase: "idle" };
|
|
1273
2372
|
if (status.isFailed) {
|
|
1274
2373
|
return {
|
|
@@ -1281,12 +2380,12 @@ function useVideoState(media, options) {
|
|
|
1281
2380
|
if (status.isReady) {
|
|
1282
2381
|
if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
|
|
1283
2382
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
1284
|
-
const playlist =
|
|
2383
|
+
const playlist = buildMediaUrl2(baseUrl, mediaPath3(media), { token: viewerToken });
|
|
1285
2384
|
const captions = (locales ?? status.captionLocales).flatMap((locale) => {
|
|
1286
|
-
const path =
|
|
2385
|
+
const path = mediaCaptionPath2(media, locale);
|
|
1287
2386
|
if (!path) return [];
|
|
1288
2387
|
const label = CaptionLocaleLabel[locale] ?? locale;
|
|
1289
|
-
return [{ locale, label, src:
|
|
2388
|
+
return [{ locale, label, src: buildMediaUrl2(baseUrl, path, { token: viewerToken }) }];
|
|
1290
2389
|
});
|
|
1291
2390
|
return {
|
|
1292
2391
|
phase: "ready",
|
|
@@ -1312,23 +2411,22 @@ function useVideoState(media, options) {
|
|
|
1312
2411
|
}
|
|
1313
2412
|
function useMediaUpload(options) {
|
|
1314
2413
|
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
1315
|
-
const api =
|
|
1316
|
-
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
const
|
|
1321
|
-
const [
|
|
1322
|
-
const [
|
|
1323
|
-
const [
|
|
1324
|
-
const [
|
|
1325
|
-
const [
|
|
1326
|
-
const
|
|
1327
|
-
const
|
|
1328
|
-
const
|
|
1329
|
-
const lastKeyRef = React8.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);
|
|
1330
2428
|
const videoState = useVideoState(media, { locales });
|
|
1331
|
-
const triggerProcess =
|
|
2429
|
+
const triggerProcess = React22.useCallback(
|
|
1332
2430
|
(key) => {
|
|
1333
2431
|
if (!api.processVideo) {
|
|
1334
2432
|
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
@@ -1341,7 +2439,7 @@ function useMediaUpload(options) {
|
|
|
1341
2439
|
},
|
|
1342
2440
|
[api.processVideo, processVideoAction]
|
|
1343
2441
|
);
|
|
1344
|
-
const runUpload =
|
|
2442
|
+
const runUpload = React22.useCallback(
|
|
1345
2443
|
async (file) => {
|
|
1346
2444
|
const controller = new AbortController();
|
|
1347
2445
|
abortRef.current = controller;
|
|
@@ -1393,7 +2491,7 @@ function useMediaUpload(options) {
|
|
|
1393
2491
|
};
|
|
1394
2492
|
setMedia(uploaded);
|
|
1395
2493
|
setPhase("uploaded");
|
|
1396
|
-
if (replaces) deleteMediaAction({ key:
|
|
2494
|
+
if (replaces) deleteMediaAction({ key: mediaPath3(replaces) }).catch(() => {
|
|
1397
2495
|
});
|
|
1398
2496
|
onComplete?.(uploaded);
|
|
1399
2497
|
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
@@ -1412,10 +2510,10 @@ function useMediaUpload(options) {
|
|
|
1412
2510
|
onError
|
|
1413
2511
|
]
|
|
1414
2512
|
);
|
|
1415
|
-
const cancel =
|
|
2513
|
+
const cancel = React22.useCallback(() => {
|
|
1416
2514
|
abortRef.current?.abort();
|
|
1417
2515
|
}, []);
|
|
1418
|
-
const reset =
|
|
2516
|
+
const reset = React22.useCallback(() => {
|
|
1419
2517
|
abortRef.current?.abort();
|
|
1420
2518
|
setPhase("idle");
|
|
1421
2519
|
setPercent(0);
|
|
@@ -1424,15 +2522,15 @@ function useMediaUpload(options) {
|
|
|
1424
2522
|
setUploadFailure(null);
|
|
1425
2523
|
setProcessRequested(false);
|
|
1426
2524
|
}, []);
|
|
1427
|
-
const retry =
|
|
2525
|
+
const retry = React22.useCallback(() => {
|
|
1428
2526
|
if (phase === "upload-failed" && lastFileRef.current) {
|
|
1429
2527
|
runUpload(lastFileRef.current);
|
|
1430
2528
|
return;
|
|
1431
2529
|
}
|
|
1432
2530
|
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
1433
2531
|
}, [phase, runUpload, triggerProcess]);
|
|
1434
|
-
|
|
1435
|
-
const state =
|
|
2532
|
+
React22.useEffect(() => () => abortRef.current?.abort(), []);
|
|
2533
|
+
const state = React22.useMemo(
|
|
1436
2534
|
() => deriveUploadState(
|
|
1437
2535
|
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
1438
2536
|
videoState,
|
|
@@ -1440,23 +2538,38 @@ function useMediaUpload(options) {
|
|
|
1440
2538
|
),
|
|
1441
2539
|
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
1442
2540
|
);
|
|
1443
|
-
const upload =
|
|
2541
|
+
const upload = React22.useCallback((file) => runUpload(file), [runUpload]);
|
|
1444
2542
|
return { upload, state, cancel, reset };
|
|
1445
2543
|
}
|
|
1446
2544
|
export {
|
|
2545
|
+
AttachmentCard,
|
|
1447
2546
|
CropUpload,
|
|
2547
|
+
DevulturCapabilityError,
|
|
1448
2548
|
DevulturProvider,
|
|
2549
|
+
DevulturViewerProvider,
|
|
2550
|
+
DocumentLink,
|
|
1449
2551
|
DownloadLink,
|
|
1450
2552
|
Image,
|
|
1451
2553
|
MediaFetchError,
|
|
2554
|
+
MediaLibrary,
|
|
2555
|
+
MediaPicker,
|
|
1452
2556
|
PdfPreviewProvider,
|
|
1453
2557
|
PdfThumbnail,
|
|
1454
2558
|
Preview,
|
|
2559
|
+
defaultAttachmentLabels,
|
|
2560
|
+
defaultMediaLibraryLabels,
|
|
2561
|
+
defaultMediaPickerLabels,
|
|
1455
2562
|
defaultPreviewLabels,
|
|
2563
|
+
formatDuration,
|
|
2564
|
+
formatFileSize,
|
|
2565
|
+
matchesAccept,
|
|
1456
2566
|
useDevultur,
|
|
2567
|
+
useDevulturApi,
|
|
1457
2568
|
useDevulturMedia,
|
|
1458
2569
|
useDevulturProgress,
|
|
2570
|
+
useMediaLibrary,
|
|
1459
2571
|
useMediaObjectUrl,
|
|
2572
|
+
useMediaPicker,
|
|
1460
2573
|
useMediaUpload,
|
|
1461
2574
|
useMediaUrl,
|
|
1462
2575
|
useVideoProps,
|