296 lines
9.0 KiB
TypeScript
296 lines
9.0 KiB
TypeScript
import { useEffect, useState, useRef } from "react";
|
||
import { observer } from "mobx-react-lite";
|
||
import { useNavigate, useParams } from "react-router-dom";
|
||
import {
|
||
Box,
|
||
Button,
|
||
CircularProgress,
|
||
FormControl,
|
||
InputLabel,
|
||
MenuItem,
|
||
Paper,
|
||
Select,
|
||
TextField,
|
||
Typography,
|
||
Alert,
|
||
Snackbar,
|
||
} from "@mui/material";
|
||
import { Save, ArrowLeft } from "lucide-react";
|
||
import { authInstance, mediaStore, MEDIA_TYPE_LABELS } from "@shared";
|
||
import { MediaViewer } from "@widgets";
|
||
|
||
export const MediaEditPage = observer(() => {
|
||
const { id } = useParams<{ id: string }>();
|
||
const navigate = useNavigate();
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [success, setSuccess] = useState(false);
|
||
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
const [newFile, setNewFile] = useState<File | null>(null);
|
||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false); // State for the upload dialog
|
||
|
||
const media = id ? mediaStore.media.find((m) => m.id === id) : null;
|
||
const [mediaName, setMediaName] = useState(media?.media_name ?? "");
|
||
const [mediaFilename, setMediaFilename] = useState(media?.filename ?? "");
|
||
const [mediaType, setMediaType] = useState(media?.media_type ?? 1);
|
||
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>([]);
|
||
|
||
useEffect(() => {
|
||
if (id) {
|
||
mediaStore.getOneMedia(id);
|
||
}
|
||
console.log(newFile);
|
||
console.log(uploadDialogOpen);
|
||
}, [id]);
|
||
|
||
useEffect(() => {
|
||
if (media) {
|
||
setMediaName(media.media_name);
|
||
setMediaFilename(media.filename);
|
||
setMediaType(media.media_type);
|
||
|
||
// Set available media types based on current file extension
|
||
const extension = media.filename.split(".").pop()?.toLowerCase();
|
||
if (extension) {
|
||
if (["glb", "gltf"].includes(extension)) {
|
||
setAvailableMediaTypes([6]); // 3D model
|
||
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
|
||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||
setAvailableMediaTypes([2]); // Video
|
||
}
|
||
}
|
||
}
|
||
}, [media]);
|
||
|
||
// const handleDrop = (e: DragEvent<HTMLDivElement>) => {
|
||
// e.preventDefault();
|
||
// e.stopPropagation();
|
||
// setIsDragging(false);
|
||
|
||
// const files = Array.from(e.dataTransfer.files);
|
||
// if (files.length > 0) {
|
||
// setNewFile(files[0]);
|
||
// setMediaFilename(files[0].name);
|
||
// setUploadDialogOpen(true); // Open dialog on file drop
|
||
// }
|
||
// };
|
||
|
||
// const handleDragOver = (e: DragEvent<HTMLDivElement>) => {
|
||
// e.preventDefault();
|
||
// setIsDragging(true);
|
||
// };
|
||
|
||
// const handleDragLeave = () => {
|
||
// setIsDragging(false);
|
||
// };
|
||
|
||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const files = e.target.files;
|
||
if (files && files.length > 0) {
|
||
const file = files[0];
|
||
setNewFile(file);
|
||
setMediaFilename(file.name);
|
||
|
||
// Determine media type based on file extension
|
||
const extension = file.name.split(".").pop()?.toLowerCase();
|
||
if (extension) {
|
||
if (["glb", "gltf"].includes(extension)) {
|
||
setAvailableMediaTypes([6]); // 3D model
|
||
setMediaType(6);
|
||
} else if (["jpg", "jpeg", "png", "gif"].includes(extension)) {
|
||
setAvailableMediaTypes([1, 3, 4, 5]); // Photo, Icon, Watermark, Panorama
|
||
setMediaType(1); // Default to Photo
|
||
} else if (["mp4", "webm", "mov"].includes(extension)) {
|
||
setAvailableMediaTypes([2]); // Video
|
||
setMediaType(2);
|
||
}
|
||
}
|
||
|
||
setUploadDialogOpen(true); // Open dialog on file selection
|
||
}
|
||
};
|
||
|
||
const handleSave = async () => {
|
||
if (!id) return;
|
||
|
||
setIsLoading(true);
|
||
setError(null);
|
||
|
||
try {
|
||
await authInstance.patch(`/media/${id}`, {
|
||
media_name: mediaName,
|
||
filename: mediaFilename,
|
||
type: mediaType,
|
||
});
|
||
|
||
// If a new file was selected, the actual file upload will happen
|
||
// via the UploadMediaDialog. We just need to make sure the metadata
|
||
// is updated correctly before or after.
|
||
// Since the dialog handles the actual upload, we don't call updateMediaFile here.
|
||
|
||
setSuccess(true);
|
||
handleUploadSuccess();
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "Failed to save media");
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleUploadSuccess = () => {
|
||
// After successful upload in the dialog, refresh media data if needed
|
||
if (id) {
|
||
mediaStore.getOneMedia(id);
|
||
}
|
||
setNewFile(null); // Clear the new file state after successful upload
|
||
setUploadDialogOpen(false);
|
||
setSuccess(true);
|
||
};
|
||
|
||
if (!media && id) {
|
||
// Only show loading if an ID is present and media is not yet loaded
|
||
return (
|
||
<Box className="flex justify-center items-center h-screen">
|
||
<CircularProgress />
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Box className="p-6 max-w-4xl mx-auto">
|
||
<Box className="flex items-center gap-4 mb-6">
|
||
<Button
|
||
variant="outlined"
|
||
startIcon={<ArrowLeft size={20} />}
|
||
onClick={() => navigate(-1)}
|
||
>
|
||
Назад
|
||
</Button>
|
||
<Typography variant="h5">Редактирование медиа</Typography>
|
||
</Box>
|
||
|
||
<Paper className="p-6">
|
||
<input
|
||
type="file"
|
||
ref={fileInputRef}
|
||
className="hidden"
|
||
onChange={handleFileSelect}
|
||
accept="image/*,video/*,.glb,.gltf"
|
||
/>
|
||
<Box className="flex flex-col gap-6">
|
||
<Box className="flex gap-4">
|
||
<TextField
|
||
fullWidth
|
||
value={mediaName}
|
||
onChange={(e) => setMediaName(e.target.value)}
|
||
label="Название медиа"
|
||
disabled={isLoading}
|
||
/>
|
||
<TextField
|
||
fullWidth
|
||
value={mediaFilename}
|
||
onChange={(e) => setMediaFilename(e.target.value)}
|
||
label="Название файла"
|
||
disabled={isLoading}
|
||
/>
|
||
</Box>
|
||
|
||
<FormControl fullWidth sx={{ width: "50%" }}>
|
||
<InputLabel>Тип медиа</InputLabel>
|
||
<Select
|
||
value={mediaType}
|
||
label="Тип медиа"
|
||
onChange={(e) => setMediaType(Number(e.target.value))}
|
||
disabled={isLoading}
|
||
>
|
||
{availableMediaTypes.length > 0
|
||
? availableMediaTypes.map((type) => (
|
||
<MenuItem key={type} value={type}>
|
||
{
|
||
MEDIA_TYPE_LABELS[
|
||
type as keyof typeof MEDIA_TYPE_LABELS
|
||
]
|
||
}
|
||
</MenuItem>
|
||
))
|
||
: Object.entries(MEDIA_TYPE_LABELS).map(([type, label]) => (
|
||
<MenuItem key={type} value={Number(type)}>
|
||
{label}
|
||
</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
|
||
<Box className="flex gap-6">
|
||
<Paper
|
||
elevation={2}
|
||
sx={{
|
||
flex: 1,
|
||
p: 2,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
minHeight: 400,
|
||
}}
|
||
>
|
||
<MediaViewer
|
||
media={{
|
||
id: id || "",
|
||
media_type: mediaType,
|
||
filename: mediaFilename,
|
||
}}
|
||
/>
|
||
</Paper>
|
||
|
||
<Box className="flex flex-col gap-4 self-end">
|
||
<Button
|
||
variant="contained"
|
||
color="primary"
|
||
startIcon={<Save size={20} />}
|
||
onClick={handleSave}
|
||
disabled={isLoading || (!mediaName && !mediaFilename)}
|
||
>
|
||
{isLoading ? <CircularProgress size={20} /> : "Сохранить"}
|
||
</Button>
|
||
{/* Only show "Replace file" button if no new file is currently selected */}
|
||
</Box>
|
||
</Box>
|
||
|
||
{error && (
|
||
<Typography color="error" className="mt-2">
|
||
{error}
|
||
</Typography>
|
||
)}
|
||
{success && (
|
||
<Typography color="success.main" className="mt-2">
|
||
Медиа успешно сохранено
|
||
</Typography>
|
||
)}
|
||
</Box>
|
||
</Paper>
|
||
|
||
<Snackbar
|
||
open={!!error}
|
||
autoHideDuration={6000}
|
||
onClose={() => setError(null)}
|
||
>
|
||
<Alert severity="error" onClose={() => setError(null)}>
|
||
{error}
|
||
</Alert>
|
||
</Snackbar>
|
||
|
||
<Snackbar
|
||
open={success}
|
||
autoHideDuration={2000}
|
||
onClose={() => setSuccess(false)}
|
||
>
|
||
<Alert severity="success" onClose={() => setSuccess(false)}>
|
||
Медиа успешно сохранено
|
||
</Alert>
|
||
</Snackbar>
|
||
</Box>
|
||
);
|
||
});
|