@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,194 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Container,
|
|
5
|
+
SearchBar,
|
|
6
|
+
Typography,
|
|
7
|
+
cls,
|
|
8
|
+
AddIcon,
|
|
9
|
+
CircularProgress,
|
|
10
|
+
RefreshIcon,
|
|
11
|
+
IconButton,
|
|
12
|
+
Tooltip,
|
|
13
|
+
AppsIcon,
|
|
14
|
+
Icon
|
|
15
|
+
} from "@firecms/ui";
|
|
16
|
+
import { useMediaManager } from "../MediaManagerProvider";
|
|
17
|
+
import { MediaAssetCard } from "./MediaAssetCard";
|
|
18
|
+
import { MediaAssetDetails } from "./MediaAssetDetails";
|
|
19
|
+
import { MediaUploadDialog } from "./MediaUploadDialog";
|
|
20
|
+
|
|
21
|
+
export interface MediaLibraryViewProps {
|
|
22
|
+
maxFileSize?: number;
|
|
23
|
+
acceptedMimeTypes?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Main view component for the Media Library.
|
|
28
|
+
* Displays a grid of assets with search, upload, and management capabilities.
|
|
29
|
+
*/
|
|
30
|
+
export function MediaLibraryView({
|
|
31
|
+
maxFileSize,
|
|
32
|
+
acceptedMimeTypes
|
|
33
|
+
}: MediaLibraryViewProps) {
|
|
34
|
+
const controller = useMediaManager();
|
|
35
|
+
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
|
36
|
+
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
|
37
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
38
|
+
|
|
39
|
+
const handleSearch = useCallback((query?: string) => {
|
|
40
|
+
controller.searchAssets(query ?? "");
|
|
41
|
+
}, [controller]);
|
|
42
|
+
|
|
43
|
+
const handleUploadClick = useCallback(() => {
|
|
44
|
+
setUploadDialogOpen(true);
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleFileSelect = useCallback(async (files: File[]) => {
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
await controller.uploadFile(file);
|
|
50
|
+
}
|
|
51
|
+
setUploadDialogOpen(false);
|
|
52
|
+
}, [controller]);
|
|
53
|
+
|
|
54
|
+
const handleRefresh = useCallback(() => {
|
|
55
|
+
controller.refreshAssets();
|
|
56
|
+
}, [controller]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<div
|
|
62
|
+
className="flex-shrink-0 border-b border-surface-accent-200 dark:border-surface-accent-700 bg-surface-50 dark:bg-surface-900">
|
|
63
|
+
<Container maxWidth="6xl" className="py-4">
|
|
64
|
+
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
65
|
+
<div className="flex items-center gap-3">
|
|
66
|
+
<Typography variant="h5" className="font-semibold">
|
|
67
|
+
Media Library
|
|
68
|
+
</Typography>
|
|
69
|
+
{controller.totalCount !== undefined && (
|
|
70
|
+
<Typography
|
|
71
|
+
variant="caption"
|
|
72
|
+
className="bg-surface-accent-100 dark:bg-surface-accent-800 px-2 py-0.5 rounded-full"
|
|
73
|
+
>
|
|
74
|
+
{controller.totalCount} assets
|
|
75
|
+
</Typography>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
80
|
+
<SearchBar
|
|
81
|
+
onTextSearch={handleSearch}
|
|
82
|
+
placeholder="Search assets..."
|
|
83
|
+
className="flex-1 sm:w-64"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<div
|
|
87
|
+
className="flex items-center gap-1 border-l border-surface-accent-200 dark:border-surface-accent-700 pl-2 ml-2">
|
|
88
|
+
<Tooltip title="Grid view">
|
|
89
|
+
<IconButton
|
|
90
|
+
onClick={() => setViewMode("grid")}
|
|
91
|
+
className={cls(
|
|
92
|
+
viewMode === "grid" && "bg-surface-accent-100 dark:bg-surface-accent-800"
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<AppsIcon size="small"/>
|
|
96
|
+
</IconButton>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
<Tooltip title="List view">
|
|
99
|
+
<IconButton
|
|
100
|
+
onClick={() => setViewMode("list")}
|
|
101
|
+
className={cls(
|
|
102
|
+
viewMode === "list" && "bg-surface-accent-100 dark:bg-surface-accent-800"
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
<Icon iconKey="list" size="small"/>
|
|
106
|
+
</IconButton>
|
|
107
|
+
</Tooltip>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<Tooltip title="Refresh">
|
|
111
|
+
<IconButton onClick={handleRefresh} disabled={controller.loading}>
|
|
112
|
+
<RefreshIcon size="small"/>
|
|
113
|
+
</IconButton>
|
|
114
|
+
</Tooltip>
|
|
115
|
+
|
|
116
|
+
<Button
|
|
117
|
+
variant="filled"
|
|
118
|
+
onClick={handleUploadClick}
|
|
119
|
+
>
|
|
120
|
+
<AddIcon size="small"/>
|
|
121
|
+
Upload
|
|
122
|
+
</Button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</Container>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{/* Content */}
|
|
129
|
+
<div className="flex-1 overflow-auto">
|
|
130
|
+
<Container maxWidth="6xl" className="py-6">
|
|
131
|
+
{controller.loading && controller.assets.length === 0 ? (
|
|
132
|
+
<div className="flex items-center justify-center h-64">
|
|
133
|
+
<CircularProgress/>
|
|
134
|
+
</div>
|
|
135
|
+
) : controller.error ? (
|
|
136
|
+
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
137
|
+
<Typography className="text-red-500">
|
|
138
|
+
Error loading assets: {controller.error.message}
|
|
139
|
+
</Typography>
|
|
140
|
+
<Button onClick={handleRefresh}>
|
|
141
|
+
Try Again
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
) : controller.assets.length === 0 ? (
|
|
145
|
+
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
146
|
+
<Typography className="text-surface-accent-500">
|
|
147
|
+
No media assets yet
|
|
148
|
+
</Typography>
|
|
149
|
+
<Button onClick={handleUploadClick}>
|
|
150
|
+
<AddIcon size="small"/>
|
|
151
|
+
Upload your first file
|
|
152
|
+
</Button>
|
|
153
|
+
</div>
|
|
154
|
+
) : (
|
|
155
|
+
<div className={cls(
|
|
156
|
+
viewMode === "grid"
|
|
157
|
+
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
|
|
158
|
+
: "flex flex-col gap-2"
|
|
159
|
+
)}>
|
|
160
|
+
{controller.assets.map(asset => (
|
|
161
|
+
<MediaAssetCard
|
|
162
|
+
key={asset.id}
|
|
163
|
+
asset={asset}
|
|
164
|
+
viewMode={viewMode}
|
|
165
|
+
onClick={() => controller.selectAsset(asset)}
|
|
166
|
+
selected={controller.selectedAsset?.id === asset.id}
|
|
167
|
+
/>
|
|
168
|
+
))}
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</Container>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Details Panel */}
|
|
175
|
+
{controller.selectedAsset && (
|
|
176
|
+
<MediaAssetDetails
|
|
177
|
+
asset={controller.selectedAsset}
|
|
178
|
+
onClose={() => controller.selectAsset(undefined)}
|
|
179
|
+
onUpdate={controller.updateAsset}
|
|
180
|
+
onDelete={controller.deleteAsset}
|
|
181
|
+
/>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{/* Upload Dialog */}
|
|
185
|
+
<MediaUploadDialog
|
|
186
|
+
open={uploadDialogOpen}
|
|
187
|
+
onClose={() => setUploadDialogOpen(false)}
|
|
188
|
+
onUpload={handleFileSelect}
|
|
189
|
+
maxFileSize={maxFileSize}
|
|
190
|
+
acceptedMimeTypes={acceptedMimeTypes}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
Typography,
|
|
5
|
+
cls,
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogActions,
|
|
8
|
+
DialogContent,
|
|
9
|
+
CloudUploadIcon,
|
|
10
|
+
CircularProgress
|
|
11
|
+
} from "@firecms/ui";
|
|
12
|
+
|
|
13
|
+
export interface MediaUploadDialogProps {
|
|
14
|
+
open: boolean;
|
|
15
|
+
onClose: () => void;
|
|
16
|
+
onUpload: (files: File[]) => Promise<void>;
|
|
17
|
+
maxFileSize?: number;
|
|
18
|
+
acceptedMimeTypes?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dialog component for uploading files to the media library.
|
|
23
|
+
* Supports drag-and-drop and file browser selection.
|
|
24
|
+
*/
|
|
25
|
+
export function MediaUploadDialog({
|
|
26
|
+
open,
|
|
27
|
+
onClose,
|
|
28
|
+
onUpload,
|
|
29
|
+
maxFileSize = 52428800, // 50MB default
|
|
30
|
+
acceptedMimeTypes
|
|
31
|
+
}: MediaUploadDialogProps) {
|
|
32
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
33
|
+
const [uploading, setUploading] = useState(false);
|
|
34
|
+
const [error, setError] = useState<string | null>(null);
|
|
35
|
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
36
|
+
|
|
37
|
+
const validateFiles = useCallback((files: File[]): { valid: File[]; errors: string[] } => {
|
|
38
|
+
const valid: File[] = [];
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
if (maxFileSize && file.size > maxFileSize) {
|
|
43
|
+
errors.push(`${file.name}: File too large (max ${formatSize(maxFileSize)})`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (acceptedMimeTypes && !acceptedMimeTypes.some(type => {
|
|
47
|
+
if (type.endsWith("/*")) {
|
|
48
|
+
return file.type.startsWith(type.slice(0, -1));
|
|
49
|
+
}
|
|
50
|
+
return file.type === type;
|
|
51
|
+
})) {
|
|
52
|
+
errors.push(`${file.name}: File type not allowed`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
valid.push(file);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { valid, errors };
|
|
59
|
+
}, [maxFileSize, acceptedMimeTypes]);
|
|
60
|
+
|
|
61
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
setIsDragging(true);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
setIsDragging(false);
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
setIsDragging(false);
|
|
74
|
+
|
|
75
|
+
const files = Array.from(e.dataTransfer.files);
|
|
76
|
+
const { valid, errors } = validateFiles(files);
|
|
77
|
+
|
|
78
|
+
if (errors.length > 0) {
|
|
79
|
+
setError(errors.join("\n"));
|
|
80
|
+
} else {
|
|
81
|
+
setError(null);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setSelectedFiles(prev => [...prev, ...valid]);
|
|
85
|
+
}, [validateFiles]);
|
|
86
|
+
|
|
87
|
+
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
88
|
+
const files = Array.from(e.target.files ?? []);
|
|
89
|
+
const { valid, errors } = validateFiles(files);
|
|
90
|
+
|
|
91
|
+
if (errors.length > 0) {
|
|
92
|
+
setError(errors.join("\n"));
|
|
93
|
+
} else {
|
|
94
|
+
setError(null);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
setSelectedFiles(prev => [...prev, ...valid]);
|
|
98
|
+
}, [validateFiles]);
|
|
99
|
+
|
|
100
|
+
const handleRemoveFile = useCallback((index: number) => {
|
|
101
|
+
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
const handleUpload = useCallback(async () => {
|
|
105
|
+
if (selectedFiles.length === 0) return;
|
|
106
|
+
|
|
107
|
+
setUploading(true);
|
|
108
|
+
setError(null);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await onUpload(selectedFiles);
|
|
112
|
+
setSelectedFiles([]);
|
|
113
|
+
onClose();
|
|
114
|
+
} catch (err) {
|
|
115
|
+
setError(err instanceof Error ? err.message : "Upload failed");
|
|
116
|
+
} finally {
|
|
117
|
+
setUploading(false);
|
|
118
|
+
}
|
|
119
|
+
}, [selectedFiles, onUpload, onClose]);
|
|
120
|
+
|
|
121
|
+
const handleClose = useCallback(() => {
|
|
122
|
+
if (!uploading) {
|
|
123
|
+
setSelectedFiles([]);
|
|
124
|
+
setError(null);
|
|
125
|
+
onClose();
|
|
126
|
+
}
|
|
127
|
+
}, [uploading, onClose]);
|
|
128
|
+
|
|
129
|
+
const formatSize = (bytes: number): string => {
|
|
130
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
131
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
132
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<Dialog
|
|
137
|
+
open={open}
|
|
138
|
+
onOpenChange={(open) => !open && handleClose()}
|
|
139
|
+
maxWidth="md"
|
|
140
|
+
>
|
|
141
|
+
<DialogContent className="p-0">
|
|
142
|
+
<div className="p-4 border-b border-surface-accent-200 dark:border-surface-accent-700">
|
|
143
|
+
<Typography variant="h6">
|
|
144
|
+
Upload Files
|
|
145
|
+
</Typography>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="p-4">
|
|
149
|
+
{/* Drop Zone */}
|
|
150
|
+
<div
|
|
151
|
+
className={cls(
|
|
152
|
+
"border-2 border-dashed rounded-lg p-8",
|
|
153
|
+
"flex flex-col items-center justify-center gap-4",
|
|
154
|
+
"transition-colors duration-150",
|
|
155
|
+
isDragging
|
|
156
|
+
? "border-primary bg-primary/5"
|
|
157
|
+
: "border-surface-accent-300 dark:border-surface-accent-600",
|
|
158
|
+
"hover:border-primary hover:bg-primary/5",
|
|
159
|
+
"cursor-pointer"
|
|
160
|
+
)}
|
|
161
|
+
onDragOver={handleDragOver}
|
|
162
|
+
onDragLeave={handleDragLeave}
|
|
163
|
+
onDrop={handleDrop}
|
|
164
|
+
onClick={() => document.getElementById("file-upload-input")?.click()}
|
|
165
|
+
>
|
|
166
|
+
<CloudUploadIcon
|
|
167
|
+
size="large"
|
|
168
|
+
className={cls(
|
|
169
|
+
isDragging ? "text-primary" : "text-surface-accent-400"
|
|
170
|
+
)}
|
|
171
|
+
/>
|
|
172
|
+
<div className="text-center">
|
|
173
|
+
<Typography variant="body1" className="font-medium">
|
|
174
|
+
Drop files here or click to browse
|
|
175
|
+
</Typography>
|
|
176
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
177
|
+
Maximum file size: {formatSize(maxFileSize)}
|
|
178
|
+
</Typography>
|
|
179
|
+
</div>
|
|
180
|
+
<input
|
|
181
|
+
id="file-upload-input"
|
|
182
|
+
type="file"
|
|
183
|
+
multiple
|
|
184
|
+
accept={acceptedMimeTypes?.join(",")}
|
|
185
|
+
onChange={handleFileSelect}
|
|
186
|
+
className="hidden"
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{/* Error Display */}
|
|
191
|
+
{error && (
|
|
192
|
+
<Typography variant="caption" className="text-red-500 mt-2 block whitespace-pre-line">
|
|
193
|
+
{error}
|
|
194
|
+
</Typography>
|
|
195
|
+
)}
|
|
196
|
+
|
|
197
|
+
{/* Selected Files */}
|
|
198
|
+
{selectedFiles.length > 0 && (
|
|
199
|
+
<div className="mt-4 space-y-2">
|
|
200
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
201
|
+
Selected files ({selectedFiles.length})
|
|
202
|
+
</Typography>
|
|
203
|
+
<div className="max-h-40 overflow-auto space-y-1">
|
|
204
|
+
{selectedFiles.map((file, index) => (
|
|
205
|
+
<div
|
|
206
|
+
key={`${file.name}-${index}`}
|
|
207
|
+
className={cls(
|
|
208
|
+
"flex items-center justify-between p-2 rounded",
|
|
209
|
+
"bg-surface-accent-50 dark:bg-surface-accent-800"
|
|
210
|
+
)}
|
|
211
|
+
>
|
|
212
|
+
<div className="flex-1 min-w-0 mr-2">
|
|
213
|
+
<Typography variant="body2" className="truncate">
|
|
214
|
+
{file.name}
|
|
215
|
+
</Typography>
|
|
216
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
217
|
+
{formatSize(file.size)}
|
|
218
|
+
</Typography>
|
|
219
|
+
</div>
|
|
220
|
+
<Button
|
|
221
|
+
variant="text"
|
|
222
|
+
size="small"
|
|
223
|
+
onClick={(e) => {
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
handleRemoveFile(index);
|
|
226
|
+
}}
|
|
227
|
+
disabled={uploading}
|
|
228
|
+
>
|
|
229
|
+
Remove
|
|
230
|
+
</Button>
|
|
231
|
+
</div>
|
|
232
|
+
))}
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
</DialogContent>
|
|
238
|
+
|
|
239
|
+
<DialogActions>
|
|
240
|
+
<Button
|
|
241
|
+
variant="text"
|
|
242
|
+
onClick={handleClose}
|
|
243
|
+
disabled={uploading}
|
|
244
|
+
>
|
|
245
|
+
Cancel
|
|
246
|
+
</Button>
|
|
247
|
+
<Button
|
|
248
|
+
variant="filled"
|
|
249
|
+
onClick={handleUpload}
|
|
250
|
+
disabled={selectedFiles.length === 0 || uploading}
|
|
251
|
+
>
|
|
252
|
+
{uploading ? (
|
|
253
|
+
<>
|
|
254
|
+
<CircularProgress size="smallest" />
|
|
255
|
+
Uploading...
|
|
256
|
+
</>
|
|
257
|
+
) : (
|
|
258
|
+
`Upload ${selectedFiles.length > 0 ? `(${selectedFiles.length})` : ""}`
|
|
259
|
+
)}
|
|
260
|
+
</Button>
|
|
261
|
+
</DialogActions>
|
|
262
|
+
</Dialog>
|
|
263
|
+
);
|
|
264
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { DataSourceDelegate, StorageSource } from "@firecms/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configuration for a thumbnail size.
|
|
5
|
+
* Thumbnails are generated client-side before upload.
|
|
6
|
+
*/
|
|
7
|
+
export interface ThumbnailSize {
|
|
8
|
+
/**
|
|
9
|
+
* Unique name for this size (e.g., "small", "medium", "large").
|
|
10
|
+
* Used as a key in the thumbnails record.
|
|
11
|
+
*/
|
|
12
|
+
name: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Maximum width in pixels.
|
|
16
|
+
* The image will be scaled to fit within this width while maintaining aspect ratio.
|
|
17
|
+
*/
|
|
18
|
+
width: number;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Maximum height in pixels.
|
|
22
|
+
* The image will be scaled to fit within this height while maintaining aspect ratio.
|
|
23
|
+
*/
|
|
24
|
+
height: number;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* JPEG quality for the thumbnail (0-1).
|
|
28
|
+
* @default 0.8
|
|
29
|
+
*/
|
|
30
|
+
quality?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Configuration options for the Media Manager plugin.
|
|
35
|
+
*/
|
|
36
|
+
export interface MediaManagerConfig {
|
|
37
|
+
/**
|
|
38
|
+
* Storage source for file operations (upload, download, delete).
|
|
39
|
+
* Typically useFirebaseStorageSource() or similar.
|
|
40
|
+
*/
|
|
41
|
+
storageSource: StorageSource;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Data source delegate for metadata persistence.
|
|
45
|
+
* Typically useFirestoreDelegate() or similar.
|
|
46
|
+
*/
|
|
47
|
+
dataSourceDelegate: DataSourceDelegate;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Path in storage where media files will be uploaded.
|
|
51
|
+
* @default "media"
|
|
52
|
+
*/
|
|
53
|
+
storagePath?: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collection path in the database for storing asset metadata.
|
|
57
|
+
* @default "media_assets"
|
|
58
|
+
*/
|
|
59
|
+
collectionPath?: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional bucket name for storage operations.
|
|
63
|
+
* If not specified, uses the default bucket.
|
|
64
|
+
*/
|
|
65
|
+
bucket?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Maximum file size allowed for uploads (in bytes).
|
|
69
|
+
* @default 52428800 (50MB)
|
|
70
|
+
*/
|
|
71
|
+
maxFileSize?: number;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Allowed MIME types for uploads.
|
|
75
|
+
* If not specified, all file types are allowed.
|
|
76
|
+
*/
|
|
77
|
+
acceptedMimeTypes?: string[];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Thumbnail sizes to generate on upload.
|
|
81
|
+
* Thumbnails are generated client-side using canvas before upload.
|
|
82
|
+
* If not specified, no thumbnails are generated.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* thumbnailSizes: [
|
|
87
|
+
* { name: "small", width: 150, height: 150 },
|
|
88
|
+
* { name: "medium", width: 400, height: 400 }
|
|
89
|
+
* ]
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
thumbnailSizes?: ThumbnailSize[];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Subfolder path for storing thumbnails.
|
|
96
|
+
* Thumbnails are stored under `{storagePath}/{thumbnailPath}/{sizeName}/`.
|
|
97
|
+
* @default "thumbs"
|
|
98
|
+
*/
|
|
99
|
+
thumbnailPath?: string;
|
|
100
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { MediaAsset } from "./media_asset";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Controller interface for managing media assets.
|
|
5
|
+
* Provides methods for CRUD operations and state management.
|
|
6
|
+
*/
|
|
7
|
+
export interface MediaManagerController {
|
|
8
|
+
/**
|
|
9
|
+
* Whether the controller is currently loading data
|
|
10
|
+
*/
|
|
11
|
+
loading: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Error if any operation failed
|
|
15
|
+
*/
|
|
16
|
+
error?: Error;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* List of loaded media assets
|
|
20
|
+
*/
|
|
21
|
+
assets: MediaAsset[];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Total count of assets (may differ from assets.length during pagination)
|
|
25
|
+
*/
|
|
26
|
+
totalCount?: number;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Currently selected asset (for details view)
|
|
30
|
+
*/
|
|
31
|
+
selectedAsset?: MediaAsset;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Select an asset for viewing/editing
|
|
35
|
+
*/
|
|
36
|
+
selectAsset: (asset: MediaAsset | undefined) => void;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Upload a new file to the media library
|
|
40
|
+
* @param file The file to upload
|
|
41
|
+
* @param metadata Optional metadata to attach
|
|
42
|
+
* @returns The created MediaAsset
|
|
43
|
+
*/
|
|
44
|
+
uploadFile: (file: File, metadata?: Partial<Pick<MediaAsset, "title" | "altText" | "caption" | "tags">>) => Promise<MediaAsset>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Delete an asset from storage and database
|
|
48
|
+
* @param assetId The ID of the asset to delete
|
|
49
|
+
*/
|
|
50
|
+
deleteAsset: (assetId: string) => Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Update asset metadata
|
|
54
|
+
* @param assetId The ID of the asset to update
|
|
55
|
+
* @param data The fields to update
|
|
56
|
+
*/
|
|
57
|
+
updateAsset: (assetId: string, data: Partial<MediaAsset>) => Promise<void>;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refresh the assets list from the database
|
|
61
|
+
*/
|
|
62
|
+
refreshAssets: () => Promise<void>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Search assets by text
|
|
66
|
+
* @param query Search query string
|
|
67
|
+
*/
|
|
68
|
+
searchAssets: (query: string) => void;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Storage path configuration
|
|
72
|
+
*/
|
|
73
|
+
storagePath: string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Collection path configuration
|
|
77
|
+
*/
|
|
78
|
+
collectionPath: string;
|
|
79
|
+
}
|