@stoker-platform/web-app 0.5.108 → 0.5.110
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/CHANGELOG.md +19 -0
- package/package.json +4 -4
- package/src/Files.tsx +163 -46
- package/src/Form.tsx +24 -2
- package/src/utils/prepareFile.ts +130 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @stoker-platform/web-app
|
|
2
2
|
|
|
3
|
+
## 0.5.110
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- feat: add image thumbnails option
|
|
8
|
+
- @stoker-platform/node-client@0.5.62
|
|
9
|
+
- @stoker-platform/utils@0.5.53
|
|
10
|
+
- @stoker-platform/web-client@0.5.63
|
|
11
|
+
|
|
12
|
+
## 0.5.109
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- feat: add file options
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @stoker-platform/utils@0.5.52
|
|
19
|
+
- @stoker-platform/node-client@0.5.61
|
|
20
|
+
- @stoker-platform/web-client@0.5.62
|
|
21
|
+
|
|
3
22
|
## 0.5.108
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stoker-platform/web-app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.110",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"scripts": {
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
52
|
"@react-google-maps/api": "^2.20.8",
|
|
53
53
|
"@sentry/react": "^10.50.0",
|
|
54
|
-
"@stoker-platform/node-client": "0.5.
|
|
55
|
-
"@stoker-platform/utils": "0.5.
|
|
56
|
-
"@stoker-platform/web-client": "0.5.
|
|
54
|
+
"@stoker-platform/node-client": "0.5.62",
|
|
55
|
+
"@stoker-platform/utils": "0.5.53",
|
|
56
|
+
"@stoker-platform/web-client": "0.5.63",
|
|
57
57
|
"@tanstack/react-table": "^8.21.3",
|
|
58
58
|
"@types/react": "18.3.13",
|
|
59
59
|
"@types/react-dom": "18.3.1",
|
package/src/Files.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { Helmet } from "react-helmet"
|
|
|
2
2
|
import {
|
|
3
3
|
CollectionMeta,
|
|
4
4
|
CollectionSchema,
|
|
5
|
+
FileOptions,
|
|
5
6
|
StokerCollection,
|
|
6
7
|
StokerRecord,
|
|
7
8
|
StorageItem,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
getMetadata,
|
|
27
28
|
updateMetadata,
|
|
28
29
|
} from "firebase/storage"
|
|
30
|
+
import type { FirebaseStorage } from "firebase/storage"
|
|
29
31
|
import { getAuth } from "firebase/auth"
|
|
30
32
|
import { Progress } from "./components/ui/progress"
|
|
31
33
|
import { Button } from "./components/ui/button"
|
|
@@ -68,6 +70,84 @@ import {
|
|
|
68
70
|
AlertDialogTitle,
|
|
69
71
|
} from "./components/ui/alert-dialog"
|
|
70
72
|
import { FilePermissionsDialog, FilePermissions } from "./FilePermissions"
|
|
73
|
+
import { prepareFile } from "./utils/prepareFile"
|
|
74
|
+
|
|
75
|
+
const IMAGE_FILE_EXTENSIONS = new Set([
|
|
76
|
+
"avif",
|
|
77
|
+
"bmp",
|
|
78
|
+
"gif",
|
|
79
|
+
"heic",
|
|
80
|
+
"heif",
|
|
81
|
+
"ico",
|
|
82
|
+
"jpeg",
|
|
83
|
+
"jpg",
|
|
84
|
+
"png",
|
|
85
|
+
"svg",
|
|
86
|
+
"webp",
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
const isImageFile = (name: string): boolean => {
|
|
90
|
+
const dot = name.lastIndexOf(".")
|
|
91
|
+
if (dot <= 0) return false
|
|
92
|
+
return IMAGE_FILE_EXTENSIONS.has(name.slice(dot + 1).toLowerCase())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
interface FileImageThumbnailProps {
|
|
96
|
+
storage: FirebaseStorage
|
|
97
|
+
fullPath: string
|
|
98
|
+
fileName: string
|
|
99
|
+
pathPrefix: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const FileImageThumbnail = ({ storage, fullPath, fileName, pathPrefix }: FileImageThumbnailProps) => {
|
|
103
|
+
const [phase, setPhase] = useState<"loading" | "display" | "fallback">("loading")
|
|
104
|
+
const [url, setUrl] = useState<string | null>(null)
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!pathPrefix || !fullPath.startsWith(pathPrefix)) {
|
|
108
|
+
setPhase("fallback")
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
let cancelled = false
|
|
112
|
+
setPhase("loading")
|
|
113
|
+
setUrl(null)
|
|
114
|
+
const load = async () => {
|
|
115
|
+
try {
|
|
116
|
+
const fileRef = ref(storage, fullPath)
|
|
117
|
+
const downloadUrl = await getDownloadURL(fileRef)
|
|
118
|
+
if (!cancelled) {
|
|
119
|
+
setUrl(downloadUrl)
|
|
120
|
+
setPhase("display")
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
if (!cancelled) {
|
|
124
|
+
setPhase("fallback")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
void load()
|
|
129
|
+
return () => {
|
|
130
|
+
cancelled = true
|
|
131
|
+
}
|
|
132
|
+
}, [storage, fullPath, pathPrefix])
|
|
133
|
+
|
|
134
|
+
if (phase === "loading") {
|
|
135
|
+
return <div className="h-12 w-12 shrink-0 rounded-md border bg-muted animate-pulse" aria-hidden />
|
|
136
|
+
}
|
|
137
|
+
if (phase === "fallback" || !url) {
|
|
138
|
+
return <File className="h-5 w-5 shrink-0 text-gray-500" aria-hidden />
|
|
139
|
+
}
|
|
140
|
+
return (
|
|
141
|
+
<img
|
|
142
|
+
src={url}
|
|
143
|
+
alt={`Thumbnail for ${fileName}`}
|
|
144
|
+
className="max-h-12 w-12 shrink-0 rounded-md object-contain"
|
|
145
|
+
loading="lazy"
|
|
146
|
+
decoding="async"
|
|
147
|
+
onError={() => setPhase("fallback")}
|
|
148
|
+
/>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
71
151
|
|
|
72
152
|
interface FilesProps {
|
|
73
153
|
collection: CollectionSchema
|
|
@@ -88,6 +168,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
88
168
|
|
|
89
169
|
const [collectionTitle, setCollectionTitle] = useState("")
|
|
90
170
|
const [meta, setMeta] = useState<CollectionMeta | undefined>(undefined)
|
|
171
|
+
const [fileOptions, setFileOptions] = useState<FileOptions | undefined>(undefined)
|
|
91
172
|
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([])
|
|
92
173
|
const [isDragOver, setIsDragOver] = useState(false)
|
|
93
174
|
const [currentPath, setCurrentPath] = useState("")
|
|
@@ -168,6 +249,8 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
168
249
|
setCollectionTitle(titles?.collection || labels.collection)
|
|
169
250
|
const meta = await getCachedConfigValue(customization, [...collectionAdminPath, "meta"])
|
|
170
251
|
setMeta(meta)
|
|
252
|
+
const fileOptions = await getCachedConfigValue(customization, [...collectionAdminPath, "fileOptions"])
|
|
253
|
+
setFileOptions(fileOptions)
|
|
171
254
|
}
|
|
172
255
|
initialize()
|
|
173
256
|
}, [])
|
|
@@ -238,7 +321,15 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
238
321
|
const fileArray = Array.from(files)
|
|
239
322
|
|
|
240
323
|
for (const file of fileArray) {
|
|
241
|
-
|
|
324
|
+
let preferredFilename = (customFilename || file.name).trim()
|
|
325
|
+
let uploadFile = file
|
|
326
|
+
if (fileOptions?.maxImageWidth) {
|
|
327
|
+
const prepared = await prepareFile(file, preferredFilename, fileOptions)
|
|
328
|
+
uploadFile = prepared.file
|
|
329
|
+
preferredFilename = prepared.filename
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const filename = preferredFilename
|
|
242
333
|
const validationError = validateStorageName(filename)
|
|
243
334
|
if (validationError) {
|
|
244
335
|
toast({
|
|
@@ -251,7 +342,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
251
342
|
const filePath = currentPath ? `${basePath}/${currentPath}/${filename}` : `${basePath}/${filename}`
|
|
252
343
|
const storageRef = ref(storage, filePath)
|
|
253
344
|
const uploadItem: UploadProgress = {
|
|
254
|
-
file,
|
|
345
|
+
file: uploadFile,
|
|
255
346
|
progress: 0,
|
|
256
347
|
status: "uploading",
|
|
257
348
|
}
|
|
@@ -280,46 +371,62 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
280
371
|
})
|
|
281
372
|
} catch {
|
|
282
373
|
setUploadProgress((prev) =>
|
|
283
|
-
prev.map((item) => (item.file ===
|
|
374
|
+
prev.map((item) => (item.file === uploadFile ? { ...item, status: "error" } : item)),
|
|
284
375
|
)
|
|
285
376
|
continue
|
|
286
377
|
}
|
|
287
378
|
|
|
288
|
-
const uploadTask = uploadBytesResumable(storageRef,
|
|
379
|
+
const uploadTask = uploadBytesResumable(storageRef, uploadFile, metadata)
|
|
289
380
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
381
|
+
await new Promise<void>((resolve) => {
|
|
382
|
+
uploadTask.on(
|
|
383
|
+
"state_changed",
|
|
384
|
+
(snapshot) => {
|
|
385
|
+
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
|
|
386
|
+
setUploadProgress((prev) =>
|
|
387
|
+
prev.map((item) => (item.file === uploadFile ? { ...item, progress } : item)),
|
|
388
|
+
)
|
|
389
|
+
},
|
|
390
|
+
(error) => {
|
|
391
|
+
void runHooks("postFileAddError", globalConfig, customization, {
|
|
392
|
+
record,
|
|
393
|
+
fullPath: filePath,
|
|
394
|
+
filename,
|
|
395
|
+
permissions: {
|
|
396
|
+
read: metadata.customMetadata.read,
|
|
397
|
+
update: metadata.customMetadata.update,
|
|
398
|
+
delete: metadata.customMetadata.delete,
|
|
399
|
+
},
|
|
400
|
+
error,
|
|
401
|
+
}).catch(() => {})
|
|
402
|
+
setUploadProgress((prev) =>
|
|
403
|
+
prev.map((item) =>
|
|
404
|
+
item.file === uploadFile
|
|
405
|
+
? { ...item, status: "error", error: error.message }
|
|
406
|
+
: item,
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
console.error(error.message)
|
|
410
|
+
toast({
|
|
411
|
+
title: "Upload failed",
|
|
412
|
+
description: `Failed to upload ${filename}`,
|
|
413
|
+
variant: "destructive",
|
|
414
|
+
})
|
|
415
|
+
resolve()
|
|
416
|
+
},
|
|
417
|
+
async () => {
|
|
418
|
+
setUploadProgress((prev) =>
|
|
419
|
+
prev.map((item) =>
|
|
420
|
+
item.file === uploadFile
|
|
421
|
+
? { ...item, status: "completed", completedAt: Date.now() }
|
|
422
|
+
: item,
|
|
423
|
+
),
|
|
424
|
+
)
|
|
425
|
+
toast({
|
|
426
|
+
title: "Upload successful",
|
|
427
|
+
description: `${filename} uploaded successfully`,
|
|
428
|
+
})
|
|
321
429
|
|
|
322
|
-
try {
|
|
323
430
|
await runHooks("postFileAdd", globalConfig, customization, {
|
|
324
431
|
record,
|
|
325
432
|
fullPath: filePath,
|
|
@@ -329,14 +436,13 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
329
436
|
update: metadata.customMetadata.update,
|
|
330
437
|
delete: metadata.customMetadata.delete,
|
|
331
438
|
},
|
|
332
|
-
})
|
|
333
|
-
} catch {
|
|
334
|
-
return
|
|
335
|
-
}
|
|
439
|
+
}).catch(() => {})
|
|
336
440
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
441
|
+
loadDirectory(currentPath)
|
|
442
|
+
resolve()
|
|
443
|
+
},
|
|
444
|
+
)
|
|
445
|
+
})
|
|
340
446
|
}
|
|
341
447
|
|
|
342
448
|
setShowFilenameDialog(false)
|
|
@@ -346,7 +452,7 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
346
452
|
setPendingUploadFiles([])
|
|
347
453
|
setIsMultipleFileUpload(false)
|
|
348
454
|
},
|
|
349
|
-
[record, currentPath, basePath, currentUser],
|
|
455
|
+
[record, currentPath, basePath, currentUser, fileOptions],
|
|
350
456
|
)
|
|
351
457
|
|
|
352
458
|
const handleFileUpload = useCallback(
|
|
@@ -1010,9 +1116,20 @@ export const RecordFiles = ({ collection, record }: FilesProps) => {
|
|
|
1010
1116
|
>
|
|
1011
1117
|
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
1012
1118
|
{item.isFolder ? (
|
|
1013
|
-
<Folder className="h-5 w-5 text-blue-500" />
|
|
1119
|
+
<Folder className="h-5 w-5 shrink-0 text-blue-500" />
|
|
1120
|
+
) : fileOptions?.thumbnails === true && isImageFile(item.name) ? (
|
|
1121
|
+
<FileImageThumbnail
|
|
1122
|
+
storage={storage}
|
|
1123
|
+
fullPath={item.fullPath}
|
|
1124
|
+
fileName={item.name}
|
|
1125
|
+
pathPrefix={
|
|
1126
|
+
record
|
|
1127
|
+
? `${tenantId}/${labels.collection}/${record.id}`
|
|
1128
|
+
: ""
|
|
1129
|
+
}
|
|
1130
|
+
/>
|
|
1014
1131
|
) : (
|
|
1015
|
-
<File className="h-5 w-5 text-gray-500" />
|
|
1132
|
+
<File className="h-5 w-5 shrink-0 text-gray-500" />
|
|
1016
1133
|
)}
|
|
1017
1134
|
|
|
1018
1135
|
{editingFile === item.name && !isDisabled ? (
|
package/src/Form.tsx
CHANGED
|
@@ -2621,7 +2621,18 @@ function RecordForm({
|
|
|
2621
2621
|
uploadTask.on(
|
|
2622
2622
|
"state_changed",
|
|
2623
2623
|
undefined,
|
|
2624
|
-
() => {
|
|
2624
|
+
(error) => {
|
|
2625
|
+
void runHooks("postFileAddError", globalConfig, customization, {
|
|
2626
|
+
record: targetRecord,
|
|
2627
|
+
fullPath: filePath,
|
|
2628
|
+
filename,
|
|
2629
|
+
permissions: {
|
|
2630
|
+
read: metadata.customMetadata.read,
|
|
2631
|
+
update: metadata.customMetadata.update,
|
|
2632
|
+
delete: metadata.customMetadata.delete,
|
|
2633
|
+
},
|
|
2634
|
+
error,
|
|
2635
|
+
}).catch(() => {})
|
|
2625
2636
|
toast({
|
|
2626
2637
|
title: "Upload failed",
|
|
2627
2638
|
description: `Failed to upload ${filename}`,
|
|
@@ -2768,7 +2779,18 @@ function RecordForm({
|
|
|
2768
2779
|
uploadTask.on(
|
|
2769
2780
|
"state_changed",
|
|
2770
2781
|
undefined,
|
|
2771
|
-
() => {
|
|
2782
|
+
(error) => {
|
|
2783
|
+
void runHooks("postFileAddError", globalConfig, customization, {
|
|
2784
|
+
record: targetRecord,
|
|
2785
|
+
fullPath: filePath,
|
|
2786
|
+
filename,
|
|
2787
|
+
permissions: {
|
|
2788
|
+
read: metadata.customMetadata.read,
|
|
2789
|
+
update: metadata.customMetadata.update,
|
|
2790
|
+
delete: metadata.customMetadata.delete,
|
|
2791
|
+
},
|
|
2792
|
+
error,
|
|
2793
|
+
}).catch(() => {})
|
|
2772
2794
|
setIsUploading((prev) => ({ ...prev, [fieldName]: false }))
|
|
2773
2795
|
toast({
|
|
2774
2796
|
title: "Upload failed",
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { FileOptions } from "@stoker-platform/types"
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_WIDTH = 1920
|
|
4
|
+
const DEFAULT_JPEG_QUALITY = 1
|
|
5
|
+
|
|
6
|
+
const replaceExtension = (filename: string, newExt: string): string => {
|
|
7
|
+
const trimmed = filename.trim()
|
|
8
|
+
const dot = trimmed.lastIndexOf(".")
|
|
9
|
+
if (dot <= 0) {
|
|
10
|
+
return `${trimmed}${newExt}`
|
|
11
|
+
}
|
|
12
|
+
return `${trimmed.slice(0, dot)}${newExt}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const loadImageFromFile = (file: File): Promise<HTMLImageElement> => {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const url = URL.createObjectURL(file)
|
|
18
|
+
const img = new Image()
|
|
19
|
+
img.decoding = "async"
|
|
20
|
+
img.onload = () => {
|
|
21
|
+
URL.revokeObjectURL(url)
|
|
22
|
+
resolve(img)
|
|
23
|
+
}
|
|
24
|
+
img.onerror = () => {
|
|
25
|
+
URL.revokeObjectURL(url)
|
|
26
|
+
reject(new Error("Image decode failed"))
|
|
27
|
+
}
|
|
28
|
+
img.src = url
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const prepareFile = async (
|
|
33
|
+
file: File,
|
|
34
|
+
preferredFileName: string,
|
|
35
|
+
fileOptions: FileOptions,
|
|
36
|
+
): Promise<{ file: File; filename: string }> => {
|
|
37
|
+
if (!file.type.startsWith("image/") || file.type === "image/svg+xml") {
|
|
38
|
+
return { file, filename: preferredFileName }
|
|
39
|
+
}
|
|
40
|
+
const usePngOutput = file.type === "image/png"
|
|
41
|
+
|
|
42
|
+
let bitmap: ImageBitmap | undefined
|
|
43
|
+
try {
|
|
44
|
+
let width: number
|
|
45
|
+
let height: number
|
|
46
|
+
let img: HTMLImageElement | undefined
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
bitmap = await createImageBitmap(file)
|
|
50
|
+
width = bitmap.width
|
|
51
|
+
height = bitmap.height
|
|
52
|
+
} catch {
|
|
53
|
+
img = await loadImageFromFile(file)
|
|
54
|
+
width = img.naturalWidth
|
|
55
|
+
height = img.naturalHeight
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (width < 1 || height < 1) {
|
|
59
|
+
return { file, filename: preferredFileName }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const maxWidth =
|
|
63
|
+
typeof fileOptions.maxImageWidth === "number" && fileOptions.maxImageWidth > 0
|
|
64
|
+
? Math.floor(fileOptions.maxImageWidth)
|
|
65
|
+
: DEFAULT_MAX_WIDTH
|
|
66
|
+
|
|
67
|
+
let targetW = width
|
|
68
|
+
let targetH = height
|
|
69
|
+
if (width > maxWidth || height > maxWidth) {
|
|
70
|
+
if (width >= height) {
|
|
71
|
+
targetW = maxWidth
|
|
72
|
+
targetH = Math.max(1, Math.round((height / width) * maxWidth))
|
|
73
|
+
} else {
|
|
74
|
+
targetH = maxWidth
|
|
75
|
+
targetW = Math.max(1, Math.round((width / height) * maxWidth))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const didDownscale = targetW < width || targetH < height
|
|
79
|
+
|
|
80
|
+
const canvas = document.createElement("canvas")
|
|
81
|
+
canvas.width = targetW
|
|
82
|
+
canvas.height = targetH
|
|
83
|
+
const ctx = canvas.getContext("2d")
|
|
84
|
+
if (!ctx) {
|
|
85
|
+
return { file, filename: preferredFileName }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (bitmap) {
|
|
89
|
+
ctx.drawImage(bitmap, 0, 0, targetW, targetH)
|
|
90
|
+
} else if (img) {
|
|
91
|
+
ctx.drawImage(img, 0, 0, targetW, targetH)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const blob = await new Promise<Blob | null>((resolve) => {
|
|
95
|
+
if (usePngOutput) {
|
|
96
|
+
canvas.toBlob((b) => resolve(b), "image/png")
|
|
97
|
+
} else {
|
|
98
|
+
canvas.toBlob((b) => resolve(b), "image/jpeg", DEFAULT_JPEG_QUALITY)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
if (!blob) {
|
|
103
|
+
return { file, filename: preferredFileName }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const noBenefit =
|
|
107
|
+
blob.size >= file.size &&
|
|
108
|
+
!didDownscale &&
|
|
109
|
+
((usePngOutput && file.type === "image/png") || (!usePngOutput && file.type === "image/jpeg"))
|
|
110
|
+
if (noBenefit) {
|
|
111
|
+
return { file, filename: preferredFileName }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const outMime = usePngOutput ? "image/png" : "image/jpeg"
|
|
115
|
+
const ext = usePngOutput ? ".png" : ".jpg"
|
|
116
|
+
const nextFilename = replaceExtension(preferredFileName, ext)
|
|
117
|
+
const outFile = new File([blob], nextFilename, {
|
|
118
|
+
type: outMime,
|
|
119
|
+
lastModified: Date.now(),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return { file: outFile, filename: nextFilename }
|
|
123
|
+
} catch {
|
|
124
|
+
return { file, filename: preferredFileName }
|
|
125
|
+
} finally {
|
|
126
|
+
if (bitmap) {
|
|
127
|
+
bitmap.close()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|