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