@devultur/convex 1.2.0 → 1.3.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 +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 +181 -1
- package/dist/react.js +757 -90
- package/dist/validators.d.ts +5 -5
- 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,528 @@ 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
|
+
|
|
385
|
+
// src/preview-labels.ts
|
|
386
|
+
var ERROR_TEXT = {
|
|
387
|
+
http_error: "The file couldn't be loaded.",
|
|
388
|
+
too_large: "The file is too large to preview.",
|
|
389
|
+
unsupported_type: "This file type can't be previewed.",
|
|
390
|
+
type_mismatch: "The file's content doesn't match its type."
|
|
391
|
+
};
|
|
392
|
+
var defaultPreviewLabels = {
|
|
393
|
+
document: (title, pageCount) => `${title}, ${pageCount === 1 ? "1 page" : `${pageCount} pages`}`,
|
|
394
|
+
page: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
395
|
+
loading: "Loading\u2026",
|
|
396
|
+
pageCap: (shown, total) => `Showing the first ${shown} of ${total} pages.`,
|
|
397
|
+
error: (code) => ERROR_TEXT[code],
|
|
398
|
+
thumbnail: (title) => title,
|
|
399
|
+
untitledPdf: "PDF",
|
|
400
|
+
untitledMedia: "Preview",
|
|
401
|
+
download: "Download"
|
|
402
|
+
};
|
|
403
|
+
function mergePreviewLabels(...sources) {
|
|
404
|
+
const merged = { ...defaultPreviewLabels };
|
|
405
|
+
for (const source of sources) {
|
|
406
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
407
|
+
if (value !== void 0) merged[key] = value;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return merged;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// src/pdf-context.tsx
|
|
414
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
415
|
+
var PdfPreviewContext = React3.createContext({});
|
|
416
|
+
function PdfPreviewProvider({
|
|
417
|
+
pdfjs,
|
|
418
|
+
workerSrc,
|
|
419
|
+
labels,
|
|
420
|
+
children
|
|
421
|
+
}) {
|
|
422
|
+
const config = React3.useMemo(() => ({ pdfjs, workerSrc, labels }), [pdfjs, workerSrc, labels]);
|
|
423
|
+
return /* @__PURE__ */ jsx2(PdfPreviewContext.Provider, { value: config, children });
|
|
424
|
+
}
|
|
425
|
+
var loadedModules = /* @__PURE__ */ new WeakMap();
|
|
426
|
+
function loadOnce(loader) {
|
|
427
|
+
const cached = loadedModules.get(loader);
|
|
428
|
+
if (cached) return cached;
|
|
429
|
+
const loading = loader().catch(() => null);
|
|
430
|
+
loadedModules.set(loader, loading);
|
|
431
|
+
return loading;
|
|
432
|
+
}
|
|
433
|
+
function usePdfjs(workerSrcOverride) {
|
|
434
|
+
const { pdfjs: loader, workerSrc } = React3.useContext(PdfPreviewContext);
|
|
435
|
+
const [loaded, setLoaded] = React3.useState(null);
|
|
436
|
+
React3.useEffect(() => {
|
|
437
|
+
if (!loader) return;
|
|
438
|
+
let isCurrent = true;
|
|
439
|
+
loadOnce(loader).then((pdfjs) => {
|
|
440
|
+
if (isCurrent) setLoaded({ loader, pdfjs });
|
|
441
|
+
});
|
|
442
|
+
return () => {
|
|
443
|
+
isCurrent = false;
|
|
444
|
+
};
|
|
445
|
+
}, [loader]);
|
|
446
|
+
if (!loader) return { status: "unavailable" };
|
|
447
|
+
if (loaded?.loader !== loader) return { status: "loading" };
|
|
448
|
+
if (!loaded.pdfjs) return { status: "unavailable" };
|
|
449
|
+
return { status: "ready", pdfjs: loaded.pdfjs, workerSrc: workerSrcOverride ?? workerSrc };
|
|
450
|
+
}
|
|
451
|
+
function usePreviewLabels(labels) {
|
|
452
|
+
const providerLabels = React3.useContext(PdfPreviewContext).labels;
|
|
453
|
+
return mergePreviewLabels(providerLabels, labels);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/pdf-thumbnail.tsx
|
|
457
|
+
import React6 from "react";
|
|
458
|
+
|
|
459
|
+
// src/pdf-hooks.ts
|
|
460
|
+
import React4 from "react";
|
|
461
|
+
|
|
462
|
+
// src/pdf-render.ts
|
|
463
|
+
var MOBILE_MAX_CANVAS_PIXELS = 5242880;
|
|
464
|
+
var DESKTOP_MAX_CANVAS_PIXELS = 2 ** 25;
|
|
465
|
+
function openPdf(pdfjs, bytes, workerSrc) {
|
|
466
|
+
if (workerSrc) pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
|
|
467
|
+
const source = { data: bytes.slice(), enableScripting: false, isEvalSupported: false };
|
|
468
|
+
return pdfjs.getDocument(source);
|
|
469
|
+
}
|
|
470
|
+
function isMobileDevice() {
|
|
471
|
+
if (typeof navigator === "undefined") return false;
|
|
472
|
+
return /Android|iPhone|iPad|iPod/.test(navigator.userAgent) || navigator.maxTouchPoints > 1;
|
|
473
|
+
}
|
|
474
|
+
function maxCanvasPixels() {
|
|
475
|
+
return isMobileDevice() ? MOBILE_MAX_CANVAS_PIXELS : DESKTOP_MAX_CANVAS_PIXELS;
|
|
476
|
+
}
|
|
477
|
+
function outputScale(cssWidth, cssHeight, maxPixels) {
|
|
478
|
+
const devicePixelRatio = typeof window !== "undefined" && window.devicePixelRatio || 1;
|
|
479
|
+
const cssPixels = cssWidth * cssHeight;
|
|
480
|
+
if (cssPixels <= 0) return devicePixelRatio;
|
|
481
|
+
if (cssPixels * devicePixelRatio ** 2 <= maxPixels) return devicePixelRatio;
|
|
482
|
+
return Math.sqrt(maxPixels / cssPixels);
|
|
483
|
+
}
|
|
484
|
+
function renderPage(page, canvas, cssWidth) {
|
|
485
|
+
const naturalViewport = page.getViewport({ scale: 1 });
|
|
486
|
+
const cssScale = cssWidth > 0 ? cssWidth / naturalViewport.width : 1;
|
|
487
|
+
const cssViewport = page.getViewport({ scale: cssScale });
|
|
488
|
+
const scale = outputScale(cssViewport.width, cssViewport.height, maxCanvasPixels());
|
|
489
|
+
const viewport = page.getViewport({ scale: cssScale * scale });
|
|
490
|
+
canvas.width = Math.floor(viewport.width);
|
|
491
|
+
canvas.height = Math.floor(viewport.height);
|
|
492
|
+
canvas.style.width = `${Math.floor(cssViewport.width)}px`;
|
|
493
|
+
canvas.style.height = `${Math.floor(cssViewport.height)}px`;
|
|
494
|
+
return page.render({ canvas, viewport });
|
|
495
|
+
}
|
|
496
|
+
async function pageText(page) {
|
|
497
|
+
const { items } = await page.getTextContent();
|
|
498
|
+
return items.map((item) => "str" in item && typeof item.str === "string" ? item.str : "").join(" ").replace(/\s+/g, " ").trim();
|
|
499
|
+
}
|
|
500
|
+
var isRenderCancelled = (error) => error instanceof Error && error.name === "RenderingCancelledException";
|
|
501
|
+
|
|
502
|
+
// src/pdf-hooks.ts
|
|
503
|
+
function useIsVisible(ref, rootMargin = "200px") {
|
|
504
|
+
const [isVisible, setIsVisible] = React4.useState(false);
|
|
505
|
+
React4.useEffect(() => {
|
|
506
|
+
const element = ref.current;
|
|
507
|
+
if (isVisible || !element) return;
|
|
508
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
509
|
+
setIsVisible(true);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const observer = new IntersectionObserver(
|
|
513
|
+
(entries) => {
|
|
514
|
+
if (!entries.some((entry) => entry.isIntersecting)) return;
|
|
515
|
+
setIsVisible(true);
|
|
516
|
+
observer.disconnect();
|
|
517
|
+
},
|
|
518
|
+
{ rootMargin }
|
|
519
|
+
);
|
|
520
|
+
observer.observe(element);
|
|
521
|
+
return () => observer.disconnect();
|
|
522
|
+
}, [ref, rootMargin, isVisible]);
|
|
523
|
+
return isVisible;
|
|
524
|
+
}
|
|
525
|
+
function usePdfDocument({ pdfjs, bytes, workerSrc }) {
|
|
526
|
+
const [state, setState] = React4.useState({
|
|
527
|
+
bytes: null,
|
|
528
|
+
document: null,
|
|
529
|
+
error: null
|
|
530
|
+
});
|
|
531
|
+
React4.useEffect(() => {
|
|
532
|
+
let isCurrent = true;
|
|
533
|
+
const loadingTask = openPdf(pdfjs, bytes, workerSrc);
|
|
534
|
+
loadingTask.promise.then((document2) => {
|
|
535
|
+
if (isCurrent) setState({ bytes, document: document2, error: null });
|
|
536
|
+
}).catch((error) => {
|
|
537
|
+
if (!isCurrent) return;
|
|
538
|
+
setState({ bytes, document: null, error: error instanceof Error ? error : new Error(String(error)) });
|
|
539
|
+
});
|
|
540
|
+
return () => {
|
|
541
|
+
isCurrent = false;
|
|
542
|
+
void loadingTask.destroy();
|
|
543
|
+
};
|
|
544
|
+
}, [pdfjs, bytes, workerSrc]);
|
|
545
|
+
if (state.bytes !== bytes) return { document: null, error: null };
|
|
546
|
+
return { document: state.document, error: state.error };
|
|
547
|
+
}
|
|
548
|
+
function usePageRender(options) {
|
|
549
|
+
const { document: document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText = false } = options;
|
|
550
|
+
const [state, setState] = React4.useState({ isRendered: false, text: "" });
|
|
551
|
+
React4.useEffect(() => {
|
|
552
|
+
const canvas = canvasRef.current;
|
|
553
|
+
if (!document2 || !isEnabled || !canvas) return;
|
|
554
|
+
let isCurrent = true;
|
|
555
|
+
let page = null;
|
|
556
|
+
let renderTask = null;
|
|
557
|
+
const draw = async () => {
|
|
558
|
+
page = await document2.getPage(pageNumber);
|
|
559
|
+
if (!isCurrent) return;
|
|
560
|
+
renderTask = renderPage(page, canvas, width);
|
|
561
|
+
await renderTask.promise;
|
|
562
|
+
if (isCurrent) setState((previous) => ({ ...previous, isRendered: true }));
|
|
563
|
+
if (!shouldExtractText) return;
|
|
564
|
+
const text = await pageText(page);
|
|
565
|
+
if (isCurrent) setState((previous) => ({ ...previous, text }));
|
|
566
|
+
};
|
|
567
|
+
draw().catch((error) => {
|
|
568
|
+
if (isRenderCancelled(error) || !isCurrent) return;
|
|
569
|
+
console.warn(`[devultur] PDF page ${pageNumber} failed to render`, error);
|
|
570
|
+
});
|
|
571
|
+
return () => {
|
|
572
|
+
isCurrent = false;
|
|
573
|
+
renderTask?.cancel();
|
|
574
|
+
page?.cleanup();
|
|
575
|
+
};
|
|
576
|
+
}, [document2, pageNumber, canvasRef, width, isEnabled, shouldExtractText]);
|
|
577
|
+
return state;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/pdf-preview.tsx
|
|
581
|
+
import React5 from "react";
|
|
582
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
583
|
+
var VISUALLY_HIDDEN = {
|
|
584
|
+
position: "absolute",
|
|
585
|
+
width: 1,
|
|
586
|
+
height: 1,
|
|
587
|
+
margin: -1,
|
|
588
|
+
padding: 0,
|
|
589
|
+
overflow: "hidden",
|
|
590
|
+
clip: "rect(0 0 0 0)",
|
|
591
|
+
whiteSpace: "nowrap",
|
|
592
|
+
border: 0
|
|
593
|
+
};
|
|
594
|
+
var DEFAULT_MAX_PAGES = 500;
|
|
595
|
+
var keepBytes = (bytes) => bytes;
|
|
596
|
+
function PdfPreview(props) {
|
|
597
|
+
const { media, expectedType, maxBytes, renderFallback, loading, labels, title, className, pdfWorkerSrc } = props;
|
|
598
|
+
const pdfjsState = usePdfjs(pdfWorkerSrc);
|
|
599
|
+
const { value: bytes, error } = useMediaResource(media, {
|
|
600
|
+
isEnabled: true,
|
|
601
|
+
maxBytes,
|
|
602
|
+
contentType: expectedType,
|
|
603
|
+
decode: keepBytes
|
|
604
|
+
});
|
|
605
|
+
if (error) return /* @__PURE__ */ jsx3(Fragment, { children: renderFallback(error) });
|
|
606
|
+
if (!bytes || pdfjsState.status === "loading") return /* @__PURE__ */ jsx3(Fragment, { children: loading });
|
|
607
|
+
const label = title ?? media.filename ?? labels.untitledPdf;
|
|
608
|
+
if (pdfjsState.status === "unavailable") return /* @__PURE__ */ jsx3(PdfFrame, { bytes, label, className });
|
|
609
|
+
return /* @__PURE__ */ jsx3(
|
|
610
|
+
PdfCanvasDocument,
|
|
611
|
+
{
|
|
612
|
+
pdfjs: pdfjsState.pdfjs,
|
|
613
|
+
workerSrc: pdfjsState.workerSrc,
|
|
614
|
+
bytes,
|
|
615
|
+
label,
|
|
616
|
+
className,
|
|
617
|
+
renderFallback,
|
|
618
|
+
loading,
|
|
619
|
+
labels,
|
|
620
|
+
maxPages: props.maxPages ?? DEFAULT_MAX_PAGES
|
|
621
|
+
}
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
function PdfCanvasDocument(props) {
|
|
625
|
+
const { pdfjs, workerSrc, bytes, label, className, renderFallback, loading, labels, maxPages } = props;
|
|
626
|
+
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
627
|
+
const containerRef = React5.useRef(null);
|
|
628
|
+
const [width, setWidth] = React5.useState(null);
|
|
629
|
+
React5.useLayoutEffect(() => {
|
|
630
|
+
if (document2) setWidth(containerRef.current?.clientWidth ?? 0);
|
|
631
|
+
}, [document2]);
|
|
632
|
+
if (error) return /* @__PURE__ */ jsx3(Fragment, { children: renderFallback(error) });
|
|
633
|
+
if (!document2) return /* @__PURE__ */ jsx3(Fragment, { children: loading });
|
|
634
|
+
const shownPageCount = Math.min(document2.numPages, Math.max(1, maxPages));
|
|
635
|
+
const isTruncated = shownPageCount < document2.numPages;
|
|
636
|
+
const pageNumbers = Array.from({ length: shownPageCount }, (_, index) => index + 1);
|
|
637
|
+
return /* @__PURE__ */ jsxs2(
|
|
638
|
+
"div",
|
|
639
|
+
{
|
|
640
|
+
ref: containerRef,
|
|
641
|
+
role: "document",
|
|
642
|
+
"aria-label": labels.document(label, document2.numPages),
|
|
643
|
+
className,
|
|
644
|
+
children: [
|
|
645
|
+
width !== null && pageNumbers.map((pageNumber) => /* @__PURE__ */ jsx3(
|
|
646
|
+
PdfPageView,
|
|
647
|
+
{
|
|
648
|
+
document: document2,
|
|
649
|
+
pageNumber,
|
|
650
|
+
width,
|
|
651
|
+
label: labels.page(pageNumber, document2.numPages)
|
|
652
|
+
},
|
|
653
|
+
pageNumber
|
|
654
|
+
)),
|
|
655
|
+
isTruncated && /* @__PURE__ */ jsxs2("p", { children: [
|
|
656
|
+
labels.pageCap(shownPageCount, document2.numPages),
|
|
657
|
+
" ",
|
|
658
|
+
renderFallback(null)
|
|
659
|
+
] })
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
function PdfPageView({ document: document2, pageNumber, width, label }) {
|
|
665
|
+
const pageRef = React5.useRef(null);
|
|
666
|
+
const canvasRef = React5.useRef(null);
|
|
667
|
+
const isVisible = useIsVisible(pageRef);
|
|
668
|
+
const { isRendered, text } = usePageRender({
|
|
669
|
+
document: document2,
|
|
670
|
+
pageNumber,
|
|
671
|
+
canvasRef,
|
|
672
|
+
width,
|
|
673
|
+
isEnabled: isVisible,
|
|
674
|
+
shouldExtractText: true
|
|
675
|
+
});
|
|
676
|
+
const placeholderStyle = isRendered ? void 0 : { aspectRatio: "1 / 1.4142", maxWidth: "100%" };
|
|
677
|
+
return /* @__PURE__ */ jsxs2("section", { ref: pageRef, "aria-label": label, style: placeholderStyle, children: [
|
|
678
|
+
/* @__PURE__ */ jsx3("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%" } }),
|
|
679
|
+
text && /* @__PURE__ */ jsx3("p", { style: VISUALLY_HIDDEN, children: text })
|
|
680
|
+
] });
|
|
681
|
+
}
|
|
682
|
+
function PdfFrame({ bytes, label, className }) {
|
|
683
|
+
const [objectUrl, setObjectUrl] = React5.useState(null);
|
|
684
|
+
React5.useEffect(() => {
|
|
685
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
686
|
+
setObjectUrl(url);
|
|
687
|
+
return () => URL.revokeObjectURL(url);
|
|
688
|
+
}, [bytes]);
|
|
689
|
+
if (!objectUrl) return null;
|
|
690
|
+
return /* @__PURE__ */ jsx3("iframe", { src: objectUrl, title: label, referrerPolicy: "no-referrer", className });
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/pdf-thumbnail.tsx
|
|
694
|
+
import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
695
|
+
var DEFAULT_THUMBNAIL_WIDTH = 96;
|
|
696
|
+
var DEFAULT_THUMBNAIL_MAX_BYTES = 50 * 1024 * 1024;
|
|
697
|
+
function PdfThumbnail(props) {
|
|
698
|
+
const { media, contentType, width = DEFAULT_THUMBNAIL_WIDTH, fallback = null, loading = null } = props;
|
|
699
|
+
const containerRef = React6.useRef(null);
|
|
700
|
+
const isVisible = useIsVisible(containerRef);
|
|
701
|
+
const pdfjsState = usePdfjs(props.pdfWorkerSrc);
|
|
702
|
+
const labels = usePreviewLabels(props.labels);
|
|
703
|
+
const expectedType = media ? expectedContentType(media, contentType) : null;
|
|
704
|
+
const isPdf = previewKindOf(expectedType) === "pdf";
|
|
705
|
+
const { value: bytes, error } = useMediaResource(media, {
|
|
706
|
+
isEnabled: isVisible && isPdf && pdfjsState.status === "ready",
|
|
707
|
+
maxBytes: props.maxBytes ?? DEFAULT_THUMBNAIL_MAX_BYTES,
|
|
708
|
+
contentType: expectedType ?? void 0,
|
|
709
|
+
decode: keepBytes
|
|
710
|
+
});
|
|
711
|
+
if (!media) return null;
|
|
712
|
+
const isUnavailable = !isPdf || pdfjsState.status === "unavailable" || error !== null;
|
|
713
|
+
const content = () => {
|
|
714
|
+
if (isUnavailable) return fallback;
|
|
715
|
+
if (!bytes || pdfjsState.status !== "ready") return loading;
|
|
716
|
+
return /* @__PURE__ */ jsx4(
|
|
717
|
+
ThumbnailCanvas,
|
|
718
|
+
{
|
|
719
|
+
pdfjs: pdfjsState.pdfjs,
|
|
720
|
+
workerSrc: pdfjsState.workerSrc,
|
|
721
|
+
bytes,
|
|
722
|
+
width,
|
|
723
|
+
label: labels.thumbnail(props.title ?? media.filename ?? labels.untitledPdf),
|
|
724
|
+
fallback,
|
|
725
|
+
loading
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
};
|
|
729
|
+
return /* @__PURE__ */ jsx4("div", { ref: containerRef, className: props.className, style: { width }, children: content() });
|
|
730
|
+
}
|
|
731
|
+
function ThumbnailCanvas({ pdfjs, workerSrc, bytes, width, label, fallback, loading }) {
|
|
732
|
+
const canvasRef = React6.useRef(null);
|
|
733
|
+
const { document: document2, error } = usePdfDocument({ pdfjs, bytes, workerSrc });
|
|
734
|
+
const { isRendered } = usePageRender({ document: document2, pageNumber: 1, canvasRef, width, isEnabled: true });
|
|
735
|
+
if (error) return /* @__PURE__ */ jsx4(Fragment2, { children: fallback });
|
|
736
|
+
return /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
737
|
+
!isRendered && loading,
|
|
738
|
+
/* @__PURE__ */ jsx4("canvas", { ref: canvasRef, role: "img", "aria-label": label, style: { display: "block" } })
|
|
739
|
+
] });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/preview.tsx
|
|
743
|
+
import { VideoPlayer } from "@devultur/react";
|
|
744
|
+
import { Fragment as Fragment3, jsx as jsx5 } from "react/jsx-runtime";
|
|
745
|
+
var DEFAULT_MAX_TEXT_BYTES = 1024 * 1024;
|
|
746
|
+
var DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
747
|
+
function Preview({ media, contentType, fallback, loading, labels, ...viewProps }) {
|
|
748
|
+
const text = usePreviewLabels(labels);
|
|
749
|
+
if (!media) return null;
|
|
750
|
+
if (media.kind === "video") return /* @__PURE__ */ jsx5(VideoPreview, { media, className: viewProps.className });
|
|
751
|
+
const renderFallback = (error) => {
|
|
752
|
+
const fetchError = error instanceof MediaFetchError ? error : null;
|
|
753
|
+
if (typeof fallback === "function") return fallback(fetchError);
|
|
754
|
+
if (fallback !== void 0) return fallback;
|
|
755
|
+
return /* @__PURE__ */ jsx5(DefaultFallback, { media, error: fetchError, labels: text });
|
|
756
|
+
};
|
|
757
|
+
const expectedType = expectedContentType(media, contentType);
|
|
758
|
+
const kind = previewKindOf(expectedType);
|
|
759
|
+
if (!expectedType || !kind) return /* @__PURE__ */ jsx5(Fragment3, { children: renderFallback(unsupportedType(expectedType)) });
|
|
760
|
+
const loadingNode = loading === void 0 ? /* @__PURE__ */ jsx5(LoadingStatus, { text: text.loading }) : loading;
|
|
761
|
+
const props = { ...viewProps, media, expectedType, renderFallback, loading: loadingNode, labels: text };
|
|
762
|
+
if (kind === "text") return /* @__PURE__ */ jsx5(TextPreview, { ...props });
|
|
763
|
+
if (kind === "pdf") return /* @__PURE__ */ jsx5(PdfPreview, { ...props, maxBytes: props.maxBytes ?? DEFAULT_MAX_BYTES });
|
|
764
|
+
return /* @__PURE__ */ jsx5(BinaryPreview, { ...props, kind });
|
|
765
|
+
}
|
|
766
|
+
var unsupportedType = (contentType) => new MediaFetchError("unsupported_type", `Can't preview ${contentType ?? "an unknown type"} inline`);
|
|
767
|
+
function DefaultFallback({ media, error, labels }) {
|
|
768
|
+
return /* @__PURE__ */ jsx5(DownloadLink, { media, title: error ? labels.error(error.code) : void 0, children: media.filename ?? labels.download });
|
|
769
|
+
}
|
|
770
|
+
function LoadingStatus({ text }) {
|
|
771
|
+
return /* @__PURE__ */ jsx5("span", { role: "status", style: VISUALLY_HIDDEN, children: text });
|
|
772
|
+
}
|
|
773
|
+
function VideoPreview({ media, className }) {
|
|
774
|
+
const videoProps = useVideoProps(media);
|
|
775
|
+
if (!videoProps) return null;
|
|
776
|
+
return /* @__PURE__ */ jsx5(VideoPlayer, { ...videoProps, className });
|
|
777
|
+
}
|
|
778
|
+
function BinaryPreview({
|
|
779
|
+
media,
|
|
780
|
+
kind,
|
|
781
|
+
expectedType,
|
|
782
|
+
maxBytes = DEFAULT_MAX_BYTES,
|
|
783
|
+
renderFallback,
|
|
784
|
+
loading,
|
|
785
|
+
labels,
|
|
786
|
+
title,
|
|
787
|
+
className
|
|
788
|
+
}) {
|
|
789
|
+
const { url } = useDevultur();
|
|
790
|
+
const isPublicImage = kind === "image" && media.visibility === "public";
|
|
791
|
+
const verified = useMediaObjectUrl(media, { enabled: !isPublicImage, contentType: expectedType, maxBytes });
|
|
792
|
+
const objectUrl = isPublicImage ? url(media) : verified.objectUrl;
|
|
793
|
+
if (verified.error) return /* @__PURE__ */ jsx5(Fragment3, { children: renderFallback(verified.error) });
|
|
794
|
+
if (!objectUrl) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
|
|
795
|
+
const label = title ?? media.filename ?? labels.untitledMedia;
|
|
796
|
+
if (kind === "image") {
|
|
797
|
+
return /* @__PURE__ */ jsx5("img", { src: objectUrl, alt: label, width: media.width, height: media.height, className });
|
|
798
|
+
}
|
|
799
|
+
return /* @__PURE__ */ jsx5("audio", { src: objectUrl, controls: true, "aria-label": label, className });
|
|
800
|
+
}
|
|
801
|
+
var decodeText = (bytes) => new TextDecoder().decode(bytes);
|
|
802
|
+
function TextPreview({
|
|
803
|
+
media,
|
|
804
|
+
expectedType,
|
|
805
|
+
maxTextBytes = DEFAULT_MAX_TEXT_BYTES,
|
|
806
|
+
renderFallback,
|
|
807
|
+
loading,
|
|
808
|
+
title,
|
|
809
|
+
className
|
|
810
|
+
}) {
|
|
811
|
+
const { value: text, error } = useMediaResource(media, {
|
|
812
|
+
isEnabled: true,
|
|
813
|
+
maxBytes: maxTextBytes,
|
|
814
|
+
contentType: expectedType,
|
|
815
|
+
decode: decodeText
|
|
816
|
+
});
|
|
817
|
+
if (error) return /* @__PURE__ */ jsx5(Fragment3, { children: renderFallback(error) });
|
|
818
|
+
if (text === null) return /* @__PURE__ */ jsx5(Fragment3, { children: loading });
|
|
819
|
+
return /* @__PURE__ */ jsx5("pre", { title, className, children: text });
|
|
820
|
+
}
|
|
821
|
+
|
|
162
822
|
// src/react.tsx
|
|
163
|
-
import { Fragment, jsx as
|
|
164
|
-
var DevulturContext =
|
|
165
|
-
var DevulturApiContext =
|
|
166
|
-
var ProgressContext =
|
|
823
|
+
import { Fragment as Fragment4, jsx as jsx6 } from "react/jsx-runtime";
|
|
824
|
+
var DevulturContext = React7.createContext(null);
|
|
825
|
+
var DevulturApiContext = React7.createContext(null);
|
|
826
|
+
var ProgressContext = React7.createContext(null);
|
|
167
827
|
var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
168
828
|
var MIN_REFRESH_MS = 6e4;
|
|
169
829
|
var DEFAULT_TTL_MS = DEFAULT_TOKEN_EXPIRY_SECONDS * 1e3;
|
|
@@ -173,16 +833,16 @@ function extractTokenResult(result) {
|
|
|
173
833
|
return { tokenValue, ttlMs };
|
|
174
834
|
}
|
|
175
835
|
function DevulturProvider({ api, baseUrl, children }) {
|
|
176
|
-
const [token, setToken] =
|
|
177
|
-
const [sessionSecret] =
|
|
836
|
+
const [token, setToken] = React7.useState(null);
|
|
837
|
+
const [sessionSecret] = React7.useState(() => typeof crypto !== "undefined" ? crypto.randomUUID() : "");
|
|
178
838
|
const issueViewerToken = useAction(api.issueViewerToken);
|
|
179
839
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
180
840
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
181
841
|
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
182
842
|
const deleteVideoAction = useAction(api.deleteVideo ?? api.deleteMedia);
|
|
183
|
-
const refreshTimerRef =
|
|
184
|
-
const reissueInFlightRef =
|
|
185
|
-
const scheduleRefresh =
|
|
843
|
+
const refreshTimerRef = React7.useRef(null);
|
|
844
|
+
const reissueInFlightRef = React7.useRef(null);
|
|
845
|
+
const scheduleRefresh = React7.useCallback(
|
|
186
846
|
(ttlMs) => {
|
|
187
847
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
188
848
|
const refreshIn = Math.max(ttlMs - REFRESH_BUFFER_MS, MIN_REFRESH_MS);
|
|
@@ -196,7 +856,7 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
196
856
|
},
|
|
197
857
|
[issueViewerToken, sessionSecret]
|
|
198
858
|
);
|
|
199
|
-
|
|
859
|
+
React7.useEffect(() => {
|
|
200
860
|
if (token) return;
|
|
201
861
|
issueViewerToken({ sessionSecret }).then((result) => {
|
|
202
862
|
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
@@ -204,7 +864,7 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
204
864
|
scheduleRefresh(ttlMs);
|
|
205
865
|
});
|
|
206
866
|
}, [token, issueViewerToken, scheduleRefresh, sessionSecret]);
|
|
207
|
-
const reissueToken =
|
|
867
|
+
const reissueToken = React7.useCallback(() => {
|
|
208
868
|
if (reissueInFlightRef.current) return reissueInFlightRef.current;
|
|
209
869
|
const pending = issueViewerToken({ sessionSecret }).then((result) => {
|
|
210
870
|
const { tokenValue, ttlMs } = extractTokenResult(result);
|
|
@@ -219,19 +879,19 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
219
879
|
reissueInFlightRef.current = pending;
|
|
220
880
|
return pending;
|
|
221
881
|
}, [issueViewerToken, scheduleRefresh, sessionSecret]);
|
|
222
|
-
|
|
882
|
+
React7.useEffect(() => {
|
|
223
883
|
return () => {
|
|
224
884
|
if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current);
|
|
225
885
|
};
|
|
226
886
|
}, []);
|
|
227
|
-
const uploadUrl =
|
|
887
|
+
const uploadUrl = React7.useCallback(
|
|
228
888
|
async (file) => {
|
|
229
889
|
const result = await createUploadUrlAction({ filename: file.name, contentType: file.type });
|
|
230
890
|
return { url: result.url, key: result.key };
|
|
231
891
|
},
|
|
232
892
|
[createUploadUrlAction]
|
|
233
893
|
);
|
|
234
|
-
const upload =
|
|
894
|
+
const upload = React7.useCallback(
|
|
235
895
|
async (file, options) => {
|
|
236
896
|
const dimensions = await readImageDimensions(file);
|
|
237
897
|
const target = await createUploadUrlAction({
|
|
@@ -252,21 +912,21 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
252
912
|
},
|
|
253
913
|
[createUploadUrlAction, confirmUploadAction, api.confirmUpload]
|
|
254
914
|
);
|
|
255
|
-
const url =
|
|
915
|
+
const url = React7.useCallback(
|
|
256
916
|
(media, options) => {
|
|
257
917
|
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
258
|
-
return
|
|
918
|
+
return mediaUrl2(baseUrl, media, { ...options, token: token ?? void 0 });
|
|
259
919
|
},
|
|
260
920
|
[baseUrl, token]
|
|
261
921
|
);
|
|
262
|
-
const getCaptionsText =
|
|
922
|
+
const getCaptionsText = React7.useCallback(
|
|
263
923
|
async (media, locale) => {
|
|
264
924
|
if (!baseUrl) throw new Error("Devultur `getCaptionsText()` requires `baseUrl` on <DevulturProvider>");
|
|
265
925
|
const path = mediaCaptionPath(media, locale);
|
|
266
926
|
if (!path) throw new Error("getCaptionsText: the reference is not a video");
|
|
267
927
|
const isPrivate = media.visibility === "private";
|
|
268
928
|
const fetchWith = (viewerToken) => fetch(buildMediaUrl(baseUrl, path, { token: viewerToken }), {
|
|
269
|
-
headers: sessionSecret ? { [
|
|
929
|
+
headers: sessionSecret ? { [SESSION_HEADER2]: sessionSecret } : void 0
|
|
270
930
|
});
|
|
271
931
|
let response = await fetchWith(isPrivate ? token ?? void 0 : void 0);
|
|
272
932
|
if (isPrivate && response.status === 401 && await isSessionMismatch(response)) {
|
|
@@ -278,17 +938,17 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
278
938
|
},
|
|
279
939
|
[baseUrl, token, reissueToken, sessionSecret]
|
|
280
940
|
);
|
|
281
|
-
const deleteMedia =
|
|
941
|
+
const deleteMedia = React7.useCallback(
|
|
282
942
|
(media) => {
|
|
283
943
|
if (media.kind === "video" && api.deleteVideo) {
|
|
284
944
|
deleteVideoAction({ videoId: media.id, visibility: media.visibility }).catch(console.error);
|
|
285
945
|
return;
|
|
286
946
|
}
|
|
287
|
-
deleteMediaAction({ key:
|
|
947
|
+
deleteMediaAction({ key: mediaPath2(media) }).catch(console.error);
|
|
288
948
|
},
|
|
289
949
|
[deleteMediaAction, deleteVideoAction, api.deleteVideo]
|
|
290
950
|
);
|
|
291
|
-
const devulturValue =
|
|
951
|
+
const devulturValue = React7.useMemo(
|
|
292
952
|
() => ({
|
|
293
953
|
token,
|
|
294
954
|
baseUrl: baseUrl ?? null,
|
|
@@ -302,9 +962,9 @@ function DevulturProvider({ api, baseUrl, children }) {
|
|
|
302
962
|
}),
|
|
303
963
|
[token, baseUrl, sessionSecret, uploadUrl, upload, url, getCaptionsText, deleteMedia, reissueToken]
|
|
304
964
|
);
|
|
305
|
-
let content = /* @__PURE__ */
|
|
965
|
+
let content = /* @__PURE__ */ jsx6(DevulturApiContext, { value: api, children: /* @__PURE__ */ jsx6(DevulturContext, { value: devulturValue, children }) });
|
|
306
966
|
if (api.saveProgress && api.loadProgress && api.markComplete && api.resetProgress) {
|
|
307
|
-
content = /* @__PURE__ */
|
|
967
|
+
content = /* @__PURE__ */ jsx6(
|
|
308
968
|
ProgressBridge,
|
|
309
969
|
{
|
|
310
970
|
save: api.saveProgress,
|
|
@@ -328,13 +988,13 @@ function ProgressBridge({
|
|
|
328
988
|
const saveMutation = useMutation(save);
|
|
329
989
|
const markCompleteMutation = useMutation(markComplete);
|
|
330
990
|
const resetMutation = useMutation(reset);
|
|
331
|
-
const onSave =
|
|
991
|
+
const onSave = React7.useCallback(
|
|
332
992
|
async (videoId, position) => {
|
|
333
993
|
await saveMutation({ videoId, position });
|
|
334
994
|
},
|
|
335
995
|
[saveMutation]
|
|
336
996
|
);
|
|
337
|
-
const onLoad =
|
|
997
|
+
const onLoad = React7.useCallback(
|
|
338
998
|
async (videoId) => {
|
|
339
999
|
const result = await convex.query(load, { videoId });
|
|
340
1000
|
if (!result) return null;
|
|
@@ -345,37 +1005,37 @@ function ProgressBridge({
|
|
|
345
1005
|
},
|
|
346
1006
|
[convex, load]
|
|
347
1007
|
);
|
|
348
|
-
const onMarkComplete =
|
|
1008
|
+
const onMarkComplete = React7.useCallback(
|
|
349
1009
|
async (videoId) => {
|
|
350
1010
|
await markCompleteMutation({ videoId });
|
|
351
1011
|
},
|
|
352
1012
|
[markCompleteMutation]
|
|
353
1013
|
);
|
|
354
|
-
const onReset =
|
|
1014
|
+
const onReset = React7.useCallback(
|
|
355
1015
|
async (videoId) => {
|
|
356
1016
|
await resetMutation({ videoId });
|
|
357
1017
|
},
|
|
358
1018
|
[resetMutation]
|
|
359
1019
|
);
|
|
360
|
-
const value =
|
|
1020
|
+
const value = React7.useMemo(
|
|
361
1021
|
() => ({ save: onSave, load: onLoad, markComplete: onMarkComplete, reset: onReset }),
|
|
362
1022
|
[onSave, onLoad, onMarkComplete, onReset]
|
|
363
1023
|
);
|
|
364
|
-
return /* @__PURE__ */
|
|
1024
|
+
return /* @__PURE__ */ jsx6(ProgressContext, { value, children });
|
|
365
1025
|
}
|
|
366
1026
|
function useDevultur() {
|
|
367
|
-
const context =
|
|
1027
|
+
const context = React7.use(DevulturContext);
|
|
368
1028
|
if (!context) throw new Error("useDevultur must be used within <DevulturProvider>");
|
|
369
1029
|
return context;
|
|
370
1030
|
}
|
|
371
1031
|
function useDevulturProgress() {
|
|
372
|
-
const context =
|
|
1032
|
+
const context = React7.use(ProgressContext);
|
|
373
1033
|
if (!context) throw new Error("useDevulturProgress requires progress API in DevulturProvider");
|
|
374
1034
|
return context;
|
|
375
1035
|
}
|
|
376
1036
|
function useMediaUrl(key, options) {
|
|
377
1037
|
const { token, baseUrl } = useDevultur();
|
|
378
|
-
return
|
|
1038
|
+
return React7.useMemo(() => {
|
|
379
1039
|
if (!key) return null;
|
|
380
1040
|
if (!baseUrl) throw new Error("useMediaUrl requires `baseUrl` on <DevulturProvider>");
|
|
381
1041
|
const isPublic = visibilityFromKey(key) === "public";
|
|
@@ -403,7 +1063,7 @@ async function readImageDimensions(file) {
|
|
|
403
1063
|
function useVideoProps(media, options) {
|
|
404
1064
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
405
1065
|
const locales = options?.locales;
|
|
406
|
-
return
|
|
1066
|
+
return React7.useMemo(() => {
|
|
407
1067
|
if (media?.kind !== "video") return null;
|
|
408
1068
|
if (!baseUrl) throw new Error("useVideoProps requires `baseUrl` on <DevulturProvider>");
|
|
409
1069
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
@@ -414,7 +1074,7 @@ function useVideoProps(media, options) {
|
|
|
414
1074
|
return [{ locale, label, src: buildMediaUrl(baseUrl, path, { token: viewerToken }) }];
|
|
415
1075
|
});
|
|
416
1076
|
return {
|
|
417
|
-
src: buildMediaUrl(baseUrl,
|
|
1077
|
+
src: buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken }),
|
|
418
1078
|
token: viewerToken,
|
|
419
1079
|
captions,
|
|
420
1080
|
sessionSecret: media.visibility === "private" ? sessionSecret : void 0
|
|
@@ -434,25 +1094,32 @@ function Image({
|
|
|
434
1094
|
alt,
|
|
435
1095
|
...imgProps
|
|
436
1096
|
}) {
|
|
437
|
-
const { url, reissueToken, sessionSecret } = useDevultur();
|
|
438
|
-
const [failed, setFailed] =
|
|
439
|
-
const [blobSrc, setBlobSrc] =
|
|
440
|
-
const
|
|
1097
|
+
const { url, token, baseUrl, reissueToken, sessionSecret } = useDevultur();
|
|
1098
|
+
const [failed, setFailed] = React7.useState(false);
|
|
1099
|
+
const [blobSrc, setBlobSrc] = React7.useState(null);
|
|
1100
|
+
const sessionRef = React7.useRef({ token, reissueToken });
|
|
1101
|
+
React7.useEffect(() => {
|
|
1102
|
+
sessionRef.current = { token, reissueToken };
|
|
1103
|
+
});
|
|
441
1104
|
const isPrivate = media?.visibility === "private";
|
|
442
|
-
|
|
443
|
-
|
|
1105
|
+
const hasToken = token !== null;
|
|
1106
|
+
React7.useEffect(() => {
|
|
444
1107
|
setFailed(false);
|
|
445
1108
|
setBlobSrc(null);
|
|
446
1109
|
}, [media]);
|
|
447
|
-
|
|
448
|
-
if (!isPrivate || failed || !media) return;
|
|
1110
|
+
React7.useEffect(() => {
|
|
1111
|
+
if (!isPrivate || failed || !media || !hasToken) return;
|
|
449
1112
|
let cancelled = false;
|
|
450
1113
|
let objectUrl = null;
|
|
451
|
-
|
|
1114
|
+
if (!baseUrl) throw new Error("Devultur `url()` requires `baseUrl` on <DevulturProvider>");
|
|
1115
|
+
const transforms = { width, height, fit, format, quality };
|
|
452
1116
|
(async () => {
|
|
453
1117
|
try {
|
|
454
|
-
const response = await
|
|
455
|
-
|
|
1118
|
+
const response = await fetchMedia({
|
|
1119
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { ...transforms, token: viewerToken }),
|
|
1120
|
+
token: sessionRef.current.token,
|
|
1121
|
+
sessionSecret,
|
|
1122
|
+
reissueToken: sessionRef.current.reissueToken
|
|
456
1123
|
});
|
|
457
1124
|
if (response.ok) {
|
|
458
1125
|
objectUrl = URL.createObjectURL(await response.blob());
|
|
@@ -463,11 +1130,6 @@ function Image({
|
|
|
463
1130
|
setBlobSrc(objectUrl);
|
|
464
1131
|
return;
|
|
465
1132
|
}
|
|
466
|
-
if (response.status === 401 && !retriedRef.current && await isSessionMismatch(response)) {
|
|
467
|
-
retriedRef.current = true;
|
|
468
|
-
await reissueToken();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
1133
|
} catch {
|
|
472
1134
|
}
|
|
473
1135
|
if (!cancelled) setFailed(true);
|
|
@@ -476,8 +1138,8 @@ function Image({
|
|
|
476
1138
|
cancelled = true;
|
|
477
1139
|
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
|
478
1140
|
};
|
|
479
|
-
}, [isPrivate, failed, media,
|
|
480
|
-
const handleError =
|
|
1141
|
+
}, [isPrivate, failed, media, hasToken, baseUrl, sessionSecret, width, height, fit, format, quality]);
|
|
1142
|
+
const handleError = React7.useCallback(() => setFailed(true), []);
|
|
481
1143
|
let src;
|
|
482
1144
|
if (failed || !media) {
|
|
483
1145
|
src = externalFallback ?? null;
|
|
@@ -486,13 +1148,13 @@ function Image({
|
|
|
486
1148
|
} else {
|
|
487
1149
|
src = url(media, { width, height, fit, format, quality });
|
|
488
1150
|
}
|
|
489
|
-
if (!src) return /* @__PURE__ */
|
|
1151
|
+
if (!src) return /* @__PURE__ */ jsx6(Fragment4, { children: fallback });
|
|
490
1152
|
if (render) {
|
|
491
|
-
return
|
|
1153
|
+
return React7.cloneElement(render, {
|
|
492
1154
|
style: { ...render.props.style, backgroundImage: `url("${src}")` }
|
|
493
1155
|
});
|
|
494
1156
|
}
|
|
495
|
-
return /* @__PURE__ */
|
|
1157
|
+
return /* @__PURE__ */ jsx6(
|
|
496
1158
|
"img",
|
|
497
1159
|
{
|
|
498
1160
|
src,
|
|
@@ -506,17 +1168,16 @@ function Image({
|
|
|
506
1168
|
}
|
|
507
1169
|
function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
508
1170
|
const { url, token, baseUrl, sessionSecret, reissueToken } = useDevultur();
|
|
509
|
-
const handleClick =
|
|
1171
|
+
const handleClick = React7.useCallback(
|
|
510
1172
|
async (event) => {
|
|
511
1173
|
if (media?.visibility !== "private" || !baseUrl) return;
|
|
512
1174
|
event.preventDefault();
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
}
|
|
1175
|
+
const response = await fetchMedia({
|
|
1176
|
+
buildUrl: (viewerToken) => mediaUrl2(baseUrl, media, { download: filename ?? true, token: viewerToken }),
|
|
1177
|
+
token,
|
|
1178
|
+
sessionSecret,
|
|
1179
|
+
reissueToken
|
|
1180
|
+
});
|
|
520
1181
|
if (!response.ok) return;
|
|
521
1182
|
const objectUrl = URL.createObjectURL(await response.blob());
|
|
522
1183
|
const anchor = document.createElement("a");
|
|
@@ -528,10 +1189,10 @@ function DownloadLink({ media, filename, children, ...anchorProps }) {
|
|
|
528
1189
|
[media, baseUrl, token, filename, sessionSecret, reissueToken]
|
|
529
1190
|
);
|
|
530
1191
|
if (!media) return null;
|
|
531
|
-
return /* @__PURE__ */
|
|
1192
|
+
return /* @__PURE__ */ jsx6("a", { href: url(media, { download: filename ?? true }), download: true, ...anchorProps, onClick: handleClick, children });
|
|
532
1193
|
}
|
|
533
1194
|
function useDevulturMedia(key) {
|
|
534
|
-
const apiCtx =
|
|
1195
|
+
const apiCtx = React7.use(DevulturApiContext);
|
|
535
1196
|
if (!apiCtx?.getMedia) {
|
|
536
1197
|
throw new Error("useDevulturMedia requires getMedia in DevulturProvider api");
|
|
537
1198
|
}
|
|
@@ -588,7 +1249,7 @@ function useVideoState(media, options) {
|
|
|
588
1249
|
const { token, baseUrl, sessionSecret } = useDevultur();
|
|
589
1250
|
const status = useDevulturMedia(media?.kind === "video" ? media.id : null);
|
|
590
1251
|
const locales = options?.locales;
|
|
591
|
-
return
|
|
1252
|
+
return React7.useMemo(() => {
|
|
592
1253
|
if (media?.kind !== "video") return { phase: "idle" };
|
|
593
1254
|
if (status.isFailed) {
|
|
594
1255
|
return {
|
|
@@ -601,7 +1262,7 @@ function useVideoState(media, options) {
|
|
|
601
1262
|
if (status.isReady) {
|
|
602
1263
|
if (!baseUrl) throw new Error("useVideoState requires `baseUrl` on <DevulturProvider>");
|
|
603
1264
|
const viewerToken = media.visibility === "private" ? token ?? void 0 : void 0;
|
|
604
|
-
const playlist = buildMediaUrl(baseUrl,
|
|
1265
|
+
const playlist = buildMediaUrl(baseUrl, mediaPath2(media), { token: viewerToken });
|
|
605
1266
|
const captions = (locales ?? status.captionLocales).flatMap((locale) => {
|
|
606
1267
|
const path = mediaCaptionPath(media, locale);
|
|
607
1268
|
if (!path) return [];
|
|
@@ -632,23 +1293,23 @@ function useVideoState(media, options) {
|
|
|
632
1293
|
}
|
|
633
1294
|
function useMediaUpload(options) {
|
|
634
1295
|
const { visibility, onComplete, onError, replaces, process, locales } = options ?? {};
|
|
635
|
-
const api =
|
|
1296
|
+
const api = React7.use(DevulturApiContext);
|
|
636
1297
|
if (!api) throw new Error("useMediaUpload must be used within <DevulturProvider>");
|
|
637
1298
|
const createUploadUrlAction = useAction(api.createUploadUrl);
|
|
638
1299
|
const deleteMediaAction = useAction(api.deleteMedia);
|
|
639
1300
|
const confirmUploadAction = useAction(api.confirmUpload ?? api.createUploadUrl);
|
|
640
1301
|
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 =
|
|
1302
|
+
const [phase, setPhase] = React7.useState("idle");
|
|
1303
|
+
const [percent, setPercent] = React7.useState(0);
|
|
1304
|
+
const [currentFile, setCurrentFile] = React7.useState(null);
|
|
1305
|
+
const [media, setMedia] = React7.useState(null);
|
|
1306
|
+
const [uploadFailure, setUploadFailure] = React7.useState(null);
|
|
1307
|
+
const [processRequested, setProcessRequested] = React7.useState(false);
|
|
1308
|
+
const abortRef = React7.useRef(null);
|
|
1309
|
+
const lastFileRef = React7.useRef(null);
|
|
1310
|
+
const lastKeyRef = React7.useRef(null);
|
|
650
1311
|
const videoState = useVideoState(media, { locales });
|
|
651
|
-
const triggerProcess =
|
|
1312
|
+
const triggerProcess = React7.useCallback(
|
|
652
1313
|
(key) => {
|
|
653
1314
|
if (!api.processVideo) {
|
|
654
1315
|
throw new Error("useMediaUpload: processing a video requires `processVideo` in the <DevulturProvider> api");
|
|
@@ -661,7 +1322,7 @@ function useMediaUpload(options) {
|
|
|
661
1322
|
},
|
|
662
1323
|
[api.processVideo, processVideoAction]
|
|
663
1324
|
);
|
|
664
|
-
const runUpload =
|
|
1325
|
+
const runUpload = React7.useCallback(
|
|
665
1326
|
async (file) => {
|
|
666
1327
|
const controller = new AbortController();
|
|
667
1328
|
abortRef.current = controller;
|
|
@@ -713,7 +1374,7 @@ function useMediaUpload(options) {
|
|
|
713
1374
|
};
|
|
714
1375
|
setMedia(uploaded);
|
|
715
1376
|
setPhase("uploaded");
|
|
716
|
-
if (replaces) deleteMediaAction({ key:
|
|
1377
|
+
if (replaces) deleteMediaAction({ key: mediaPath2(replaces) }).catch(() => {
|
|
717
1378
|
});
|
|
718
1379
|
onComplete?.(uploaded);
|
|
719
1380
|
if ((process ?? true) && uploaded.kind === "video") triggerProcess(result.key);
|
|
@@ -732,10 +1393,10 @@ function useMediaUpload(options) {
|
|
|
732
1393
|
onError
|
|
733
1394
|
]
|
|
734
1395
|
);
|
|
735
|
-
const cancel =
|
|
1396
|
+
const cancel = React7.useCallback(() => {
|
|
736
1397
|
abortRef.current?.abort();
|
|
737
1398
|
}, []);
|
|
738
|
-
const reset =
|
|
1399
|
+
const reset = React7.useCallback(() => {
|
|
739
1400
|
abortRef.current?.abort();
|
|
740
1401
|
setPhase("idle");
|
|
741
1402
|
setPercent(0);
|
|
@@ -744,15 +1405,15 @@ function useMediaUpload(options) {
|
|
|
744
1405
|
setUploadFailure(null);
|
|
745
1406
|
setProcessRequested(false);
|
|
746
1407
|
}, []);
|
|
747
|
-
const retry =
|
|
1408
|
+
const retry = React7.useCallback(() => {
|
|
748
1409
|
if (phase === "upload-failed" && lastFileRef.current) {
|
|
749
1410
|
runUpload(lastFileRef.current);
|
|
750
1411
|
return;
|
|
751
1412
|
}
|
|
752
1413
|
if (lastKeyRef.current) triggerProcess(lastKeyRef.current);
|
|
753
1414
|
}, [phase, runUpload, triggerProcess]);
|
|
754
|
-
|
|
755
|
-
const state =
|
|
1415
|
+
React7.useEffect(() => () => abortRef.current?.abort(), []);
|
|
1416
|
+
const state = React7.useMemo(
|
|
756
1417
|
() => deriveUploadState(
|
|
757
1418
|
{ phase, percent, file: currentFile, failure: uploadFailure, media, processRequested },
|
|
758
1419
|
videoState,
|
|
@@ -760,7 +1421,7 @@ function useMediaUpload(options) {
|
|
|
760
1421
|
),
|
|
761
1422
|
[phase, percent, currentFile, uploadFailure, media, processRequested, videoState, retry]
|
|
762
1423
|
);
|
|
763
|
-
const upload =
|
|
1424
|
+
const upload = React7.useCallback((file) => runUpload(file), [runUpload]);
|
|
764
1425
|
return { upload, state, cancel, reset };
|
|
765
1426
|
}
|
|
766
1427
|
export {
|
|
@@ -768,9 +1429,15 @@ export {
|
|
|
768
1429
|
DevulturProvider,
|
|
769
1430
|
DownloadLink,
|
|
770
1431
|
Image,
|
|
1432
|
+
MediaFetchError,
|
|
1433
|
+
PdfPreviewProvider,
|
|
1434
|
+
PdfThumbnail,
|
|
1435
|
+
Preview,
|
|
1436
|
+
defaultPreviewLabels,
|
|
771
1437
|
useDevultur,
|
|
772
1438
|
useDevulturMedia,
|
|
773
1439
|
useDevulturProgress,
|
|
1440
|
+
useMediaObjectUrl,
|
|
774
1441
|
useMediaUpload,
|
|
775
1442
|
useMediaUrl,
|
|
776
1443
|
useVideoProps,
|