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(null); const [success, setSuccess] = useState(false); const [mediaName, setMediaName] = useState(""); const [mediaFilename, setMediaFilename] = useState(""); const [mediaType, setMediaType] = useState(0); const [mediaFile, setMediaFile] = useState(null); const { fileToUpload, uploadMedia } = editSightStore; const [mediaUrl, setMediaUrl] = useState(null); const [availableMediaTypes, setAvailableMediaTypes] = useState( [] ); const [isPreviewLoaded, setIsPreviewLoaded] = useState(false); const previousMediaUrlRef = useRef(null); useEffect(() => { if (initialFile) { // Очищаем предыдущий blob URL если он существует 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]); // Очистка blob URL при размонтировании компонента useEffect(() => { return () => { if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") ) { clearBlobAndGLTFCache(previousMediaUrlRef.current); } }; }, []); // Пустой массив зависимостей - выполняется только при размонтировании useEffect(() => { if (fileToUpload) { setMediaFile(fileToUpload); setMediaFilename(fileToUpload.name); // Try to determine media type from file extension 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]); // Фото, Иконка, Водяной знак, Панорама, 3Д-модель 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) { // Очищаем предыдущий blob URL и кеш GLTF если он существует if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") ) { clearBlobAndGLTFCache(previousMediaUrlRef.current); } const newBlobUrl = URL.createObjectURL(mediaFile as Blob); setMediaUrl(newBlobUrl); previousMediaUrlRef.current = newBlobUrl; // Сохраняем новый URL в ref setIsPreviewLoaded(false); // Сбрасываем состояние загрузки при смене файла } }, [mediaFile]); // Убираем mediaUrl из зависимостей чтобы избежать зацикливания // const fileFormat = useEffect(() => { // const handleKeyPress = (event: KeyboardEvent) => { // if (event.key.toLowerCase() === "enter" && !event.ctrlKey) { // event.preventDefault(); // onClose(); // } // }; // window.addEventListener("keydown", handleKeyPress); // return () => window.removeEventListener("keydown", handleKeyPress); // }, [onClose]); 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 = () => { // Очищаем blob URL и кеш GLTF при закрытии диалога if ( previousMediaUrlRef.current && previousMediaUrlRef.current.startsWith("blob:") ) { clearBlobAndGLTFCache(previousMediaUrlRef.current); } setError(null); setSuccess(false); setMediaUrl(null); setMediaFile(null); setIsPreviewLoaded(false); previousMediaUrlRef.current = null; // Очищаем ref onClose(); }; return ( <> Просмотр медиа setMediaName(e.target.value)} label="Название медиа" disabled={isLoading} /> setMediaFilename(e.target.value)} label="Название файла" disabled={isLoading} /> Тип медиа {!isPreviewLoaded && mediaUrl && ( )} {mediaType == 2 && mediaUrl && ( setError(null)} > setError(null)}> {error} setSuccess(false)} > setSuccess(false)}> Медиа успешно сохранено ); } );