Files
WhiteNightsAdminPanel/src/shared/modals/UploadMediaDialog/index.tsx
2025-11-06 00:58:10 +03:00

467 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
MEDIA_TYPE_LABELS,
MEDIA_TYPE_VALUES,
editSightStore,
generateDefaultMediaName,
clearBlobAndGLTFCache,
} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState, useRef } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
Paper,
Box,
CircularProgress,
Alert,
Snackbar,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@mui/material";
import { Save } from "lucide-react";
import { ModelViewer3D } from "@widgets";
interface UploadMediaDialogProps {
open: boolean;
onClose: () => void;
afterUpload?: (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => void;
afterUploadSight?: (id: string) => void;
hardcodeType?:
| "thumbnail"
| "watermark_lu"
| "watermark_rd"
| "image"
| "video_preview"
| null;
contextObjectName?: string;
contextType?:
| "sight"
| "city"
| "carrier"
| "country"
| "vehicle"
| "station";
isArticle?: boolean;
articleName?: string;
initialFile?: File;
}
export const UploadMediaDialog = observer(
({
open,
onClose,
afterUpload,
afterUploadSight,
hardcodeType,
contextObjectName,
isArticle,
articleName,
initialFile,
}: UploadMediaDialogProps) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [mediaName, setMediaName] = useState("");
const [mediaFilename, setMediaFilename] = useState("");
const [mediaType, setMediaType] = useState(0);
const [mediaFile, setMediaFile] = useState<File | null>(null);
const { fileToUpload, uploadMedia } = editSightStore;
const [mediaUrl, setMediaUrl] = useState<string | null>(null);
const [availableMediaTypes, setAvailableMediaTypes] = useState<number[]>(
[]
);
const [isPreviewLoaded, setIsPreviewLoaded] = useState(false);
const previousMediaUrlRef = useRef<string | null>(null);
useEffect(() => {
if (initialFile) {
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
) {
clearBlobAndGLTFCache(previousMediaUrlRef.current);
}
setMediaFile(initialFile);
setMediaFilename(initialFile.name);
setAvailableMediaTypes([2]);
setMediaType(2);
const newBlobUrl = URL.createObjectURL(initialFile);
setMediaUrl(newBlobUrl);
previousMediaUrlRef.current = newBlobUrl;
setMediaName(initialFile.name.replace(/\.[^/.]+$/, ""));
}
}, [initialFile]);
useEffect(() => {
return () => {
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
) {
clearBlobAndGLTFCache(previousMediaUrlRef.current);
}
};
}, []);
useEffect(() => {
if (fileToUpload) {
setMediaFile(fileToUpload);
setMediaFilename(fileToUpload.name);
const extension = fileToUpload.name.split(".").pop()?.toLowerCase();
if (extension) {
if (["glb", "gltf"].includes(extension)) {
setAvailableMediaTypes([6]);
setMediaType(6);
}
if (
["jpg", "jpeg", "png", "gif", "svg", "webp", "bmp"].includes(
extension
)
) {
setAvailableMediaTypes([1, 3, 4, 5]);
setMediaType(1);
} else if (["mp4", "webm", "mov"].includes(extension)) {
setAvailableMediaTypes([2]);
setMediaType(2);
}
}
if (fileToUpload.name) {
let defaultName = "";
if (isArticle && articleName && contextObjectName) {
defaultName = generateDefaultMediaName(
contextObjectName,
fileToUpload.name,
articleName,
true
);
} else if (contextObjectName && contextObjectName.trim() !== "") {
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: 1;
defaultName = generateDefaultMediaName(
contextObjectName,
fileToUpload.name,
currentMediaType,
false
);
} else {
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: 1;
defaultName = generateDefaultMediaName(
"",
fileToUpload.name,
currentMediaType,
false
);
}
setMediaName(defaultName);
}
}
}, [fileToUpload, contextObjectName, hardcodeType, isArticle, articleName]);
useEffect(() => {
if (mediaFilename && mediaType > 0) {
let defaultName = "";
if (isArticle && articleName && contextObjectName) {
defaultName = generateDefaultMediaName(
contextObjectName,
mediaFilename,
articleName,
true
);
} else if (contextObjectName && contextObjectName.trim() !== "") {
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: mediaType;
defaultName = generateDefaultMediaName(
contextObjectName,
mediaFilename,
currentMediaType,
false
);
} else {
const currentMediaType = hardcodeType
? MEDIA_TYPE_VALUES[hardcodeType]
: mediaType;
defaultName = generateDefaultMediaName(
"",
mediaFilename,
currentMediaType,
false
);
}
setMediaName(defaultName);
}
}, [
mediaType,
contextObjectName,
mediaFilename,
hardcodeType,
isArticle,
articleName,
]);
useEffect(() => {
if (mediaFile) {
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
) {
clearBlobAndGLTFCache(previousMediaUrlRef.current);
}
const newBlobUrl = URL.createObjectURL(mediaFile as Blob);
setMediaUrl(newBlobUrl);
previousMediaUrlRef.current = newBlobUrl;
setIsPreviewLoaded(false);
}
}, [mediaFile]);
const handleSave = async () => {
if (!mediaFile) return;
setIsLoading(true);
setError(null);
try {
const media = await uploadMedia(
mediaFilename,
hardcodeType
? (MEDIA_TYPE_VALUES[hardcodeType] as number)
: mediaType,
mediaFile,
mediaName
);
if (media) {
if (afterUploadSight) {
await afterUploadSight(media.id);
} else if (afterUpload) {
await afterUpload(media);
}
}
setSuccess(true);
setTimeout(() => {
handleClose();
}, 1000);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to save media");
} finally {
setIsLoading(false);
}
};
const handleClose = () => {
if (
previousMediaUrlRef.current &&
previousMediaUrlRef.current.startsWith("blob:")
) {
clearBlobAndGLTFCache(previousMediaUrlRef.current);
}
setError(null);
setSuccess(false);
setMediaUrl(null);
setMediaFile(null);
setIsPreviewLoaded(false);
previousMediaUrlRef.current = null;
onClose();
};
return (
<>
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth>
<DialogTitle>Просмотр медиа</DialogTitle>
<DialogContent
className="flex gap-4"
dividers
sx={{
display: "flex",
flexDirection: "column",
gap: 2,
pt: 2,
}}
>
<Box className="flex flex-col gap-4">
<Box className="flex gap-2">
<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={
hardcodeType ? MEDIA_TYPE_VALUES[hardcodeType] : mediaType
}
disabled={!!hardcodeType}
label="Тип медиа"
onChange={(e) => setMediaType(Number(e.target.value))}
>
{availableMediaTypes.map((type) => (
<MenuItem key={type} value={type}>
{
MEDIA_TYPE_LABELS[
type as keyof typeof MEDIA_TYPE_LABELS
]
}
</MenuItem>
))}
</Select>
</FormControl>
<Box className="flex gap-4 h-[40vh]">
<Paper
elevation={2}
sx={{
flex: 1,
p: 2,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%",
position: "relative",
}}
>
{!isPreviewLoaded && mediaUrl && (
<Box
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: 1,
}}
>
<CircularProgress />
</Box>
)}
{mediaType == 2 && mediaUrl && (
<video
src={mediaUrl}
autoPlay
muted
loop
controls
style={{ maxWidth: "100%", maxHeight: "100%" }}
onLoadedData={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/>
)}
{mediaType === 6 && mediaUrl && (
<ModelViewer3D
fileUrl={mediaUrl}
height="100%"
onLoad={() => setIsPreviewLoaded(true)}
/>
)}
{mediaType !== 6 && mediaType !== 2 && mediaUrl && (
<img
src={mediaUrl ?? ""}
alt="Uploaded media"
style={{
height: "100%",
objectFit: "contain",
}}
onLoad={() => setIsPreviewLoaded(true)}
onError={() => setIsPreviewLoaded(true)}
/>
)}
</Paper>
<Box className="flex flex-col gap-2 self-end">
<Button
variant="contained"
sx={{
backgroundColor: isLoading ? "#9e9e9e" : "#4caf50",
"&:hover": {
backgroundColor: isLoading ? "#9e9e9e" : "#45a049",
},
}}
startIcon={
isLoading ? (
<CircularProgress size={16} color="inherit" />
) : (
<Save size={16} />
)
}
onClick={handleSave}
disabled={
isLoading ||
(!mediaName && !mediaFilename) ||
!isPreviewLoaded
}
>
{isLoading
? "Сохранение..."
: !isPreviewLoaded
? "Загрузка превью..."
: "Сохранить"}
</Button>
</Box>
</Box>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} disabled={isLoading}>
Отмена
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={!!error}
autoHideDuration={6000}
onClose={() => setError(null)}
>
<Alert severity="error" onClose={() => setError(null)}>
{error}
</Alert>
</Snackbar>
<Snackbar
open={success}
autoHideDuration={3000}
onClose={() => setSuccess(false)}
>
<Alert severity="success" onClose={() => setSuccess(false)}>
Медиа успешно сохранено
</Alert>
</Snackbar>
</>
);
}
);