@firecms/media_manager 3.1.0-canary.1df3b2c
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/LICENSE +6 -0
- package/dist/MediaManagerProvider.d.ts +14 -0
- package/dist/components/MediaAssetCard.d.ts +13 -0
- package/dist/components/MediaAssetDetails.d.ts +11 -0
- package/dist/components/MediaLibraryCard.d.ts +8 -0
- package/dist/components/MediaLibraryView.d.ts +9 -0
- package/dist/components/MediaUploadDialog.d.ts +13 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.es.js +1280 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +1276 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/types/config.d.ts +87 -0
- package/dist/types/controller.d.ts +66 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/media_asset.d.ts +70 -0
- package/dist/useMediaManagerController.d.ts +18 -0
- package/dist/useMediaManagerPlugin.d.ts +23 -0
- package/package.json +58 -0
- package/src/MediaManagerProvider.tsx +34 -0
- package/src/components/MediaAssetCard.tsx +134 -0
- package/src/components/MediaAssetDetails.tsx +353 -0
- package/src/components/MediaLibraryCard.tsx +55 -0
- package/src/components/MediaLibraryView.tsx +194 -0
- package/src/components/MediaUploadDialog.tsx +264 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +10 -0
- package/src/types/config.ts +100 -0
- package/src/types/controller.ts +79 -0
- package/src/types/index.ts +3 -0
- package/src/types/media_asset.ts +84 -0
- package/src/useMediaManagerController.tsx +387 -0
- package/src/useMediaManagerPlugin.tsx +97 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a media asset stored in the media library.
|
|
3
|
+
* This interface defines the metadata stored in the database for each file.
|
|
4
|
+
*/
|
|
5
|
+
export interface MediaAsset {
|
|
6
|
+
/**
|
|
7
|
+
* Unique identifier for the asset (document ID in the database)
|
|
8
|
+
*/
|
|
9
|
+
id: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Original file name
|
|
13
|
+
*/
|
|
14
|
+
fileName: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Path in storage where the file is located
|
|
18
|
+
*/
|
|
19
|
+
storagePath: string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Cached download URL for quick access
|
|
23
|
+
*/
|
|
24
|
+
downloadURL?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* MIME type of the file (e.g., "image/jpeg", "application/pdf")
|
|
28
|
+
*/
|
|
29
|
+
mimeType: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* File size in bytes
|
|
33
|
+
*/
|
|
34
|
+
size: number;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Dimensions for image/video files
|
|
38
|
+
*/
|
|
39
|
+
dimensions?: {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* User-defined title for the asset
|
|
46
|
+
*/
|
|
47
|
+
title?: string;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Alternative text for accessibility (SEO recommended)
|
|
51
|
+
*/
|
|
52
|
+
altText?: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Optional caption/description
|
|
56
|
+
*/
|
|
57
|
+
caption?: string;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* User-defined tags for categorization
|
|
61
|
+
*/
|
|
62
|
+
tags?: string[];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Timestamp when the asset was created
|
|
66
|
+
*/
|
|
67
|
+
createdAt: Date;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Timestamp when the asset was last updated
|
|
71
|
+
*/
|
|
72
|
+
updatedAt: Date;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Storage bucket name (for multi-bucket setups)
|
|
76
|
+
*/
|
|
77
|
+
bucket?: string;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Thumbnail URLs keyed by size name (e.g., "small", "medium").
|
|
81
|
+
* These are generated during upload based on thumbnailSizes config.
|
|
82
|
+
*/
|
|
83
|
+
thumbnails?: Record<string, string>;
|
|
84
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { DataSourceDelegate, StorageSource } from "@firecms/core";
|
|
3
|
+
import { MediaAsset, MediaManagerController, ThumbnailSize } from "./types";
|
|
4
|
+
import Compressor from "compressorjs";
|
|
5
|
+
|
|
6
|
+
export interface UseMediaManagerControllerProps {
|
|
7
|
+
storageSource: StorageSource;
|
|
8
|
+
dataSourceDelegate: DataSourceDelegate;
|
|
9
|
+
storagePath: string;
|
|
10
|
+
collectionPath: string;
|
|
11
|
+
bucket?: string;
|
|
12
|
+
/** Thumbnail sizes to generate on upload */
|
|
13
|
+
thumbnailSizes?: ThumbnailSize[];
|
|
14
|
+
/** Path prefix for thumbnails. Default: "thumbs" */
|
|
15
|
+
thumbnailPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_THUMBNAIL_PATH = "thumbs";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hook that creates a MediaManagerController for managing media assets.
|
|
22
|
+
* Handles all CRUD operations for files and their metadata.
|
|
23
|
+
*/
|
|
24
|
+
export function useMediaManagerController({
|
|
25
|
+
storageSource,
|
|
26
|
+
dataSourceDelegate,
|
|
27
|
+
storagePath,
|
|
28
|
+
collectionPath,
|
|
29
|
+
bucket,
|
|
30
|
+
thumbnailSizes,
|
|
31
|
+
thumbnailPath = DEFAULT_THUMBNAIL_PATH
|
|
32
|
+
}: UseMediaManagerControllerProps): MediaManagerController {
|
|
33
|
+
const [loading, setLoading] = useState(true);
|
|
34
|
+
const [error, setError] = useState<Error | undefined>();
|
|
35
|
+
const [assets, setAssets] = useState<MediaAsset[]>([]);
|
|
36
|
+
const [selectedAsset, setSelectedAsset] = useState<MediaAsset | undefined>();
|
|
37
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
38
|
+
|
|
39
|
+
// Helper to fetch download URL for an asset
|
|
40
|
+
const fetchDownloadURL = useCallback(async (asset: Omit<MediaAsset, "downloadURL">): Promise<MediaAsset> => {
|
|
41
|
+
try {
|
|
42
|
+
const downloadConfig = await storageSource.getDownloadURL(asset.storagePath, asset.bucket);
|
|
43
|
+
return {
|
|
44
|
+
...asset,
|
|
45
|
+
downloadURL: downloadConfig.url ?? undefined
|
|
46
|
+
};
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.warn(`Failed to get download URL for ${asset.storagePath}:`, err);
|
|
49
|
+
return { ...asset, downloadURL: undefined };
|
|
50
|
+
}
|
|
51
|
+
}, [storageSource]);
|
|
52
|
+
|
|
53
|
+
// Fetch assets from the database
|
|
54
|
+
const refreshAssets = useCallback(async () => {
|
|
55
|
+
setLoading(true);
|
|
56
|
+
setError(undefined);
|
|
57
|
+
try {
|
|
58
|
+
console.log("Fetching media assets from:", collectionPath);
|
|
59
|
+
const entities = await dataSourceDelegate.fetchCollection<Record<string, any>>({
|
|
60
|
+
path: collectionPath,
|
|
61
|
+
orderBy: "createdAt",
|
|
62
|
+
order: "desc"
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log("Fetched entities:", entities.length);
|
|
66
|
+
|
|
67
|
+
if (entities.length === 0) {
|
|
68
|
+
setAssets([]);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Convert entities to assets and fetch download URLs
|
|
73
|
+
const loadedAssets: MediaAsset[] = await Promise.all(
|
|
74
|
+
entities.map(async (entity) => {
|
|
75
|
+
const values = entity.values;
|
|
76
|
+
const baseAsset: Omit<MediaAsset, "downloadURL"> = {
|
|
77
|
+
id: entity.id,
|
|
78
|
+
fileName: values.fileName,
|
|
79
|
+
storagePath: values.storagePath,
|
|
80
|
+
mimeType: values.mimeType,
|
|
81
|
+
size: values.size,
|
|
82
|
+
dimensions: values.dimensions,
|
|
83
|
+
title: values.title,
|
|
84
|
+
altText: values.altText,
|
|
85
|
+
caption: values.caption,
|
|
86
|
+
tags: values.tags,
|
|
87
|
+
bucket: values.bucket,
|
|
88
|
+
createdAt: values.createdAt instanceof Date
|
|
89
|
+
? values.createdAt
|
|
90
|
+
: (values.createdAt?.toDate ? values.createdAt.toDate() : new Date(values.createdAt)),
|
|
91
|
+
updatedAt: values.updatedAt instanceof Date
|
|
92
|
+
? values.updatedAt
|
|
93
|
+
: (values.updatedAt?.toDate ? values.updatedAt.toDate() : new Date(values.updatedAt))
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Fetch download URL for images
|
|
97
|
+
if (baseAsset.mimeType?.startsWith("image/") || baseAsset.mimeType?.startsWith("video/")) {
|
|
98
|
+
return await fetchDownloadURL(baseAsset);
|
|
99
|
+
}
|
|
100
|
+
return { ...baseAsset, downloadURL: undefined };
|
|
101
|
+
})
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
console.log("Loaded assets with URLs:", loadedAssets.length);
|
|
105
|
+
setAssets(loadedAssets);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("Error fetching media assets:", err);
|
|
108
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
109
|
+
} finally {
|
|
110
|
+
setLoading(false);
|
|
111
|
+
}
|
|
112
|
+
}, [dataSourceDelegate, collectionPath, fetchDownloadURL]);
|
|
113
|
+
|
|
114
|
+
// Initial load
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
refreshAssets();
|
|
117
|
+
}, [refreshAssets]);
|
|
118
|
+
|
|
119
|
+
// Upload a new file
|
|
120
|
+
const uploadFile = useCallback(async (
|
|
121
|
+
file: File,
|
|
122
|
+
metadata?: Partial<Pick<MediaAsset, "title" | "altText" | "caption" | "tags">>
|
|
123
|
+
): Promise<MediaAsset> => {
|
|
124
|
+
console.log("Uploading file:", file.name, "to path:", storagePath);
|
|
125
|
+
|
|
126
|
+
// Upload to storage
|
|
127
|
+
const uploadResult = await storageSource.uploadFile({
|
|
128
|
+
file,
|
|
129
|
+
fileName: file.name,
|
|
130
|
+
path: storagePath,
|
|
131
|
+
bucket
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log("Upload result:", uploadResult);
|
|
135
|
+
|
|
136
|
+
// Get download URL immediately after upload
|
|
137
|
+
let downloadURL: string | undefined;
|
|
138
|
+
try {
|
|
139
|
+
const downloadConfig = await storageSource.getDownloadURL(uploadResult.path, uploadResult.bucket);
|
|
140
|
+
downloadURL = downloadConfig.url ?? undefined;
|
|
141
|
+
console.log("Got download URL:", downloadURL);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.warn("Failed to get download URL after upload:", err);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get dimensions for images
|
|
147
|
+
let dimensions: { width: number; height: number } | undefined;
|
|
148
|
+
if (file.type.startsWith("image/")) {
|
|
149
|
+
try {
|
|
150
|
+
dimensions = await getImageDimensions(file);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.warn("Failed to get image dimensions:", err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Generate thumbnails if configured and file is a raster image (skip SVGs)
|
|
157
|
+
const thumbnails: Record<string, string> = {};
|
|
158
|
+
const isRasterImage = file.type.startsWith("image/") && file.type !== "image/svg+xml";
|
|
159
|
+
if (thumbnailSizes && thumbnailSizes.length > 0 && isRasterImage) {
|
|
160
|
+
console.log("Generating thumbnails for sizes:", thumbnailSizes.map(s => s.name));
|
|
161
|
+
|
|
162
|
+
for (const size of thumbnailSizes) {
|
|
163
|
+
try {
|
|
164
|
+
const thumbnailBlob = await generateThumbnail(file, size.width, size.height, size.quality ?? 0.8);
|
|
165
|
+
const thumbFileName = `${Date.now()}_${size.name}_${file.name}`;
|
|
166
|
+
const thumbPath = `${storagePath}/${thumbnailPath}/${size.name}`;
|
|
167
|
+
|
|
168
|
+
// Upload thumbnail
|
|
169
|
+
const thumbUploadResult = await storageSource.uploadFile({
|
|
170
|
+
file: new File([thumbnailBlob], thumbFileName, { type: "image/jpeg" }),
|
|
171
|
+
fileName: thumbFileName,
|
|
172
|
+
path: thumbPath,
|
|
173
|
+
bucket
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Get download URL for thumbnail
|
|
177
|
+
const thumbDownloadConfig = await storageSource.getDownloadURL(thumbUploadResult.path, thumbUploadResult.bucket);
|
|
178
|
+
if (thumbDownloadConfig.url) {
|
|
179
|
+
thumbnails[size.name] = thumbDownloadConfig.url;
|
|
180
|
+
console.log(`Generated ${size.name} thumbnail:`, thumbDownloadConfig.url);
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.warn(`Failed to generate ${size.name} thumbnail:`, err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const now = new Date();
|
|
189
|
+
|
|
190
|
+
// Build asset data, only including defined values (Firestore doesn't allow undefined)
|
|
191
|
+
const assetData: Record<string, any> = {
|
|
192
|
+
fileName: file.name,
|
|
193
|
+
storagePath: uploadResult.path,
|
|
194
|
+
mimeType: file.type,
|
|
195
|
+
size: file.size,
|
|
196
|
+
createdAt: now,
|
|
197
|
+
updatedAt: now
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Only add optional fields if they are defined
|
|
201
|
+
if (dimensions) assetData.dimensions = dimensions;
|
|
202
|
+
if (metadata?.title) assetData.title = metadata.title;
|
|
203
|
+
if (metadata?.altText) assetData.altText = metadata.altText;
|
|
204
|
+
if (metadata?.caption) assetData.caption = metadata.caption;
|
|
205
|
+
if (metadata?.tags && metadata.tags.length > 0) assetData.tags = metadata.tags;
|
|
206
|
+
if (uploadResult.bucket) assetData.bucket = uploadResult.bucket;
|
|
207
|
+
// Store downloadURL in database for quick access later
|
|
208
|
+
if (downloadURL) assetData.downloadURL = downloadURL;
|
|
209
|
+
// Store thumbnails if any were generated
|
|
210
|
+
if (Object.keys(thumbnails).length > 0) assetData.thumbnails = thumbnails;
|
|
211
|
+
|
|
212
|
+
console.log("Saving asset data to database:", assetData);
|
|
213
|
+
|
|
214
|
+
// Save metadata to database
|
|
215
|
+
const entity = await dataSourceDelegate.saveEntity<Record<string, any>>({
|
|
216
|
+
path: collectionPath,
|
|
217
|
+
values: assetData,
|
|
218
|
+
status: "new"
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
console.log("Saved entity:", entity.id);
|
|
222
|
+
|
|
223
|
+
const newAsset: MediaAsset = {
|
|
224
|
+
id: entity.id,
|
|
225
|
+
fileName: file.name,
|
|
226
|
+
storagePath: uploadResult.path,
|
|
227
|
+
downloadURL,
|
|
228
|
+
mimeType: file.type,
|
|
229
|
+
size: file.size,
|
|
230
|
+
createdAt: now,
|
|
231
|
+
updatedAt: now,
|
|
232
|
+
dimensions,
|
|
233
|
+
title: metadata?.title,
|
|
234
|
+
altText: metadata?.altText,
|
|
235
|
+
caption: metadata?.caption,
|
|
236
|
+
tags: metadata?.tags,
|
|
237
|
+
bucket: uploadResult.bucket,
|
|
238
|
+
thumbnails: Object.keys(thumbnails).length > 0 ? thumbnails : undefined
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
setAssets(prev => [newAsset, ...prev]);
|
|
242
|
+
return newAsset;
|
|
243
|
+
}, [storageSource, dataSourceDelegate, storagePath, collectionPath, bucket, thumbnailSizes, thumbnailPath]);
|
|
244
|
+
|
|
245
|
+
// Delete an asset
|
|
246
|
+
const deleteAsset = useCallback(async (assetId: string): Promise<void> => {
|
|
247
|
+
const asset = assets.find(a => a.id === assetId);
|
|
248
|
+
if (!asset) {
|
|
249
|
+
throw new Error(`Asset with id ${assetId} not found`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log("Deleting asset:", assetId, asset.storagePath);
|
|
253
|
+
|
|
254
|
+
// Delete from storage
|
|
255
|
+
try {
|
|
256
|
+
await storageSource.deleteFile(asset.storagePath, asset.bucket);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
console.warn("Failed to delete from storage (may not exist):", err);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Delete from database
|
|
262
|
+
await dataSourceDelegate.deleteEntity({
|
|
263
|
+
entity: {
|
|
264
|
+
id: assetId,
|
|
265
|
+
path: collectionPath,
|
|
266
|
+
values: asset
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
setAssets(prev => prev.filter(a => a.id !== assetId));
|
|
271
|
+
if (selectedAsset?.id === assetId) {
|
|
272
|
+
setSelectedAsset(undefined);
|
|
273
|
+
}
|
|
274
|
+
}, [assets, storageSource, dataSourceDelegate, collectionPath, selectedAsset]);
|
|
275
|
+
|
|
276
|
+
// Update asset metadata
|
|
277
|
+
const updateAsset = useCallback(async (
|
|
278
|
+
assetId: string,
|
|
279
|
+
data: Partial<MediaAsset>
|
|
280
|
+
): Promise<void> => {
|
|
281
|
+
const asset = assets.find(a => a.id === assetId);
|
|
282
|
+
if (!asset) {
|
|
283
|
+
throw new Error(`Asset with id ${assetId} not found`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Filter out undefined values
|
|
287
|
+
const cleanData: Record<string, any> = {};
|
|
288
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
289
|
+
if (value !== undefined) {
|
|
290
|
+
cleanData[key] = value;
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
cleanData.updatedAt = new Date();
|
|
294
|
+
|
|
295
|
+
console.log("Updating asset:", assetId, cleanData);
|
|
296
|
+
|
|
297
|
+
await dataSourceDelegate.saveEntity({
|
|
298
|
+
path: collectionPath,
|
|
299
|
+
entityId: assetId,
|
|
300
|
+
values: cleanData,
|
|
301
|
+
previousValues: asset,
|
|
302
|
+
status: "existing"
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
setAssets(prev => prev.map(a =>
|
|
306
|
+
a.id === assetId ? { ...a, ...cleanData } : a
|
|
307
|
+
));
|
|
308
|
+
|
|
309
|
+
if (selectedAsset?.id === assetId) {
|
|
310
|
+
setSelectedAsset(prev => prev ? { ...prev, ...cleanData } : prev);
|
|
311
|
+
}
|
|
312
|
+
}, [assets, dataSourceDelegate, collectionPath, selectedAsset]);
|
|
313
|
+
|
|
314
|
+
// Search assets (client-side filtering for now)
|
|
315
|
+
const searchAssets = useCallback((query: string) => {
|
|
316
|
+
setSearchQuery(query);
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
const selectAsset = useCallback((asset: MediaAsset | undefined) => {
|
|
320
|
+
setSelectedAsset(asset);
|
|
321
|
+
}, []);
|
|
322
|
+
|
|
323
|
+
// Filter assets based on search query
|
|
324
|
+
const filteredAssets = searchQuery
|
|
325
|
+
? assets.filter(asset =>
|
|
326
|
+
asset.fileName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
327
|
+
asset.title?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
328
|
+
asset.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
329
|
+
)
|
|
330
|
+
: assets;
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
loading,
|
|
334
|
+
error,
|
|
335
|
+
assets: filteredAssets,
|
|
336
|
+
totalCount: assets.length,
|
|
337
|
+
selectedAsset,
|
|
338
|
+
selectAsset,
|
|
339
|
+
uploadFile,
|
|
340
|
+
deleteAsset,
|
|
341
|
+
updateAsset,
|
|
342
|
+
refreshAssets,
|
|
343
|
+
searchAssets,
|
|
344
|
+
storagePath,
|
|
345
|
+
collectionPath
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Helper to get image dimensions from a File
|
|
351
|
+
*/
|
|
352
|
+
function getImageDimensions(file: File): Promise<{ width: number; height: number }> {
|
|
353
|
+
return new Promise((resolve, reject) => {
|
|
354
|
+
const img = new Image();
|
|
355
|
+
img.onload = () => {
|
|
356
|
+
resolve({ width: img.width, height: img.height });
|
|
357
|
+
URL.revokeObjectURL(img.src);
|
|
358
|
+
};
|
|
359
|
+
img.onerror = reject;
|
|
360
|
+
img.src = URL.createObjectURL(file);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Generate a thumbnail from an image file using compressorjs.
|
|
366
|
+
* Uses the same library as the core FireCMS image resize implementation.
|
|
367
|
+
* Maintains aspect ratio while fitting within maxWidth x maxHeight.
|
|
368
|
+
*/
|
|
369
|
+
async function generateThumbnail(
|
|
370
|
+
file: File,
|
|
371
|
+
maxWidth: number,
|
|
372
|
+
maxHeight: number,
|
|
373
|
+
quality: number
|
|
374
|
+
): Promise<Blob> {
|
|
375
|
+
return new Promise<Blob>((resolve, reject) => {
|
|
376
|
+
new Compressor(file, {
|
|
377
|
+
quality,
|
|
378
|
+
maxWidth,
|
|
379
|
+
maxHeight,
|
|
380
|
+
mimeType: "image/jpeg",
|
|
381
|
+
success: (result) => {
|
|
382
|
+
resolve(result);
|
|
383
|
+
},
|
|
384
|
+
error: reject,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useMemo, createContext, useContext, PropsWithChildren } from "react";
|
|
2
|
+
import { FireCMSPlugin, CMSView } from "@firecms/core";
|
|
3
|
+
import { MediaManagerConfig } from "./types";
|
|
4
|
+
import { MediaManagerProvider } from "./MediaManagerProvider";
|
|
5
|
+
import { useMediaManagerController } from "./useMediaManagerController";
|
|
6
|
+
import { MediaLibraryCard } from "./components/MediaLibraryCard";
|
|
7
|
+
import { MediaLibraryView } from "./components/MediaLibraryView";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_STORAGE_PATH = "media";
|
|
10
|
+
const DEFAULT_COLLECTION_PATH = "media_assets";
|
|
11
|
+
|
|
12
|
+
export interface MediaManagerPluginProps extends MediaManagerConfig { }
|
|
13
|
+
|
|
14
|
+
// Context to store the config
|
|
15
|
+
const MediaManagerConfigContext = createContext<MediaManagerConfig | null>(null);
|
|
16
|
+
|
|
17
|
+
function useMediaManagerConfig(): MediaManagerConfig {
|
|
18
|
+
const config = useContext(MediaManagerConfigContext);
|
|
19
|
+
if (!config) {
|
|
20
|
+
throw new Error("useMediaManagerConfig must be used within MediaManagerConfigProvider");
|
|
21
|
+
}
|
|
22
|
+
return config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Internal wrapper that reads config from context
|
|
27
|
+
*/
|
|
28
|
+
function MediaLibraryViewInternal() {
|
|
29
|
+
const config = useMediaManagerConfig();
|
|
30
|
+
const controller = useMediaManagerController({
|
|
31
|
+
storageSource: config.storageSource,
|
|
32
|
+
dataSourceDelegate: config.dataSourceDelegate,
|
|
33
|
+
storagePath: config.storagePath ?? DEFAULT_STORAGE_PATH,
|
|
34
|
+
collectionPath: config.collectionPath ?? DEFAULT_COLLECTION_PATH,
|
|
35
|
+
bucket: config.bucket,
|
|
36
|
+
thumbnailSizes: config.thumbnailSizes,
|
|
37
|
+
thumbnailPath: config.thumbnailPath
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<MediaManagerProvider controller={controller}>
|
|
42
|
+
<MediaLibraryView
|
|
43
|
+
maxFileSize={config.maxFileSize}
|
|
44
|
+
acceptedMimeTypes={config.acceptedMimeTypes}
|
|
45
|
+
/>
|
|
46
|
+
</MediaManagerProvider>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build the media view - this is a static object that doesn't change
|
|
52
|
+
*/
|
|
53
|
+
function buildMediaView(): CMSView {
|
|
54
|
+
return {
|
|
55
|
+
path: "media",
|
|
56
|
+
name: "Media Library",
|
|
57
|
+
description: "Manage your media files and assets",
|
|
58
|
+
group: "Media",
|
|
59
|
+
icon: "perm_media",
|
|
60
|
+
view: <MediaLibraryViewInternal />
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Single static instance of the view
|
|
65
|
+
const MEDIA_VIEW = buildMediaView();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Hook to create the Media Manager plugin for FireCMS.
|
|
69
|
+
*
|
|
70
|
+
* The plugin automatically registers the Media Library view in the navigation.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```tsx
|
|
74
|
+
* const { plugin: mediaManagerPlugin } = useMediaManagerPlugin({
|
|
75
|
+
* storageSource,
|
|
76
|
+
* dataSourceDelegate: firestoreDelegate,
|
|
77
|
+
* storagePath: "media",
|
|
78
|
+
* collectionPath: "media_assets"
|
|
79
|
+
* });
|
|
80
|
+
*
|
|
81
|
+
* // Add plugin to your plugins array - view is auto-registered
|
|
82
|
+
* const plugins = [mediaManagerPlugin, ...otherPlugins];
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export function useMediaManagerPlugin(props: MediaManagerPluginProps): FireCMSPlugin {
|
|
86
|
+
return useMemo(() => ({
|
|
87
|
+
key: "media_manager",
|
|
88
|
+
views: [MEDIA_VIEW],
|
|
89
|
+
provider: {
|
|
90
|
+
Component: ({ children }: PropsWithChildren) => (
|
|
91
|
+
<MediaManagerConfigContext.Provider value={props}>
|
|
92
|
+
{children}
|
|
93
|
+
</MediaManagerConfigContext.Provider>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
} satisfies FireCMSPlugin), []);
|
|
97
|
+
}
|