@firecms/media_manager 3.1.0-canary.02232f4
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 +1741 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +1737 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/locales/de.d.ts +45 -0
- package/dist/locales/en.d.ts +45 -0
- package/dist/locales/es.d.ts +45 -0
- package/dist/locales/fr.d.ts +45 -0
- package/dist/locales/hi.d.ts +45 -0
- package/dist/locales/it.d.ts +45 -0
- package/dist/locales/pt.d.ts +45 -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 +58 -0
- package/src/components/MediaLibraryView.tsx +196 -0
- package/src/components/MediaUploadDialog.tsx +266 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +10 -0
- package/src/locales/de.ts +45 -0
- package/src/locales/en.ts +45 -0
- package/src/locales/es.ts +45 -0
- package/src/locales/fr.ts +45 -0
- package/src/locales/hi.ts +45 -0
- package/src/locales/it.ts +45 -0
- package/src/locales/pt.ts +45 -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 +114 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import React, { useCallback, useState } from "react";
|
|
2
|
+
import { useCreateFormex } from "@firecms/formex";
|
|
3
|
+
import {
|
|
4
|
+
Button,
|
|
5
|
+
Typography,
|
|
6
|
+
cls,
|
|
7
|
+
CloseIcon,
|
|
8
|
+
DeleteIcon,
|
|
9
|
+
DownloadIcon,
|
|
10
|
+
IconButton,
|
|
11
|
+
TextField,
|
|
12
|
+
Chip,
|
|
13
|
+
CircularProgress,
|
|
14
|
+
Dialog,
|
|
15
|
+
DialogActions,
|
|
16
|
+
DialogContent
|
|
17
|
+
} from "@firecms/ui";
|
|
18
|
+
import { useSnackbarController, useStorageSource, useTranslation } from "@firecms/core";
|
|
19
|
+
import { MediaAsset } from "../types";
|
|
20
|
+
|
|
21
|
+
export interface MediaAssetDetailsProps {
|
|
22
|
+
asset: MediaAsset;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
onUpdate: (assetId: string, data: Partial<MediaAsset>) => Promise<void>;
|
|
25
|
+
onDelete: (assetId: string) => Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Side panel component for viewing and editing media asset details.
|
|
30
|
+
*/
|
|
31
|
+
export function MediaAssetDetails({
|
|
32
|
+
asset,
|
|
33
|
+
onClose,
|
|
34
|
+
onUpdate,
|
|
35
|
+
onDelete
|
|
36
|
+
}: MediaAssetDetailsProps) {
|
|
37
|
+
const snackbarController = useSnackbarController();
|
|
38
|
+
const storageSource = useStorageSource();
|
|
39
|
+
const [saving, setSaving] = useState(false);
|
|
40
|
+
const [deleting, setDeleting] = useState(false);
|
|
41
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
|
42
|
+
const [tagInput, setTagInput] = useState("");
|
|
43
|
+
const { t } = useTranslation();
|
|
44
|
+
|
|
45
|
+
const { values, setFieldValue, dirty } = useCreateFormex<Partial<MediaAsset>>({
|
|
46
|
+
initialValues: {
|
|
47
|
+
title: asset.title ?? "",
|
|
48
|
+
altText: asset.altText ?? "",
|
|
49
|
+
caption: asset.caption ?? "",
|
|
50
|
+
tags: asset.tags ?? []
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const handleSave = useCallback(async () => {
|
|
55
|
+
setSaving(true);
|
|
56
|
+
try {
|
|
57
|
+
await onUpdate(asset.id, values);
|
|
58
|
+
snackbarController.open({
|
|
59
|
+
type: "success",
|
|
60
|
+
message: t("media_asset_updated")
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
snackbarController.open({
|
|
64
|
+
type: "error",
|
|
65
|
+
message: t("media_error_updating", { message: error instanceof Error ? error.message : String(error) })
|
|
66
|
+
});
|
|
67
|
+
} finally {
|
|
68
|
+
setSaving(false);
|
|
69
|
+
}
|
|
70
|
+
}, [asset.id, values, onUpdate, snackbarController]);
|
|
71
|
+
|
|
72
|
+
const handleDelete = useCallback(async () => {
|
|
73
|
+
setDeleting(true);
|
|
74
|
+
try {
|
|
75
|
+
await onDelete(asset.id);
|
|
76
|
+
snackbarController.open({
|
|
77
|
+
type: "success",
|
|
78
|
+
message: t("media_asset_deleted")
|
|
79
|
+
});
|
|
80
|
+
onClose();
|
|
81
|
+
} catch (error) {
|
|
82
|
+
snackbarController.open({
|
|
83
|
+
type: "error",
|
|
84
|
+
message: t("media_error_deleting", { message: error instanceof Error ? error.message : String(error) })
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
setDeleting(false);
|
|
88
|
+
setDeleteDialogOpen(false);
|
|
89
|
+
}
|
|
90
|
+
}, [asset.id, onDelete, snackbarController, onClose]);
|
|
91
|
+
|
|
92
|
+
const handleDownload = useCallback(async () => {
|
|
93
|
+
try {
|
|
94
|
+
const downloadConfig = await storageSource.getDownloadURL(asset.storagePath, asset.bucket);
|
|
95
|
+
if (downloadConfig.url) {
|
|
96
|
+
window.open(downloadConfig.url, "_blank");
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
snackbarController.open({
|
|
100
|
+
type: "error",
|
|
101
|
+
message: t("media_error_getting_url")
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}, [asset, storageSource, snackbarController]);
|
|
105
|
+
|
|
106
|
+
const handleAddTag = useCallback(() => {
|
|
107
|
+
const tag = tagInput.trim();
|
|
108
|
+
if (tag && !values.tags?.includes(tag)) {
|
|
109
|
+
setFieldValue("tags", [...(values.tags ?? []), tag]);
|
|
110
|
+
setTagInput("");
|
|
111
|
+
}
|
|
112
|
+
}, [tagInput, values.tags, setFieldValue]);
|
|
113
|
+
|
|
114
|
+
const handleRemoveTag = useCallback((tagToRemove: string) => {
|
|
115
|
+
setFieldValue("tags", values.tags?.filter((t: string) => t !== tagToRemove) ?? []);
|
|
116
|
+
}, [values.tags, setFieldValue]);
|
|
117
|
+
|
|
118
|
+
const formatSize = (bytes: number): string => {
|
|
119
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
120
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
121
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const formatDate = (date: Date): string => {
|
|
125
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
126
|
+
year: "numeric",
|
|
127
|
+
month: "short",
|
|
128
|
+
day: "numeric"
|
|
129
|
+
}).format(date);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const isImage = asset.mimeType.startsWith("image/");
|
|
133
|
+
const isVideo = asset.mimeType.startsWith("video/");
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<div className={cls(
|
|
138
|
+
"fixed inset-y-0 right-0 w-full sm:w-96 lg:w-[480px]",
|
|
139
|
+
"bg-surface-50 dark:bg-surface-900",
|
|
140
|
+
"border-l border-surface-accent-200 dark:border-surface-accent-700",
|
|
141
|
+
"shadow-xl z-50",
|
|
142
|
+
"flex flex-col",
|
|
143
|
+
"animate-slide-in-right"
|
|
144
|
+
)}>
|
|
145
|
+
{/* Header */}
|
|
146
|
+
<div className="flex items-center justify-between p-4 border-b border-surface-accent-200 dark:border-surface-accent-700">
|
|
147
|
+
<Typography variant="subtitle1" className="font-medium truncate flex-1 mr-2">
|
|
148
|
+
{asset.title || asset.fileName}
|
|
149
|
+
</Typography>
|
|
150
|
+
<div className="flex items-center gap-1">
|
|
151
|
+
<IconButton onClick={handleDownload}>
|
|
152
|
+
<DownloadIcon size="small" />
|
|
153
|
+
</IconButton>
|
|
154
|
+
<IconButton
|
|
155
|
+
onClick={() => setDeleteDialogOpen(true)}
|
|
156
|
+
className="text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20"
|
|
157
|
+
>
|
|
158
|
+
<DeleteIcon size="small" />
|
|
159
|
+
</IconButton>
|
|
160
|
+
<IconButton onClick={onClose}>
|
|
161
|
+
<CloseIcon size="small" />
|
|
162
|
+
</IconButton>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Preview */}
|
|
167
|
+
<div className="p-4 bg-surface-accent-100 dark:bg-surface-accent-800 flex items-center justify-center min-h-48 max-h-64">
|
|
168
|
+
{isImage && asset.downloadURL ? (
|
|
169
|
+
<img
|
|
170
|
+
src={asset.downloadURL}
|
|
171
|
+
alt={asset.altText || asset.fileName}
|
|
172
|
+
className="max-w-full max-h-full object-contain"
|
|
173
|
+
/>
|
|
174
|
+
) : isVideo && asset.downloadURL ? (
|
|
175
|
+
<video
|
|
176
|
+
src={asset.downloadURL}
|
|
177
|
+
className="max-w-full max-h-full"
|
|
178
|
+
controls
|
|
179
|
+
/>
|
|
180
|
+
) : (
|
|
181
|
+
<div className="text-surface-accent-400">
|
|
182
|
+
{t("media_preview_not_available")}
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
{/* Content */}
|
|
188
|
+
<div className="flex-1 overflow-auto p-4 space-y-4">
|
|
189
|
+
{/* Metadata */}
|
|
190
|
+
<div className="grid grid-cols-2 gap-3">
|
|
191
|
+
{asset.dimensions && (
|
|
192
|
+
<div>
|
|
193
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
194
|
+
{t("media_dimensions")}
|
|
195
|
+
</Typography>
|
|
196
|
+
<Typography variant="body2">
|
|
197
|
+
{asset.dimensions.width} × {asset.dimensions.height} px
|
|
198
|
+
</Typography>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<div>
|
|
202
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
203
|
+
{t("media_size")}
|
|
204
|
+
</Typography>
|
|
205
|
+
<Typography variant="body2">
|
|
206
|
+
{formatSize(asset.size)}
|
|
207
|
+
</Typography>
|
|
208
|
+
</div>
|
|
209
|
+
<div>
|
|
210
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
211
|
+
{t("media_type")}
|
|
212
|
+
</Typography>
|
|
213
|
+
<Typography variant="body2">
|
|
214
|
+
{asset.mimeType}
|
|
215
|
+
</Typography>
|
|
216
|
+
</div>
|
|
217
|
+
<div>
|
|
218
|
+
<Typography variant="caption" className="text-surface-accent-500">
|
|
219
|
+
{t("media_created")}
|
|
220
|
+
</Typography>
|
|
221
|
+
<Typography variant="body2">
|
|
222
|
+
{formatDate(asset.createdAt)}
|
|
223
|
+
</Typography>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<hr className="border-surface-accent-200 dark:border-surface-accent-700" />
|
|
228
|
+
|
|
229
|
+
{/* Editable Fields */}
|
|
230
|
+
<TextField
|
|
231
|
+
label={t("media_file_name")}
|
|
232
|
+
value={asset.fileName}
|
|
233
|
+
disabled
|
|
234
|
+
size="small"
|
|
235
|
+
/>
|
|
236
|
+
|
|
237
|
+
<TextField
|
|
238
|
+
label={t("media_title")}
|
|
239
|
+
value={values.title ?? ""}
|
|
240
|
+
onChange={(e) => setFieldValue("title", e.target.value)}
|
|
241
|
+
size="small"
|
|
242
|
+
/>
|
|
243
|
+
|
|
244
|
+
<div>
|
|
245
|
+
<TextField
|
|
246
|
+
label={t("media_alt_text")}
|
|
247
|
+
value={values.altText ?? ""}
|
|
248
|
+
onChange={(e) => setFieldValue("altText", e.target.value)}
|
|
249
|
+
size="small"
|
|
250
|
+
/>
|
|
251
|
+
<Typography variant="caption" className="text-surface-accent-500 mt-1">
|
|
252
|
+
{t("media_recommended_seo")}
|
|
253
|
+
</Typography>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<TextField
|
|
257
|
+
label={t("media_caption")}
|
|
258
|
+
value={values.caption ?? ""}
|
|
259
|
+
onChange={(e) => setFieldValue("caption", e.target.value)}
|
|
260
|
+
size="small"
|
|
261
|
+
multiline
|
|
262
|
+
/>
|
|
263
|
+
|
|
264
|
+
{/* Tags */}
|
|
265
|
+
<div>
|
|
266
|
+
<Typography variant="caption" className="text-surface-accent-500 mb-1 block">
|
|
267
|
+
{t("media_tags")}
|
|
268
|
+
</Typography>
|
|
269
|
+
<div className="flex flex-wrap gap-1 mb-2">
|
|
270
|
+
{values.tags?.map((tag: string) => (
|
|
271
|
+
<Chip
|
|
272
|
+
key={tag}
|
|
273
|
+
size="small"
|
|
274
|
+
colorScheme="blueLighter"
|
|
275
|
+
onClick={() => handleRemoveTag(tag)}
|
|
276
|
+
>
|
|
277
|
+
{tag} ×
|
|
278
|
+
</Chip>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
<div className="flex gap-2">
|
|
282
|
+
<TextField
|
|
283
|
+
placeholder={t("media_add_tag")}
|
|
284
|
+
value={tagInput}
|
|
285
|
+
onChange={(e) => setTagInput(e.target.value)}
|
|
286
|
+
size="small"
|
|
287
|
+
className="flex-1"
|
|
288
|
+
onKeyDown={(e) => {
|
|
289
|
+
if (e.key === "Enter") {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
handleAddTag();
|
|
292
|
+
}
|
|
293
|
+
}}
|
|
294
|
+
/>
|
|
295
|
+
<Button
|
|
296
|
+
variant="text"
|
|
297
|
+
size="small"
|
|
298
|
+
onClick={handleAddTag}
|
|
299
|
+
disabled={!tagInput.trim()}
|
|
300
|
+
>
|
|
301
|
+
{t("media_add")}
|
|
302
|
+
</Button>
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
{/* Footer */}
|
|
308
|
+
<div className="flex-shrink-0 p-4 border-t border-surface-accent-200 dark:border-surface-accent-700">
|
|
309
|
+
<Button
|
|
310
|
+
variant="filled"
|
|
311
|
+
onClick={handleSave}
|
|
312
|
+
disabled={!dirty || saving}
|
|
313
|
+
className="w-full"
|
|
314
|
+
>
|
|
315
|
+
{saving ? <CircularProgress size="small" /> : t("media_save_changes")}
|
|
316
|
+
</Button>
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
{/* Delete Confirmation Dialog */}
|
|
321
|
+
<Dialog
|
|
322
|
+
open={deleteDialogOpen}
|
|
323
|
+
onOpenChange={setDeleteDialogOpen}
|
|
324
|
+
>
|
|
325
|
+
<DialogContent>
|
|
326
|
+
<Typography variant="subtitle1" className="font-medium mb-2">
|
|
327
|
+
{t("media_delete_asset")}
|
|
328
|
+
</Typography>
|
|
329
|
+
<Typography className="text-surface-accent-600 dark:text-surface-accent-400">
|
|
330
|
+
{t("media_delete_confirmation", { name: asset.title || asset.fileName })}
|
|
331
|
+
</Typography>
|
|
332
|
+
</DialogContent>
|
|
333
|
+
<DialogActions>
|
|
334
|
+
<Button
|
|
335
|
+
variant="text"
|
|
336
|
+
onClick={() => setDeleteDialogOpen(false)}
|
|
337
|
+
disabled={deleting}
|
|
338
|
+
>
|
|
339
|
+
{t("cancel")}
|
|
340
|
+
</Button>
|
|
341
|
+
<Button
|
|
342
|
+
variant="filled"
|
|
343
|
+
color="error"
|
|
344
|
+
onClick={handleDelete}
|
|
345
|
+
disabled={deleting}
|
|
346
|
+
>
|
|
347
|
+
{deleting ? <CircularProgress size="small" /> : t("delete")}
|
|
348
|
+
</Button>
|
|
349
|
+
</DialogActions>
|
|
350
|
+
</Dialog>
|
|
351
|
+
</>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
Typography,
|
|
6
|
+
cls,
|
|
7
|
+
ImageIcon
|
|
8
|
+
} from "@firecms/ui";
|
|
9
|
+
import { useTranslation } from "@firecms/core";
|
|
10
|
+
|
|
11
|
+
export interface MediaLibraryCardProps {
|
|
12
|
+
group?: string;
|
|
13
|
+
context?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Card component displayed on the home page that links to the Media Library.
|
|
18
|
+
*/
|
|
19
|
+
export function MediaLibraryCard({ group }: MediaLibraryCardProps) {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
|
|
22
|
+
// Only render in the "Media" group
|
|
23
|
+
if (group !== "Media") return null;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<Link to="/media" className="no-underline">
|
|
27
|
+
<Card
|
|
28
|
+
className={cls(
|
|
29
|
+
"p-4 cursor-pointer",
|
|
30
|
+
"hover:bg-surface-accent-100 dark:hover:bg-surface-accent-800",
|
|
31
|
+
"transition-colors duration-200",
|
|
32
|
+
"flex flex-col gap-2"
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<div className="flex items-center gap-3">
|
|
36
|
+
<div className={cls(
|
|
37
|
+
"w-10 h-10 rounded-lg",
|
|
38
|
+
"bg-primary/10 dark:bg-primary/20",
|
|
39
|
+
"flex items-center justify-center"
|
|
40
|
+
)}>
|
|
41
|
+
<ImageIcon className="text-primary" size="medium" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="flex-1">
|
|
44
|
+
<Typography variant="subtitle2" className="font-medium">
|
|
45
|
+
{t("media_library")}
|
|
46
|
+
</Typography>
|
|
47
|
+
<Typography
|
|
48
|
+
variant="caption"
|
|
49
|
+
className="text-surface-accent-600 dark:text-surface-accent-400"
|
|
50
|
+
>
|
|
51
|
+
{t("media_library_description")}
|
|
52
|
+
</Typography>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</Card>
|
|
56
|
+
</Link>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
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 { useTranslation } from "@firecms/core";
|
|
17
|
+
import { useMediaManager } from "../MediaManagerProvider";
|
|
18
|
+
import { MediaAssetCard } from "./MediaAssetCard";
|
|
19
|
+
import { MediaAssetDetails } from "./MediaAssetDetails";
|
|
20
|
+
import { MediaUploadDialog } from "./MediaUploadDialog";
|
|
21
|
+
|
|
22
|
+
export interface MediaLibraryViewProps {
|
|
23
|
+
maxFileSize?: number;
|
|
24
|
+
acceptedMimeTypes?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Main view component for the Media Library.
|
|
29
|
+
* Displays a grid of assets with search, upload, and management capabilities.
|
|
30
|
+
*/
|
|
31
|
+
export function MediaLibraryView({
|
|
32
|
+
maxFileSize,
|
|
33
|
+
acceptedMimeTypes
|
|
34
|
+
}: MediaLibraryViewProps) {
|
|
35
|
+
const controller = useMediaManager();
|
|
36
|
+
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
|
37
|
+
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
|
38
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
|
|
41
|
+
const handleSearch = useCallback((query?: string) => {
|
|
42
|
+
controller.searchAssets(query ?? "");
|
|
43
|
+
}, [controller]);
|
|
44
|
+
|
|
45
|
+
const handleUploadClick = useCallback(() => {
|
|
46
|
+
setUploadDialogOpen(true);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const handleFileSelect = useCallback(async (files: File[]) => {
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
await controller.uploadFile(file);
|
|
52
|
+
}
|
|
53
|
+
setUploadDialogOpen(false);
|
|
54
|
+
}, [controller]);
|
|
55
|
+
|
|
56
|
+
const handleRefresh = useCallback(() => {
|
|
57
|
+
controller.refreshAssets();
|
|
58
|
+
}, [controller]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="h-full flex flex-col overflow-hidden">
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div
|
|
64
|
+
className="flex-shrink-0 border-b border-surface-accent-200 dark:border-surface-accent-700 bg-surface-50 dark:bg-surface-900">
|
|
65
|
+
<Container maxWidth="6xl" className="py-4">
|
|
66
|
+
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
67
|
+
<div className="flex items-center gap-3">
|
|
68
|
+
<Typography variant="h5" className="font-semibold">
|
|
69
|
+
{t("media_library")}
|
|
70
|
+
</Typography>
|
|
71
|
+
{controller.totalCount !== undefined && (
|
|
72
|
+
<Typography
|
|
73
|
+
variant="caption"
|
|
74
|
+
className="bg-surface-accent-100 dark:bg-surface-accent-800 px-2 py-0.5 rounded-full"
|
|
75
|
+
>
|
|
76
|
+
{t("media_assets_count", { count: controller.totalCount.toString() })}
|
|
77
|
+
</Typography>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="flex items-center gap-2 w-full sm:w-auto">
|
|
82
|
+
<SearchBar
|
|
83
|
+
onTextSearch={handleSearch}
|
|
84
|
+
placeholder={t("media_search_assets")}
|
|
85
|
+
className="flex-1 sm:w-64"
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<div
|
|
89
|
+
className="flex items-center gap-1 border-l border-surface-accent-200 dark:border-surface-accent-700 pl-2 ml-2">
|
|
90
|
+
<Tooltip title={t("media_grid_view")}>
|
|
91
|
+
<IconButton
|
|
92
|
+
onClick={() => setViewMode("grid")}
|
|
93
|
+
className={cls(
|
|
94
|
+
viewMode === "grid" && "bg-surface-accent-100 dark:bg-surface-accent-800"
|
|
95
|
+
)}
|
|
96
|
+
>
|
|
97
|
+
<AppsIcon size="small"/>
|
|
98
|
+
</IconButton>
|
|
99
|
+
</Tooltip>
|
|
100
|
+
<Tooltip title={t("media_list_view")}>
|
|
101
|
+
<IconButton
|
|
102
|
+
onClick={() => setViewMode("list")}
|
|
103
|
+
className={cls(
|
|
104
|
+
viewMode === "list" && "bg-surface-accent-100 dark:bg-surface-accent-800"
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<Icon iconKey="list" size="small"/>
|
|
108
|
+
</IconButton>
|
|
109
|
+
</Tooltip>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<Tooltip title={t("media_refresh")}>
|
|
113
|
+
<IconButton onClick={handleRefresh} disabled={controller.loading}>
|
|
114
|
+
<RefreshIcon size="small"/>
|
|
115
|
+
</IconButton>
|
|
116
|
+
</Tooltip>
|
|
117
|
+
|
|
118
|
+
<Button
|
|
119
|
+
variant="filled"
|
|
120
|
+
onClick={handleUploadClick}
|
|
121
|
+
>
|
|
122
|
+
<AddIcon size="small"/>
|
|
123
|
+
{t("media_upload")}
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</Container>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Content */}
|
|
131
|
+
<div className="flex-1 overflow-auto">
|
|
132
|
+
<Container maxWidth="6xl" className="py-6">
|
|
133
|
+
{controller.loading && controller.assets.length === 0 ? (
|
|
134
|
+
<div className="flex items-center justify-center h-64">
|
|
135
|
+
<CircularProgress/>
|
|
136
|
+
</div>
|
|
137
|
+
) : controller.error ? (
|
|
138
|
+
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
139
|
+
<Typography className="text-red-500">
|
|
140
|
+
{t("media_error_loading", { message: controller.error.message })}
|
|
141
|
+
</Typography>
|
|
142
|
+
<Button onClick={handleRefresh}>
|
|
143
|
+
{t("media_try_again")}
|
|
144
|
+
</Button>
|
|
145
|
+
</div>
|
|
146
|
+
) : controller.assets.length === 0 ? (
|
|
147
|
+
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
|
148
|
+
<Typography className="text-surface-accent-500">
|
|
149
|
+
{t("media_no_assets")}
|
|
150
|
+
</Typography>
|
|
151
|
+
<Button onClick={handleUploadClick}>
|
|
152
|
+
<AddIcon size="small"/>
|
|
153
|
+
{t("media_upload_first_file")}
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<div className={cls(
|
|
158
|
+
viewMode === "grid"
|
|
159
|
+
? "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
|
|
160
|
+
: "flex flex-col gap-2"
|
|
161
|
+
)}>
|
|
162
|
+
{controller.assets.map(asset => (
|
|
163
|
+
<MediaAssetCard
|
|
164
|
+
key={asset.id}
|
|
165
|
+
asset={asset}
|
|
166
|
+
viewMode={viewMode}
|
|
167
|
+
onClick={() => controller.selectAsset(asset)}
|
|
168
|
+
selected={controller.selectedAsset?.id === asset.id}
|
|
169
|
+
/>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
</Container>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{/* Details Panel */}
|
|
177
|
+
{controller.selectedAsset && (
|
|
178
|
+
<MediaAssetDetails
|
|
179
|
+
asset={controller.selectedAsset}
|
|
180
|
+
onClose={() => controller.selectAsset(undefined)}
|
|
181
|
+
onUpdate={controller.updateAsset}
|
|
182
|
+
onDelete={controller.deleteAsset}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
{/* Upload Dialog */}
|
|
187
|
+
<MediaUploadDialog
|
|
188
|
+
open={uploadDialogOpen}
|
|
189
|
+
onClose={() => setUploadDialogOpen(false)}
|
|
190
|
+
onUpload={handleFileSelect}
|
|
191
|
+
maxFileSize={maxFileSize}
|
|
192
|
+
acceptedMimeTypes={acceptedMimeTypes}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|