@devultur/convex 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -7
- package/dist/pdf-render-CiCK6V1X.d.ts +43 -0
- package/dist/pdfjs.d.ts +11 -0
- package/dist/pdfjs.js +11 -0
- package/dist/react.d.ts +143 -1
- package/dist/react.js +689 -90
- package/package.json +13 -4
package/dist/react.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/react.tsx
|
|
4
|
-
import
|
|
4
|
+
import React7 from "react";
|
|
5
5
|
import {
|
|
6
6
|
buildMediaUrl,
|
|
7
7
|
CaptionLocaleLabel,
|
|
8
8
|
DEFAULT_TOKEN_EXPIRY_SECONDS,
|
|
9
9
|
MediaStatus,
|
|
10
10
|
mediaCaptionPath,
|
|
11
|
-
mediaPath,
|
|
12
|
-
mediaUrl,
|
|
13
|
-
SESSION_HEADER,
|
|
11
|
+
mediaPath as mediaPath2,
|
|
12
|
+
mediaUrl as mediaUrl2,
|
|
13
|
+
SESSION_HEADER as SESSION_HEADER2,
|
|
14
14
|
visibilityFromKey
|
|
15
15
|
} from "@devultur/core";
|
|
16
16
|
import {
|
|
@@ -29,6 +29,149 @@ async function isSessionMismatch(response) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/media-fetch.ts
|
|
33
|
+
import { SESSION_HEADER } from "@devultur/core";
|
|
34
|
+
|
|
35
|
+
// src/preview-type.ts
|
|
36
|
+
var PREVIEW_KIND_BY_TYPE = /* @__PURE__ */ new Map([
|
|
37
|
+
["image/png", "image"],
|
|
38
|
+
["image/jpeg", "image"],
|
|
39
|
+
["image/webp", "image"],
|
|
40
|
+
["image/gif", "image"],
|
|
41
|
+
["image/avif", "image"],
|
|
42
|
+
["application/pdf", "pdf"],
|
|
43
|
+
["audio/mpeg", "audio"],
|
|
44
|
+
["audio/mp4", "audio"],
|
|
45
|
+
["audio/aac", "audio"],
|
|
46
|
+
["audio/ogg", "audio"],
|
|
47
|
+
["audio/wav", "audio"],
|
|
48
|
+
["audio/webm", "audio"],
|
|
49
|
+
["audio/flac", "audio"],
|
|
50
|
+
["text/plain", "text"],
|
|
51
|
+
["text/csv", "text"],
|
|
52
|
+
["application/json", "text"]
|
|
53
|
+
]);
|
|
54
|
+
var TYPE_BY_EXTENSION = /* @__PURE__ */ new Map([
|
|
55
|
+
["png", "image/png"],
|
|
56
|
+
["jpg", "image/jpeg"],
|
|
57
|
+
["jpeg", "image/jpeg"],
|
|
58
|
+
["webp", "image/webp"],
|
|
59
|
+
["gif", "image/gif"],
|
|
60
|
+
["avif", "image/avif"],
|
|
61
|
+
["pdf", "application/pdf"],
|
|
62
|
+
["mp3", "audio/mpeg"],
|
|
63
|
+
["m4a", "audio/mp4"],
|
|
64
|
+
["aac", "audio/aac"],
|
|
65
|
+
["ogg", "audio/ogg"],
|
|
66
|
+
["oga", "audio/ogg"],
|
|
67
|
+
["opus", "audio/ogg"],
|
|
68
|
+
["wav", "audio/wav"],
|
|
69
|
+
["weba", "audio/webm"],
|
|
70
|
+
["flac", "audio/flac"],
|
|
71
|
+
["txt", "text/plain"],
|
|
72
|
+
["csv", "text/csv"],
|
|
73
|
+
["json", "application/json"]
|
|
74
|
+
]);
|
|
75
|
+
var OPAQUE_CONTENT_TYPE = "application/octet-stream";
|
|
76
|
+
function normalizeContentType(value) {
|
|
77
|
+
const essence = value?.split(";")[0]?.trim().toLowerCase();
|
|
78
|
+
return essence || null;
|
|
79
|
+
}
|
|
80
|
+
function previewKindOf(contentType) {
|
|
81
|
+
const normalized = normalizeContentType(contentType);
|
|
82
|
+
if (!normalized) return null;
|
|
83
|
+
return PREVIEW_KIND_BY_TYPE.get(normalized) ?? null;
|
|
84
|
+
}
|
|
85
|
+
function expectedContentType(media, declaredType) {
|
|
86
|
+
if (declaredType) return normalizeContentType(declaredType);
|
|
87
|
+
const extension = media.filename?.split(".").pop()?.toLowerCase();
|
|
88
|
+
if (!extension || extension === media.filename?.toLowerCase()) return null;
|
|
89
|
+
return TYPE_BY_EXTENSION.get(extension) ?? null;
|
|
90
|
+
}
|
|
91
|
+
function trustedContentType(expectedType, responseType) {
|
|
92
|
+
const receivedKind = previewKindOf(responseType);
|
|
93
|
+
if (!expectedType) {
|
|
94
|
+
const contentType = receivedKind ? normalizeContentType(responseType) : null;
|
|
95
|
+
return { ok: true, contentType: contentType ?? OPAQUE_CONTENT_TYPE };
|
|
96
|
+
}
|
|
97
|
+
const expectedKind = previewKindOf(expectedType);
|
|
98
|
+
if (!expectedKind) return { ok: false, reason: "unsupported_type" };
|
|
99
|
+
if (receivedKind !== expectedKind) return { ok: false, reason: "type_mismatch" };
|
|
100
|
+
return { ok: true, contentType: normalizeContentType(expectedType) ?? OPAQUE_CONTENT_TYPE };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/media-fetch.ts
|
|
104
|
+
var MediaFetchError = class extends Error {
|
|
105
|
+
code;
|
|
106
|
+
/** HTTP status, for `http_error`. */
|
|
107
|
+
status;
|
|
108
|
+
constructor(code, message, status) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "MediaFetchError";
|
|
111
|
+
this.code = code;
|
|
112
|
+
this.status = status;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
async function fetchMedia(request) {
|
|
116
|
+
const { buildUrl, token, sessionSecret, reissueToken, signal, isPrivate = true, canReissue = true } = request;
|
|
117
|
+
const headers = isPrivate && sessionSecret ? { [SESSION_HEADER]: sessionSecret } : void 0;
|
|
118
|
+
const response = await fetch(buildUrl(isPrivate ? token ?? void 0 : void 0), { headers, signal });
|
|
119
|
+
if (!isPrivate || !canReissue || response.status !== 401) return response;
|
|
120
|
+
if (!await isSessionMismatch(response)) return response;
|
|
121
|
+
request.onReissue?.();
|
|
122
|
+
const freshToken = await reissueToken();
|
|
123
|
+
if (!freshToken) return response;
|
|
124
|
+
return fetch(buildUrl(freshToken), { headers, signal });
|
|
125
|
+
}
|
|
126
|
+
async function loadMedia(request) {
|
|
127
|
+
const response = await fetchMedia(request);
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new MediaFetchError("http_error", `Media fetch failed: ${response.status}`, response.status);
|
|
130
|
+
}
|
|
131
|
+
const trusted = trustedContentType(request.contentType, response.headers.get("content-type"));
|
|
132
|
+
if (!trusted.ok) {
|
|
133
|
+
await response.body?.cancel();
|
|
134
|
+
throw new MediaFetchError(trusted.reason, `Refusing to preview: ${trusted.reason}`);
|
|
135
|
+
}
|
|
136
|
+
const bytes = await readMediaBody(response, request.maxBytes);
|
|
137
|
+
return { bytes, contentType: trusted.contentType };
|
|
138
|
+
}
|
|
139
|
+
function tooLarge(maxBytes) {
|
|
140
|
+
return new MediaFetchError("too_large", `Media is larger than ${maxBytes} bytes`);
|
|
141
|
+
}
|
|
142
|
+
async function readMediaBody(response, maxBytes) {
|
|
143
|
+
if (maxBytes === void 0) return new Uint8Array(await response.arrayBuffer());
|
|
144
|
+
const declaredBytes = Number(response.headers.get("content-length") ?? Number.NaN);
|
|
145
|
+
if (declaredBytes > maxBytes) {
|
|
146
|
+
await response.body?.cancel();
|
|
147
|
+
throw tooLarge(maxBytes);
|
|
148
|
+
}
|
|
149
|
+
if (!response.body) return new Uint8Array(0);
|
|
150
|
+
return readCapped(response.body, maxBytes);
|
|
151
|
+
}
|
|
152
|
+
async function readCapped(body, maxBytes) {
|
|
153
|
+
const reader = body.getReader();
|
|
154
|
+
const chunks = [];
|
|
155
|
+
let byteCount = 0;
|
|
156
|
+
for (; ; ) {
|
|
157
|
+
const { done, value } = await reader.read();
|
|
158
|
+
if (done) break;
|
|
159
|
+
byteCount += value.byteLength;
|
|
160
|
+
if (byteCount > maxBytes) {
|
|
161
|
+
await reader.cancel();
|
|
162
|
+
throw tooLarge(maxBytes);
|
|
163
|
+
}
|
|
164
|
+
chunks.push(value);
|
|
165
|
+
}
|
|
166
|
+
const bytes = new Uint8Array(byteCount);
|
|
167
|
+
let offset = 0;
|
|
168
|
+
for (const chunk of chunks) {
|
|
169
|
+
bytes.set(chunk, offset);
|
|
170
|
+
offset += chunk.byteLength;
|
|
171
|
+
}
|
|
172
|
+
return bytes;
|
|
173
|
+
}
|
|
174
|
+
|
|
32
175
|
// src/media-upload-derive.ts
|
|
33
176
|
function deriveUploadState(local, videoState, retry) {
|
|
34
177
|
const { phase, percent, file, failure, media, processRequested } = local;
|
|
@@ -159,11 +302,461 @@ function CropUpload({
|
|
|
159
302
|
] });
|
|
160
303
|
}
|
|
161
304
|
|
|
305
|
+
// src/media-resource.ts
|
|
306
|
+
import React2 from "react";
|
|
307
|
+
import { mediaPath, mediaUrl } from "@devultur/core";
|
|
308
|
+
var IDLE = { value: null, contentType: null, isLoading: false, error: null };
|
|
309
|
+
var LOADING = { value: null, contentType: null, isLoading: true, error: null };
|
|
310
|
+
function useMediaResource(media, options) {
|
|
311
|
+
const devultur = useDevultur();
|
|
312
|
+
const { isEnabled, maxBytes, contentType } = options;
|
|
313
|
+
const isPrivate = media?.visibility === "private";
|
|
314
|
+
const isReady = !isPrivate || devultur.token !== null;
|
|
315
|
+
const requestKey = media && isEnabled ? [devultur.baseUrl, media.visibility, mediaPath(media), contentType ?? "", maxBytes ?? ""].join("|") : null;
|
|
316
|
+
const [settled, setSettled] = React2.useState(null);
|
|
317
|
+
const inputsRef = React2.useRef({ media, devultur, options });
|
|
318
|
+
React2.useEffect(() => {
|
|
319
|
+
inputsRef.current = { media, devultur, options };
|
|
320
|
+
});
|
|
321
|
+
React2.useEffect(() => {
|
|
322
|
+
if (!requestKey || !isReady) return;
|
|
323
|
+
const { media: target, devultur: context, options: current } = inputsRef.current;
|
|
324
|
+
if (!target || !context.baseUrl) return;
|
|
325
|
+
const baseUrl = context.baseUrl;
|
|
326
|
+
const controller = new AbortController();
|
|
327
|
+
let value = null;
|
|
328
|
+
loadMedia({
|
|
329
|
+
buildUrl: (viewerToken) => mediaUrl(baseUrl, target, { token: viewerToken }),
|
|
330
|
+
token: context.token,
|
|
331
|
+
sessionSecret: context.sessionSecret,
|
|
332
|
+
reissueToken: context.reissueToken,
|
|
333
|
+
isPrivate: target.visibility === "private",
|
|
334
|
+
signal: controller.signal,
|
|
335
|
+
maxBytes: current.maxBytes,
|
|
336
|
+
contentType: current.contentType
|
|
337
|
+
}).then((loaded) => {
|
|
338
|
+
if (controller.signal.aborted) return;
|
|
339
|
+
value = current.decode(loaded.bytes, loaded.contentType);
|
|
340
|
+
setSettled({ requestKey, value, contentType: loaded.contentType, isLoading: false, error: null });
|
|
341
|
+
}).catch((error) => {
|
|
342
|
+
if (controller.signal.aborted) return;
|
|
343
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
344
|
+
setSettled({ requestKey, value: null, contentType: null, isLoading: false, error: failure });
|
|
345
|
+
});
|
|
346
|
+
return () => {
|
|
347
|
+
controller.abort();
|
|
348
|
+
if (value !== null) current.release?.(value);
|
|
349
|
+
};
|
|
350
|
+
}, [requestKey, isReady]);
|
|
351
|
+
if (!requestKey) return IDLE;
|
|
352
|
+
if (!devultur.baseUrl) throw new Error("Devultur media previews require `baseUrl` on <DevulturProvider>");
|
|
353
|
+
if (settled?.requestKey !== requestKey) return LOADING;
|
|
354
|
+
return { value: settled.value, contentType: settled.contentType, isLoading: false, error: settled.error };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/media-object-url.ts
|
|
358
|
+
var createObjectUrl = (bytes, contentType) => URL.createObjectURL(new Blob([bytes], { type: contentType }));
|
|
359
|
+
var revokeObjectUrl = (objectUrl) => URL.revokeObjectURL(objectUrl);
|
|
360
|
+
function useMediaObjectUrl(media, options) {
|
|
361
|
+
const { url } = useDevultur();
|
|
362
|
+
const isEnabled = (options?.enabled ?? true) && Boolean(media);
|
|
363
|
+
const isUnverifiedPublic = media?.visibility === "public" && !options?.contentType;
|
|
364
|
+
const resource = useMediaResource(media, {
|
|
365
|
+
isEnabled: isEnabled && !isUnverifiedPublic,
|
|
366
|
+
maxBytes: options?.maxBytes,
|
|
367
|
+
contentType: options?.contentType,
|
|
368
|
+
decode: createObjectUrl,
|
|
369
|
+
release: revokeObjectUrl
|
|
370
|
+
});
|
|
371
|
+
if (media && isEnabled && isUnverifiedPublic) {
|
|
372
|
+
return { objectUrl: url(media), contentType: null, isLoading: false, error: null };
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
objectUrl: resource.value,
|
|
376
|
+
contentType: resource.contentType,
|
|
377
|
+
isLoading: resource.isLoading,
|
|
378
|
+
error: resource.error
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/pdf-context.tsx
|
|
383
|
+
import React3 from "react";
|
|
384
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
385
|
+
var PdfPreviewContext = React3.createContext({});
|
|
386
|
+
function PdfPreviewProvider({ pdfjs, workerSrc, children }) {
|
|
387
|
+
const config = React3.useMemo(() => ({ pdfjs, workerSrc }), [pdfjs, workerSrc]);
|
|
388
|
+
return /* @__PURE__ */ jsx2(PdfPreviewContext.Provider, { value: config, children });
|
|
389
|
+
}
|
|
390
|
+
var loadedModules = /* @__PURE__ */ new WeakMap();
|
|
391
|
+
function loadOnce(loader) {
|
|
392
|
+
const cached = loadedModules.get(loader);
|
|
393
|
+
if (cached) return cached;
|
|
394
|
+
const loading = loader().catch(() => null);
|
|
395
|
+
loadedModules.set(loader, loading);
|
|
396
|
+
return loading;
|
|
397
|
+
}
|
|
398
|
+
function usePdfjs(workerSrcOverride) {
|
|
399
|
+
const { pdfjs: loader, workerSrc } = React3.useContext(PdfPreviewContext);
|
|
400
|
+
const [loaded, setLoaded] = React3.useState(null);
|
|
401
|
+
React3.useEffect(() => {
|
|
402
|
+
if (!loader) return;
|
|
403
|
+
let isCurrent = true;
|
|
404
|
+
loadOnce(loader).then((pdfjs) => {
|
|
405
|
+
if (isCurrent) setLoaded({ loader, pdfjs });
|
|
406
|
+
});
|
|
407
|
+
return () => {
|
|
408
|
+
isCurrent = false;
|
|
409
|
+
};
|
|
410
|
+
}, [loader]);
|
|
411
|
+
if (!loader) return { status: "unavailable" };
|
|
412
|
+
if (loaded?.loader !== loader) return { status: "loading" };
|
|
413
|
+
if (!loaded.pdfjs) return { status: "unavailable" };
|
|
414
|
+
return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/pdf-thumbnail.tsx
|
|
418
|
+
import React6 from "react";
|
|
419
|
+
|
|
420
|
+
// src/pdf-hooks.ts
|
|
421
|
+
import React4 from "react";
|
|
422
|
+
|
|
423
|
+
// src/pdf-render.ts
|
|
424
|
+
var MOBILE_MAX_CANVAS_PIXELS = 5242880;
|
|
425
|
+
var DESKTOP_MAX_CANVAS_PIXELS = 2 ** 25;
|
|
426
|
+
function openPdf(pdfjs, bytes, workerSrc) {
|
|
427
|
+
if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
428
|
+
const source = { data: bytes.slice(), enableScripting: false, isEvalSupported: false };
|
|
429
|
+
return pdfjs.getDocument(source);
|
|
430
|
+
}
|
|
431
|
+
function isMobileDevice() {
|
|
432
|
+
if (typeof navigator === "undefined") return false;
|
|
433
|
+
return /Android|iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;
|
|
434
|
+
}
|
|
435
|
+
function maxCanvasPixels() {
|
|
436
|
+
return isMobileDevice() ? MOBILE_MAX_CANVAS_PIXELS : DESKTOP_MAX_CANVAS_PIXELS;
|
|
437
|
+
}
|
|
438
|
+
function outputScale(cssWidth, cssHeight, maxPixels) {
|
|
439
|
+
const devicePixelRatio = typeof window !== "undefined" && window.devicePixelRatio || 1;
|
|
440
|
+
const cssPixels = cssWidth * cssHeight;
|
|
441
|
+
if (cssPixels <= 0) return devicePixelRatio;
|
|
442
|
+
if (cssPixels * devicePixelRatio ** 2 <= maxPixels) return devicePixelRatio;
|
|
443
|
+
return Math.sqrt(maxPixels / cssPixels);
|
|
444
|
+
}
|
|
445
|
+
function renderPage(page, canvas, cssWidth) {
|
|
446
|
+
const naturalViewport = page.getViewport({ scale: 1 });
|
|
447
|
+
const cssScale = cssWidth > 0 ? cssWidth / naturalViewport.width : 1;
|
|
448
|
+
const cssViewport = page.getViewport({ scale: cssScale });
|
|
449
|
+
const scale = outputScale(cssViewport.width, cssViewport.height, maxCanvasPixels());
|
|
450
|
+
const viewport = page.getViewport({ scale: cssScale * scale });
|
|
451
|
+
canvas.width = Math.floor(viewport.width);
|
|
452
|
+
canvas.height = Math.floor(viewport.height);
|
|
453
|
+
canvas.style.width = `${Math.floor(cssViewport.width)}px`;
|
|
454
|
+
canvas.style.height = `${Math.floor(cssViewport.height)}px`;
|
|
455
|
+
return page.render({ canvas, viewport });
|
|
456
|
+
}
|
|
457
|
+
async function pageText(page) {
|
|
458
|
+
const { items } = await page.getTextContent();
|
|
459
|
+
return items.map((item) => "str" in item && typeof item.str === "string" ? item.str : "").join(" ").replace(/\s+/g, " ").trim();
|
|
460
|
+
}
|
|
461
|
+
var isRenderCancelled = (error) => error instanceof Error && error.name === "RenderingCancelledException";
|
|
462
|
+
|
|
463
|
+
// src/pdf-hooks.ts
|
|
464
|
+
function useIsVisible(ref, rootMargin = "200px") {
|
|
465
|
+
const [isVisible, setIsVisible] = React4.useState(false);
|
|
466
|
+
React4.useEffect(() => {
|
|
467
|
+
const element = ref.current;
|
|
468
|
+
if (isVisible || !element) return;
|
|
469
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
470
|
+
setIsVisible(true);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const observer = new IntersectionObserver(
|
|
474
|
+
(entries) => {
|
|
475
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
476
|
+
setIsVisible(true);
|
|
477
|
+
observer.disconnect();
|
|
478
|
+
},
|
|
479
|
+
{ rootMargin }
|
|
480
|
+
);
|
|
481
|
+
observer.observe(element);
|
|
482
|
+
return () => observer.disconnect();
|
|
483
|
+
}, [ref, rootMargin, isVisible]);
|
|
484
|
+
return isVisible;
|
|
485
|
+
}
|
|
486
|
+
function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
487
|
+
const [state, setState] = React4.useState({
|
|
488
|
+
bytes: null,
|
|
489
|
+
document: null,
|
|
490
|
+
error: null
|
|
491
|
+
});
|
|
492
|
+
React4.useEffect(() => {
|
|
493
|
+
let isCurrent = true;
|
|
494
|
+
const loadingTask = openPdf(pdfjs, bytes, workerSrc);
|
|
495
|
+
loadingTask.promise.then((document2) => {
|
|
496
|
+
if (isCurrent) setState({ bytes, document: document2, error: null });
|
|
497
|
+
}).catch((error) => {
|
|
498
|
+
if (!isCurrent) return;
|
|
499
|
+
setState({ bytes, document: null, error: error instanceof Error ? error : new Error(String(error)) });
|
|
500
|
+
});
|
|
501
|
+
return () => {
|
|
502
|
+
isCurrent = false;
|
|
503
|
+
void loadingTask.destroy();
|
|
504
|
+
};
|
|
505
|
+
}, [pdfjs, bytes, workerSrc]);
|
|
506
|
+
if (state.bytes !== bytes) return { document: null, error: null };
|
|
507
|
+
return { document: state.document, error: state.error };
|
|
508
|
+
}
|
|
509
|
+
function usePageRender(options) {
|
|
510
|
+
const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
|
|
511
|
+
const [state, setState] = React4.useState({ isRendered: false, text: "" });
|
|
512
|
+
React4.useEffect(() => {
|
|
513
|
+
const canvas = canvasRef.current;
|
|
514
|
+
if (!document2 || !isEnabled || !canvas) return;
|
|
515
|
+
let isCurrent = true;
|
|
516
|
+
let page = null;
|
|
517
|
+
let renderTask = null;
|
|
518
|
+
const draw = async () => {
|
|
519
|
+
page = await document2.getPage(pageNumber);
|
|
520
|
+
if (!isCurrent) return;
|
|
521
|
+
renderTask = renderPage(page, canvas, width);
|
|
522
|
+
await renderTask.promise;
|
|
523
|
+
if (isCurrent) setState((previous) => ({ ...previous, isRendered: true }));
|
|
524
|
+
if (!shouldExtractText) return;
|
|
525
|
+
const text = await pageText(page);
|
|
526
|
+
if (isCurrent) setState((previous) => ({ ...previous, text }));
|
|
527
|
+
};
|
|
528
|
+
draw().catch((error) => {
|
|
529
|
+
if (isRenderCancelled(error) || !isCurrent) return;
|
|
530
|
+
console.warn(`[devultur] PDF page ${pageNumber} failed to render`, error);
|
|
531
|
+
});
|
|
532
|
+
return () => {
|
|
533
|
+
isCurrent = false;
|
|
534
|
+
renderTask?.cancel();
|
|
535
|
+
page?.cleanup();
|
|
536
|
+
};
|
|
537
|
+
}, [document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText]);
|
|
538
|
+
return state;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/pdf-preview.tsx
|
|
542
|
+
import React5 from "react";
|
|
543
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
544
|
+
var VISUALLY_HIDDEN = {
|
|
545
|
+
position: "absolute",
|
|
546
|
+
width: 1,
|
|
547
|
+
height: 1,
|
|
548
|
+
margin: -1,
|
|
549
|
+
padding: 0,
|
|
550
|
+
overflow: "hidden",
|
|
551
|
+
clip: "rect(0 0 0 0)",
|
|
552
|
+
whiteSpace: "nowrap",
|
|
553
|
+
border: 0
|
|
554
|
+
};
|
|
555
|
+
var DEFAULT_MAX_PAGES = 500;
|
|
556
|
+
var keepBytes = (bytes) => bytes;
|
|
557
|
+
function PdfPreview(props) {
|
|
558
|
+
const { media, expectedType, maxBytes, fallback, loading, title, className, pdfWorkerSrc, maxPages } = props;
|
|
559
|
+
const pdfjsState = usePdfjs(pdfWorkerSrc);
|
|
560
|
+
const { value: bytes, error } = useMediaResource(media, {
|
|
561
|
+
isEnabled: true,
|
|
562
|
+
maxBytes,
|
|
563
|
+
contentType: expectedType,
|
|
564
|
+
decode: keepBytes
|
|
565
|
+
});
|
|
566
|
+
if (error) return /* @__PURE__ */ jsx3(Fragment, { children: fallback });
|
|
567
|
+
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx3(Fragment, { children: loading });
|
|
568
|
+
const label = title ?? media.filename ?? "PDF";
|
|
569
|
+
if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx3(PdfFrame, { bytes, label, className });
|
|
570
|
+
return /* @__PURE__ */ jsx3(
|
|
571
|
+
PdfCanvasDocument,
|
|
572
|
+
{
|
|
573
|
+
pdfjs: pdfjsState.pdfjs,
|
|
574
|
+
workerSrc: pdfjsState.workerSrc,
|
|
575
|
+
bytes,
|
|
576
|
+
label,
|
|
577
|
+
className,
|
|
578
|
+
fallback,
|
|
579
|
+
loading,
|
|
580
|
+
maxPages: maxPages ?? DEFAULT_MAX_PAGES
|
|
581
|
+
}
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
function PdfCanvasDocument(props) {
|
|
585
|
+
const { pdfjs, workerSrc, bytes, label, className, fallback, loading, maxPages } = props;
|
|
586
|
+
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
587
|
+
const containerRef = React5.useRef(null);
|
|
588
|
+
const [width, setWidth] = React5.useState(null);
|
|
589
|
+
React5.useLayoutEffect(() => {
|
|
590
|
+
if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
|
|
591
|
+
}, [document2]);
|
|
592
|
+
if (error) return /* @__PURE__ */ jsx3(Fragment, { children: fallback });
|
|
593
|
+
if (!document2) return /* @__PURE__ */ jsx3(Fragment, { children: loading });
|
|
594
|
+
const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
|
|
595
|
+
const isTruncated = shownPageCount < document2.numPages;
|
|
596
|
+
const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
|
|
597
|
+
const pageCountLabel = document2.numPages === 1 ? "1 page" : `${document2.numPages} pages`;
|
|
598
|
+
return /* @__PURE__ */ jsxs2("div", { ref: containerRef, role: "document", "aria-label": `${label}, ${pageCountLabel}`, className, children: [
|
|
599
|
+
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx3(PdfPageView, { document: document2, pageNumber, width }, pageNumber)),
|
|
600
|
+
isTruncated && /* @__PURE__ */ jsxs2("p", { children: [
|
|
601
|
+
"Showing the first ",
|
|
602
|
+
shownPageCount,
|
|
603
|
+
" of ",
|
|
604
|
+
document2.numPages,
|
|
605
|
+
" pages. ",
|
|
606
|
+
fallback
|
|
607
|
+
] })
|
|
608
|
+
] });
|
|
609
|
+
}
|
|
610
|
+
function PdfPageView({ document: document2, pageNumber, width }) {
|
|
611
|
+
const pageRef = React5.useRef(null);
|
|
612
|
+
const canvasRef = React5.useRef(null);
|
|
613
|
+
const isVisible = useIsVisible(pageRef);
|
|
614
|
+
const { isRendered, text } = usePageRender({
|
|
615
|
+
document: document2,
|
|
616
|
+
pageNumber,
|
|
617
|
+
canvasRef,
|
|
618
|
+
width,
|
|
619
|
+
isEnabled: isVisible,
|
|
620
|
+
shouldExtractText: true
|
|
621
|
+
});
|
|
622
|
+
const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
|
|
623
|
+
return /* @__PURE__ */ jsxs2("section", { ref: pageRef, "aria-label": `Page ${pageNumber} of ${document2.numPages}`, style: placeholderStyle, children: [
|
|
624
|
+
/* @__PURE__ */ jsx3("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
|
|
625
|
+
text && /* @__PURE__ */ jsx3("p", { style: VISUALLY_HIDDEN, children: text })
|
|
626
|
+
] });
|
|
627
|
+
}
|
|
628
|
+
function PdfFrame({ bytes, label, className }) {
|
|
629
|
+
const [objectUrl, setObjectUrl] = React5.useState(null);
|
|
630
|
+
React5.useEffect(() => {
|
|
631
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
632
|
+
setObjectUrl(url);
|
|
633
|
+
return () => URL.revokeObjectURL(url);
|
|
634
|
+
}, [bytes]);
|
|
635
|
+
if (!objectUrl) return null;
|
|
636
|
+
return /* @__PURE__ */ jsx3("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/pdf-thumbnail.tsx
|
|
640
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
641
|
+
var DEFAULT_THUMBNAIL_WIDTH = 96;
|
|
642
|
+
var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
|
|
643
|
+
function PdfThumbnail(props) {
|
|
644
|
+
const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
|
|
645
|
+
const containerRef = React6.useRef(null);
|
|
646
|
+
const isVisible = useIsVisible(containerRef);
|
|
647
|
+
const pdfjsState = usePdfjs(props.pdfWorkerSrc);
|
|
648
|
+
const expectedType = media ? expectedContentType(media, contentType) : null;
|
|
649
|
+
const isPdf = previewKindOf(expectedType) === "pdf";
|
|
650
|
+
const { value: bytes, error } = useMediaResource(media, {
|
|
651
|
+
isEnabled: isVisible && isPdf && pdfjsState.status === "ready",
|
|
652
|
+
maxBytes: props.maxBytes ?? DEFAULT_THUMBNAIL_MAX_BYTES,
|
|
653
|
+
contentType: expectedType ?? void 0,
|
|
654
|
+
decode: keepBytes
|
|
655
|
+
});
|
|
656
|
+
if (!media) return null;
|
|
657
|
+
const isUnavailable = !isPdf || pdfjsState.status === "unavailable" || error !== null;
|
|
658
|
+
const content = () => {
|
|
659
|
+
if (isUnavailable) return fallback;
|
|
660
|
+
if (!bytes || pdfjsState.status !== "ready") return loading;
|
|
661
|
+
return /* @__PURE__ */ jsx4(
|
|
662
|
+
ThumbnailCanvas,
|
|
663
|
+
{
|
|
664
|
+
pdfjs: pdfjsState.pdfjs,
|
|
665
|
+
workerSrc: pdfjsState.workerSrc,
|
|
666
|
+
bytes,
|
|
667
|
+
width,
|
|
668
|
+
label: props.title ?? media.filename ?? "PDF",
|
|
669
|
+
fallback,
|
|
670
|
+
loading
|
|
671
|
+
}
|
|
672
|
+
);
|
|
673
|
+
};
|
|
674
|
+
return /* @__PURE__ */ jsx4("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
|
|
675
|
+
}
|
|
676
|
+
function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
|
|
677
|
+
const canvasRef = React6.useRef(null);
|
|
678
|
+
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
679
|
+
const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
|
|
680
|
+
if (error) return /* @__PURE__ */ jsx4(Fragment2, { children: fallback });
|
|
681
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
682
|
+
!isRendered && loading,
|
|
683
|
+
/* @__PURE__ */ jsx4("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
|
|
684
|
+
] });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// src/preview.tsx
|
|
688
|
+
import { VideoPlayer } from "@devultur/react";
|
|
689
|
+
import { Fragment as Fragment3, jsx as jsx5 } from "react/jsx-runtime";
|
|
690
|
+
var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
|
|
691
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
692
|
+
function Preview({ media, contentType, fallback, ...viewProps }) {
|
|
693
|
+
if (!media) return null;
|
|
694
|
+
const fallbackNode = fallback === void 0 ? /* @__PURE__ */ jsx5(DownloadLink, { media, children: media.filename ?? "Download" }) : fallback;
|
|
695
|
+
if (media.kind === "video") return /* @__PURE__ */ jsx5(VideoPreview, { media, className: viewProps.className });
|
|
696
|
+
const expectedType = expectedContentType(media, contentType);
|
|
697
|
+
const kind = previewKindOf(expectedType);
|
|
698
|
+
if (!expectedType || !kind) return /* @__PURE__ */ jsx5(Fragment3, { children: fallbackNode });
|
|
699
|
+
const props = { ...viewProps, media, expectedType, fallback: fallbackNode };
|
|
700
|
+
if (kind === "text") return /* @__PURE__ */ jsx5(TextPreview, { ...props });
|
|
701
|
+
if (kind === "pdf") {
|
|
702
|
+
const { maxBytes = DEFAULT_MAX_BYTES, loading = null } = props;
|
|
703
|
+
return /* @__PURE__ */ jsx5(PdfPreview, { ...props, maxBytes, loading });
|
|
704
|
+
}
|
|
705
|
+
return /* @__PURE__ */ jsx5(BinaryPreview, { ...props, kind });
|
|
706
|
+
}
|
|
707
|
+
function VideoPreview({ media, className }) {
|
|
708
|
+
const videoProps = useVideoProps(media);
|
|
709
|
+
if (!videoProps) return null;
|
|
710
|
+
return /* @__PURE__ */ jsx5(VideoPlayer, { ...videoProps, className });
|
|
711
|
+
}
|
|
712
|
+
function BinaryPreview({
|
|
713
|
+
media,
|
|
714
|
+
kind,
|
|
715
|
+
expectedType,
|
|
716
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
717
|
+
fallback,
|
|
718
|
+
loading = null,
|
|
719
|
+
title,
|
|
720
|
+
className
|
|
721
|
+
}) {
|
|
722
|
+
const { url } = useDevultur();
|
|
723
|
+
const isPublicImage = kind === "image" && media.visibility === "public";
|
|
724
|
+
const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
|
|
725
|
+
const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
|
|
726
|
+
if (verified.error) return /* @__PURE__ */ jsx5(Fragment3, { children: fallback });
|
|
727
|
+
if (!objectUrl) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
|
|
728
|
+
const label = title ?? media.filename ?? "Preview";
|
|
729
|
+
if (kind === "image") {
|
|
730
|
+
return /* @__PURE__ */ jsx5("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
|
|
731
|
+
}
|
|
732
|
+
return /* @__PURE__ */ jsx5("audio", { src: objectUrl, controls: true, "aria-label": label, className });
|
|
733
|
+
}
|
|
734
|
+
var decodeText = (bytes) => new TextDecoder().decode(bytes);
|
|
735
|
+
function TextPreview({
|
|
736
|
+
media,
|
|
737
|
+
expectedType,
|
|
738
|
+
maxTextBytes = DEFAULT_MAX_TEXT_BYTES,
|
|
739
|
+
fallback,
|
|
740
|
+
loading = null,
|
|
741
|
+
title,
|
|
742
|
+
className
|
|
743
|
+
}) {
|
|
744
|
+
const { value: text, error } = useMediaResource(media, {
|
|
745
|
+
isEnabled: true,
|
|
746
|
+
maxBytes: maxTextBytes,
|
|
747
|
+
contentType: expectedType,
|
|
748
|
+
decode: decodeText
|
|
749
|
+
});
|
|
750
|
+
if (error) return /* @__PURE__ */ jsx5(Fragment3, { children: fallback });
|
|
751
|
+
if (text === null) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
|
|
752
|
+
return /* @__PURE__ */ jsx5("pre", { title, className, children: text });
|
|
753
|
+
}
|
|
754
|
+
|
|
162
755
|
// src/react.tsx
|
|
163
|
-
import { Fragment, jsx as
|
|
164
|
-
var DevulturContext =
|
|
165
|
-
var DevulturApiContext =
|
|
166
|
-
var ProgressContext =
|
|
756
|
+
import { Fragment as Fragment4, jsx as jsx6 } from "react/jsx-runtime";
|
|
757
|
+
var DevulturContext = React7.createContext(null);
|
|
758
|
+
var DevulturApiContext = React7.createContext(null);
|
|
759
|
+
var ProgressContext = React7.createContext(null);
|
|
167
760
|
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
168
761
|
var MIN_REFRESH_MS = 6e4;
|
|
169
762
|
var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
@@ -173,16 +766,16 @@ function extractTokenResult(result) {
|
|
|
173
766
|
return { tokenValue, ttlMs };
|
|
174
767
|
}
|
|
175
768
|
function DevulturProvider({ api, baseUrl, children }) {
|
|
176
|
-
const [token, setToken] =
|
|
177
|
-
const [sessionSecret] =
|
|
769
|
+
const [token, setToken] = React7.useState(null);
|
|
770
|
+
const [sessionSecret] = React7.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
|
|
178
771
|
const issueViewerToken = useAction(api.issueViewerToken);
|
|
179
772
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
180
773
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
181
774
|
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
182
775
|
const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
|
|
183
|
-
const refreshTimerRef =
|
|
184
|
-
const reissueInFlightRef =
|
|
185
|
-
const scheduleRefresh =
|
|
776
|
+
const refreshTimerRef = React7.useRef(null);
|
|
777
|
+
const reissueInFlightRef = React7.useRef(null);
|
|
778
|
+
const scheduleRefresh = React7.useCallback(
|
|
186
779
|
(ttlMs) => {
|
|
187
780
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
188
781
|
const refreshIn = Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS);
|
|
@@ -196,7 +789,7 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
196
789
|
},
|
|
197
790
|
[issueViewerToken, sessionSecret]
|
|
198
791
|
);
|
|
199
|
-
|
|
792
|
+
React7.useEffect(() => {
|
|
200
793
|
if (token) return;
|
|
201
794
|
issueViewerToken({ sessionSecret }).then((result) => {
|
|
202
795
|
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
@@ -204,7 +797,7 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
204
797
|
scheduleRefresh(ttlMs);
|
|
205
798
|
});
|
|
206
799
|
}, [token, issueViewerToken, scheduleRefresh, sessionSecret]);
|
|
207
|
-
const reissueToken =
|
|
800
|
+
const reissueToken = React7.useCallback(() => {
|
|
208
801
|
if (reissueInFlightRef.current) return reissueInFlightRef.current;
|
|
209
802
|
const pending = issueViewerToken({ sessionSecret }).then((result) => {
|
|
210
803
|
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
@@ -219,19 +812,19 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
219
812
|
reissueInFlightRef.current = pending;
|
|
220
813
|
return pending;
|
|
221
814
|
}, [issueViewerToken, scheduleRefresh, sessionSecret]);
|
|
222
|
-
|
|
815
|
+
React7.useEffect(() => {
|
|
223
816
|
return () => {
|
|
224
817
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
225
818
|
};
|
|
226
819
|
}, []);
|
|
227
|
-
const uploadUrl =
|
|
820
|
+
const uploadUrl = React7.useCallback(
|
|
228
821
|
async (file) => {
|
|
229
822
|
const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
|
|
230
823
|
return { url: result.url, key: result.key };
|
|
231
824
|
},
|
|
232
825
|
[createUploadUrlAction]
|
|
233
826
|
);
|
|
234
|
-
const upload =
|
|
827
|
+
const upload = React7.useCallback(
|
|
235
828
|
async (file, options) => {
|
|
236
829
|
const dimensions = await readImageDimensions(file);
|
|
237
830
|
const target = await createUploadUrlAction({
|
|
@@ -252,21 +845,21 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
252
845
|
},
|
|
253
846
|
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
254
847
|
);
|
|
255
|
-
const url =
|
|
848
|
+
const url = React7.useCallback(
|
|
256
849
|
(media, options) => {
|
|
257
850
|
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
258
|
-
return
|
|
851
|
+
return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
259
852
|
},
|
|
260
853
|
[baseUrl, token]
|
|
261
854
|
);
|
|
262
|
-
const getCaptionsText =
|
|
855
|
+
const getCaptionsText = React7.useCallback(
|
|
263
856
|
async (media, locale) => {
|
|
264
857
|
if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
|
|
265
858
|
const path = mediaCaptionPath(media, locale);
|
|
266
859
|
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
267
860
|
const isPrivate = media.visibility === "private";
|
|
268
861
|
const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
|
|
269
|
-
headers: sessionSecret ? { [
|
|
862
|
+
headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
|
|
270
863
|
});
|
|
271
864
|
let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
|
|
272
865
|
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
@@ -278,17 +871,17 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
278
871
|
},
|
|
279
872
|
[baseUrl, token, reissueToken, sessionSecret]
|
|
280
873
|
);
|
|
281
|
-
const deleteMedia =
|
|
874
|
+
const deleteMedia = React7.useCallback(
|
|
282
875
|
(media) => {
|
|
283
876
|
if (media.kind === "video" && api.deleteVideo) {
|
|
284
877
|
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
285
878
|
return;
|
|
286
879
|
}
|
|
287
|
-
deleteMediaAction({ key:
|
|
880
|
+
deleteMediaAction({ key: mediaPath2(media) }).catch(console.error);
|
|
288
881
|
},
|
|
289
882
|
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
290
883
|
);
|
|
291
|
-
const devulturValue =
|
|
884
|
+
const devulturValue = React7.useMemo(
|
|
292
885
|
() => ({
|
|
293
886
|
token,
|
|
294
887
|
baseUrl: baseUrl ?? null,
|
|
@@ -302,9 +895,9 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
302
895
|
}),
|
|
303
896
|
[token, baseUrl, sessionSecret, uploadUrl, upload, url, getCaptionsText, deleteMedia, reissueToken]
|
|
304
897
|
);
|
|
305
|
-
let content = /* @__PURE__ */
|
|
898
|
+
let content = /* @__PURE__ */ jsx6(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx6(DevulturContext, { value: devulturValue, children }) });
|
|
306
899
|
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
307
|
-
content = /* @__PURE__ */
|
|
900
|
+
content = /* @__PURE__ */ jsx6(
|
|
308
901
|
ProgressBridge,
|
|
309
902
|
{
|
|
310
903
|
save: api.saveProgress,
|
|
@@ -328,13 +921,13 @@ function ProgressBridge({
|
|
|
328
921
|
const saveMutation = useMutation(save);
|
|
329
922
|
const markCompleteMutation = useMutation(markComplete);
|
|
330
923
|
const resetMutation = useMutation(reset);
|
|
331
|
-
const onSave =
|
|
924
|
+
const onSave = React7.useCallback(
|
|
332
925
|
async (videoId, position) => {
|
|
333
926
|
await saveMutation({ videoId, position });
|
|
334
927
|
},
|
|
335
928
|
[saveMutation]
|
|
336
929
|
);
|
|
337
|
-
const onLoad =
|
|
930
|
+
const onLoad = React7.useCallback(
|
|
338
931
|
async (videoId) => {
|
|
339
932
|
const result = await convex.query(load, { videoId });
|
|
340
933
|
if (!result) return null;
|
|
@@ -345,37 +938,37 @@ function ProgressBridge({
|
|
|
345
938
|
},
|
|
346
939
|
[convex, load]
|
|
347
940
|
);
|
|
348
|
-
const onMarkComplete =
|
|
941
|
+
const onMarkComplete = React7.useCallback(
|
|
349
942
|
async (videoId) => {
|
|
350
943
|
await markCompleteMutation({ videoId });
|
|
351
944
|
},
|
|
352
945
|
[markCompleteMutation]
|
|
353
946
|
);
|
|
354
|
-
const onReset =
|
|
947
|
+
const onReset = React7.useCallback(
|
|
355
948
|
async (videoId) => {
|
|
356
949
|
await resetMutation({ videoId });
|
|
357
950
|
},
|
|
358
951
|
[resetMutation]
|
|
359
952
|
);
|
|
360
|
-
const value =
|
|
953
|
+
const value = React7.useMemo(
|
|
361
954
|
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
362
955
|
[onSave, onLoad, onMarkComplete, onReset]
|
|
363
956
|
);
|
|
364
|
-
return /* @__PURE__ */
|
|
957
|
+
return /* @__PURE__ */ jsx6(ProgressContext, { value, children });
|
|
365
958
|
}
|
|
366
959
|
function useDevultur() {
|
|
367
|
-
const context =
|
|
960
|
+
const context = React7.use(DevulturContext);
|
|
368
961
|
if (!context) throw new Error("useDevultur must be used within <DevulturProvider>");
|
|
369
962
|
return context;
|
|
370
963
|
}
|
|
371
964
|
function useDevulturProgress() {
|
|
372
|
-
const context =
|
|
965
|
+
const context = React7.use(ProgressContext);
|
|
373
966
|
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
374
967
|
return context;
|
|
375
968
|
}
|
|
376
969
|
function useMediaUrl(key, options) {
|
|
377
970
|
const { token, baseUrl } = useDevultur();
|
|
378
|
-
return
|
|
971
|
+
return React7.useMemo(() => {
|
|
379
972
|
if (!key) return null;
|
|
380
973
|
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
381
974
|
const isPublic = visibilityFromKey(key) === "public";
|
|
@@ -403,7 +996,7 @@ async function readImageDimensions(file) {
|
|
|
403
996
|
function useVideoProps(media, options) {
|
|
404
997
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
405
998
|
const locales = options?.locales;
|
|
406
|
-
return
|
|
999
|
+
return React7.useMemo(() => {
|
|
407
1000
|
if (media?.kind !== "video") return null;
|
|
408
1001
|
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
409
1002
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
@@ -414,7 +1007,7 @@ function useVideoProps(media, options) {
|
|
|
414
1007
|
return [{ locale, label, src: buildMediaUrl(baseUrl, path, { token: viewerToken }) }];
|
|
415
1008
|
});
|
|
416
1009
|
return {
|
|
417
|
-
src: buildMediaUrl(baseUrl,
|
|
1010
|
+
src: buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken }),
|
|
418
1011
|
token: viewerToken,
|
|
419
1012
|
captions,
|
|
420
1013
|
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
@@ -434,25 +1027,32 @@ function Image({
|
|
|
434
1027
|
alt,
|
|
435
1028
|
...imgProps
|
|
436
1029
|
}) {
|
|
437
|
-
const { url, reissueToken, sessionSecret } = useDevultur();
|
|
438
|
-
const [failed, setFailed] =
|
|
439
|
-
const [blobSrc, setBlobSrc] =
|
|
440
|
-
const
|
|
1030
|
+
const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
|
|
1031
|
+
const [failed, setFailed] = React7.useState(false);
|
|
1032
|
+
const [blobSrc, setBlobSrc] = React7.useState(null);
|
|
1033
|
+
const sessionRef = React7.useRef({ token, reissueToken });
|
|
1034
|
+
React7.useEffect(() => {
|
|
1035
|
+
sessionRef.current = { token, reissueToken };
|
|
1036
|
+
});
|
|
441
1037
|
const isPrivate = media?.visibility === "private";
|
|
442
|
-
|
|
443
|
-
|
|
1038
|
+
const hasToken = token !== null;
|
|
1039
|
+
React7.useEffect(() => {
|
|
444
1040
|
setFailed(false);
|
|
445
1041
|
setBlobSrc(null);
|
|
446
1042
|
}, [media]);
|
|
447
|
-
|
|
448
|
-
if (!isPrivate || failed || !media) return;
|
|
1043
|
+
React7.useEffect(() => {
|
|
1044
|
+
if (!isPrivate || failed || !media || !hasToken) return;
|
|
449
1045
|
let cancelled = false;
|
|
450
1046
|
let objectUrl = null;
|
|
451
|
-
|
|
1047
|
+
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
1048
|
+
const transforms = { width, height, fit, format, quality };
|
|
452
1049
|
(async () => {
|
|
453
1050
|
try {
|
|
454
|
-
const response = await
|
|
455
|
-
|
|
1051
|
+
const response = await fetchMedia({
|
|
1052
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...transforms, token: viewerToken }),
|
|
1053
|
+
token: sessionRef.current.token,
|
|
1054
|
+
sessionSecret,
|
|
1055
|
+
reissueToken: sessionRef.current.reissueToken
|
|
456
1056
|
});
|
|
457
1057
|
if (response.ok) {
|
|
458
1058
|
objectUrl = URL.createObjectURL(await response.blob());
|
|
@@ -463,11 +1063,6 @@ function Image({
|
|
|
463
1063
|
setBlobSrc(objectUrl);
|
|
464
1064
|
return;
|
|
465
1065
|
}
|
|
466
|
-
if (response.status === 401 && !retriedRef.current && await isSessionMismatch(response)) {
|
|
467
|
-
retriedRef.current = true;
|
|
468
|
-
await reissueToken();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
1066
|
} catch {
|
|
472
1067
|
}
|
|
473
1068
|
if (!cancelled) setFailed(true);
|
|
@@ -476,8 +1071,8 @@ function Image({
|
|
|
476
1071
|
cancelled = true;
|
|
477
1072
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
478
1073
|
};
|
|
479
|
-
}, [isPrivate, failed, media,
|
|
480
|
-
const handleError =
|
|
1074
|
+
}, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
1075
|
+
const handleError = React7.useCallback(() => setFailed(true), []);
|
|
481
1076
|
let src;
|
|
482
1077
|
if (failed || !media) {
|
|
483
1078
|
src = externalFallback ?? null;
|
|
@@ -486,13 +1081,13 @@ function Image({
|
|
|
486
1081
|
} else {
|
|
487
1082
|
src = url(media, { width, height, fit, format, quality });
|
|
488
1083
|
}
|
|
489
|
-
if (!src) return /* @__PURE__ */
|
|
1084
|
+
if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
|
|
490
1085
|
if (render) {
|
|
491
|
-
return
|
|
1086
|
+
return React7.cloneElement(render, {
|
|
492
1087
|
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
493
1088
|
});
|
|
494
1089
|
}
|
|
495
|
-
return /* @__PURE__ */
|
|
1090
|
+
return /* @__PURE__ */ jsx6(
|
|
496
1091
|
"img",
|
|
497
1092
|
{
|
|
498
1093
|
src,
|
|
@@ -506,17 +1101,16 @@ function Image({
|
|
|
506
1101
|
}
|
|
507
1102
|
function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
508
1103
|
const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
509
|
-
const handleClick =
|
|
1104
|
+
const handleClick = React7.useCallback(
|
|
510
1105
|
async (event) => {
|
|
511
1106
|
if (media?.visibility !== "private" || !baseUrl) return;
|
|
512
1107
|
event.preventDefault();
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
1108
|
+
const response = await fetchMedia({
|
|
1109
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
1110
|
+
token,
|
|
1111
|
+
sessionSecret,
|
|
1112
|
+
reissueToken
|
|
1113
|
+
});
|
|
520
1114
|
if (!response.ok) return;
|
|
521
1115
|
const objectUrl = URL.createObjectURL(await response.blob());
|
|
522
1116
|
const anchor = document.createElement("a");
|
|
@@ -528,10 +1122,10 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
|
528
1122
|
[media, baseUrl, token, filename, sessionSecret, reissueToken]
|
|
529
1123
|
);
|
|
530
1124
|
if (!media) return null;
|
|
531
|
-
return /* @__PURE__ */
|
|
1125
|
+
return /* @__PURE__ */ jsx6("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
|
|
532
1126
|
}
|
|
533
1127
|
function useDevulturMedia(key) {
|
|
534
|
-
const apiCtx =
|
|
1128
|
+
const apiCtx = React7.use(DevulturApiContext);
|
|
535
1129
|
if (!apiCtx?.getMedia) {
|
|
536
1130
|
throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
|
|
537
1131
|
}
|
|
@@ -588,7 +1182,7 @@ function useVideoState(media, options) {
|
|
|
588
1182
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
589
1183
|
const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
|
|
590
1184
|
const locales = options?.locales;
|
|
591
|
-
return
|
|
1185
|
+
return React7.useMemo(() => {
|
|
592
1186
|
if (media?.kind !== "video") return { phase: "idle" };
|
|
593
1187
|
if (status.isFailed) {
|
|
594
1188
|
return {
|
|
@@ -601,7 +1195,7 @@ function useVideoState(media, options) {
|
|
|
601
1195
|
if (status.isReady) {
|
|
602
1196
|
if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
|
|
603
1197
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
604
|
-
const playlist = buildMediaUrl(baseUrl,
|
|
1198
|
+
const playlist = buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken });
|
|
605
1199
|
const captions = (locales ?? status.captionLocales).flatMap((locale) => {
|
|
606
1200
|
const path = mediaCaptionPath(media, locale);
|
|
607
1201
|
if (!path) return [];
|
|
@@ -632,23 +1226,23 @@ function useVideoState(media, options) {
|
|
|
632
1226
|
}
|
|
633
1227
|
function useMediaUpload(options) {
|
|
634
1228
|
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
635
|
-
const api =
|
|
1229
|
+
const api = React7.use(DevulturApiContext);
|
|
636
1230
|
if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
|
|
637
1231
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
638
1232
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
639
1233
|
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
640
1234
|
const processVideoAction = useAction(api.processVideo ?? api.createUploadUrl);
|
|
641
|
-
const [phase, setPhase] =
|
|
642
|
-
const [percent, setPercent] =
|
|
643
|
-
const [currentFile, setCurrentFile] =
|
|
644
|
-
const [media, setMedia] =
|
|
645
|
-
const [uploadFailure, setUploadFailure] =
|
|
646
|
-
const [processRequested, setProcessRequested] =
|
|
647
|
-
const abortRef =
|
|
648
|
-
const lastFileRef =
|
|
649
|
-
const lastKeyRef =
|
|
1235
|
+
const [phase, setPhase] = React7.useState("idle");
|
|
1236
|
+
const [percent, setPercent] = React7.useState(0);
|
|
1237
|
+
const [currentFile, setCurrentFile] = React7.useState(null);
|
|
1238
|
+
const [media, setMedia] = React7.useState(null);
|
|
1239
|
+
const [uploadFailure, setUploadFailure] = React7.useState(null);
|
|
1240
|
+
const [processRequested, setProcessRequested] = React7.useState(false);
|
|
1241
|
+
const abortRef = React7.useRef(null);
|
|
1242
|
+
const lastFileRef = React7.useRef(null);
|
|
1243
|
+
const lastKeyRef = React7.useRef(null);
|
|
650
1244
|
const videoState = useVideoState(media, { locales });
|
|
651
|
-
const triggerProcess =
|
|
1245
|
+
const triggerProcess = React7.useCallback(
|
|
652
1246
|
(key) => {
|
|
653
1247
|
if (!api.processVideo) {
|
|
654
1248
|
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
@@ -661,7 +1255,7 @@ function useMediaUpload(options) {
|
|
|
661
1255
|
},
|
|
662
1256
|
[api.processVideo, processVideoAction]
|
|
663
1257
|
);
|
|
664
|
-
const runUpload =
|
|
1258
|
+
const runUpload = React7.useCallback(
|
|
665
1259
|
async (file) => {
|
|
666
1260
|
const controller = new AbortController();
|
|
667
1261
|
abortRef.current = controller;
|
|
@@ -713,7 +1307,7 @@ function useMediaUpload(options) {
|
|
|
713
1307
|
};
|
|
714
1308
|
setMedia(uploaded);
|
|
715
1309
|
setPhase("uploaded");
|
|
716
|
-
if (replaces) deleteMediaAction({ key:
|
|
1310
|
+
if (replaces) deleteMediaAction({ key: mediaPath2(replaces) }).catch(() => {
|
|
717
1311
|
});
|
|
718
1312
|
onComplete?.(uploaded);
|
|
719
1313
|
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
@@ -732,10 +1326,10 @@ function useMediaUpload(options) {
|
|
|
732
1326
|
onError
|
|
733
1327
|
]
|
|
734
1328
|
);
|
|
735
|
-
const cancel =
|
|
1329
|
+
const cancel = React7.useCallback(() => {
|
|
736
1330
|
abortRef.current?.abort();
|
|
737
1331
|
}, []);
|
|
738
|
-
const reset =
|
|
1332
|
+
const reset = React7.useCallback(() => {
|
|
739
1333
|
abortRef.current?.abort();
|
|
740
1334
|
setPhase("idle");
|
|
741
1335
|
setPercent(0);
|
|
@@ -744,15 +1338,15 @@ function useMediaUpload(options) {
|
|
|
744
1338
|
setUploadFailure(null);
|
|
745
1339
|
setProcessRequested(false);
|
|
746
1340
|
}, []);
|
|
747
|
-
const retry =
|
|
1341
|
+
const retry = React7.useCallback(() => {
|
|
748
1342
|
if (phase === "upload-failed" && lastFileRef.current) {
|
|
749
1343
|
runUpload(lastFileRef.current);
|
|
750
1344
|
return;
|
|
751
1345
|
}
|
|
752
1346
|
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
753
1347
|
}, [phase, runUpload, triggerProcess]);
|
|
754
|
-
|
|
755
|
-
const state =
|
|
1348
|
+
React7.useEffect(() => () => abortRef.current?.abort(), []);
|
|
1349
|
+
const state = React7.useMemo(
|
|
756
1350
|
() => deriveUploadState(
|
|
757
1351
|
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
758
1352
|
videoState,
|
|
@@ -760,7 +1354,7 @@ function useMediaUpload(options) {
|
|
|
760
1354
|
),
|
|
761
1355
|
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
762
1356
|
);
|
|
763
|
-
const upload =
|
|
1357
|
+
const upload = React7.useCallback((file) => runUpload(file), [runUpload]);
|
|
764
1358
|
return { upload, state, cancel, reset };
|
|
765
1359
|
}
|
|
766
1360
|
export {
|
|
@@ -768,9 +1362,14 @@ export {
|
|
|
768
1362
|
DevulturProvider,
|
|
769
1363
|
DownloadLink,
|
|
770
1364
|
Image,
|
|
1365
|
+
MediaFetchError,
|
|
1366
|
+
PdfPreviewProvider,
|
|
1367
|
+
PdfThumbnail,
|
|
1368
|
+
Preview,
|
|
771
1369
|
useDevultur,
|
|
772
1370
|
useDevulturMedia,
|
|
773
1371
|
useDevulturProgress,
|
|
1372
|
+
useMediaObjectUrl,
|
|
774
1373
|
useMediaUpload,
|
|
775
1374
|
useMediaUrl,
|
|
776
1375
|
useVideoProps,
|