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