@ttt-productions/file-input 0.11.16 → 0.11.18
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/dist/lib/image-utils.d.ts +0 -1
- package/dist/lib/image-utils.d.ts.map +1 -1
- package/dist/lib/image-utils.js +1 -2
- package/dist/lib/image-utils.js.map +1 -1
- package/dist/lib/infer-content-type.d.ts +0 -23
- package/dist/lib/infer-content-type.d.ts.map +1 -1
- package/dist/lib/infer-content-type.js +2 -56
- package/dist/lib/infer-content-type.js.map +1 -1
- package/dist/lib/read-media-meta.d.ts.map +1 -1
- package/dist/lib/read-media-meta.js +5 -15
- package/dist/lib/read-media-meta.js.map +1 -1
- package/dist/lib/validate-media-duration.d.ts +1 -0
- package/dist/lib/validate-media-duration.d.ts.map +1 -1
- package/dist/lib/validate-media-duration.js +11 -11
- package/dist/lib/validate-media-duration.js.map +1 -1
- package/dist/lib/validate-upload-kind.d.ts +0 -34
- package/dist/lib/validate-upload-kind.d.ts.map +1 -1
- package/dist/lib/validate-upload-kind.js +0 -17
- package/dist/lib/validate-upload-kind.js.map +1 -1
- package/dist/react/components/audio-input.d.ts +1 -1
- package/dist/react/components/audio-input.d.ts.map +1 -1
- package/dist/react/components/audio-input.js.map +1 -1
- package/dist/react/components/file-input.d.ts +22 -1
- package/dist/react/components/file-input.d.ts.map +1 -1
- package/dist/react/components/file-input.js +2 -6
- package/dist/react/components/file-input.js.map +1 -1
- package/dist/react/components/image-cropper-modal.d.ts +1 -2
- package/dist/react/components/image-cropper-modal.d.ts.map +1 -1
- package/dist/react/components/image-cropper-modal.js +1 -4
- package/dist/react/components/image-cropper-modal.js.map +1 -1
- package/dist/react/components/image-input.d.ts +1 -1
- package/dist/react/components/image-input.d.ts.map +1 -1
- package/dist/react/components/image-input.js.map +1 -1
- package/dist/react/components/media-constraints-hint.d.ts.map +1 -1
- package/dist/react/components/media-constraints-hint.js +0 -1
- package/dist/react/components/media-constraints-hint.js.map +1 -1
- package/dist/react/components/media-input.d.ts.map +1 -1
- package/dist/react/components/media-input.js +6 -73
- package/dist/react/components/media-input.js.map +1 -1
- package/dist/react/components/photo-capture-modal.d.ts.map +1 -1
- package/dist/react/components/photo-capture-modal.js +5 -8
- package/dist/react/components/photo-capture-modal.js.map +1 -1
- package/dist/react/components/record-dialog.d.ts +0 -4
- package/dist/react/components/record-dialog.d.ts.map +1 -1
- package/dist/react/components/record-dialog.js +35 -106
- package/dist/react/components/record-dialog.js.map +1 -1
- package/dist/react/components/video-input.d.ts +1 -1
- package/dist/react/components/video-input.d.ts.map +1 -1
- package/dist/react/components/video-input.js.map +1 -1
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/types.d.ts +0 -75
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Area } from 'react-easy-crop';
|
|
2
2
|
import { type MediaCropSpec } from '@ttt-productions/media-schemas';
|
|
3
|
-
/** How a crop is encoded; the fields an origin's crop spec declares. */
|
|
4
3
|
export type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;
|
|
5
4
|
export declare function getCroppedImg(imageSrc: string, pixelCrop: Area, rotation?: number, flip?: {
|
|
6
5
|
horizontal: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,
|
|
1
|
+
{"version":3,"file":"image-utils.d.ts","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEnF,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC;AA0BrE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,IAAI,EACf,QAAQ,SAAI,EACZ,IAAI;;;CAAyC,EAC7C,WAAW,CAAC,EAAE,MAAM,EACpB,YAAY,CAAC,EAAE,MAAM,EACrB,QAAQ,GAAE,YAAiB,GAC1B,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAgDtB"}
|
package/dist/lib/image-utils.js
CHANGED
|
@@ -40,8 +40,7 @@ export async function getCroppedImg(imageSrc, pixelCrop, rotation = 0, flip = {
|
|
|
40
40
|
croppedCanvas.width = outputWidth || pixelCrop.width;
|
|
41
41
|
croppedCanvas.height = outputHeight || pixelCrop.height;
|
|
42
42
|
croppedCtx.drawImage(canvas, pixelCrop.x, pixelCrop.y, pixelCrop.width, pixelCrop.height, 0, 0, croppedCanvas.width, croppedCanvas.height);
|
|
43
|
-
//
|
|
44
|
-
// quality; a browser that cannot encode the format returns PNG, and the blob's type says so.
|
|
43
|
+
// A browser that cannot encode the requested format returns PNG; the blob's type says which.
|
|
45
44
|
const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all[0];
|
|
46
45
|
const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;
|
|
47
46
|
return new Promise((resolve, reject) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAsB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"image-utils.js","sourceRoot":"","sources":["../../src/lib/image-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Area } from 'react-easy-crop';\nimport { MEDIA_FORMATS, type MediaCropSpec } from '@ttt-productions/media-schemas';\n\nexport type CropEncoding = Pick<MediaCropSpec, 'format' | 'quality'>;\n\nconst DEFAULT_CROP_FORMAT: NonNullable<MediaCropSpec['format']> = 'jpeg';\nconst DEFAULT_CROP_QUALITY = 80;\n\nconst createImage = (url: string): Promise<HTMLImageElement> =>\n new Promise((resolve, reject) => {\n const image = new Image();\n image.addEventListener('load', () => resolve(image));\n image.addEventListener('error', (error) => reject(error));\n image.setAttribute('crossOrigin', 'anonymous');\n image.src = url;\n });\n\nfunction getRadianAngle(deg: number) {\n return (deg * Math.PI) / 180;\n}\n\nfunction rotateSize(width: number, height: number, rotation: number) {\n const rotRad = getRadianAngle(rotation);\n return {\n width: Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),\n height: Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),\n };\n}\n\nexport async function getCroppedImg(\n imageSrc: string,\n pixelCrop: Area,\n rotation = 0,\n flip = { horizontal: false, vertical: false },\n outputWidth?: number,\n outputHeight?: number,\n encoding: CropEncoding = {},\n): Promise<Blob | null> {\n const image = await createImage(imageSrc);\n const canvas = document.createElement('canvas');\n const ctx = canvas.getContext('2d');\n if (!ctx) return null;\n\n const rotRad = getRadianAngle(rotation);\n const { width: bBoxWidth, height: bBoxHeight } = rotateSize(image.width, image.height, rotation);\n\n canvas.width = bBoxWidth;\n canvas.height = bBoxHeight;\n\n ctx.translate(bBoxWidth / 2, bBoxHeight / 2);\n ctx.rotate(rotRad);\n ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);\n ctx.translate(-image.width / 2, -image.height / 2);\n ctx.drawImage(image, 0, 0);\n\n const croppedCanvas = document.createElement('canvas');\n const croppedCtx = croppedCanvas.getContext('2d');\n if (!croppedCtx) return null;\n\n croppedCanvas.width = outputWidth || pixelCrop.width;\n croppedCanvas.height = outputHeight || pixelCrop.height;\n\n croppedCtx.drawImage(\n canvas,\n pixelCrop.x,\n pixelCrop.y,\n pixelCrop.width,\n pixelCrop.height,\n 0,\n 0,\n croppedCanvas.width,\n croppedCanvas.height,\n );\n\n // A browser that cannot encode the requested format returns PNG; the blob's type says which.\n const mime = MEDIA_FORMATS[encoding.format ?? DEFAULT_CROP_FORMAT].mimes.all![0];\n const quality = (encoding.quality ?? DEFAULT_CROP_QUALITY) / 100;\n\n return new Promise((resolve, reject) => {\n croppedCanvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error('Canvas to Blob conversion failed'))),\n mime,\n quality,\n );\n });\n}\n"]}
|
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
export type InferKind = 'image' | 'video' | 'audio';
|
|
2
|
-
/**
|
|
3
|
-
* Returns a valid, parameter-less media MIME for the given File.
|
|
4
|
-
*
|
|
5
|
-
* Precedence:
|
|
6
|
-
* 1. The base of file.type (parameters stripped, lowercased) when valid —
|
|
7
|
-
* unless `fallbackKind` is supplied and disagrees with the declared
|
|
8
|
-
* top-level kind. A caller-declared kind (e.g. the record dialog's chosen
|
|
9
|
-
* recording kind) is authoritative context, so a conflicting declared type
|
|
10
|
-
* is treated as untrustworthy and resolution falls through to the
|
|
11
|
-
* extension.
|
|
12
|
-
* 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
|
|
13
|
-
* and video) a supplied `fallbackKind` picks the matching side; unambiguous
|
|
14
|
-
* extensions keep their single mapping.
|
|
15
|
-
* 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
|
|
16
|
-
* unknown picker file is never rewritten into a false media fact.
|
|
17
|
-
*/
|
|
18
2
|
export declare function inferContentType(file: File, fallbackKind?: InferKind): string;
|
|
19
|
-
/**
|
|
20
|
-
* Returns a File whose .type is exactly the valid, parameter-less media MIME
|
|
21
|
-
* from inferContentType. If the original file already carries that exact
|
|
22
|
-
* type, it is returned unchanged; otherwise (missing type, parameterized
|
|
23
|
-
* type, wrong case, or a type overridden by fallbackKind precedence) the
|
|
24
|
-
* bytes are wrapped in a new File with the normalized type.
|
|
25
|
-
*/
|
|
26
3
|
export declare function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File;
|
|
27
4
|
//# sourceMappingURL=infer-content-type.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"infer-content-type.d.ts","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAuDpD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,MAAM,CAc7E;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,CAAC,EAAE,SAAS,GAAG,IAAI,CAIpF"}
|
|
@@ -1,24 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// with a caller-declared kind (recorder/capture — the caller KNOWS what it
|
|
4
|
-
// produced) fall back to that kind's default container.
|
|
5
|
-
//
|
|
6
|
-
// MediaRecorder (and some pickers) report parameterized types such as
|
|
7
|
-
// "audio/webm;codecs=opus". Downstream storage rules validate a
|
|
8
|
-
// parameter-less base type, so we always emit the base ("audio/webm"),
|
|
9
|
-
// never the parameterized form.
|
|
10
|
-
//
|
|
11
|
-
// UNKNOWN PICKER METADATA IS NOT FABRICATED (canonical-upload-content-
|
|
12
|
-
// classification, 2026-07-31): when there is no declared kind, no usable MIME,
|
|
13
|
-
// and no known extension, the file passes through as the NEUTRAL
|
|
14
|
-
// `application/octet-stream` so the server's canonical byte inspector — the
|
|
15
|
-
// only classification authority — sees "unknown" as unknown. The previous
|
|
16
|
-
// behavior defaulted such files to `image/jpeg`, which invented a false media
|
|
17
|
-
// fact the whole pipeline then trusted. upload-core accepts the neutral type
|
|
18
|
-
// only via its explicit opt-in (`allowNeutralContentType`).
|
|
1
|
+
// MediaRecorder and some pickers report parameterized types ("audio/webm;codecs=opus"), and
|
|
2
|
+
// downstream checks validate the parameter-less base, so only the base is ever emitted.
|
|
19
3
|
import { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';
|
|
20
4
|
const EXT_TO_MIME = {
|
|
21
|
-
// image
|
|
22
5
|
jpg: 'image/jpeg',
|
|
23
6
|
jpeg: 'image/jpeg',
|
|
24
7
|
png: 'image/png',
|
|
@@ -27,13 +10,11 @@ const EXT_TO_MIME = {
|
|
|
27
10
|
avif: 'image/avif',
|
|
28
11
|
heic: 'image/heic',
|
|
29
12
|
heif: 'image/heif',
|
|
30
|
-
// video
|
|
31
13
|
mp4: 'video/mp4',
|
|
32
14
|
m4v: 'video/mp4',
|
|
33
15
|
mov: 'video/quicktime',
|
|
34
16
|
webm: 'video/webm',
|
|
35
17
|
mkv: 'video/x-matroska',
|
|
36
|
-
// audio
|
|
37
18
|
mp3: 'audio/mpeg',
|
|
38
19
|
m4a: 'audio/mp4',
|
|
39
20
|
aac: 'audio/aac',
|
|
@@ -42,10 +23,6 @@ const EXT_TO_MIME = {
|
|
|
42
23
|
oga: 'audio/ogg',
|
|
43
24
|
flac: 'audio/flac',
|
|
44
25
|
};
|
|
45
|
-
// Containers that legitimately hold either audio or video. When the caller
|
|
46
|
-
// declares a kind (e.g. the record dialog's chosen recording kind), that kind
|
|
47
|
-
// picks the matching side; without a kind we keep the historical single-kind
|
|
48
|
-
// guess in EXT_TO_MIME (webm → video, ogg → audio).
|
|
49
26
|
const AMBIGUOUS_EXT_TO_MIME = {
|
|
50
27
|
webm: { audio: 'audio/webm', video: 'video/webm' },
|
|
51
28
|
ogg: { audio: 'audio/ogg', video: 'video/ogg' },
|
|
@@ -56,12 +33,6 @@ const KIND_DEFAULT = {
|
|
|
56
33
|
audio: 'audio/webm',
|
|
57
34
|
};
|
|
58
35
|
const BASE_MEDIA_MIME_RE = /^(image|video|audio)\/[a-z0-9.+-]+$/;
|
|
59
|
-
/**
|
|
60
|
-
* Extracts the parameter-less base MIME ("audio/webm" from
|
|
61
|
-
* "audio/webm;codecs=opus"), lowercased, when it is a usable media type.
|
|
62
|
-
* Returns null for empty values, application/octet-stream, and anything
|
|
63
|
-
* whose base is not image/*, video/*, or audio/*.
|
|
64
|
-
*/
|
|
65
36
|
function parseBaseMime(mime) {
|
|
66
37
|
if (!mime)
|
|
67
38
|
return null;
|
|
@@ -79,22 +50,6 @@ function extOf(name) {
|
|
|
79
50
|
return null;
|
|
80
51
|
return name.slice(dot + 1).toLowerCase();
|
|
81
52
|
}
|
|
82
|
-
/**
|
|
83
|
-
* Returns a valid, parameter-less media MIME for the given File.
|
|
84
|
-
*
|
|
85
|
-
* Precedence:
|
|
86
|
-
* 1. The base of file.type (parameters stripped, lowercased) when valid —
|
|
87
|
-
* unless `fallbackKind` is supplied and disagrees with the declared
|
|
88
|
-
* top-level kind. A caller-declared kind (e.g. the record dialog's chosen
|
|
89
|
-
* recording kind) is authoritative context, so a conflicting declared type
|
|
90
|
-
* is treated as untrustworthy and resolution falls through to the
|
|
91
|
-
* extension.
|
|
92
|
-
* 2. Extension lookup. For ambiguous containers (webm/ogg exist as both audio
|
|
93
|
-
* and video) a supplied `fallbackKind` picks the matching side; unambiguous
|
|
94
|
-
* extensions keep their single mapping.
|
|
95
|
-
* 3. The kind default for `fallbackKind`; without one, the NEUTRAL type — an
|
|
96
|
-
* unknown picker file is never rewritten into a false media fact.
|
|
97
|
-
*/
|
|
98
53
|
export function inferContentType(file, fallbackKind) {
|
|
99
54
|
const base = parseBaseMime(file.type);
|
|
100
55
|
if (base && (!fallbackKind || kindOfMime(base) === fallbackKind))
|
|
@@ -109,17 +64,8 @@ export function inferContentType(file, fallbackKind) {
|
|
|
109
64
|
}
|
|
110
65
|
if (fallbackKind)
|
|
111
66
|
return KIND_DEFAULT[fallbackKind];
|
|
112
|
-
// No declared kind, no usable MIME, no known extension: NEUTRAL, never a
|
|
113
|
-
// fabricated media type. The server inspector owns classification.
|
|
114
67
|
return NEUTRAL_CONTENT_TYPE;
|
|
115
68
|
}
|
|
116
|
-
/**
|
|
117
|
-
* Returns a File whose .type is exactly the valid, parameter-less media MIME
|
|
118
|
-
* from inferContentType. If the original file already carries that exact
|
|
119
|
-
* type, it is returned unchanged; otherwise (missing type, parameterized
|
|
120
|
-
* type, wrong case, or a type overridden by fallbackKind precedence) the
|
|
121
|
-
* bytes are wrapped in a new File with the normalized type.
|
|
122
|
-
*/
|
|
123
69
|
export function ensureFileWithContentType(file, fallbackKind) {
|
|
124
70
|
const type = inferContentType(file, fallbackKind);
|
|
125
71
|
if (file.type === type)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"infer-content-type.js","sourceRoot":"","sources":["../../src/lib/infer-content-type.ts"],"names":[],"mappings":"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","sourcesContent":["// MediaRecorder and some pickers report parameterized types (\"audio/webm;codecs=opus\"), and\n// downstream checks validate the parameter-less base, so only the base is ever emitted.\n\nimport { NEUTRAL_CONTENT_TYPE } from '@ttt-productions/media-schemas';\n\nexport type InferKind = 'image' | 'video' | 'audio';\n\nconst EXT_TO_MIME: Record<string, string> = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n avif: 'image/avif',\n heic: 'image/heic',\n heif: 'image/heif',\n mp4: 'video/mp4',\n m4v: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n mkv: 'video/x-matroska',\n mp3: 'audio/mpeg',\n m4a: 'audio/mp4',\n aac: 'audio/aac',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n oga: 'audio/ogg',\n flac: 'audio/flac',\n};\n\nconst AMBIGUOUS_EXT_TO_MIME: Record<string, Partial<Record<InferKind, string>>> = {\n webm: { audio: 'audio/webm', video: 'video/webm' },\n ogg: { audio: 'audio/ogg', video: 'video/ogg' },\n};\n\nconst KIND_DEFAULT: Record<InferKind, string> = {\n image: 'image/jpeg',\n video: 'video/webm',\n audio: 'audio/webm',\n};\n\nconst BASE_MEDIA_MIME_RE = /^(image|video|audio)\\/[a-z0-9.+-]+$/;\n\nfunction parseBaseMime(mime: string | undefined | null): string | null {\n if (!mime) return null;\n const base = mime.split(';')[0].trim().toLowerCase();\n if (base === NEUTRAL_CONTENT_TYPE) return null;\n return BASE_MEDIA_MIME_RE.test(base) ? base : null;\n}\n\nfunction kindOfMime(base: string): InferKind {\n return base.slice(0, base.indexOf('/')) as InferKind;\n}\n\nfunction extOf(name: string): string | null {\n const dot = name.lastIndexOf('.');\n if (dot < 0 || dot === name.length - 1) return null;\n return name.slice(dot + 1).toLowerCase();\n}\n\nexport function inferContentType(file: File, fallbackKind?: InferKind): string {\n const base = parseBaseMime(file.type);\n if (base && (!fallbackKind || kindOfMime(base) === fallbackKind)) return base;\n\n const ext = extOf(file.name);\n if (ext) {\n const byKind = fallbackKind ? AMBIGUOUS_EXT_TO_MIME[ext]?.[fallbackKind] : undefined;\n if (byKind) return byKind;\n if (EXT_TO_MIME[ext]) return EXT_TO_MIME[ext];\n }\n\n if (fallbackKind) return KIND_DEFAULT[fallbackKind];\n\n return NEUTRAL_CONTENT_TYPE;\n}\n\nexport function ensureFileWithContentType(file: File, fallbackKind?: InferKind): File {\n const type = inferContentType(file, fallbackKind);\n if (file.type === type) return file;\n return new File([file], file.name, { type, lastModified: file.lastModified });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-media-meta.d.ts","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"read-media-meta.d.ts","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAGlF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AA4ED,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAAC,CA6B5E"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';
|
|
2
|
+
import { releaseObjectUrl } from './validate-media-duration.js';
|
|
2
3
|
function simplifiedToKind(s) {
|
|
3
4
|
if (s === 'image' || s === 'video' || s === 'audio')
|
|
4
5
|
return s;
|
|
@@ -26,10 +27,7 @@ async function readImage(file) {
|
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
img.onerror = () => {
|
|
29
|
-
|
|
30
|
-
URL.revokeObjectURL(url);
|
|
31
|
-
}
|
|
32
|
-
catch { }
|
|
30
|
+
releaseObjectUrl(url);
|
|
33
31
|
resolve({});
|
|
34
32
|
};
|
|
35
33
|
img.src = url;
|
|
@@ -43,9 +41,7 @@ async function readAV(file) {
|
|
|
43
41
|
const url = URL.createObjectURL(file);
|
|
44
42
|
el.onloadedmetadata = () => {
|
|
45
43
|
try {
|
|
46
|
-
const durationSec = Number.isFinite(el.duration)
|
|
47
|
-
? el.duration
|
|
48
|
-
: undefined;
|
|
44
|
+
const durationSec = Number.isFinite(el.duration) ? el.duration : undefined;
|
|
49
45
|
if (isVideo) {
|
|
50
46
|
const v = el;
|
|
51
47
|
const width = v.videoWidth || undefined;
|
|
@@ -62,17 +58,11 @@ async function readAV(file) {
|
|
|
62
58
|
}
|
|
63
59
|
}
|
|
64
60
|
finally {
|
|
65
|
-
|
|
66
|
-
URL.revokeObjectURL(url);
|
|
67
|
-
}
|
|
68
|
-
catch { }
|
|
61
|
+
releaseObjectUrl(url);
|
|
69
62
|
}
|
|
70
63
|
};
|
|
71
64
|
el.onerror = () => {
|
|
72
|
-
|
|
73
|
-
URL.revokeObjectURL(url);
|
|
74
|
-
}
|
|
75
|
-
catch { }
|
|
65
|
+
releaseObjectUrl(url);
|
|
76
66
|
resolve({});
|
|
77
67
|
};
|
|
78
68
|
el.src = url;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-media-meta.js","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"read-media-meta.js","sourceRoot":"","sources":["../../src/lib/read-media-meta.ts"],"names":[],"mappings":"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","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport type { MediaKind, VideoOrientation } from '@ttt-productions/media-schemas';\nimport { releaseObjectUrl } from './validate-media-duration.js';\n\nexport interface ReadMediaMetaResult {\n kind: MediaKind;\n mime?: string;\n sizeBytes: number;\n width?: number;\n height?: number;\n durationSec?: number;\n orientation?: VideoOrientation;\n aspectRatio?: number;\n}\n\nfunction simplifiedToKind(s: ReturnType<typeof getSimplifiedMediaType>): MediaKind {\n if (s === 'image' || s === 'video' || s === 'audio') return s;\n return 'file';\n}\n\nfunction computeOrientation(width?: number, height?: number): VideoOrientation | undefined {\n if (!width || !height) return undefined;\n if (width === height) return 'any';\n return height > width ? 'vertical' : 'horizontal';\n}\n\nasync function readImage(\n file: File,\n): Promise<Pick<ReadMediaMetaResult, 'width' | 'height' | 'aspectRatio'>> {\n return new Promise((resolve) => {\n const url = URL.createObjectURL(file);\n const img = new Image();\n img.onload = () => {\n try {\n const width = img.naturalWidth;\n const height = img.naturalHeight;\n resolve({ width, height, aspectRatio: height ? width / height : undefined });\n } finally {\n URL.revokeObjectURL(url);\n }\n };\n img.onerror = () => {\n releaseObjectUrl(url);\n resolve({});\n };\n img.src = url;\n });\n}\n\nasync function readAV(\n file: File,\n): Promise<Pick<ReadMediaMetaResult, 'width' | 'height' | 'durationSec' | 'aspectRatio'>> {\n return new Promise((resolve) => {\n const isVideo = file.type.startsWith('video/');\n const el = document.createElement(isVideo ? 'video' : 'audio');\n el.preload = 'metadata';\n\n const url = URL.createObjectURL(file);\n\n el.onloadedmetadata = () => {\n try {\n const durationSec = Number.isFinite(el.duration) ? el.duration : undefined;\n if (isVideo) {\n const v = el as HTMLVideoElement;\n const width = v.videoWidth || undefined;\n const height = v.videoHeight || undefined;\n resolve({\n width,\n height,\n durationSec,\n aspectRatio: height ? width! / height : undefined,\n });\n } else {\n resolve({ durationSec });\n }\n } finally {\n releaseObjectUrl(url);\n }\n };\n\n el.onerror = () => {\n releaseObjectUrl(url);\n resolve({});\n };\n\n el.src = url;\n });\n}\n\nexport async function readMediaMeta(file: File): Promise<ReadMediaMetaResult> {\n const simplified = getSimplifiedMediaType(file);\n const kind = simplifiedToKind(simplified);\n\n const base: ReadMediaMetaResult = {\n kind,\n mime: file.type || undefined,\n sizeBytes: file.size,\n };\n\n if (kind === 'image') {\n const img = await readImage(file);\n return {\n ...base,\n ...img,\n orientation: computeOrientation(img.width, img.height),\n };\n }\n\n if (kind === 'video' || kind === 'audio') {\n const av = await readAV(file);\n return {\n ...base,\n ...av,\n orientation: kind === 'video' ? computeOrientation(av.width, av.height) : undefined,\n };\n }\n\n return base;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"validate-media-duration.d.ts","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAMlD;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BhG"}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';
|
|
2
|
+
export function releaseObjectUrl(url) {
|
|
3
|
+
try {
|
|
4
|
+
URL.revokeObjectURL(url);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
// A runtime without object-URL support throws here; revoking is cleanup, so nothing is lost.
|
|
8
|
+
}
|
|
9
|
+
}
|
|
2
10
|
export async function validateMediaDuration(file, maxDurationSec) {
|
|
3
11
|
return new Promise((resolve) => {
|
|
4
12
|
const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');
|
|
@@ -8,10 +16,7 @@ export async function validateMediaDuration(file, maxDurationSec) {
|
|
|
8
16
|
if (settled)
|
|
9
17
|
return;
|
|
10
18
|
settled = true;
|
|
11
|
-
|
|
12
|
-
URL.revokeObjectURL(el.src);
|
|
13
|
-
}
|
|
14
|
-
catch { }
|
|
19
|
+
releaseObjectUrl(el.src);
|
|
15
20
|
resolve(ok);
|
|
16
21
|
};
|
|
17
22
|
el.onloadedmetadata = () => {
|
|
@@ -19,18 +24,13 @@ export async function validateMediaDuration(file, maxDurationSec) {
|
|
|
19
24
|
finish(el.duration <= maxDurationSec);
|
|
20
25
|
return;
|
|
21
26
|
}
|
|
22
|
-
// Chromium MediaRecorder blobs
|
|
23
|
-
//
|
|
24
|
-
// recording. Resolve the real duration via the shared seek workaround;
|
|
25
|
-
// if it still cannot be determined, let the file through — the server
|
|
26
|
-
// measures every file's length and refuses one over the limit or with
|
|
27
|
-
// no length at all.
|
|
27
|
+
// Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround
|
|
28
|
+
// learns the real one.
|
|
28
29
|
void resolveInfiniteDuration(el).then((duration) => {
|
|
29
30
|
finish(duration === null ? true : duration <= maxDurationSec);
|
|
30
31
|
});
|
|
31
32
|
};
|
|
32
33
|
el.onerror = () => {
|
|
33
|
-
// Fail open on client (backend can enforce again).
|
|
34
34
|
finish(true);
|
|
35
35
|
};
|
|
36
36
|
el.src = URL.createObjectURL(file);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,
|
|
1
|
+
{"version":3,"file":"validate-media-duration.js","sourceRoot":"","sources":["../../src/lib/validate-media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AAExE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,IAAI,CAAC;QACH,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,6FAA6F;IAC/F,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,IAAU,EAAE,cAAsB;IAC5E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,EAAE,GAAG,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtF,EAAE,CAAC,OAAO,GAAG,UAAU,CAAC;QAExB,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,MAAM,GAAG,CAAC,EAAW,EAAE,EAAE;YAC7B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,gBAAgB,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;YACzB,OAAO,CAAC,EAAE,CAAC,CAAC;QACd,CAAC,CAAC;QAEF,EAAE,CAAC,gBAAgB,GAAG,GAAG,EAAE;YACzB,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,MAAM,CAAC,EAAE,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;gBACtC,OAAO;YACT,CAAC;YACD,+FAA+F;YAC/F,uBAAuB;YACvB,KAAK,uBAAuB,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE;gBACjD,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,IAAI,cAAc,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,EAAE,CAAC,OAAO,GAAG,GAAG,EAAE;YAChB,MAAM,CAAC,IAAI,CAAC,CAAC;QACf,CAAC,CAAC;QAEF,EAAE,CAAC,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { resolveInfiniteDuration } from '@ttt-productions/media-viewer';\n\nexport function releaseObjectUrl(url: string): void {\n try {\n URL.revokeObjectURL(url);\n } catch {\n // A runtime without object-URL support throws here; revoking is cleanup, so nothing is lost.\n }\n}\n\nexport async function validateMediaDuration(file: File, maxDurationSec: number): Promise<boolean> {\n return new Promise((resolve) => {\n const el = document.createElement(file.type.startsWith('video/') ? 'video' : 'audio');\n el.preload = 'metadata';\n\n let settled = false;\n const finish = (ok: boolean) => {\n if (settled) return;\n settled = true;\n releaseObjectUrl(el.src);\n resolve(ok);\n };\n\n el.onloadedmetadata = () => {\n if (Number.isFinite(el.duration)) {\n finish(el.duration <= maxDurationSec);\n return;\n }\n // Chromium MediaRecorder blobs report duration Infinity at loadedmetadata; the seek workaround\n // learns the real one.\n void resolveInfiniteDuration(el).then((duration) => {\n finish(duration === null ? true : duration <= maxDurationSec);\n });\n };\n\n el.onerror = () => {\n finish(true);\n };\n\n el.src = URL.createObjectURL(file);\n });\n}\n"]}
|
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
type SingleKind = 'image' | 'video' | 'audio';
|
|
2
2
|
export type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];
|
|
3
|
-
/**
|
|
4
|
-
* Diagnostic codes emitted by validateAndNormalizeUploadFile when validation
|
|
5
|
-
* fails. Consumers can route these to monitoring/Sentry via the onError option.
|
|
6
|
-
*/
|
|
7
3
|
export type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';
|
|
8
4
|
export interface ValidateUploadFileOptions {
|
|
9
|
-
/**
|
|
10
|
-
* Optional diagnostic callback fired before the function throws.
|
|
11
|
-
* Useful for routing to Sentry / monitoring-core. The thrown Error
|
|
12
|
-
* carries the user-facing message; this callback gets the machine
|
|
13
|
-
* code + structured extras.
|
|
14
|
-
*/
|
|
15
5
|
onError?: (code: ValidateUploadFileErrorCode, extra: {
|
|
16
6
|
fileOrigin: string;
|
|
17
7
|
expectedKind: string;
|
|
@@ -20,32 +10,8 @@ export interface ValidateUploadFileOptions {
|
|
|
20
10
|
fileSize: number;
|
|
21
11
|
fileType: string;
|
|
22
12
|
}) => void;
|
|
23
|
-
/**
|
|
24
|
-
* FAIL-OPEN for indeterminate picker metadata: when the file's kind cannot
|
|
25
|
-
* be determined from browser MIME/extension, pass it through with the
|
|
26
|
-
* NEUTRAL `application/octet-stream` type (never a fabricated media type)
|
|
27
|
-
* instead of throwing, so the server's canonical byte inspector — the only
|
|
28
|
-
* classification authority — decides. The kind-mismatch check still throws
|
|
29
|
-
* when the client metadata IS determinate. Requires the upload path to opt
|
|
30
|
-
* into `allowNeutralContentType`.
|
|
31
|
-
*/
|
|
32
13
|
allowIndeterminate?: boolean;
|
|
33
14
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Validates and normalizes a file for upload.
|
|
36
|
-
*
|
|
37
|
-
* - Throws a user-friendly Error if the file's kind cannot be determined
|
|
38
|
-
* (unless `allowIndeterminate` fails open to the neutral content type).
|
|
39
|
-
* - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't
|
|
40
|
-
* match the detected kind.
|
|
41
|
-
* - Otherwise returns a File with a guaranteed valid media contentType.
|
|
42
|
-
*
|
|
43
|
-
* Optional `onError` callback fires with a machine-readable code + structured
|
|
44
|
-
* extras before the throw, so consumers can route to monitoring without this
|
|
45
|
-
* package depending on any monitoring library.
|
|
46
|
-
*
|
|
47
|
-
* Use this at the top of every upload mutation, before uploadFileResumable.
|
|
48
|
-
*/
|
|
49
15
|
export declare function validateAndNormalizeUploadFile(file: File, fileOrigin: string, expectedKind: ExpectedKind, options?: ValidateUploadFileOptions): File;
|
|
50
16
|
export {};
|
|
51
17
|
//# sourceMappingURL=validate-upload-kind.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE
|
|
1
|
+
{"version":3,"file":"validate-upload-kind.d.ts","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAGA,KAAK,UAAU,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAC9C,MAAM,MAAM,YAAY,GAAG,UAAU,GAAG,KAAK,GAAG,SAAS,UAAU,EAAE,CAAC;AAEtE,MAAM,MAAM,2BAA2B,GAAG,2BAA2B,GAAG,sBAAsB,CAAC;AAE/F,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,CACR,IAAI,EAAE,2BAA2B,EACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC;QACpC,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,KACE,IAAI,CAAC;IACV,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAcD,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,YAAY,EAC1B,OAAO,CAAC,EAAE,yBAAyB,GAClC,IAAI,CAmCN"}
|
|
@@ -14,27 +14,10 @@ function matchesExpected(detected, expected) {
|
|
|
14
14
|
return expected.includes(detected);
|
|
15
15
|
return detected === expected;
|
|
16
16
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Validates and normalizes a file for upload.
|
|
19
|
-
*
|
|
20
|
-
* - Throws a user-friendly Error if the file's kind cannot be determined
|
|
21
|
-
* (unless `allowIndeterminate` fails open to the neutral content type).
|
|
22
|
-
* - Throws a user-friendly Error if `expectedKind` is not 'any' and doesn't
|
|
23
|
-
* match the detected kind.
|
|
24
|
-
* - Otherwise returns a File with a guaranteed valid media contentType.
|
|
25
|
-
*
|
|
26
|
-
* Optional `onError` callback fires with a machine-readable code + structured
|
|
27
|
-
* extras before the throw, so consumers can route to monitoring without this
|
|
28
|
-
* package depending on any monitoring library.
|
|
29
|
-
*
|
|
30
|
-
* Use this at the top of every upload mutation, before uploadFileResumable.
|
|
31
|
-
*/
|
|
32
17
|
export function validateAndNormalizeUploadFile(file, fileOrigin, expectedKind, options) {
|
|
33
18
|
const detected = getSimplifiedMediaType(file);
|
|
34
19
|
if (detected === 'other') {
|
|
35
20
|
if (options?.allowIndeterminate === true) {
|
|
36
|
-
// No fallback kind: inferContentType resolves an unknown file to the
|
|
37
|
-
// NEUTRAL type, never a fabricated media fact.
|
|
38
21
|
return ensureFileWithContentType(file);
|
|
39
22
|
}
|
|
40
23
|
options?.onError?.('upload_kind_indeterminate', {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-upload-kind.js","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"validate-upload-kind.js","sourceRoot":"","sources":["../../src/lib/validate-upload-kind.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAsBpE,SAAS,gBAAgB,CAAC,QAAsB;IAC9C,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,wBAAwB,CAAC;IACxD,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1D,OAAO,QAAkB,CAAC;AAC5B,CAAC;AAED,SAAS,eAAe,CAAC,QAAoB,EAAE,QAAsB;IACnE,IAAI,QAAQ,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IACpC,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;QAAE,OAAQ,QAAkC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAC3F,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,8BAA8B,CAC5C,IAAU,EACV,UAAkB,EAClB,YAA0B,EAC1B,OAAmC;IAEnC,MAAM,QAAQ,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAE9C,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,IAAI,OAAO,EAAE,kBAAkB,KAAK,IAAI,EAAE,CAAC;YACzC,OAAO,yBAAyB,CAAC,IAAI,CAAC,CAAC;QACzC,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,CAAC,2BAA2B,EAAE;YAC9C,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,oGAAoG,CACrG,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,eAAe,CAAC,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,OAAO,EAAE,CAAC,sBAAsB,EAAE;YACzC,UAAU;YACV,YAAY,EAAE,gBAAgB,CAAC,YAAY,CAAC;YAC5C,YAAY,EAAE,QAAQ;YACtB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,QAAQ,EAAE,IAAI,CAAC,IAAI;SACpB,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CACb,0BAA0B,gBAAgB,CAAC,YAAY,CAAC,gCAAgC,QAAQ,GAAG,CACpG,CAAC;IACJ,CAAC;IAED,OAAO,yBAAyB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACnD,CAAC","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\nimport { ensureFileWithContentType } from './infer-content-type.js';\n\ntype SingleKind = 'image' | 'video' | 'audio';\nexport type ExpectedKind = SingleKind | 'any' | readonly SingleKind[];\n\nexport type ValidateUploadFileErrorCode = 'upload_kind_indeterminate' | 'upload_kind_mismatch';\n\nexport interface ValidateUploadFileOptions {\n onError?: (\n code: ValidateUploadFileErrorCode,\n extra: {\n fileOrigin: string;\n expectedKind: string;\n detectedKind?: SingleKind | 'other';\n fileName: string;\n fileSize: number;\n fileType: string;\n },\n ) => void;\n allowIndeterminate?: boolean;\n}\n\nfunction describeExpected(expected: ExpectedKind): string {\n if (expected === 'any') return 'image, video, or audio';\n if (Array.isArray(expected)) return expected.join(' or ');\n return expected as string;\n}\n\nfunction matchesExpected(detected: SingleKind, expected: ExpectedKind): boolean {\n if (expected === 'any') return true;\n if (Array.isArray(expected)) return (expected as readonly SingleKind[]).includes(detected);\n return detected === expected;\n}\n\nexport function validateAndNormalizeUploadFile(\n file: File,\n fileOrigin: string,\n expectedKind: ExpectedKind,\n options?: ValidateUploadFileOptions,\n): File {\n const detected = getSimplifiedMediaType(file);\n\n if (detected === 'other') {\n if (options?.allowIndeterminate === true) {\n return ensureFileWithContentType(file);\n }\n options?.onError?.('upload_kind_indeterminate', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n 'Could not determine the file type. Please try a different file or re-export it in a common format.',\n );\n }\n\n if (!matchesExpected(detected, expectedKind)) {\n options?.onError?.('upload_kind_mismatch', {\n fileOrigin,\n expectedKind: describeExpected(expectedKind),\n detectedKind: detected,\n fileName: file.name,\n fileSize: file.size,\n fileType: file.type,\n });\n throw new Error(\n `This slot only accepts ${describeExpected(expectedKind)} files. The selected file is ${detected}.`,\n );\n }\n\n return ensureFileWithContentType(file, detected);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/audio-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"audio-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/audio-input.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,wBAAgB,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,aAAa,CAAC,+BAEpE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audio-input.js","sourceRoot":"","sources":["../../../src/react/components/audio-input.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,UAAU,UAAU,CAAC,KAA0C;IACnE,OAAO,KAAC,SAAS,OAAK,KAAK,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,GAAI,CAAC;AAC1D,CAAC","sourcesContent":["import type { FileInputProps } from '
|
|
1
|
+
{"version":3,"file":"audio-input.js","sourceRoot":"","sources":["../../../src/react/components/audio-input.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,MAAM,UAAU,UAAU,CAAC,KAA0C;IACnE,OAAO,KAAC,SAAS,OAAK,KAAK,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,GAAI,CAAC;AAC1D,CAAC","sourcesContent":["import type { FileInputProps } from './file-input.js';\nimport { FileInput } from './file-input.js';\n\nexport function AudioInput(props: Omit<FileInputProps, 'acceptTypes'>) {\n return <FileInput {...props} acceptTypes={['audio']} />;\n}\n"]}
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type SimplifiedMediaType } from '@ttt-productions/media-schemas';
|
|
2
|
+
import { type ButtonProps } from '@ttt-productions/ui-core/react';
|
|
3
|
+
import type { CropConfig, FileInputChangePayload, FileInputError } from '../../types.js';
|
|
4
|
+
export interface FileInputProps {
|
|
5
|
+
acceptTypes: SimplifiedMediaType[];
|
|
6
|
+
maxSizeMB: Partial<Record<SimplifiedMediaType, number>>;
|
|
7
|
+
selectedFile?: File | null;
|
|
8
|
+
onChange: (payload: FileInputChangePayload) => void;
|
|
9
|
+
onError?: (error: FileInputError) => void;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
buttonLabel?: string;
|
|
13
|
+
clearLabel?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
variant?: ButtonProps['variant'];
|
|
16
|
+
size?: ButtonProps['size'];
|
|
17
|
+
uploadProgress?: number | null;
|
|
18
|
+
videoMaxDurationSec?: number;
|
|
19
|
+
audioMaxDurationSec?: number;
|
|
20
|
+
cropConfig?: CropConfig;
|
|
21
|
+
defaultShowDetails?: boolean;
|
|
22
|
+
}
|
|
2
23
|
export declare function FileInput(props: FileInputProps): import("react").JSX.Element;
|
|
3
24
|
//# sourceMappingURL=file-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-input.d.ts","sourceRoot":"","sources":["../../../src/react/components/file-input.tsx"],"names":[],"mappings":"AAGA,OAAO,EAA0B,KAAK,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAClG,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,gCAAgC,CAAC;AAIxC,OAAO,KAAK,EAAE,UAAU,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiCzF,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,YAAY,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAE3B,QAAQ,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACpD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAE1C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,OAAO,CAAC,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAE3B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAsV9C"}
|
|
@@ -7,6 +7,7 @@ import { cn } from '@ttt-productions/ui-core';
|
|
|
7
7
|
import { Info, X, Upload, Film, Music, Paperclip, AlertTriangle } from 'lucide-react';
|
|
8
8
|
import { validateMediaDuration } from '../../lib/validate-media-duration.js';
|
|
9
9
|
import { ImageCropperModal } from './image-cropper-modal.js';
|
|
10
|
+
import { releaseObjectUrl } from '../../lib/validate-media-duration.js';
|
|
10
11
|
const MIME_MAP = {
|
|
11
12
|
image: 'image/jpeg, image/png, image/gif, image/webp, image/svg+xml, image/bmp, image/avif',
|
|
12
13
|
video: 'video/*, .mkv',
|
|
@@ -35,10 +36,7 @@ export function FileInput(props) {
|
|
|
35
36
|
const revokeLastPreviewUrl = useCallback(() => {
|
|
36
37
|
if (!lastPreviewUrlRef.current)
|
|
37
38
|
return;
|
|
38
|
-
|
|
39
|
-
URL.revokeObjectURL(lastPreviewUrlRef.current);
|
|
40
|
-
}
|
|
41
|
-
catch { }
|
|
39
|
+
releaseObjectUrl(lastPreviewUrlRef.current);
|
|
42
40
|
lastPreviewUrlRef.current = null;
|
|
43
41
|
}, []);
|
|
44
42
|
const makePreviewUrl = useCallback((blob) => {
|
|
@@ -138,7 +136,6 @@ export function FileInput(props) {
|
|
|
138
136
|
return;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
|
-
// cropping path
|
|
142
139
|
if (cropConfig && fileType === 'image') {
|
|
143
140
|
const reader = new FileReader();
|
|
144
141
|
reader.onload = () => {
|
|
@@ -152,7 +149,6 @@ export function FileInput(props) {
|
|
|
152
149
|
setInternalSelected(file);
|
|
153
150
|
return;
|
|
154
151
|
}
|
|
155
|
-
// normal selection
|
|
156
152
|
setInternalSelected(file);
|
|
157
153
|
const previewUrl = makePreviewUrl(file);
|
|
158
154
|
onChange({ type: fileType, file, previewUrl, revokePreviewUrl: revokeLastPreviewUrl });
|